用 Rust 写个 AI 运维助手:从环境配置到 GLM-4 接入
为什么选 Rust 写 AI 助手
在 AI Agent 和自动化脚本泛滥的今天,大多数工具还是 Python 写的。Python 确实快,但在需要长期运行、高并发或者对内存敏感的场景下,Rust 的优势就出来了。它没有垃圾回收的停顿,二进制文件体积小,部署起来也方便——扔到一个 Linux 服务器上就能跑,不用配一堆虚拟环境。
这个项目叫 rust-shell-assistant,核心逻辑很简单:用户输入自然语言,AI 把它翻译成 Shell 命令,用户确认后执行。听起来不复杂,但要把这件事做得稳定、安全,还得处理好网络请求、异步运行时、系统调用这些底层细节,Rust 的类型系统和所有权机制能帮上大忙。
这里用的大模型服务来自蓝耘(Lanyun)的 MAAS 平台,接的是智谱的 GLM-4.7。选它主要是因为中文指令遵循能力不错,而且接口兼容 OpenAI 标准,改个 URL 就能用,省去了适配不同厂商 SDK 的麻烦。
先把开发环境搭好
在 Linux(以 Debian/Ubuntu 为例)上搞 Rust 开发,第一步不是写代码,而是把编译链装全。Rust 编译器 rustc 最后生成二进制文件时,得调用系统的链接器。如果项目里用了涉及网络加密的库(比如 reqwest 默认带的 TLS 支持),那就离不开 OpenSSL。
安装系统依赖
很多新手直接装 rustup,结果编译时报错说找不到 openssl 或 pkg-config。这是因为 Rust 的 openssl-sys crate 是通过 FFI(外部函数接口)调用 C 语言版的 OpenSSL 库的,所以系统里得有头文件和静态库。
先更新包列表,然后装上构建 essentials 和 OpenSSL 开发包:
sudo apt update
sudo apt install curl build-essential pkg-config libssl-devbuild-essential 里包含了 GCC、Make 和 libc 开发库,这是编译任何非纯 Rust 项目的基石。libssl-dev 则提供了 OpenSSL 的头文件,缺了它,cargo build 会在链接阶段直接挂掉。
安装 Rust 工具链
官方推荐的安装方式是用 rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh这个脚本会自动检测你的 CPU 架构(x86_64 或 aarch64),下载对应的预编译二进制。装完后,它会把 $HOME/.cargo/bin 加到 PATH 里。为了让当前终端立即生效,执行:
. "$HOME/.cargo/env"顺便把这行加到 ~/.bashrc 或 ~/.zshrc 里,不然下次登录还得手动 source。
验证一下:
rustc --version
cargo --version能看到版本号就说明环境没问题了。
接入蓝耘 MAAS 平台
这个助手的“脑子”是 GLM-4.7。蓝耘的 MAAS 平台提供兼容 OpenAI 格式的 API,这意味着我们不需要引入复杂的专用 SDK,直接用通用的 HTTP 客户端发 JSON 请求就行。
- 注册与获取 Key:去蓝耘控制台注册账号,创建一个 API Key。这串字符是你的身份凭证,也是计费的依据,千万别硬编码在代码里提交到 Git。
- 确认端点:GLM-4.7 的聊天补全接口地址通常是
https://maas-api.lanyun.net/v1/chat/completions。模型路径填/maas/zhipuai/GLM-4.7。
这些信息后面都要写到环境变量里。
项目结构与依赖管理
用 Cargo 初始化项目:
cargo new rust-shell-assistant
cd rust-shell-assistant关键依赖解析
Cargo.toml 里的依赖选型直接决定了项目的健壮性。以下是几个核心 crate 的作用:
- tokio: Rust 异步生态的事实标准。我们要发 HTTP 请求、处理用户输入,这些都可能是阻塞操作,用
tokio的full特性可以拿到完整的异步运行时支持。 - reqwest: 基于 tokio 的高级 HTTP 客户端。开启
json特性后,它能自动把 Rust 结构体序列化成 JSON 发送,也能把响应反序列化回来,省去了手动拼字符串的麻烦。 - serde / serde_json: 序列化框架。通过
derive宏,给结构体加上#[derive(Serialize, Deserialize)],JSON 转换就全自动了。 - clap: 命令行参数解析。用 derive 模式定义结构体,它自动生成帮助信息、参数校验,比手写
args解析优雅得多。 - rustyline: 实现类似 Bash 的交互式输入体验,支持历史记录、光标移动。没有它,用户输错一个字得重打整行,体验极差。
- colored: 给终端输出加点颜色,区分用户输入、AI 回复和系统提示,视觉上更清晰。
- dotenv: 从
.env文件加载环境变量,符合 12-Factor App 原则,方便本地调试和部署隔离。 - anyhow / thiserror: 错误处理组合拳。
anyhow用于应用层快速传播错误,thiserror用于定义具体的错误类型。
Cargo.toml 大致长这样:
[dependencies]
tokio = { version = "1.35", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "4.4", features = ["derive"] }
colored = "2.1"
rustyline = "13.0"
anyhow = "1.0"
dotenv = "0.15"
log = "0.4"
env_logger = "0.11"核心模块实现
项目拆成几个模块:ai_client 负责跟大模型说话,shell_executor 负责执行命令,config 管配置,main 串联流程。
AI 客户端:封装 HTTP 请求
ai_client.rs 的核心是构造符合 OpenAI 格式的 JSON 请求。我们定义 ChatRequest 和 ChatResponse 结构体,映射 API 的字段。
关键点在于 Prompt 的设计。为了让 AI 只返回命令,不加废话,System Prompt 必须写得足够强硬:
let system_prompt = r#"你是一个专业的 Linux Shell 命令助手。
你的任务是将用户的自然语言描述转换为准确的 Shell 命令。
规则:
1. 只返回命令本身,不要有任何解释或额外文字
2. 如果需要多个命令,用 && 连接
3. 确保命令安全且符合最佳实践
4. 优先使用常见的 Linux 命令
5. 不要使用危险的命令(如 rm -rf / 等)
"#;然后用 reqwest 发起 POST 请求。注意设置超时时间,避免网络卡顿时程序假死。
pub async fn chat(&self, messages: Vec<Message>) -> Result<String> {
let request_body = ChatRequest {
model: self.config.model.clone(),
messages,
max_tokens: self.config.max_tokens,
temperature: self.config.temperature,
};
let mut request = self.client.post(&self.config.api_url).json(&request_body);
if let Some(api_key) = &self.config.api_key {
request = request.bearer_auth(api_key);
}
let response = request.send().await.context("发送 API 请求失败")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("API 请求失败: {} - {}", status, error_text);
}
let chat_response: ChatResponse = response.json().await.context("解析 API 响应失败")?;
chat_response.choices.first()
.map(|choice| choice.message.content.clone())
.context("API 响应中没有内容")
}Shell 执行器:安全第一
拿到 AI 返回的命令字符串后,不能直接扔给 system()。shell_executor.rs 做了两件事:安全检查和中转执行。
首先是黑名单过滤。虽然 AI 被要求不生成危险命令,但模型可能会幻觉,或者被恶意 Prompt 注入。所以在执行前,代码里硬编码了一些高危模式:
fn is_dangerous_command(&self, command: &str) -> bool {
let dangerous_patterns = vec![
"rm -rf /",
"rm -rf /*",
"mkfs",
"dd if=/dev/zero",
"> /dev/sda",
":(){ :|:& };:", // Fork bomb
"chmod -R 777 /",
];
dangerous_patterns.iter().any(|pattern| command.contains(pattern))
}如果命中任何一条,直接拒绝执行并报错。
其次是跨平台执行。Linux/macOS 用 sh -c,Windows 用 cmd /C。这里用 std::process::Command 捕获 stdout 和 stderr,而不是直接打印,这样主程序可以对结果进行格式化展示。
pub fn execute(&self, command: &str) -> Result<CommandResult> {
if self.is_dangerous_command(command) {
anyhow::bail!("检测到危险命令,拒绝执行: {}", command);
}
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(["/C", command]).output()
} else {
Command::new("sh").arg("-c").arg(command).output()
}.context("执行命令失败")?;
Ok(CommandResult::from_output(output))
}配置管理:环境变量优先
config.rs 采用分层策略。优先读环境变量,没有则用默认值。这样在本地开发时改 .env 文件即可,部署到服务器时直接注入环境变量,无需改代码。
pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok(); // 忽略加载失败,允许无 .env 文件
let api_url = env::var("AI_API_URL")
.unwrap_or_else(|_| "https://maas-api.lanyun.net/v1/chat/completions".to_string());
// ... 其他字段类似
Ok(Config { api_url, model, api_key, max_tokens, temperature, timeout_seconds })
}主程序:REPL 循环
main.rs 是入口。用 clap 解析参数,支持两种模式:
- 单次查询:
rust-shell-assistant --query "列出文件",适合脚本调用。 - 交互模式:不带参数启动,进入 REPL 循环。
交互模式里,用 rustyline 读取用户输入。除了普通查询,还加了几个特殊指令:
exit/quit: 退出。help: 显示帮助。history: 查看历史命令。explain <command>: 让 AI 解释某条命令的意思,适合学习。
主循环逻辑大致如下:
loop {
let readline = rl.readline(">> ");
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() { continue; }
if matches!(line, "exit" | "quit") { break; }
// 处理特殊指令...
// 普通查询:AI 生成命令 -> 用户确认 -> 执行
handle_query(&ai_client, &shell_executor, line, &mut history).await?;
}
Err(ReadlineError::Interrupted) => continue,
Err(ReadlineError::Eof) => break,
Err(err) => { eprintln!("Error: {:?}", err); break; }
}
}踩坑记录:编译与链接
开发过程中遇到两个典型问题,记录下来供参考。
1. OpenSSL 链接错误
初次编译时,报错 could not find openssl via pkg-config。这是因为 reqwest 默认启用 native-tls,它在 Linux 上依赖系统的 OpenSSL。解决方法前面提过:安装 pkg-config 和 libssl-dev。
如果不想装系统库,也可以在 Cargo.toml 里给 reqwest 加上 default-features = false 并启用 rustls-tls 特性,这样就用纯 Rust 实现的 TLS 库,不依赖系统 OpenSSL。但对于运维工具来说,依赖系统库通常更稳妥,因为系统库会随安全补丁更新。
2. Trait 约束与类型推导
在使用 colored 库时,遇到 the trait bound &String: Colorize is not satisfied 的错误。colored 的扩展方法主要实现在 &str 上。虽然 Rust 有 Deref Coercion,但在某些泛型上下文或引用层级较多时,自动推导会失效。
解决办法是显式调用 .as_str() 或者确保引入 use colored::Colorize; 并在正确的类型上调用。另外,对于结构体中暂时未使用的字段(如 exit_code),加上 #[allow(dead_code)] 属性可以消除编译警告,保持输出干净。
实战演示
编译发布版:
cargo build --release生成的二进制在 target/release/rust-shell-assistant。可以把它复制到 /usr/local/bin 方便全局调用。
交互模式
运行程序,输入:“查看当前目录大文件”。
AI 返回:du -ah . | sort -rh | head -n 10
用户输入 y 确认,程序执行并展示结果。整个过程不到两秒,响应速度取决于网络和模型推理速度。
解释模式
输入:“explain chmod 777”。
AI 返回:“该命令将文件权限设置为所有用户可读、可写、可执行。这通常是不安全的,建议仅用于测试环境。”
这种即时反馈对新手运维很有帮助。
成本监控
每次调用都会消耗 Token。在蓝耘控制台可以看到详细的用量统计。对于个人开发者来说,GLM-4 的性价比很高,日常运维查询的成本几乎可以忽略不计。
总结
用 Rust 写 AI 助手,最大的感受是“稳”。类型系统逼你在编译期就把错误处理想清楚,异步运行时让并发变得简单,二进制部署免去了环境配置的痛苦。虽然前期上手曲线比 Python 陡,但一旦跑通,维护成本很低。
这个工具目前还比较基础,未来可以加入更多功能:比如支持多轮对话上下文、集成更多系统监控指标、甚至通过 Function Calling 直接调用预设的工具函数,而不仅仅是生成 Shell 字符串。对于运维工程师来说,这样一个贴身的小助手,确实能省下不少查文档的时间。