AI 驱动的 CLI 工具开发:用 Rust 构建智能命令行 Agent

一、命令行工具不该只是"命令的集合",它应该理解你的意图
我之前用 Python 写过不少 CLI 工具,argparse 一套、subcommand 一堆,最后变成一个"命令字典"——你得记住每个子命令的参数和顺序。后来接触了 Rust 的 clap 和大模型 API,突然想到:如果 CLI 工具能理解自然语言呢?不用记命令,直接说"帮我找出最近 7 天修改过的 .rs 文件",工具自己解析意图、选择子命令、填充参数。
这不是科幻。Rust 的强类型系统和错误处理让 CLI 工具的骨架很稳,加上大模型的自然语言理解能力,就能构建一个"听得懂人话"的命令行 Agent。这篇文章记录我从零开始踩坑的全过程。
二、智能 CLI Agent 的架构设计
flowchart TB
A[用户输入<br/>自然语言或命令] --> B[输入解析层]
B --> B1[传统解析<br/>clap 子命令]
B --> B2[NLU 解析<br/>大模型意图识别]
B1 --> C[意图路由]
B2 --> C
C --> C2[文件搜索意图]
C --> C3[代码分析意图]
C --> C4[系统操作意图]
C --> C5[未知意图<br/>回退到大模型]
C2 --> D[工具执行层]
C3 --> D
C4 --> D
C5 --> D
D --> D1[文件系统操作<br/>walkdir + regex]
D --> D2[代码解析<br/>tree-sitter]
D --> D3[Shell 命令<br/>安全沙箱执行]
D1 --> E[结果格式化<br/>rich 终端输出]
D2 --> E
D3 --> E
E --> F[反馈学习<br/>记录用户修正]
style B2 fill:#e3f2fd
style C5 fill:#fff3e0
style F fill:#e8f5e9
智能 CLI Agent 的三层架构:输入解析层(同时支持传统命令和自然语言)、意图路由层(将解析结果映射到具体工具)、工具执行层(安全地执行操作并格式化输出)。关键设计是"双通道输入"——既保留传统 CLI 的精确性,又支持自然语言的便利性。
三、代码实现与分析
3.1 项目骨架与依赖
# Cargo.toml
[package]
name = "ai-cli-agent"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
walkdir = "2"
regex = "1"
colored = "2"
indicatif = "0.17"
anyhow = "1"
3.2 双通道输入解析
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// AI 驱动的智能命令行工具
#[derive(Parser, Debug)]
#[command(name = "ai", about = "智能命令行 Agent")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
/// 自然语言输入(当不使用子命令时)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
natural_input: Vec<String>,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// 搜索文件
Search {
/// 搜索模式(支持正则)
pattern: String,
/// 搜索目录
#[arg(short, long, default_value = ".")]
path: PathBuf,
/// 文件类型过滤
#[arg(short, long)]
file_type: Option<String>,
},
/// 分析代码结构
Analyze {
/// 目标路径
path: PathBuf,
},
/// 查看系统信息
Info,
}
/// 大模型返回的意图解析结果
#[derive(Debug, Serialize, Deserialize)]
struct ParsedIntent {
intent: String,
parameters: serde_json::Value,
confidence: f64,
}
/// NLU 解析器:调用大模型解析自然语言
struct NluParser {
client: reqwest::Client,
api_url: String,
api_key: String,
}
impl NluParser {
fn new(api_key: String) -> Self {
Self {
client: reqwest::Client::new(),
api_url: "https://api.openai.com/v1/chat/completions".to_string(),
api_key,
}
}
async fn parse(&self, input: &str) -> anyhow::Result<ParsedIntent> {
let system_prompt = r#"
你是一个命令行工具的意图解析器。将用户的自然语言输入解析为结构化意图。
支持的意图:
- search: 搜索文件,参数 { pattern, path, file_type }
- analyze: 分析代码结构,参数 { path }
- info: 查看系统信息,参数 {}
- unknown: 无法识别的意图
返回 JSON 格式:{ "intent": "...", "parameters": {...}, "confidence": 0.0-1.0 }
"#;
let body = serde_json::json!({
"model": "gpt-4o-mini",
"messages": [
{ "role": "system", "content": system_prompt },
{ "role": "user", "content": input }
],
"temperature": 0.1,
"max_tokens": 256,
});
let response = self.client
.post(&self.api_url)
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
.send()
.await?;
let resp_json: serde_json::Value = response.json().await?;
let content = resp_json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("{}");
let parsed: ParsedIntent = serde_json::from_str(content)
.unwrap_or(ParsedIntent {
intent: "unknown".to_string(),
parameters: serde_json::Value::Null,
confidence: 0.0,
});
Ok(parsed)
}
}
3.3 工具执行层
use colored::*;
use walkdir::WalkDir;
use regex::Regex;
use std::time::Instant;
/// 文件搜索工具
struct FileSearcher;
impl FileSearcher {
/// 按模式搜索文件名
fn search_by_name(
pattern: &str,
root: &PathBuf,
file_type: Option<&str>,
) -> Vec<PathBuf> {
let regex = Regex::new(pattern).unwrap_or_else(|_| {
Regex::new(®ex::escape(pattern)).unwrap()
});
let start = Instant::now();
let mut results = Vec::new();
for entry in WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
// 文件类型过滤
if let Some(ft) = file_type {
if let Some(ext) = path.extension() {
if ext != ft {
continue;
}
} else {
continue;
}
}
// 文件名匹配
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if regex.is_match(name) {
results.push(path.to_path_buf());
}
}
}
let elapsed = start.elapsed();
println!(
"{} 找到 {} 个文件(耗时 {:?})",
"✓".green(),
results.len().to_string().yellow(),
elapsed,
);
results
}
}
/// 主入口
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
// 双通道路由:优先处理子命令,否则走 NLU
match cli.command {
Some(Commands::Search { pattern, path, file_type }) => {
let results = FileSearcher::search_by_name(
&pattern, &path, file_type.as_deref(),
);
for path in &results {
println!(" {}", path.display());
}
}
Some(Commands::Analyze { path }) => {
println!("{} 分析 {}", "→".blue(), path.display());
// 代码分析逻辑
}
Some(Commands::Info) => {
println!("{} 系统信息", "→".blue());
println!(" OS: {}", std::env::consts::OS);
println!(" Arch: {}", std::env::consts::ARCH);
}
None if !cli.natural_input.is_empty() => {
// 自然语言通道
let input = cli.natural_input.join(" ");
println!("{} 理解中: {}", "🤖".to_string(), input.cyan());
let api_key = std::env::var("OPENAI_API_KEY")
.map_err(|_| anyhow::anyhow!("请设置 OPENAI_API_KEY 环境变量"))?;
let parser = NluParser::new(api_key);
let intent = parser.parse(&input).await?;
println!(
" 意图: {} (置信度: {:.0%})",
intent.intent.green(),
intent.confidence,
);
match intent.intent.as_str() {
"search" => {
let pattern = intent.parameters["pattern"]
.as_str().unwrap_or("*");
let path = intent.parameters["path"]
.as_str()
.map(PathBuf::from)
.unwrap_or(PathBuf::from("."));
let file_type = intent.parameters["file_type"]
.as_str();
let results = FileSearcher::search_by_name(
pattern, &path, file_type,
);
for p in &results {
println!(" {}", p.display());
}
}
"unknown" => {
println!(
"{} 抱歉,我没理解你的意思。试试: ai search <pattern>",
"✗".red()
);
}
_ => {
println!("{} 暂不支持该意图", "✗".red());
}
}
}
None => {
println!("{} 使用 ai <命令> 或 ai <自然语言>", "提示:".yellow());
}
}
Ok(())
}
四、踩坑记录与架构权衡
NLU 延迟问题:大模型 API 调用通常需要 1-3 秒,对 CLI 工具来说太慢了。我的解决方案是:高频命令(search、info)直接用 clap 解析不走 NLU,只有"说不清楚"的输入才走 NLU。另外,可以缓存相似输入的解析结果,减少 API 调用。
安全沙箱的必要性:如果 NLU 解析出"删除所有 .tmp 文件"这样的意图,直接执行太危险。我的做法是:所有写操作(删除、移动、修改)都需要用户确认,且限制可操作的目录范围。Rust 的类型系统在这里帮了大忙——ReadOnlyAction 和 WriteAction 是不同的 trait,编译期就能区分。
离线可用性:NLU 依赖大模型 API,断网就废了。我的折中方案是:离线时只支持传统命令模式,NLU 通道自动禁用并给出提示。长期方案是用本地小模型(如 GGUF 量化的 Llama 3)做 NLU,但 4GB 模型的推理速度在 CPU 上还是太慢。
成本控制:每次 NLU 调用消耗约 200 tokens,按 GPT-4o-mini 的价格约 $0.00003/次。个人用没问题,但如果做成团队工具,每天几百次调用就需要考虑成本。可以用更小的模型(如 GPT-4o-mini)做意图分类,只在需要时才调用大模型。
五、总结
用 Rust 构建智能 CLI Agent 的核心思路是"双通道输入":保留传统 CLI 的精确性,增加 NLU 的便利性。本文的关键实践为:用 clap 处理结构化命令、用大模型 API 解析自然语言意图、用 Rust 类型系统区分读写操作保证安全、用缓存和降级策略控制延迟和成本。这不是一个"成熟方案",而是一个学习过程中的记录——NLU 的延迟和安全沙箱是当前最大的挑战,后续会尝试本地模型和更严格的权限控制。
222

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



