从零构建 AI 驱动的日志监控自愈系统
背景
AI 销售工作台(ai_sales_server)是一个基于 Eino 框架的多 Graph Agent 编排系统,调用链路复杂------多个 Graph 编排、外部 LLM 调用、MCP 工具链。线上错误依赖人工巡检日志平台发现,从发现到修复响应慢。
目标:构建一套全自动的"错误发现→根因分析→代码修复→MR 提交"闭环系统。
架构设计
scss
┌─────────────────────────────────────────────────────┐
│ Spirit Watcher │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ 日志轮询 │──▶│ 指纹去重 │──▶│ SQLite 存储 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ meta-mcp │ │ Claude │──▶│ 自动修复 │ │
│ │ (TLS日志) │ │ 根因分析 │ │ worktree+patch │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ GitLab MR 创建 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
核心模块
1. 日志轮询
通过 MCP 协议对接公司日志平台(火山云 TLS),每 30 秒拉取 ERROR 级别日志。
go
func pollLogs(ctx context.Context, topicID string) {
now := time.Now().UnixMilli()
startTime := now - 30*1000
result, err := mcpClient.CallTool("search_log", map[string]any{
"topic_id": topicID,
"query": "ERROR",
"start_time": startTime,
"end_time": now,
"limit": 100,
})
// 解析日志,过滤 level=error,存入 incident
}
调用链路:get_log_topics(服务名) → 获取 topic ID → search_log(topic_id, query) → 返回日志条目。
MCP 客户端封装了 JSON-RPC over HTTP + SSE 响应解析 + session 管理,对外只暴露 CallTool(name, args) 一个方法。
2. 指纹归一化去重
同一个错误可能因为动态内容(IP、时间戳、trace ID)不同而看起来不一样。归一化后再算 SHA-256 指纹:
go
func normalize(msg string) string {
msg = regexpIP.ReplaceAllString(msg, "<IP>")
msg = regexpTimestamp.ReplaceAllString(msg, "<TIME>")
msg = regexpUUID.ReplaceAllString(msg, "<UUID>")
msg = regexpNumericID.ReplaceAllString(msg, "<ID>")
msg = regexpHex.ReplaceAllString(msg, "<TRACE>")
return msg
}
效果:
arduino
"dial tcp 10.0.1.5:3306: connection refused at 2026-05-21T22:58:00"
"dial tcp 10.0.1.8:3306: connection refused at 2026-05-22T01:30:15"
→ 归一化后相同 → 同一个 incident,hit_count +1
3. Claude 根因分析
复用项目已有的 Eino Agent 能力,直接调 chatModel.Generate 做分析:
go
chatModel, _ := agentsales.NewChatModelFromEnv(ctx)
resp, _ := chatModel.Generate(ctx, []*schema.Message{
schema.UserMessage(prompt),
})
分析结果结构化输出:
json
{
"diagnosis": "数据库连接池耗尽,max_open_conns 设置过小",
"risk_level": "A",
"fix_plan": "将 max_open_conns 从 10 调整为 50"
}
风险等级:
- A --- 确定性修复(配置错误、空指针防护),自动修复
- B --- 需要 review 的逻辑调整,自动修复但不自动合并
- C --- 架构问题或数据问题,只记录不修复
4. 自动修复
修复在隔离的 Git worktree 中进行,不影响主分支:
markdown
1. git worktree add -b spirit/fix-{id} → 创建隔离工作区
2. LLM 生成 search-replace 补丁 → 应用到文件
3. go build ./... → 编译验证
4. 验证失败 → 回滚,incident 回到 open
5. 验证通过 → commit + push + 创建 GitLab MR
补丁格式:
json
[{
"file_path": "config/database.go",
"old_content": "MaxOpenConns: 10",
"new_content": "MaxOpenConns: 50"
}]
5. Incident 状态机
arduino
open → analyzing → analyzed → fixing → resolved → closed
↓
(验证失败回退到 analyzed)
使用 SQLite 本地存储,单进程无并发问题,零运维。通过 HTTP 接口暴露查询:
bash
curl http://localhost:9090/incidents
技术选型
| 组件 | 选型 | 原因 |
|---|---|---|
| 日志采集 | meta-mcp (HTTP JSON-RPC) | 公司已有统一日志查询接口 |
| LLM 分析 | Eino + DeepSeek | 项目已有成熟的 Agent 能力 |
| 存储 | SQLite (modernc.org/sqlite) | 纯 Go 实现,零依赖,单进程够用 |
| Git 操作 | os/exec + git CLI | 轻量,不引入额外依赖 |
| MR 创建 | GitLab REST API v4 | 直接 HTTP 调用 |
| 部署 | --script 模式 | 复用现有 CI/CD,不加新二进制 |
部署方式
作为 ai_sales_server 的 script 模式运行,和主服务共享同一个二进制,不同启动参数:
bash
# 主服务
./ai_sales_server run -c config.yaml -p 2345
# Spirit Watcher
./ai_sales_server run --script=spirit_watcher -c config.yaml
后续可拆为独立入口 cmd/watcher/main.go,独立编译部署。
文件结构
bash
ai_sales_server/
├── config/job.go # 注册 spirit_watcher 脚本
├── job/
│ ├── spirit_watcher.go # 主循环:轮询 + 去重 + 调度
│ ├── spirit_analyzer.go # Claude 根因分析
│ ├── spirit_fixer.go # 自动修复 + MR 创建
│ └── spirit/store/store.go # SQLite 存储层
└── util/meta_mcp_client.go # MCP 客户端封装
降级策略
| 场景 | 处理 |
|---|---|
| MCP 连接失败 | 启动中止,等待下次重启 |
| 日志查询超时 | 跳过本轮,30 秒后重试 |
| LLM 分析失败 | incident 回到 open,下轮重试 |
| 补丁应用失败 | 回退到 analyzed,不提交 |
| 编译验证失败 | 回退到 analyzed,不提交 |
| risk_level=C | 只记录,不自动修复 |
总结
整个系统用约 1000 行 Go 代码实现了从错误发现到 MR 提交的全自动闭环。核心设计思路:
- 复用已有基础设施 --- 日志平台、LLM 能力、GitLab,不重复造轮子
- 渐进式处理 --- 每个阶段独立,失败可回退,不会产生脏数据
- 安全优先 --- worktree 隔离、编译验证、MR 不自动合并,人始终是最后一道关