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 拆成 workspaceai-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) - ResultString, 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 都是吃过亏才学会的。一开始我用Boxdyn Error到处返回错误三天后就不知道一个错误到底来自网络还是 api 还是配置。改成thiserror的 enum 后错误追踪一下子清晰了。技能系统用 trait 而不是宏是因为我需要编译期的类型检查——宏虽然更灵活但在技能数量和复杂度上来后类型安全比灵活性重要。三、那些做错的决策和回头看的原因不是所有决策都对。以下是三个明显的误判第一个是过早优化流式输出。第三天我就花了一整天写 SSE 解析结果第四天发现基础的单次对话还不稳定。正确的顺序应该是先让核心路径稳定再做体验优化。第二个是配置系统做太复杂。我一开始写了 YAML TOML 环境变量三层合并逻辑还支持--config指定路径。两周后发现实际上我只用了环境变量和 TO TOMLYAML 支持从未被使用。做减法比做加法更需要勇气。第三个是插件系统延迟到第四周才动手。如果第二周就开始设计插件接口后面就不用为怎么把 git log 的功能塞进去改 12 个文件。好的接口设计越早确定越好因为它决定了后续所有代码的组织方式。四、工具还缺什么下个月的路线图这 31 天里写的测试覆盖率只有 32%——这是一个让我睡不着的数字。Provider 层的单元测试还好但集成测试模拟 HTTP 超时、返回格式变化、流式中断几乎为零。下个月第一优先级就是把覆盖率推到 80%。第二个缺的是 session 持久化。现在的会话管理存在内存里关掉终端就没了。下个月打算用 SQLite rusqlite做一个轻量的会话存储保留历史对话和上下文。第三个是错误恢复。目前遇到网络错误就直接退出了体验很差。应该自动重试 3 次失败后保存未发送的消息用户下次启动时可以恢复。五、总结31 天把一个 idea 变成一个能用的 AI CLI 工具对一个来说最大的收获不是这个工具本身而是学到了怎么在混乱中建立秩序。三条月度感悟先做小闭环再做广度。让一个功能稳定工作比三个功能勉强能跑更有价值。trait 抽象是 Rust 项目的脊柱。做对了 trait 设计后续扩展是加法做错了后续重构是乘法。编译速度不是虚指标。2 秒和 20 秒的区别是你能不能保持心流的关键差异。下个月不打算加新功能了——先把测试补全然后真正用这个工具一个月让实际使用中的痛点告诉我下一步该做什么。
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 拆成 workspaceai-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) - ResultString, 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 都是吃过亏才学会的。一开始我用Boxdyn Error到处返回错误三天后就不知道一个错误到底来自网络还是 api 还是配置。改成thiserror的 enum 后错误追踪一下子清晰了。技能系统用 trait 而不是宏是因为我需要编译期的类型检查——宏虽然更灵活但在技能数量和复杂度上来后类型安全比灵活性重要。三、那些做错的决策和回头看的原因不是所有决策都对。以下是三个明显的误判第一个是过早优化流式输出。第三天我就花了一整天写 SSE 解析结果第四天发现基础的单次对话还不稳定。正确的顺序应该是先让核心路径稳定再做体验优化。第二个是配置系统做太复杂。我一开始写了 YAML TOML 环境变量三层合并逻辑还支持--config指定路径。两周后发现实际上我只用了环境变量和 TO TOMLYAML 支持从未被使用。做减法比做加法更需要勇气。第三个是插件系统延迟到第四周才动手。如果第二周就开始设计插件接口后面就不用为怎么把 git log 的功能塞进去改 12 个文件。好的接口设计越早确定越好因为它决定了后续所有代码的组织方式。四、工具还缺什么下个月的路线图这 31 天里写的测试覆盖率只有 32%——这是一个让我睡不着的数字。Provider 层的单元测试还好但集成测试模拟 HTTP 超时、返回格式变化、流式中断几乎为零。下个月第一优先级就是把覆盖率推到 80%。第二个缺的是 session 持久化。现在的会话管理存在内存里关掉终端就没了。下个月打算用 SQLite rusqlite做一个轻量的会话存储保留历史对话和上下文。第三个是错误恢复。目前遇到网络错误就直接退出了体验很差。应该自动重试 3 次失败后保存未发送的消息用户下次启动时可以恢复。五、总结31 天把一个 idea 变成一个能用的 AI CLI 工具对一个来说最大的收获不是这个工具本身而是学到了怎么在混乱中建立秩序。三条月度感悟先做小闭环再做广度。让一个功能稳定工作比三个功能勉强能跑更有价值。trait 抽象是 Rust 项目的脊柱。做对了 trait 设计后续扩展是加法做错了后续重构是乘法。编译速度不是虚指标。2 秒和 20 秒的区别是你能不能保持心流的关键差异。下个月不打算加新功能了——先把测试补全然后真正用这个工具一个月让实际使用中的痛点告诉我下一步该做什么。