新闻详情

新闻详情

首页 / 资讯中心 / 详情

我的 AI CLI 工具 30 天演进:从单文件脚本到多 crate 工程的完整历程

发布时间:2026/9/4 7:19:16来源:尧图网络
我的 AI CLI 工具 30 天演进:从单文件脚本到多 crate 工程的完整历程
我的 AI CLI 工具 30 天演进从单文件脚本到多 crate 工程的完整历程一、第 1 天到第 7 天一个 main.rs 打天下最早的需求极其简单在终端里输入ai 这个错误怎么修直接拿到 GPT 的回答。用reqwest发 HTTP 请求再用serde_json解析返回第一个版本就这样诞生了。// // 第 1 天的代码全部塞在 main.rs 里 // use reqwest::Client; use serde_json::Value; /// 向 OpenAI API 发送请求获取对话补全 /// prompt: 用户输入的问题 /// api_key: 从环境变量读取的 API Key async fn ask_ai(prompt: str, api_key: str) - ResultString, Boxdyn std::error::Error { let client Client::new(); // 构建请求体messages 是 OpenAI Chat API 的核心结构 let body serde_json::json!({ model: gpt-4o-mini, messages: [{role: user, content: prompt}], max_tokens: 2048 }); let resp client.post(https://api.openai.com/v1/chat/completions) .header(Authorization, format!(Bearer {}, api_key)) .json(body) .send() .await?; let json: Value resp.json().await?; // 从嵌套的 JSON 里把回答内容抠出来 let answer json[choices][0][message][content] .as_str() .unwrap_or(无响应) .to_string(); Ok(answer) } #[tokio::main] async fn main() { let prompt std::env::args().skip(1).collect::Vec_().join( ); let api_key std::env::var(OPENAI_API_KEY).expect(请设置 OPENAI_API_KEY); match ask_ai(prompt, api_key).await { Ok(answer) println!({}, answer), Err(e) eprintln!(错误: {}, e), } }这时候的代码极度丑陋没有配置管理、没有错误分类、没有会话上下文。但它的确能用。前七天我一直在加功能支持流式输出、支持多轮对话、支持替换模型参数。main.rs从 150 行膨胀到 1200 行——典型的上帝文件。二、第 8 天到第 14 天第一次分模块——能跑就行到能用就行到了第二周每次改一行代码就要重新编译整个项目 20 秒——对一个单文件项目来说这太离谱了。而且我发现一个致命问题如果想把 OpenAI 换成 Claude就要到处改代码。于是我做了第一次架构拆分提取providertrait。// // src/provider.rs — AI Provider 抽象层 // use async_trait::async_trait; /// AI 服务提供者的统一接口 /// 定义这个 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; } /// Provider 层的统一错误类型 #[derive(Debug, thiserror::Error)] pub enum ProviderError { #[error(网络请求失败: {0})] Network(#[from] reqwest::Error), #[error(API 返回错误: {0})] Api(String), #[error(配置缺失: {0})] Config(String), }拆分后目录变成了src/provider.rs— AI 抽象层src/providers/openai.rs— OpenAI 实现src/providers/claude.rs— Claude 实现后来加的src/config.rs— 配置管理src/cli.rs— 命令行参数解析编译时间降到 12 秒因为改一个 provider 不会触发其他模块重编译。但这也带来了新问题我没想清楚模块间的依赖关系导致cli.rs同时依赖了config.rs和所有provider形成了一张紊乱的依赖图。三、第 15 天到第 21 天从 lib crate 到 workspace 架构第三周是我真正学会工程化的一周。我把项目拆成了 Cargo workspaceai-cli/ ├── crates/ │ ├── ai-core/ # 核心抽象AiProvider trait、错误类型 │ ├── ai-provider-openai/ # OpenAI 适配器 │ ├── ai-provider-claude/ # Claude 适配器 │ ├── ai-config/ # 配置解析层 │ └── ai-cli/ # CLI 入口binary crate ├── Cargo.toml # workspace 根配置 └── README.md这次重构最大的收获不是看起来更高级了而是编译隔离极其明显。改一行ai-config的代码只重编译 4 个 crate 而不是全部。增量编译从 12 秒降到了 2~3 秒。而且测试变得非常独立ai-core不依赖任何外部服务测试秒过。四、第 22 天到第 30 天最后一个关卡 —— 插件系统真正让我开悟的是第四周决定做插件系统。这个 AI CLI 不只是聊天工具了我让它能执行预定义的技能比如ai 帮我查一下这个仓库的 git logagent 会自动调用 git 命令。我想到的方案是让每个技能实现一个Skilltrait在编译期通过inventorycrate 做自动注册。// // ai-core/src/skill.rs — 技能插件系统 // use async_trait::async_trait; /// 技能插件接口 /// 每个技能实现这个 trait编译时通过 inventory 自动注册 #[async_trait] pub trait Skill: Send Sync { /// 技能名称如 git-log fn name(self) - str; /// 技能描述会注入到 system prompt 中 fn description(self) - str; /// 执行技能传入用户意图返回执行结果 async fn execute(self, intent: str) - ResultString, SkillError; } /// 注册一个技能到全局 registry /// 使用 inventory::submit! 在编译时自动收集 inventory::collect!(Boxdyn Skill); /// 用宏简化技能注册 #[macro_export] macro_rules! register_skill { ($skill:expr) { inventory::submit!(Box::new($skill) as Boxdyn Skill); }; }到这里这个项目才算真正有了软件工程的味道。它不是一团能跑的代码而是一个结构清晰、扩展方便、可以长期维护的工具了。插件系统上线后踩了一个坑inventory::collect!的注册顺序是不确定的导致两个技能注册了同一个名称但执行优先级不同。CI 里全部通过生产环境运行时注册顺序变了行为完全错乱。最后用HashMapString, Boxdyn Skill替代了inventory按名称显式注册问题解决。五、总结30 天从 1 个文件到 workspace 插件系统这段经历对我这个来说是一个重要的拐点。三个最深的教训能跑和能维护之间的鸿沟比想象中大。单文件 1200 行不是不能工作但每次改代码的心智负担会指数级增长。把 trait 抽象做对是 Rust 项目最重要的设计决策。好的抽象让换模型、换后端像换积木一样简单坏的抽象会变成到处Boxdyn Any的地狱。尽早拆 crate即使项目还小。workspace 的编译隔离效果是实打实的习惯一开始就规划清楚模块边界比事后重构省太多精力。下个月我不打算再加功能了——先把测试补到 80% 覆盖率然后写一份像样的文档。如果你也在写自己的 AI 工具希望这些经历对你有用。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

刚刚 GPT-6 Astra 发布,全球最强,AGI 时代到来! 2026/9/5 3:18:29

刚刚 GPT-6 Astra 发布,全球最强,AGI 时代到来!

大家好,我是程序员鱼皮。 时间过得真快啊,距离 GPT-5 发布竟然已经过去一年多了。 昨天凌晨 3 点多,OpenAI 终于把 GPT-6 Astra 放出来了! 官方把它叫做「全世界最智能、也最对齐的模型」。而 OpenAI 总裁 Greg Brockman 在发布…

阅读更多 →
AiP650E 国产 LED 驱动芯片|2 线串口实现 4 位数码管 + 7×4 矩阵键盘完整解析 2026/9/5 3:18:29

AiP650E 国产 LED 驱动芯片|2 线串口实现 4 位数码管 + 7×4 矩阵键盘完整解析

在小家电、温控面板、测量仪表等产品开发中,经常同时需要数码管显示和矩阵按键输入。传统方案分开选用 LED 驱动芯片外加独立键盘扫描,会占用大量单片机 IO 资源,外围器件多,PCB 布线复杂。无锡中微爱芯的AiP650E是一颗高集成度二…

阅读更多 →
PS怎么把文字贴在圆柱体上?2种实用方法教程附不规则物体贴图 2026/9/5 3:18:29

PS怎么把文字贴在圆柱体上?2种实用方法教程附不规则物体贴图

一、本文解决的核心问题在做PS文字贴图时,你是否经常遇到以下困扰?文字贴到圆柱体上总是扭曲变形,透视对不上; • 手动调整锚点费时费力,效果还显得很假; • 贴图与物体表面光影不匹配,缺乏立体…

阅读更多 →
几十块的VPS值得买吗?看清AI工具部署与API转发的真实边界 2026/9/5 3:18:29

几十块的VPS值得买吗?看清AI工具部署与API转发的真实边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
技防替代人防:智能门锁如何破解民宿网约房合规监管与运营痛点 2026/9/5 3:18:29

技防替代人防:智能门锁如何破解民宿网约房合规监管与运营痛点

随着全国各省市网约房、民宿专项监管条例落地实施,共享住宿行业正式告别粗放式野蛮生长,迈入实名核验全覆盖、入住轨迹可溯源、权限管控闭环化的合规治理新阶段。区别于传统酒店集中式前台管理模式,分散式民宿、网约房存在房源点位分散、租客…

阅读更多 →
机械狗测试平台选型与落地:四足机器人研发的关键基建 2026/9/5 3:15:29

机械狗测试平台选型与落地:四足机器人研发的关键基建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞