我的 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 工具希望这些经历对你有用。

相关新闻

基于Qwen3-VL和Dify的电商多模态检索系统实践

基于Qwen3-VL和Dify的电商多模态检索系统实践

1. 项目背景与核心价值 最近在帮一个跨境电商客户搭建商品图文检索系统时,发现传统的关键词匹配方案存在明显局限:当用户搜索"适合海边度假的碎花连衣裙"时,系统无法理解"海边度假"这个场景与"碎花"这个视觉特…

2026/9/29 1:33:58 阅读更多 →
深入解析Sinc3滤波器:高精度ADC中的噪声抑制与工频干扰消除

深入解析Sinc3滤波器:高精度ADC中的噪声抑制与工频干扰消除

1. Sinc3滤波器:高精度ADC中的噪声“清道夫” 在精密测量领域,我们常常需要从微弱的传感器信号中,准确无误地提取出我们真正关心的物理量。无论是热电偶的毫伏级温差电压,还是压力桥式传感器的微小电阻变化,这些信号都…

2026/9/29 8:03:35 阅读更多 →
Dify:可视化低代码平台,快速构建AI应用与工作流

Dify:可视化低代码平台,快速构建AI应用与工作流

这次我们来看一个能让你用拖拽方式搭建 AI 应用的开源平台——Dify。它由国内团队开发,核心目标是把大语言模型(LLM)的应用开发门槛降到最低,让你不用写复杂的代码,就能快速构建出具备对话、知识库、工作流等能力的 AI 应用。对于想快速验证 AI 想法、为团队内部搭建智能工…

2026/9/29 8:49:02 阅读更多 →

最新新闻

企业级Agent落地的避坑指南:从框架到评估的工程实践

企业级Agent落地的避坑指南:从框架到评估的工程实践

做Agent的同学应该都有同感:Demo和Demo之间,隔着一整个太平洋。我这两年看过太多团队,演示的时候一切完美,一问到生产环境,要么任务准确率断崖式下跌,要么工具调用乱成一锅粥,要么账单飚得比股价…

2026/9/30 9:00:36 阅读更多 →
C#上位机与雅马哈机器人TCP通讯:Socket直连与BTP协议实战指南

C#上位机与雅马哈机器人TCP通讯:Socket直连与BTP协议实战指南

简介:面向工业自动化领域的机器人调试与上位机开发人员,这份Word文档聚焦雅马哈机器人与上位机之间的TCP/IP网络通讯配置与编程,内容覆盖控制器IP地址、通信对象GP0、伺服模式、目标端口及换行符等基础参数设置,并给出触发拍照、接…

2026/9/30 9:00:36 阅读更多 →
用 Python 手写一个命令行待办事项工具:终端效率工作流实践

用 Python 手写一个命令行待办事项工具:终端效率工作流实践

我每天的工作流基本是在终端里完成的:打开终端、进目录、跑构建、看日志、提交代码。陆陆续续用过好几款待办事项软件,桌面端的、网页端的、手机同步的,最后都因为“切换成本”放弃了。真正让我坚持用小半年的,是一个我自己写的基…

2026/9/30 9:00:36 阅读更多 →
大模型推理加速实战:TensorRT与vLLM工程化落地指南

大模型推理加速实战:TensorRT与vLLM工程化落地指南

1. 项目概述:Model-Optimizer不是工具名,而是一类工程实践的统称“Model-Optimizer”这个标题乍看像某个开源项目或商业软件的代号,但结合NVIDIA、TensorRT-LLM、vLLM、PT文件转换TensorRT等热搜词,它实际指向的是大模型推理服务落…

2026/9/30 9:00:36 阅读更多 →
Jev模型实战指南:从API接入到本地部署全流程记录

Jev模型实战指南:从API接入到本地部署全流程记录

Jev 这阵子刷屏刷得厉害,朋友圈和各大技术群都在讨论,有人拿它接 Codex 做编码助手,有人用它搭个人知识库,还有人在低显存的老显卡上跑出了不错的生成效果。我也花了一整个周末,把申请、部署、实测完整走了一遍&#x…

2026/9/30 9:00:36 阅读更多 →
UltraTex:面向工业级3D管线的显存优化型2K纹理生成引擎

UltraTex:面向工业级3D管线的显存优化型2K纹理生成引擎

1. 这不是“又一个纹理生成工具”,而是3D内容生产链路上的显存破壁者最近在几个工业级3D资产管线里反复验证UltraTex,发现它真正解决的从来不是“能不能生成2K纹理”这个表面问题——而是当你的Blender材质球刚拖进Substance Painter、Unreal Engine 5.3…

2026/9/30 8:59:33 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 8:16:59 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/29 8:24:48 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →