Rust AI CLI 开发复盘:两周踩过的 7 个坑和对应的防范模式

📅 2026/7/12 21:21:32
Rust AI CLI 开发复盘:两周踩过的 7 个坑和对应的防范模式
Rust AI CLI 开发复盘两周踩过的 7 个坑和对应的防范模式一、这个项目的由来我为什么用 Rust 写 AI CLI这个 AI CLI 项目最开始的需求很简单在终端里直接调用 OpenAI 兼容的 API读取本地文件作为上下文然后把 AI 的回答直接输出到终端或者写回文件。听起来不复杂对吧Python 写大概半天就能跑。但我偏要用 Rust 来写一方面想练手另一方面也是被 Rust 的单文件编译能力吸引——编译完就是一个二进制文件拷到哪都能跑。结果呢两周时间踩了 7 个比较典型的坑。这篇文章就当一次完整复盘分享踩坑经历和对应的预防方法。flowchart LR A[需求分析\n想用Rust写AI CLI] -- B[原型开发\n快速搭出demo] B -- C[踩坑阶段\n网络错误/内存暴涨\n/序列化bug/异步混乱] C -- D[重构\n引入错误处理模式\n/流式解析/资源管理] D -- E[可发布版本\n单文件二进制\n/支持多平台]二、踩坑实录7 个坑的完整还原坑 1HTTP 客户端无超时请求卡死 3 分钟我最早用的是reqwest的默认配置直接reqwest::get(url)。有次网络波动API 服务响应很慢终端直接卡住没有任何反馈我一度以为是死循环。原因reqwest默认不设超时TCP 连接可以挂很久。教训任何网络请求必须显式设置连接超时和读取超时。use reqwest::Client; use std::time::Duration; fn build_client() - ResultClient, reqwest::Error { // 构建带超时配置的 HTTP 客户端 Client::builder() // TCP 连接最多等 10 秒 .connect_timeout(Duration::from_secs(10)) // 整个请求最多等 60 秒AI API 响应可能比较慢 .timeout(Duration::from_secs(60)) // 连接池空闲超时 .pool_idle_timeout(Duration::from_secs(90)) .build() }坑 2流式响应解析用了错误的解析方式AI API 的流式响应是 SSEServer-Sent Events格式一行一个data: {...}。我最开始想当然地用serde_json::from_str直接解析整行结果发现有些行前面有空格、有些行是data: [DONE]不是合法 JSON还有空行。正确做法是逐行读取、跳过空行和[DONE]标记、再解析 JSONuse futures::StreamExt; use serde_json::Value; async fn parse_sse_stream( mut stream: impl StreamExtItem reqwest::Resultbytes::Bytes Unpin, ) { while let Some(chunk) stream.next().await { let chunk match chunk { Ok(c) c, Err(e) { eprintln!(读取流数据失败: {}, e); break; } }; let text String::from_utf8_lossy(chunk); for line in text.lines() { let line line.trim(); // 去除前后空格 if line.is_empty() { continue; // 跳过空行 } // SSE 格式以 data: 开头 let data match line.strip_prefix(data: ) { Some(d) d, None continue, }; // 流结束标记不是 JSON if data [DONE] { println!(\n[AI 响应完成]); return; } // 解析 JSON 并取出文本内容 if let Ok(json) serde_json::from_str::Value(data) { if let Some(content) json[choices][0][delta][content].as_str() { print!({}, content); } } } } }坑 3文件读取时忘记处理非 UTF-8 编码在读取用户指定的本地文件作为 AI 上下文时我用std::fs::read_to_string结果用户给了一个 GBK 编码的 Windows 批处理文件程序直接 panic。修正方案使用encoding_rscrate 做编码检测和转换。use encoding_rs::*; use std::fs; fn read_file_auto_encoding(path: str) - ResultString, Boxdyn std::error::Error { // 先按字节读取不做 UTF-8 假设 let bytes fs::read(path)?; // 尝试 UTF-8绝大多数情况 if let Ok(s) String::from_utf8(bytes.clone()) { return Ok(s); } // UTF-8 失败用 encoding_rs 检测编码 // GBK 是中文 Windows 环境最常遇到的非 UTF-8 编码 let (cow, encoding, _had_errors) GBK.decode(bytes); if !encoding.is_utf8() { eprintln!(注意文件编码为 {}已自动转换, encoding.name()); } Ok(cow.into_owned()) }坑 4命令行参数解析不支持中文路径用clap处理命令行参数时我在 Windows 上测试发现中文路径传入后被截断。查了半天发现是终端编码设置问题但最终决定用clap的OsString参数类型绕过编码转换。坑 5异步运行时混用导致 panic我在main里用了tokio::main但在某个函数里又block_on了一个嵌套 runtime结果 panic 说 Cannot start a runtime from within a runtime。这是新手常见的异步混淆——在已有 tokio 上下文中又新建 runtime。修正统一使用async/await不要混用block_on。坑 6无节制的字符串拼接导致内存暴涨流式输出时我把每次 AI 返回的 delta 内容追加到一个String上。处理一个 10 万字的文档时内存从几十 MB 飙到 500MB。因为String的push_str在容量不足时会重新分配并复制全部数据。对策用VecString收集片段最后join()或预先reserve容量。坑 7API Key 硬编码在代码里差点提交到 GitHub这个太不好意思说了——我居然在main.rs写了let api_key sk-xxxxxxxx;。还好提交前用cargo check的时候无意中看了一眼 diff惊出一身冷汗。正确做法环境变量 .env文件 .gitignoreuse std::env; fn load_api_key() - ResultString, String { // 优先读环境变量不存在时读 .env 文件 env::var(OPENAI_API_KEY) .or_else(|_| { // 加载 .env 文件开发环境用 dotenvy::dotenv().ok(); env::var(OPENAI_API_KEY) }) .map_err(|_| 未找到 OPENAI_API_KEY请设置环境变量或 .env 文件.into()) }三、防范模式总结从踩坑中提炼的三条原则flowchart TD A[外部输入] -- B{编码假设检查} B --|已知编码| C[按预期处理] B --|未知编码| D[自动检测 降级] E[网络调用] -- F{超时设置} F --|已设置| G[正常请求] F --|未设置| H[阻断必须设置超时] I[资源使用] -- J{有上界吗} J --|有| K[安全] J --|无| L[加限制pre-allocate / buffer-cap]所有外部输入均做编码检测不假设文件、网络响应、命令行参数是 UTF-8。所有 I/O 操作均设超时HTTP 请求、文件读取、子进程调用都要有 timeout。资源使用必须有上界缓冲区大小、并发数、内存使用量都要设上限。四、对自学者的建议工具该有的样子这个项目让我学到最重要的不是某个技术点而是一种意识转变——从能跑就行到能在各种极端情况下稳健运行。自学容易陷入写出来就完了的心态但真正的工程是要考虑边界情况的。我的建议是把你写的工具发给朋友用。他一定会在你意想不到的场景下触发你的 bug。这是最快的成长方式。mindmap root((AI CLI 开发心得)) 编码 不假设 UTF-8 用 encoding_rs 测试 GBK、Shift-JIS 网络 超时必须设 SSE 逐行解析 重试加退避 资源 预留容量 限制缓冲区 监控内存 安全 密钥用环境变量 .env 入 .gitignore 不打印敏感信息五、总结这两周踩的坑看似很多但每一个其实都是 Rust 乃至系统编程的基础课——超时、编码、内存管理、异步模型、密钥安全这些东西在任何语言里都是必学的Rust 只是让问题暴露得更早也因此更痛苦。如果你也在用 Rust 写工具希望这篇文章能帮你少走我走过的弯路。特别是自学的朋友别怕踩坑每踩一个坑都是一次真正理解为什么的机会。下次分享我打算整理 Rust 生命周期标注的心法期待和大家交流。欢迎在评论区分享你写 CLI 工具时踩过的坑一起进步。