7 月 AI CLI 工具开发总结:从 idea 到可用的 31 天全记录与关键决策
7 月 AI CLI 工具开发总结:从 idea 到可用的 31 天全记录与关键决策
一、31 天路线图:从混乱到有序的结构化复盘
7 月 1 号我打开 cargo new 的时候,脑子里只有一句话:"我要一个能在终端里直接问 AI 的东西"。31 天后,这个工具变成了 5 个 crate 组成的 workspace,支持 OpenAI/Claude/本地模型三种后端、流式输出、会话管理和插件系统。
回过头看,这 31 天可以切成四个阶段:
第一阶段混乱但必须。我不知道自己到底需要什么功能,只知道"能跑就行"。前三天的代码全部塞在一个main.rs里——HTTP 请求、JSON 解析、参数处理,什么都往里面扔。事后复盘,这段混乱期不是浪费,它让我在实践中明确了需求边界。
第二阶段是我第一次感受到 Rust 编程的"设计感"。我提取了AiProvidertrait,让 OpenAI、Claude 和 Ollama 三种后端实现了同一个接口。这个决策让后续换模型变得极其简单——只需改一行配置,不用碰业务代码。
第三阶段是最痛苦的工程化重构。我把单 crate 拆成 workspace:ai-core(抽象层)、ai-providers(后端适配器)、ai-config(配置管理)、ai-cli(入口)。编译时间从 20 秒降到 3 秒,改一行配置不再重编译整个项目。
第四阶段开始做"真正有用的东西"——让 AI CLI 不只是聊天,还能执行技能。查 git log、生成 commit message、扫描代码漏洞,这些"技能"通过插件系统注册,每个技能都是独立的实现。
二、5 个关键决策:如果重来我还会这么选
这 31 天里做的决定不下 50 个,但事后证明最关键的是这 5 个:
决策 1 最重要。我曾被"要不要加流式输出""要不要做配置文件 GUI""要不要支持 prompt 模板"这些想法反复拉扯。最后给自己立了一个铁则:只有当前闭环能正常工作时,才往上面加东西。这个自我约束救了我——否则 31 天后我会得到一个"功能很多但一个都不能稳定运行"的东西。
决策 2 是我在 Rust 里学到的最有用的设计模式。AiProvidertrait 的样子很简单,但它的威力在于:任何实现了这个 trait 的结构体,都能无缝接入整个管道。这不是为了"设计模式大全",而是为了让我在下个月想加 Gemini 或 DeepSeek 时,不改一行已有代码。
// ============================================================ // ai-core/src/provider.rs — 统一 AI 后端抽象 // ============================================================ use async_trait::async_trait; /// AI 提供者的统一接口 /// 新增任何 AI 后端(OpenAI/Claude/Ollama/Gemini 等) /// 只需实现这个 trait,上层业务代码完全不用动 #[async_trait] pub trait AiProvider: Send + Sync { /// 发送单条消息,获得完整回复 async fn chat(&self, message: &str) -> Result<String, ProviderError>; /// 流式对话,逐 token 回调(用于打字机效果和实时展示) async fn chat_stream( &self, message: &str, on_token: &(dyn Fn(String) + Send + Sync), ) -> Result<(), ProviderError>; /// 获取当前 provider 的标识名(用于日志和错误追踪) fn name(&self) -> &str; } /// AI 调用层的统一错误,把各种后端返回的错误都收敛到这里 #[derive(Debug, thiserror::Error)] pub enum ProviderError { #[error("网络连接失败: {0}")] Network(#[from] reqwest::Error), #[error("API 返回异常: 状态码={status}, 消息={message}")] Api { status: u16, message: String }, #[error("配置缺失: {0}")] Config(String), #[error("请求超时(>{}ms)", threshold_ms)] Timeout { threshold_ms: u64 }, }决策 3 是被逼出来的。某天下午改了一行配置代码,等了 20 秒编译——对于一个只有 2000 行的项目来说,20 秒完全不可接受。当天晚上我就拆了 workspace,增量编译降到 2 秒。这个决策没有任何"设计美感"的考量,纯粹是被效率逼出来的。
决策 4 和 5 都是"吃过亏才学会的"。一开始我用Box<dyn Error>到处返回错误,三天后就不知道一个错误到底来自网络还是 api 还是配置。改成thiserror的 enum 后,错误追踪一下子清晰了。技能系统用 trait 而不是宏,是因为我需要编译期的类型检查——宏虽然更灵活,但在技能数量和复杂度上来后,类型安全比灵活性重要。
三、那些做错的决策和回头看的原因
不是所有决策都对。以下是三个明显的误判:
第一个是过早优化流式输出。第三天我就花了一整天写 SSE 解析,结果第四天发现基础的单次对话还不稳定。正确的顺序应该是:先让核心路径稳定,再做体验优化。
第二个是配置系统做太复杂。我一开始写了 YAML + TOML + 环境变量三层合并逻辑,还支持--config指定路径。两周后发现实际上我只用了环境变量和 TO TOML,YAML 支持从未被使用。做减法比做加法更需要勇气。
第三个是插件系统延迟到第四周才动手。如果第二周就开始设计插件接口,后面就不用为"怎么把 git log 的功能塞进去"改 12 个文件。好的接口设计越早确定越好,因为它决定了后续所有代码的组织方式。
四、工具还缺什么:下个月的路线图
这 31 天里写的测试覆盖率只有 32%——这是一个让我睡不着的数字。Provider 层的单元测试还好,但集成测试(模拟 HTTP 超时、返回格式变化、流式中断)几乎为零。下个月第一优先级就是把覆盖率推到 80%。
第二个缺的是 session 持久化。现在的会话管理存在内存里,关掉终端就没了。下个月打算用 SQLite +rusqlite做一个轻量的会话存储,保留历史对话和上下文。
第三个是错误恢复。目前遇到网络错误就直接退出了,体验很差。应该自动重试 3 次,失败后保存未发送的消息,用户下次启动时可以恢复。
五、总结
31 天把一个 idea 变成一个能用的 AI CLI 工具,对一个来说,最大的收获不是这个工具本身,而是学到了怎么在混乱中建立秩序。
三条月度感悟:
- 先做小闭环,再做广度。让一个功能稳定工作,比三个功能"勉强能跑"更有价值。
- trait 抽象是 Rust 项目的脊柱。做对了 trait 设计,后续扩展是加法;做错了,后续重构是乘法。
- 编译速度不是虚指标。2 秒和 20 秒的区别,是你能不能保持心流的关键差异。
下个月不打算加新功能了——先把测试补全,然后真正"用"这个工具一个月,让实际使用中的痛点告诉我下一步该做什么。