系统工具开发 7 月复盘从编码到发布的完整 check list 和关键经验一、系统工具开发的全流程一张 check list 图很多人包括我最初觉得工具开发就是写代码 cargo build。但实际完整的流程比这长得多二、编码与构建阶段从写代码到生成可发布二进制编码阶段不是在写 demo是在做产品编码阶段最容易犯的错是以功能实现作为唯一目标。但系统工具和普通函数的本质区别在于系统工具的出入口是用户而不是另一个函数。所以错误处理、日志、诊断信息和功能本身一样重要。// // 编码阶段的关键不是能跑是任何情况下都有尊严 // use clap::Parser; use log::{info, error, debug}; /// AI CLI 工具的完整入口 #[derive(Parser)] #[command( name ai, about 终端 AI 助手 — 在命令行里直接对话 AI, version env!(CARGO_PKG_VERSION), // 为每个错误场景准备清晰的提示信息 after_help 示例:\n ai \这段代码怎么优化\\n ai --model claude \解释这个算法\ )] struct Cli { #[arg(required true, help 向 AI 提出的问题)] prompt: VecString, #[arg(short, long, default_value gpt-4o-mini, help 使用的 AI 模型)] model: String, #[arg(long, default_value_t false, help 启用流式输出打字机效果)] stream: bool, #[arg(long, default_value_t false, help 显示详细调试信息)] verbose: bool, } fn main() { // 第一步根据用户传入的 verbose 参数配置日志等级 // debug 模式下输出更多诊断信息正常模式只输出关键信息 let cli Cli::parse(); if cli.verbose { env_logger::Builder::new() .filter_level(log::LevelFilter::Debug) .init(); } else { env_logger::init(); } // 第二步在实际执行前做所有可检查的校验 if cli.model.trim().is_empty() { error!(模型名不能为空使用默认值可能会有问题); std::process::exit(1); } info!(启动 AI CLI模型: {}, 流式: {}, cli.model, cli.stream); // ... 后续逻辑 }7 月我在编码阶段踩的最大的坑就是没有从第一天就加入结构化日志。前两周的输出全靠println!和eprintln!出了问题根本没办法追溯。明明执行了为什么没输出——如果能看 debug 日志这个问题两分钟就能定位。构建阶段release profile 不是加个 --release 就行Rust 的 release 构建默认已经做了 LTO 之外的优化但对于系统工具来说还可以做得更多# # Cargo.toml — 发布用的 profile 配置 # 这些配置能在不损代码质量的情况下显著减小二进制体积 # [profile.release] # 链接时优化Link Time Optimization跨 crate 做内联和死代码消除 lto true # 优化等级3 最激进的优化编译时间会增加 30~50%但运行快 5~10% opt-level 3 # 代码生成单元1 单个单元编译更慢但优化更充分 codegen-units 1 # panic 策略abort 发生 panic 时直接终止不展开栈 # 适合大部分系统工具体积小、速度快 panic abort # strip 符号表生成更小的二进制 strip symbols7 月我的 AI CLI 工具不加这些配置时 release 二进制是 8.2MB加完之后是 3.6MB。不是巨大的差别但对于一个需要cargo install下载的工具来说3.6MB 和 8.2MB 的下载体验是完全不同的。跨平台编译是另一个花了我一整天的问题。Rust 理论上支持交叉编译但实际上openssl-sys这样的 C 依赖在交叉编译时容易出问题。我的解决方案是尽量使用纯 Rust 的实现如rustls替代openssl然后在 CI 里用不同平台的 runner 做原生编译。三、配置阶段三种配置源的优先级设计系统工具的配置通常有三个来源命令行参数、环境变量、配置文件。这三个来源的优先级必须明确且一致。优先级原则命令行 环境变量 配置文件 默认值。这样用户临时切换模型--model claude不需要改配置CI/CD 脚本可以用环境变量覆盖日常使用保持配置文件。// // 配置合并命令行 → 环境变量 → 配置文件 → 默认值 // use serde::Deserialize; /// 所有可能的配置来源合并后的最终配置 #[derive(Debug, Deserialize)] struct AppConfig { api_base_url: String, api_key: String, default_model: String, timeout_secs: u64, } impl AppConfig { /// 按优先级合并所有配置来源 fn merge( cli_args: Cli, config_file: OptionConfigFile, env_vars: HashMapString, String, ) - Self { // 从低到高逐层覆盖 let mut config AppConfig::default(); // 第一层默认值已经通过 Default trait 设置 // 第二层配置文件比默认值高 if let Some(file) config_file { if let Some(url) file.api_base_url { config.api_base_url url; } if let Some(model) file.default_model { config.default_model model; } } // 第三层环境变量比配置文件高 if let Some(key) env_vars.get(AI_API_KEY) { config.api_key key.clone(); } if let Some(url) env_vars.get(AI_API_BASE_URL) { config.api_base_url url.clone(); } // 第四层命令行参数最高优先级 if !cli_args.model.is_empty() { config.default_model cli_args.model.clone(); } config } }四、发布和文档阶段让用户能在 5 分钟内用起来7 月我在发布上犯了两个大错。第一个是发了一个没有 README 里写安装步骤的版本——用户 cargo install 之后不知道怎么用。第二个是 CHANGELOG 拖了两周才补上。一个系统工具的可用性标准是一个完全不了解这个项目的人能在 5 分钟内从零到成功运行第一次。这意味着 README 要包含一句话说清楚工具是做什么的三行命令就能安装第一个示例必须跑得通常见报错的排查方法cargo install是最简单的安装方式但对非 Rust 用户来说需要先装 Rust。为此可以考虑提供cargo-binstall预编译二进制或 Homebrew formula 作为备选方案。五、总结7 月的系统工具开发让我意识到一件事写代码只是系统工具开发的 30%。剩下 70% 是错误处理、日志、配置、测试、构建优化、文档、发布——这些东西不会让工具更快但会让工具能被别人用。三条月度 check list 精华编码阶段错误处理 日志 版本号缺一不可。不要等发布前才加日志——那时已经忘了哪里需要加。构建阶段lto strip 把体积压下来跨平台编译用 CI 而不是本地交叉编译。发布阶段README 的第一段 第一个示例 安装命令这三样东西决定了 90% 的用户会不会用。8 月的目标是走通 Homebrew 发布流程让非 Rust 用户也能直接brew install ai-cli。这会带来一批新的用户和使用反馈那时候再迭代就更有方向了。