Rust 写 AI CLI:先把流式输出和错误处理做好
Rust 写 AI CLI:先把流式输出和错误处理做好
一、AI CLI 的第一版不要贪多
用 Rust 写 AI 命令行工具时,很容易一上来就想做会话管理、插件系统、文件索引和 Agent 自动执行。实际写下来会发现,第一版最重要的是两个基础能力:请求模型并稳定输出结果,以及在失败时给用户清楚的错误信息。没有这两个能力,功能再多也只是脆弱的壳。
CLI 和网页不同。用户在终端里更关注反馈是否及时、参数是否清楚、错误是否可修复。AI 接口如果一次性等完整响应返回,长回答会让终端像卡住一样。流式输出能明显改善体验。Rust 在这类工具上很合适,因为类型系统会逼着我们把网络错误、解析错误和配置错误分清楚。
二、最小链路:参数、请求、流式输出、退出码
flowchart TD A[命令行参数] --> B[读取配置] B --> C[构造模型请求] C --> D[HTTP 流式响应] D --> E[逐块输出到终端] D --> F[错误分类] F --> G[退出码]第一版可以只支持一个子命令,例如ask "解释这段报错"。参数解析用clap,HTTP 请求用reqwest,错误封装用thiserror。不要急着做交互式 REPL,因为 REPL 会引入历史记录、中断、上下文长度和终端兼容问题。先让一次性命令可靠,再往上加。
配置也要简单。API Key 可以从环境变量读取,模型名和超时时间可以放在配置文件。不要把密钥写进命令历史里,也不要在调试日志里打印完整请求。AI 工具再小,也要从第一天尊重凭据安全。
三、代码片段:用枚举表达失败类型
下面是一个简化的错误定义。它的意义不是优雅,而是让调用方知道失败来自哪里。
use thiserror::Error; #[derive(Debug, Error)] pub enum CliError { #[error("missing api key, please set AI_API_KEY")] MissingApiKey, #[error("http request failed: {0}")] Http(#[from] reqwest::Error), #[error("invalid response format: {0}")] InvalidResponse(String), #[error("io error: {0}")] Io(#[from] std::io::Error), }如果全部用anyhow::Result也能写,但学习阶段我更建议先写清楚错误枚举。这样会被迫思考哪些错误用户能修,哪些错误需要重试,哪些错误应该打印调试信息。CLI 工具的用户体验,很大一部分来自错误信息。
流式输出时,还要记得及时 flush stdout。否则模型已经返回了 token,终端却没有马上显示。小细节会影响工具手感。
四、工程边界:超时、重试和取消
AI 接口一定要设置超时。网络请求没有超时,就等于把终端交给不确定性。可以设置连接超时和总请求超时,用户按 Ctrl+C 时也要能退出。后面接入 Tokio 后,可以用异步任务和取消信号管理长请求。
重试要谨慎。网络抖动可以重试,认证失败不能重试,参数错误不能重试,模型限流可以指数退避。不要把所有错误都包成“再试一次”。重试如果没有边界,会浪费 token,也会让用户等更久。
日志也要分级。默认输出只展示用户需要看的内容;调试模式再打印请求 ID、耗时和错误细节。终端工具不能把日志刷得比回答还多。这个道理我也是写了几版才意识到。
记得第一版上线后,有用户反馈"点了 ask 就卡住了"。排查发现是模型服务偶尔应答十秒以上,而我没有设超时。如果当时先加一个 30 秒全局超时,用户就不会觉得工具死掉了。终端工具给用户的第一印象,很多时候不来自功能有多强大,而是失败时有交代。
还有一次,reqwest 的连接池在 keep-alive 下偶发 Broken Pipe,加上 retry 逻辑后成功率从 95% 提到 99.7%。
五、总结
Rust 写 AI CLI 的第一版应聚焦最小闭环:参数解析、配置读取、模型请求、流式输出和错误分类。先把基础体验做稳,再扩展会话、插件和 Agent。终端工具不怕功能少,怕的是失败时用户不知道该怎么办。
