Rust AI CLI 开发复盘:两周踩过的 7 个坑和对应的防范模式
2026/7/12 21:21:27 网站建设 项目流程

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 个坑的完整还原

坑 1:HTTP 客户端无超时,请求卡死 3 分钟

我最早用的是reqwest的默认配置,直接reqwest::get(url)。有次网络波动,API 服务响应很慢,终端直接卡住没有任何反馈,我一度以为是死循环。

原因reqwest默认不设超时,TCP 连接可以挂很久。

教训:任何网络请求必须显式设置连接超时和读取超时。

use reqwest::Client; use std::time::Duration; fn build_client() -> Result<Client, 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 的流式响应是 SSE(Server-Sent Events)格式,一行一个data: {...}。我最开始想当然地用serde_json::from_str直接解析整行,结果发现有些行前面有空格、有些行是data: [DONE](不是合法 JSON),还有空行。

正确做法是逐行读取、跳过空行和[DONE]标记、再解析 JSON:

use futures::StreamExt; use serde_json::Value; async fn parse_sse_stream( mut stream: impl StreamExt<Item = reqwest::Result<bytes::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) -> Result<String, Box<dyn 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 上测试发现中文路径传入后被截断。查了半天发现是终端编码设置问题,但最终决定用clapOsString参数类型,绕过编码转换。

坑 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+。因为Stringpush_str在容量不足时会重新分配并复制全部数据。

对策:用Vec<String>收集片段,最后join("")或预先reserve容量。

坑 7:API Key 硬编码在代码里差点提交到 GitHub

这个太不好意思说了——我居然在main.rs写了let api_key = "sk-xxxxxxxx";。还好提交前用cargo check的时候无意中看了一眼 diff,惊出一身冷汗。

正确做法:环境变量 +.env文件 +.gitignore

use std::env; fn load_api_key() -> Result<String, 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"]
  1. 所有外部输入均做编码检测:不假设文件、网络响应、命令行参数是 UTF-8。
  2. 所有 I/O 操作均设超时:HTTP 请求、文件读取、子进程调用都要有 timeout。
  3. 资源使用必须有上界:缓冲区大小、并发数、内存使用量都要设上限。

四、对自学者的建议:工具该有的样子

这个项目让我学到最重要的不是某个技术点,而是一种意识转变——从"能跑就行"到"能在各种极端情况下稳健运行"。自学容易陷入"写出来就完了"的心态,但真正的工程是要考虑边界情况的。

我的建议是:把你写的工具发给朋友用。他一定会在你意想不到的场景下触发你的 bug。这是最快的成长方式。

mindmap root((AI CLI 开发心得)) 编码 不假设 UTF-8 用 encoding_rs 测试 GBK、Shift-JIS 网络 超时必须设 SSE 逐行解析 重试加退避 资源 预留容量 限制缓冲区 监控内存 安全 密钥用环境变量 .env 入 .gitignore 不打印敏感信息

五、总结

这两周踩的坑看似很多,但每一个其实都是 Rust 乃至系统编程的"基础课"——超时、编码、内存管理、异步模型、密钥安全,这些东西在任何语言里都是必学的,Rust 只是让问题暴露得更早(也因此更痛苦)。

如果你也在用 Rust 写工具,希望这篇文章能帮你少走我走过的弯路。特别是自学的朋友,别怕踩坑,每踩一个坑都是一次真正理解"为什么"的机会。下次分享我打算整理 Rust 生命周期标注的心法,期待和大家交流。


欢迎在评论区分享你写 CLI 工具时踩过的坑,一起进步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询