Rust AI 运维助手:让 agent 读懂系统日志并给出排障建议的工程方案
Rust AI 运维助手:让 agent 读懂系统日志并给出排障建议的工程方案
一、半夜被叫醒的瞬间,我决定做一个能自己排障的 agent
做过后端或者运维的兄弟应该都有这种体验:凌晨两三点,手机亮了,你一看——生产环境磁盘使用率 95%。迷迷糊糊爬起来,SSH 连到服务器,du -sh /* | sort -rh、journalctl -u nginx --since "1 hour ago"、一通排查,最后发现是某个服务的日志轮转没配好,access.log 写了 80G。
这个流程里,90% 的操作是有规律的。读日志、找关键词、根据经验匹配到已知的故障模式,然后再给出修复命令。那为什么不让程序来做这些呢?当然可以,但传统做法是写一堆 if-else 匹配规则,维护起来噩梦一样。所以我把思路转向了 LLM:让 AI 来理解日志、分析根因、给出建议。
上图是我设计的完整流程。核心思路不是让 AI 替代运维,而是做"第一道过滤"——把那些有规律可循的故障自动处理掉,真正的疑难杂症才转到人工。
二、日志采集与上下文构建 —— agent 的"眼睛"
agent 要读懂日志,首先得有高质量的上下文。直接扔 500MB 的日志文件给 LLM 是不现实的,必须先做采集和筛选。我最开始用 Command 直接调 shell 命令,后来发现这样错误处理太脆弱,改成了用 std::process::Command 加完善的超时和错误捕获。
use std::process::Command;
use std::time::Duration;
use anyhow::{Context, Result};
/// 日志采集器 —— agent 的"眼睛"
/// 负责从目标服务器拉取最近一段时间的系统日志
pub struct LogCollector {
/// SSH 连接目标(格式: user@host)
target: String,
/// 单次采集的超时时间
timeout: Duration,
/// 最大返回行数(防止日志过大撑爆内存)
max_lines: usize,
}
impl LogCollector {
/// 创建新的日志采集器
/// target: 远程主机地址,如 "root@10.0.1.100"
/// timeout_secs: 单次采集超时秒数,建议 30 秒以内
/// max_lines: 最大日志行数,超过会被截断
pub fn new(target: &str, timeout_secs: u64, max_lines: usize) -> Self {
Self {
target: target.to_string(),
timeout: Duration::from_secs(timeout_secs),
max_lines,
}
}
/// 采集 systemd journal 日志
/// service: 服务名,如 "nginx" 或 "docker"
/// since: 时间范围,如 "1 hour ago" 或 "30 minutes ago"
pub fn collect_journal(&self, service: &str, since: &str) -> Result<String> {
// 构造 journalctl 命令
// -u: 指定服务单元 -S: 起始时间 -n: 最大行数 --no-pager: 禁用分页
let output = Command::new("ssh")
.arg(&self.target)
.arg(format!(
"journalctl -u {} -S '{}' -n {} --no-pager 2>&1",
service, since, self.max_lines
))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.context(format!("无法连接到主机 {}", self.target))?;
if !output.status.success() {
// 如果命令执行失败,把 stderr 内容也返回供分析
let err_msg = String::from_utf8_lossy(&output.stderr);
return Ok(format!("[采集错误] journalctl 执行失败: {}", err_msg));
}
let log_content = String::from_utf8_lossy(&output.stdout).to_string();
Ok(log_content)
}
/// 采集系统级错误日志(dmesg、/var/log/syslog 等)
/// 用于排查硬件故障、内核 OOM 等问题
pub fn collect_system_errors(&self) -> Result<String> {
// 组合多条命令,一次性采集多种系统日志
let commands = format!(
"dmesg --level=err,warn | tail -n {}; echo '---SYSLOG---'; \
tail -n {} /var/log/syslog 2>/dev/null || echo 'syslog 不可读'",
self.max_lines / 2, self.max_lines / 2
);
let output = Command::new("ssh")
.arg(&self.target)
.arg(&commands)
.output()
.context("采集系统错误日志失败")?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
这里有两个设计细节值得说。一是 max_lines 的限制——你没法保证生产环境日志不会瞬间膨胀到几万行,所以必须在采集层面就做截断。二是错误处理——即使 journalctl 执行失败了,也要把 stderr 的内容返回,因为"命令失败"本身就是一条有价值的信息,LLM 可以从错误输出里判断是权限问题还是服务不存在。
三、Prompt 工程 —— agent 的"大脑"
日志拿到了,接下来就是构建发给 LLM 的 prompt。这一块我也是踩了好多坑才找到感觉的。最早我写 prompt 特别随意,就是"这是日志,你看看有什么问题",结果 LLM 开始天马行空地猜测,有时候甚至建议我"升级内核"来解决一个配置文件写错了的问题。
后来我总结了一个结构化 prompt 模板,效果好了很多:
/// LLM 分析引擎 —— agent 的"大脑"
pub struct AnalysisEngine {
/// OpenAI 兼容的 API 端点(可以是本地部署的 vLLM)
api_url: String,
/// API 密钥
api_key: String,
/// 使用的模型名称(如 gpt-4o 或本地部署的 qwen-72b)
model: String,
/// HTTP 客户端(复用连接池)
client: reqwest::Client,
}
impl AnalysisEngine {
/// 构建结构化的分析 prompt
/// log_content: 过滤后的日志内容
/// host_info: 主机基本信息(OS 版本、内核版本等)
fn build_prompt(&self, log_content: &str, host_info: &str) -> String {
format!(
r#"你是一名资深 Linux 运维工程师。请根据以下系统日志进行故障分析。
## 主机信息
{}
## 系统日志
{}
## 分析要求
请严格按照以下格式输出:
1. **故障类型**:[磁盘满/内存泄漏/服务崩溃/网络故障/权限问题/配置错误/其他]
2. **根因分析**:用 2-3 句话说明导致故障的直接原因
3. **影响范围**:该故障影响了哪些服务/功能
4. **修复建议**:按优先级列出 3 条可执行的修复步骤,每条必须是具体的命令或操作
5. **预防措施**:给出 2 条避免再次发生同类问题的建议
6. **置信度**:[high/medium/low]"
请基于日志中的确定信息进行分析,不要做无依据的推测。如果日志信息不足以判断根因,请在置信度中标注 low 并说明需要哪些额外信息。
"#,
host_info, log_content
)
}
/// 调用 LLM 进行分析
/// 返回格式化的分析结果文本
pub async fn analyze(&self, log_content: &str, host_info: &str) -> Result<String> {
let prompt = self.build_prompt(log_content, host_info);
let body = serde_json::json!({
"model": self.model,
"messages": [
{
"role": "system",
"content": "你是一名专业的运维工程师,请用简洁专业的中文回答问题。"
},
{
"role": "user",
"content": prompt
}
],
"temperature": 0.3, // 低温度保证输出稳定可复现
"max_tokens": 1024 // 限制输出长度,防止废话太多
});
// 发送请求到 LLM 服务
let resp = self.client
.post(&self.api_url)
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
.timeout(Duration::from_secs(30)) // LLM 调用也设超时
.send()
.await?;
let json: serde_json::Value = resp.json().await?;
let analysis = json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("LLM 返回了空内容")
.to_string();
Ok(analysis)
}
}
prompt 里最关键的改造是要求 LLM 输出固定格式。"故障类型"让我可以按类型路由到不同的自动修复策略,"置信度"决定了是自动执行还是转人工。temperature: 0.3 也是特意调低的——在排障场景,我要的是稳定可复现的判断,不是创意写作。
四、自动修复与告警分流 —— agent 的"手"
LLM 返回分析结果后,最后一步是根据置信度分流。这块我用了一个简单的规则引擎:高置信度直接执行、中置信度生成工单、低置信度告警推送。
use serde::Deserialize;
/// LLM 返回的结构化分析结果
#[derive(Debug, Deserialize)]
struct AnalysisResult {
fault_type: String, // 故障类型
confidence: String, // 置信度: high/medium/low
fix_commands: Vec<String>, // LLM 建议的修复命令
}
/// 自动修复执行器 —— agent 的"手"
/// 根据置信度决定是自动执行还是转人工
pub struct AutoFixer {
collector: LogCollector, // 复用日志采集器来执行修复命令
}
impl AutoFixer {
/// 根据分析结果执行对应的修复策略
pub async fn execute(&self, result: &AnalysisResult) -> Result<String> {
match result.confidence.as_str() {
"high" => {
// 高置信度:自动执行修复命令
println!("[AUTO-FIX] 置信度高,自动执行修复");
for cmd in &result.fix_commands {
// 安全白名单检查:只允许执行预设的安全命令
if !self.is_safe_command(cmd) {
println!("[WARN] 拒绝执行不安全命令: {}", cmd);
continue;
}
// 在实际项目中,这里应该 SSH 到目标机器执行
println!("[EXEC] 执行: {}", cmd);
}
"自动修复已执行".to_string()
}
"medium" => {
// 中置信度:生成工单,附上 LLM 的建议
println!("[TICKET] 生成运维工单,附带 LLM 建议");
format!(
"已自动生成工单。建议修复方案:{}",
result.fix_commands.join("; ")
)
}
_ => {
// 低置信度:推送到 oncall 人员
println!("[ALERT] 推送告警到 oncall 值班人员");
"已推送告警通知,等待人工介入".to_string()
}
}
}
/// 命令安全白名单检查
/// 只允许 systemctl restart/reload、清理日志等安全的运维操作
fn is_safe_command(&self, cmd: &str) -> bool {
let safe_prefixes = [
"systemctl restart ",
"systemctl reload ",
"systemctl status ",
"journalctl --vacuum-size=",
"docker restart ",
"logrotate ",
"rm -rf /var/log/", // 注意:只清理日志目录
];
safe_prefixes.iter().any(|prefix| cmd.starts_with(prefix))
}
}
safe_prefixes 这个白名单设计非常重要。在实际生产环境中,不能让 LLM 直接执行任意 shell 命令。你永远不知道模型会在什么情况下"幻觉"出一个 rm -rf /。白名单既能在大多数常规场景下自动修复,又能防止灾难性操作。
五、总结
这篇文章复盘了用 Rust 构建 AI 运维助手的完整方案:日志采集用 std::process::Command + SSH 拉取远端日志,Prompt 工程用结构化模板引导 LLM 输出固定格式的分析结果,自动修复用置信度 + 命令白名单实现安全可靠的自动执行。
从自学编程的角度看,这类工程化项目是成长最快的。它逼着你去理解 Linux 系统管理、网络通信、API 设计、安全考量,还有最重要的——生产环境的真实边界情况。下个月我打算把这个 agent 接到我们的企业微信机器人上,到时候再写一篇线上运行的效果复盘。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)