Rust AI CLI 工具开发:从零构建一个智能命令行助手

一、为什么用 Rust 写 AI CLI 工具
我之前用 Python 写过几个 CLI 小工具,调用 OpenAI API 做文本处理。Python 写起来快,但打包分发是个噩梦——用户要么装 Python 环境,要么用 PyInstaller 打包出 50MB 的可执行文件。而且 Python CLI 的启动速度慢,冷启动要几百毫秒,体验很差。
Rust 在 CLI 工具上有天然优势:编译成单文件二进制,无运行时依赖,冷启动在 5ms 以内。对于 AI CLI 工具来说,还有一个关键优势——Rust 的异步运行时(Tokio)在处理大量并发 HTTP 请求时性能远超 Python 的 asyncio。当你需要同时调用多个 AI API 做结果对比时,这个差距会很明显。
当然,代价是开发速度慢。我写第一个版本花了三天,同等功能的 Python 版本半天就搞定了。但编译器逼着你把错误处理做完整,反而让后续迭代更安心。
二、AI CLI 工具的架构设计:请求编排与流式输出
AI CLI 工具的核心流程是:解析命令行参数 → 构建 Prompt → 调用 AI API → 流式输出结果。关键设计决策在于请求编排和流式输出。
flowchart TB
A[CLI 入口<br/>clap 解析参数] --> B[Prompt 构建<br/>模板 + 上下文]
B --> C{请求模式}
C -->|单模型| D[直接调用<br/>单一 AI API]
C -->|多模型对比| E[并发调用<br/>Tokio::join!]
D --> F[流式响应处理<br/>SSE 解析]
E --> F
F --> G{输出模式}
G -->|终端流式| H[逐 Token 输出<br/>termcolor 高亮]
G -->|管道模式| I[完整输出<br/>stdout 管道友好]
G -->|文件输出| J[写入文件<br/>Markdown 格式]
subgraph 错误处理
K[API 限流 → 指数退避重试]
L[网络超时 → 超时配置]
M[Token 超限 → 自动截断]
end
F --> K
F --> L
B --> M
流式输出是 AI CLI 工具体验的关键。用户敲完命令后盯着空白屏幕等 10 秒是不可接受的。SSE(Server-Sent Events)协议让 API 逐 Token 返回,CLI 逐 Token 显示,用户立刻看到输出在"打字"。
三、生产级代码实现:AI CLI 工具核心模块
3.1 CLI 参数解析与 Prompt 构建
use clap::{Parser, ValueEnum};
use std::path::PathBuf;
/// AI 智能命令行助手
#[derive(Parser, Debug)]
#[command(name = "ai-cli", version, about = "AI 驱动的命令行工具")]
struct Cli {
/// 输入文本或问题
#[arg(value_name = "PROMPT")]
prompt: String,
/// AI 模型选择
#[arg(short, long, default_value = "gpt-4o-mini")]
model: String,
/// 输出模式
#[arg(short, long, default_value = "stream")]
output: OutputMode,
/// 系统提示词文件
#[arg(short, long)]
system_prompt: Option<PathBuf>,
/// 最大 Token 数
#[arg(short, long, default_value_t = 2048)]
max_tokens: u32,
/// 温度参数(0.0-2.0)
#[arg(short, long, default_value_t = 0.7)]
temperature: f32,
}
#[derive(Debug, Clone, ValueEnum)]
enum OutputMode {
/// 流式输出到终端
Stream,
/// 完整输出(适合管道)
Full,
/// 输出到文件
File,
}
/// 构建 API 请求体
fn build_request_body(
prompt: &str,
model: &str,
system_prompt: Option<&str>,
max_tokens: u32,
temperature: f32,
stream: bool,
) -> serde_json::Value {
let mut messages = Vec::new();
// 系统提示词
// 为什么把系统提示词放最前面:OpenAI
// 的 Chat API 按消息顺序处理,
// 系统提示词在最前面能确保模型
// 优先遵循指令
if let Some(sys) = system_prompt {
messages.push(serde_json::json!({
"role": "system",
"content": sys
}));
}
messages.push(serde_json::json!({
"role": "user",
"content": prompt
}));
serde_json::json!({
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
"stream": stream
})
}
3.2 流式响应处理与重试机制
use reqwest::Client;
use tokio_stream::StreamExt;
use std::time::Duration;
/// AI API 客户端
struct AiClient {
client: Client,
api_key: String,
base_url: String,
max_retries: u32,
}
impl AiClient {
fn new(api_key: String, base_url: String) -> Self {
// 为什么设置超时为 60 秒:AI API 的
// 首 Token 延迟通常在 1-5 秒,
// 但长文本生成可能持续 30 秒以上;
// 60 秒足够覆盖大多数场景
let client = Client::builder()
.timeout(Duration::from_secs(60))
.connect_timeout(Duration::from_secs(10))
.build()
.expect("HTTP 客户端创建失败");
Self {
client,
api_key,
base_url,
max_retries: 3,
}
}
/// 流式调用 AI API
async fn stream_chat(
&self,
body: &serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
let mut retry_count = 0;
loop {
match self.try_stream(body).await {
Ok(()) => return Ok(()),
Err(e) => {
// 只对可重试错误进行重试
// 为什么区分可重试错误:429 限流
// 和 5xx 服务端错误可以重试,
// 4xx 客户端错误重试无意义
if !self.is_retryable(&e) {
return Err(e);
}
retry_count += 1;
if retry_count > self.max_retries {
return Err(e);
}
// 指数退避:1s, 2s, 4s
// 为什么用指数退避而非固定间隔:
// 限流通常是滑动窗口策略,
// 固定间隔可能持续撞窗口;
// 指数退避给服务端恢复时间
let delay = Duration::from_secs(
2_u64.pow(retry_count - 1));
eprintln!(
"请求失败,{}秒后重试({}/{}): {}",
delay.as_secs(),
retry_count,
self.max_retries,
e
);
tokio::time::sleep(delay).await;
}
}
}
}
/// 尝试流式请求
async fn try_stream(
&self,
body: &serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
let response = self.client
.post(format!("{}/chat/completions",
self.base_url))
.header("Authorization",
format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await?;
return Err(format!(
"API 错误 {}: {}", status, error_text
).into());
}
// 解析 SSE 流
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
let text = String::from_utf8_lossy(&chunk);
for line in text.lines() {
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
println!(); // 换行
return Ok(());
}
if let Ok(parsed) =
serde_json::from_str::<serde_json::Value>(data)
{
if let Some(content) = parsed
.get("choices")
.and_then(|c| c.get(0))
.and_then(|c| c.get("delta"))
.and_then(|d| d.get("content"))
.and_then(|c| c.as_str())
{
print!("{}", content);
// 为什么用 flush:stdout
// 默认是行缓冲,不 flush
// 的话 Token 不会立即显示
use std::io::Write;
std::io::stdout().flush()?;
}
}
}
}
}
Ok(())
}
/// 判断错误是否可重试
fn is_retryable(
&self,
error: &Box<dyn std::error::Error>,
) -> bool {
let msg = error.to_string();
// 429 限流、5xx 服务端错误、网络超时
msg.contains("429")
|| msg.contains("500")
|| msg.contains("502")
|| msg.contains("503")
|| msg.contains("timeout")
}
}
3.3 main 函数与运行时初始化
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
// 从环境变量读取 API Key
// 为什么用环境变量而非命令行参数:
// API Key 是敏感信息,命令行参数
// 会出现在进程列表和 Shell 历史中,
// 环境变量更安全
let api_key = std::env::var("OPENAI_API_KEY")
.map_err(|_| "请设置 OPENAI_API_KEY 环境变量")?;
let base_url = std::env::var("OPENAI_BASE_URL")
.unwrap_or_else(|_| "https://api.openai.com/v1"
.to_string());
// 读取系统提示词
let system_prompt = cli.system_prompt
.as_ref()
.map(|path| std::fs::read_to_string(path))
.transpose()?
.map(|s| s.trim().to_string());
// 构建请求体
let stream = matches!(cli.output, OutputMode::Stream);
let body = build_request_body(
&cli.prompt,
&cli.model,
system_prompt.as_deref(),
cli.max_tokens,
cli.temperature,
stream,
);
// 发起请求
let client = AiClient::new(api_key, base_url);
client.stream_chat(&body).await?;
Ok(())
}
四、Rust AI CLI 工具的边界:什么时候不该用 Rust
快速原型阶段:如果你只是想验证一个 AI 工具的想法,Python + Click 三十分钟就能跑起来。Rust 的编译时间和类型系统在原型阶段是负担,不是保护。
重度依赖 AI 生态:LangChain、LlamaIndex 这些框架只有 Python 和 TypeScript 版本。如果你的工具需要复杂的 Agent 编排、RAG 管线,用 Rust 需要从头实现这些组件,投入产出比很低。
团队 Rust 经验不足:Rust 的学习曲线陡峭,如果团队里没人写过 Rust,用 Rust 写 AI 工具的维护成本会很高。一个编译错误卡两小时是常态——至少我刚开始学的时候是这样。
适合用 Rust 的场景:需要分发的单文件工具、对启动速度敏感的场景、需要高并发 API 调用的场景、需要系统级能力(文件监控、进程管理)的场景。
五、总结
用 Rust 开发 AI CLI 工具的核心收益是分发简单(单文件二进制)和性能好(冷启动快、并发强)。核心成本是开发速度慢和 AI 生态弱。我的建议是:如果工具需要分发给其他人用,Rust 值得投入;如果只是自己用,Python 更快。流式输出是体验的关键,SSE 解析和逐 Token 打印的实现并不复杂,但 flush 和错误处理容易踩坑。重试机制用指数退避,API Key 用环境变量,这两点是安全性和可靠性的底线。
3万+

被折叠的 条评论
为什么被折叠?



