Rust AI 运维助手:让 agent 读懂系统日志并给出排障建议的工程方案

一、半夜被叫醒的瞬间,我决定做一个能自己排障的 agent

做过后端或者运维的兄弟应该都有这种体验:凌晨两三点,手机亮了,你一看——生产环境磁盘使用率 95%。迷迷糊糊爬起来,SSH 连到服务器,du -sh /* | sort -rhjournalctl -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 接到我们的企业微信机器人上,到时候再写一篇线上运行的效果复盘。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐