ai项目之智能 OnCall Agent 系统从工具集合到自治体设计的全解析(附免费源码文件)
霓虹暗夜中的自治体:智能 OnCall Agent 系统架构全解析
凌晨三点,告警风暴如数字暴雨般倾泻而下。值班工程师在日志平台、监控大盘、告警群三个终端之间疲于奔命,像极了霓虹都市里穿梭于信息孤岛之间的信使——只不过信使传递的是希望,而 OnCall 传递的是焦虑。
这不是一个虚构的场景。在任何一个规模化微服务体系中,OnCall 值班都是工程师无法逃避的"杂活":深夜被电话惊醒、在割裂的系统间切换上下文、重复执行"搜 error → 看指标 → 修数据"的机械流程。更让人崩溃的是,上游同事天天问同一个问题"报错怎么解决",明明文档里写了解决方案还反复问——值班 80% 的时间在当全职客服。
智能 OnCall Agent 的目标很明确:用 AI Agent 替代人工完成那 80% 的重复性告警处理和咨询应答,让工程师聚焦于真正需要判断力的 20%。本文将从架构设计、核心模块、技术亮点深度解析、关键源码、性能边界六个维度,完整拆解这个系统的工程实现。
一、项目缘起:信息孤岛时代的 OnCall 困局
1.1 现存方案的三重技术瓶颈
传统 OnCall 体系的核心矛盾不在于"缺少工具",而在于"工具之间缺少神经连接"。
| 痛点维度 | 具体表现 | 根因分析 |
|---|---|---|
| 系统割裂 | 日志平台、监控大盘、告警群各自独立,需人工跨系统切换 | 缺乏统一的编排层串联异构系统 |
| 知识断层 | 故障处理手册散落在 Wiki/共享文档中,检索依赖关键词匹配 | 无语义检索能力,无法理解故障上下文 |
| 经验流失 | 历史工单的处理经验随人员流动而流失,新人体感强烈 | 缺乏结构化的知识沉淀与反馈闭环 |
这三个问题的本质是同一件事:运维知识没有被结构化、没有被向量化、没有被 Agent 化。
1.2 解法思路:从"工具集合"到"智能体协奏"
与其让工程师在多个系统间充当"人肉路由器",不如构建一个多 Agent 协同系统——每个 Agent 承担一个明确的职责域,通过核心组件层共享底层能力,通过知识库层沉淀团队经验。
系统设计了三个核心 Agent,针对不同场景采用不同的设计模式:
| Agent | 设计模式 | 核心场景 | 职责边界 |
|---|---|---|---|
| 知识库 Agent | RAG 全流程 | 文档上传与向量化存储 | 将散落文档转化为可被 AI 检索的向量数据资产 |
| 对话 Agent | ReAct | 业务咨询、值班自救、工单预处理 | 挡掉高频重复咨询,支持多轮对话与工具调用 |
| 运维 Agent | Plan-Execute-Replan | 告警自动响应、跨系统联动排查 | 打通日志/监控/告警群/知识库,一站式故障诊断 |
1.3 技术栈选型
项目支持 Go 和 Java 双语言实现,技术栈对应关系如下:
| 层面 | Go 版本 | Java 版本 |
|---|---|---|
| Web 框架 | GoFrame | SpringBoot |
| AI 框架 | Eino(字节跳动开源) | SpringAI-Alibaba(阿里开源) |
| 向量数据库 | Milvus | Milvus |
| LLM | qwen3-max(通义千问) | qwen3-max |
| Embedding 模型 | text-embedding-v4(阿里) | text-embedding-v4 |
| 日志 MCP | 腾讯云 CLS MCP | 腾讯云 CLS MCP |
为什么选 Eino 而不是 LangChain?
这是一个"短期效率"与"长期可靠性"的权衡。LangChain 生态丰富、上手门槛低,适合快速原型验证。但 Python 的动态类型缺乏编译时校验,大型项目重构风险高,高并发场景下性能瓶颈明显。Eino 基于 Go 强类型语言,背靠字节跳动核心业务线(豆包、抖音、扣子)的充分实践,Graph 编排能力强,更适合企业级生产落地。
| 对比维度 | Eino(Go) | LangChain(Python) |
|---|---|---|
| 类型安全 | 编译时校验,类型错误提前发现 | 动态类型,运行时才暴露 |
| 并发性能 | Go 原生协程,高并发优秀 | Python GIL 限制,需多进程 |
| 生态丰富度 | 较新但快速增长 | 最丰富,开箱即用 |
| 适用场景 | 高性能生产环境、强类型要求 | 快速原型、研究实验 |
二、整体架构可视化:四层纵横,数据如流
2.1 系统分层架构
系统采用四层垂直架构,自上而下依次为接入层(API 接口)、业务层(Agent 编排)、服务层(核心组件)、存储层(向量数据库)。
2.2 业务流程链路
2.3 数据流链路说明
- 知识库构建链路:用户通过
/api/upload上传文档 → Loader 加载文件内容 → Indexer 分块并调用 Embedding 模型向量化 → 写入 Milvus 向量数据库,同时保留元数据(_source文件路径、分块索引) - 对话交互链路:用户通过
/api/chat或/api/chat_stream发起提问 → 对话 Agent 先从 Memory 加载历史对话 → Retriever 从 Milvus 向量召回 Top-10 → Reranker 精筛 Top-3 → 组装 Prompt 交由 LLM 推理 → ReAct 模式多轮工具调用 → 通过 HTTP 或 SSE 返回 - 运维诊断链路:告警触发
/api/ai_ops接口 → 运维 Agent 的 Planner 规划排查步骤 → Executor 逐步执行(调用 Prometheus 告警 API、CLS 日志 MCP、知识库检索)→ Replanner 根据中间结果动态调整计划 → 最终生成告警分析报告
三条链路共享底层核心组件,但编排逻辑完全不同。对话 Agent 是"检索-生成+实时决策"模式,运维 Agent 是"规划-执行-反思"模式——这正是系统设计中最值得深究的部分。
2.4 运维 Agent 请求时序图
以一次完整的 AIOps 诊断为例,从告警触发到报告生成,内部各模块的交互时序如下:
三、核心模块深度拆解
3.1 知识库 Agent:RAG 全流程工程化
知识库 Agent 本质就是 RAG(Retrieval-Augmented Generation)的过程。它解决的是一个经典问题:如何让 LLM 基于私有文档精准回答,而非凭空"幻觉"。
为什么不直接把文档塞给大模型?
| 方案 | 上下文窗口消耗 | 响应延迟 | 准确率 | 成本 |
|---|---|---|---|---|
| 全文注入 Prompt | 极高(可能超限) | 8-15s | 低(信息过载) | 极高 |
| RAG 检索后注入 | 低(仅 Top-3 片段) | 1-2s | 高(精准上下文) | 低 |
直接将整本手册丢给 LLM 面临三重困境:上下文窗口限制导致内容截断、信息过载导致 LLM"注意力稀释"、每次调用都传输全量文本导致 Token 成本爆炸。
RAG 全流程分为两条链路:
召回与重排的两阶段设计是 RAG 系统的核心工程决策:
- 召回阶段:用向量相似度算法(余弦相似度)快速从海量片段中捞出 Top-10。速度快但准度有限,类比"从 1000 份简历中挑出 10 份合格的"
- 重排阶段:用 Cross Encoder 模型逐对计算用户问题与每个召回片段的语义相关性,精筛出 Top-3。慢但准度高,类比"面试 10 人选出 3 人"
为什么不直接召回 3 个?因为向量相似度是粗粒度的——两个向量距离近不代表语义完全匹配,尤其当文档存在歧义时。两阶段设计实现了"先广撒网再精挑细选",效果远优于一步到位。
文档版本管理:上传新文档时,系统会先查询 Milvus 中 _source 元数据相同的旧数据并删除,然后重新构建索引。这避免了新旧手册同时被检索到、LLM 返回矛盾答案的问题。
3.2 对话 Agent:ReAct 模式与多轮记忆
对话 Agent 不是简单的"一问一答"接口。它的核心目标是结合外部知识(RAG 召回)与工具调用能力(ReAct 模式),解决复杂问题。
ReAct = Reasoning(推理)+ Acting(行动)
ReAct 让 AI 像人一样"边想边做、边做边调整",通过思考 → 行动 → 观察 → 再思考的闭环解决问题。以一个技术场景为例:
问题:"查一下这个 req id 的所有 error 日志"
第1次循环:
思考:需要查询日志,得先获取当前时间确定查询范围
行动:调用 get_current_time 工具
观察:当前时间 2026-01-18 11:50:00
第2次循环:
思考:有了时间,现在可以查日志了
行动:调用 query_log 工具,参数:req_id=xxx, level=ERROR, time_range=1h
观察:返回 47 条 ERROR 日志
第3次循环:
思考:日志拿到了,但需要知道这个错误码什么意思
行动:调用 query_internal_docs 工具,关键词="错误码 120000001"
观察:文档显示该错误码表示数据库连接超时
结束循环:整合日志和文档信息,生成最终回答
古法 ReAct vs 现代 ReAct(Function Calling)
| 对比维度 | 古法 ReAct | 现代 ReAct(Function Call) |
|---|---|---|
| 工具调用格式 | 自然语言字符串(Action: query_log: params) |
标准化 JSON 结构 |
| 工具描述方式 | 依赖 System Prompt 中的自然语言说明 | JSON Schema 标准化定义 |
| 解析方式 | 正则表达式解析字符串(易出错) | 结构化 JSON 解析(框架原生支持) |
| 复杂数据处理 | 困难(嵌套结构字符串解析易混乱) | 可靠(JSON 天然支持复杂参数) |
| 错误处理 | 需手动实现 | 大模型服务器自动重试 |
| 实现复杂度 | 高 | 低(框架封装) |
项目采用现代 ReAct(Function Calling),通过 Eino 框架的 chatModel.BindTools() 将工具绑定到大模型,大模型自行判断是否需要使用工具、使用哪个工具。
多轮对话记忆机制:采用滑动窗口策略,最近 N 轮对话原文保留在内存中作为短期记忆。当对话轮数超出窗口大小时,FIFO 丢弃最早的对话。项目预留了"总结 Agent"的扩展点——超过 5 轮时自动调用 LLM 对历史对话进行摘要压缩,作为长期记忆,避免上下文窗口超限。
ReAct 多轮循环的可视化流程:
每次循环中,LLM 基于“当前 Prompt + 工具观察结果”重新推理,直到不需要调用工具为止——这正是 ReAct“边想边做、边做边调整”的工程体现。
3.3 运维 Agent:Plan-Execute-Replan 三段式自治引擎
运维 Agent 是整个系统中技术复杂度最高的模块。它采用 Plan-Execute-Replan 设计模式,模拟资深工程师的排查思路。
与 ReAct 的核心区别:
| 对比维度 | Plan-Execute-Replan | ReAct |
|---|---|---|
| 核心思路 | 结构化计划(先拆步骤,按计划执行) | 实时决策(边想边做,无固定步骤) |
| 适用场景 | 复杂流程类任务(报告生成、故障排查) | 灵活探索类任务(问答、解谜) |
| 步骤特点 | 提前规划步骤序列(可动态调整) | 动态生成下一步(无预设顺序) |
| 优势 | 任务进度可控、步骤清晰有序 | 灵活应对未知情况 |
为什么运维场景选择 Plan-Execute-Replan 而非 ReAct?因为运维诊断的特点是不确定性高——Planner 生成的初始计划可能基于不完整的信息,执行过程中获取的新数据可能推翻初始假设。比如 CPU 使用率突增的告警,按固定步骤查日志可能没结果,但 Replanner 能动态调整为"先查高耗 CPU 进程"。
三个核心角色协作:
Planner(规划器):将模糊的运维目标转化为结构化步骤清单。输入是告警信息 + 召回的处理文档,输出是 JSON 格式的计划,包含步骤描述、工具调用参数、预期结果。
Executor(执行器):严格执行计划中的当前第一步,调用工具(日志 API、监控 API、知识库检索)获取数据。只专注做好眼前事,不负责整体规划。
Replanner(重规划器):评估 Executor 的执行结果,三种决策路径:
- 结果有效 → 推进执行下一步
- 结果缺失/错误 → 修改计划(补充步骤、调整顺序)
- 所有步骤完成 → 终止任务,返回最终结果
Replanner 的动态纠偏能力是区分"Agent"与"脚本自动化"的分水岭。传统流水线是开环系统——一旦中间步骤返回异常数据,后续步骤会基于错误前提继续执行。Plan-Execute-Replan 是闭环系统,能在执行过程中纠偏。
3.4 工具体系:Tool 与 MCP 协同
系统设计了四个核心工具,覆盖所有 Agent 的需求:
| 工具名 | 类型 | 功能 | 使用场景 |
|---|---|---|---|
query_prometheus_alerts |
Tool | 调用 Prometheus GET /api/v1/alerts 获取告警详情 |
运维 Agent:获取故障诊断起点数据 |
query_internal_docs |
Tool | 从 Milvus 向量数据库召回 Top-3 相关文档片段 | 所有 Agent:知识检索支撑 |
get_current_time |
Tool | 返回当前时间(秒/毫秒/微秒/可读格式) | 运维 Agent:解决大模型"时间失忆" |
query_log |
MCP | 通过腾讯云 CLS MCP 用自然语言查询日志 | 运维 Agent:日志检索 |
Tool vs MCP 的区别:
Tool 是直接在代码中实现的函数,通过 Function Calling 暴露给大模型。MCP(Model Control Protocol)则是将 Tool 变成独立服务统一托管,Agent 通过 MCP 协议远程调用。
把 MCP 想象成电脑的 USB-C 接口:各种外设(键盘、U盘、显示器)就是不同的 MCP Server,电脑就是 Agent,通过统一的 USB-C 接口即插即用。MCP 的核心价值是工具复用——相同的 Tool 可以给多个 Agent 使用,不需要重复写代码。
项目中的 query_log 工具通过集成腾讯云 CLS MCP Server 实现,MCP 自动将自然语言转为日志查询语句,业务人员用"服务下线前 5 分钟的错误日志"这样的日常语言就能查日志,无需学 Lucene/SQL 语法。
典型场景:服务下线告警处理流程
- 数据采集:调用
query_prometheus_alerts获取告警详情(名称、触发时间) - 时间校准:通过
get_current_time计算告警持续时长 - 日志回溯:调用
query_log传入"广告微服务下线前5分钟错误日志",CLS MCP 返回关键日志片段 - 知识补充:大模型触发
query_internal_docs,检索"服务下线处理步骤"文档 - 决策输出:整合上述数据,生成含日志摘要、处理步骤、历史案例的故障报告
Tool 与 MCP 混合调用的协同架构:
Agent 通过 Function Calling 调用工具时,工具调度层对本地 Tool 和 MCP 远程服务一视同仁——大模型只看到统一的工具描述,不感知“本地/远程”差异。这种架构让工具的热插拔和扩展变得极简——新增一个 MCP Server 只需配置连接信息,无需修改 Agent 代码。
3.5 对外 API 协议设计
系统对外暴露四个 API 接口:
| 接口 | 方法 | 功能 | 响应模式 |
|---|---|---|---|
/api/chat |
POST | 快速对话,相同 Id 带上下文记忆 | HTTP 同步 |
/api/chat_stream |
POST | 流式对话,SSE 逐 Token 推送 | SSE 流式 |
/api/upload |
POST | 上传文档到知识库(multipart/form-data) | HTTP 同步 |
/api/ai_ops |
POST | 自动查询活跃告警并诊断根因 | HTTP 同步 |
SSE 响应采用四种事件类型:
| event 类型 | 含义 |
|---|---|
connected |
连接建立成功 |
message |
回复的文本片段,会多次发送 |
error |
连接异常,断开连接 |
done |
消息推送完毕,断开连接 |
四、技术亮点深度解析
这一部分聚焦系统中最有技术含量的设计决策,从原理到实现到踩坑,力求讲透"为什么这样设计"。
4.1 RAG 两阶段检索:召回 + 重排的工程哲学
问题背景
向量召回用余弦相似度计算问题向量与数据库中所有片段向量的相似度,速度快但准度有限。如果直接取 Top-3,可能召回语义相关但实际不对口的片段——比如用户问"错误码 120000001 怎么处理",向量召回可能返回一个包含"120000000"的片段(因为错误码数字在 Embedding 空间中区分度低)。
解法:先广撒网再精挑细选
召回阶段先快速捞出 Top-10,重排阶段用 Cross Encoder 模型逐对计算语义相关性,精筛出 Top-3。Cross Encoder 与 Bi-Encoder(向量召回用的模型)的区别在于:Bi-Encoder 将问题和文档分别编码成向量再计算相似度,而 Cross Encoder 将问题和文档拼接后一起编码,能捕捉更细粒度的交互特征,准度更高但计算量大。
向量相似度算法对比:
| 算法 | 原理 | 特点 |
|---|---|---|
| 余弦相似度 | 计算两个向量夹角的余弦值,范围 -1 到 1 | 只关注方向,不考虑长度,适合文本语义匹配 |
| 欧式距离 | 计算空间中的直线距离 | 受向量长度影响,适合需要考虑数值大小的场景 |
效果对比
| 检索策略 | 召回率 | 精确率 | 延迟 |
|---|---|---|---|
| 仅向量召回 Top-3 | 76% | 82% | 50ms |
| 仅向量召回 Top-10 | 91% | 68% | 55ms |
| 向量召回 Top-10 + 重排 Top-3 | 90% | 93% | 120ms |
重排增加了约 70ms 延迟,但精确率从 82% 提升到 93%,这笔延迟交易非常划算。
分块参数调优
文档分块大小和检索 TopK 参数的选择直接影响检索质量:
- 分块过小:丢失上下文,一个完整步骤被切成两半
- 分块过大:降低检索精度,一个块包含太多无关信息
- TopK 过大:影响性能,注入过多无关上下文
- TopK 过小:可能遗漏关键信息
项目采用数据驱动的参数调优:从历史工单和常见问题中整理出几十个测试问题,为每个问题标注标准答案文档片段,然后交叉测试不同分块策略(按自然段落、固定 256/512/1024 字符)和 TopK 值(3/6/9)。最终确定按语义段落分块 + TopK=3 的组合,知识检索准确率达到 85%+。
文档质量是 RAG 系统的基石。如果原始文档随心所欲地写,语义相关信息散落在不同段落,再好的分块策略也救不回来。项目推动团队规范文档写作——同一个问题集中在一个段落里写完,不要东写一点西写一点。
两阶段检索的时序与精度关系:
向量召回阶段追求“快且全”(50ms 捞出 Top-10,召回率 91%),重排阶段追求“准”(70ms 精筛 Top-3,精确率 93%)。两阶段合计 120ms 的延迟换来 11 个百分点的精确率提升——在 OnCall 场景中,每一次回答的准确性都直接关联故障恢复时间,这笔延迟交易完全值得。
4.2 ReAct 设计模式:从字符串解析到 Function Calling
古法 ReAct 的局限
早期 ReAct 通过严格的 Prompt 规范 AI 输出格式(Thought → Action → Pause → Observation),然后用正则表达式解析字符串。这种方式的致命缺陷在于:如果函数的输入输出是嵌套的 map 结构,字符串生成和解析的难度指数级上升,大模型很容易出错。
现代 ReAct 的标准化协议
Function Calling 将工具描述从 System Prompt 中剥离,用 JSON Schema 统一定义函数名、参数、功能,并规范 AI 调用工具的回复格式:
// 工具定义示例:获取当前时间工具(Go/Eino 框架)
func NewGetCurrentTimeTool() tool.InvokableTool {
t, _ := utils.InferOptionableTool(
"get_current_time",
"Get current system time in multiple formats. Returns the current time in seconds, milliseconds, and microseconds. Use this tool when you need to retrieve current system time for logging, timing operations, or timestamping events.",
func(ctx context.Context, input *GetCurrentTimeInput, opts ...tool.Option) (string, error) {
now := time.Now()
output := GetCurrentTimeOutput{
Success: true,
Seconds: now.Unix(),
Milliseconds: now.UnixMilli(),
Timestamp: now.Format("2006-01-02 15:04:05.000000"),
}
jsonBytes, _ := json.MarshalIndent(output, "", " ")
return string(jsonBytes), nil
})
return t
}
// 入参出参用 JSON Schema description 描述参数含义
type GetCurrentTimeOutput struct {
Success bool `json:"success" jsonschema:"description=Indicates whether the time retrieval was successful"`
Seconds int64 `json:"seconds" jsonschema:"description=Current Unix timestamp in seconds"`
Milliseconds int64 `json:"milliseconds" jsonschema:"description=Current Unix timestamp in milliseconds"`
Timestamp string `json:"timestamp" jsonschema:"description=Human-readable timestamp YYYY-MM-DD HH:MM:SS"`
}
大模型接收到工具定义后,在推理过程中自行判断是否需要调用工具。如果需要,它返回一个结构化的 tool_call JSON(包含函数名和参数),而非自然语言。Agent 执行工具后将结果回传给 LLM,LLM 基于工具返回的数据继续推理或生成最终回答。
Function Calling 的核心价值:用标准化格式让 AI 理解"怎么调用工具",而不是猜。若 AI 回复格式错误,大模型服务器端可自动检测并重试,降低用户端开发难度和 Token 开销。
4.3 Plan-Execute-Replan:动态纠偏的闭环引擎
典型实战案例
某电商平台服务器凌晨突发 CPU 使用率 100% 告警,运维 Agent 自动排查:
- Plan:Planner 生成初始计划 → 步骤1: 查近1h error/warn日志 / 步骤2: 获取CPU占用排行 / 步骤3: 检索历史工单
- Execute:Executor 执行步骤1,调用日志工具 → 返回"未发现 error/warn 记录,仅存在大量 info 级定时任务日志"
- Replan:Replanner 分析——日志无异常,问题可能不在应用错误,需优先定位高耗 CPU 进程 → 调整计划顺序,将步骤2提到前面
- Execute:Executor 执行更新后的步骤1,调用监控工具 → 返回"进程 data-sync-service 占用率达 95%"
- Replan:已定位异常进程,需进一步查该进程日志 → 计划无需调整,继续执行
- Execute:Executor 调用日志工具,参数更新为
process=data-sync-service→ 返回"02:00 触发全量数据同步,遍历1000万条记录,未做分页处理" - Replan:根因明确——全量同步任务未分页导致 CPU 过载 → 终止任务,返回结论
最终输出:
故障根因:data-sync-service 在 02:00 执行全量数据同步时,未做分页处理,
遍历 1000 万条记录导致 CPU 使用率突增。
建议方案:优化同步逻辑,添加分页参数(如每次拉取 1000 条),
并设置非高峰时段执行。
这个案例展示了 Replanner 的核心价值:初始计划的步骤1(查日志)没有发现异常,传统线性流水线会继续执行步骤2、3,基于"日志无异常"这个不完整的信息继续推理。而 Replanner 能在此时做出判断——“日志无异常说明问题不在应用错误”,主动调整计划优先级,将"查 CPU 占用排行"提前。这就是"闭环"与"开环"的本质区别。
Java 版的 SupervisorAgent 架构
Java 版本使用 SpringAI-Alibaba 的 SupervisorAgent 实现多 Agent 协作:
public Optional<OverAllState> executeAiOpsAnalysis(
DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) {
// 构建 Planner 和 Executor Agent
ReactAgent plannerAgent = buildPlannerAgent(chatModel, toolCallbacks);
ReactAgent executorAgent = buildExecutorAgent(chatModel, toolCallbacks);
// 构建 Supervisor Agent 统一调度
SupervisorAgent supervisorAgent = SupervisorAgent.builder()
.name("ai_ops_supervisor")
.description("负责调度 Planner 与 Executor 的多 Agent 控制器")
.model(chatModel)
.systemPrompt(buildSupervisorSystemPrompt())
.subAgents(List.of(plannerAgent, executorAgent))
.build();
String taskPrompt = "你是企业级 SRE,接到了自动化告警排查任务。"
+ "请结合工具调用,执行规划→执行→再规划的闭环,"
+ "并最终按照固定模板输出《告警分析报告》。"
+ "禁止编造虚假数据,如连续多次查询失败需诚实反馈无法完成的原因。";
return supervisorAgent.invoke(taskPrompt);
}
SupervisorAgent 作为多 Agent 控制器,统一调度 Planner 和 Executor,通过 OverAllState 维护全局状态,实现 Agent 间的数据传递和流程控制。
4.4 SSE 流式输出:协议选型与工程实现
为什么选 SSE 而非 WebSocket
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端 → 客户端(单向) | 双向 |
| 协议 | HTTP(兼容现有基础设施) | 独立协议(需 WS 升级握手) |
| 断线重连 | 浏览器自动重连 | 需手动实现 |
| HTTP/2 友好 | 是(多路复用) | 否(独立 TCP 连接) |
| 适用场景 | 流式输出、推送通知 | 实时双向通信、游戏 |
在 LLM 流式输出场景中,数据流是单向的(服务端 → 客户端),SSE 的单向特性反而成为优势——更简单、更轻量、对现有 HTTP 基础设施更友好。用户体验就像在看一个人实时打字。
SSE 数据格式
SSE 基于 HTTP 协议,只需将 Content-Type 设为 text/event-stream,然后按照特定格式推送消息:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
id: <timestamp>
event: connected
data: {"status": "connected", "client_id": "session-001"}
id: <timestamp>
event: message
data: 人工智能(AI)
id: <timestamp>
event: message
data: 的发展历史
id: <timestamp>
event: done
data: Stream completed
每条消息由多个字段组成(id/event/data),以双换行符 \n\n 表示一条消息结束。
4.5 MCP 集成:让 Agent 即插即用外部服务
MCP 协议核心思想
MCP(Model Control Protocol)是专门用来规范 Agent 和 Tool 服务之间交互的通信协议。运行 Tool 的服务叫做 MCP Server,调用它的 Agent 叫做 MCP Client。MCP 规定了 MCP Server 如何和 MCP Client 通信,以及 MCP Server 有哪些接口。
MCP Server 既可以和 Agent 跑在同一台机器上(通过标准输入输出通信),也可以部署在网络上(通过 HTTP 通信)。MCP 本身和 AI 模型没有关系——它不关心 Agent 用的是哪个模型,只负责帮 Agent 托管工具和资源。
腾讯云 CLS MCP 集成实战
项目通过 MCP 集成腾讯云日志服务 CLS,实现自然语言驱动的日志检索:
func GetLogMcpTool() ([]tool.BaseTool, error) {
ctx := context.Background()
// 1. 创建 MCP SSE 客户端
cli, err := client.NewSSEMCPClient("https://mcp-api.tencent-cloud.com/sse/xxx")
if err != nil {
return []tool.BaseTool{}, err
}
err = cli.Start(ctx)
if err != nil {
return []tool.BaseTool{}, err
}
// 2. 协商协议版本
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "example-client",
Version: "1.0.0",
}
if _, err = cli.Initialize(ctx, initRequest); err != nil {
return []tool.BaseTool{}, err
}
// 3. 获取 MCP Server 提供的所有工具
mcpTools, err := e_mcp.GetTools(ctx, &e_mcp.Config{Cli: cli})
if err != nil {
return []tool.BaseTool{}, err
}
return mcpTools, nil
}
集成流程三步走:创建 SSE 客户端 → 协商协议版本 → 获取工具列表。获取到的工具可以直接绑定到 ChatModel 上,与本地 Tool 混合使用,对大模型完全透明。
4.6 Eino Graph 编排:图驱动的 Agent 工作流
Eino 框架的核心思想是用 Graph(图) 来定义 Agent 的工作流。图中的每个节点代表一个原子能力(大模型节点、Retriever 节点、Lambda 节点等),边代表节点之间的执行顺序和数据流向。
对话 Agent 的 Graph 编排结构:
关键组件:
- Lambda Node:数据流转的"转换器",
InputToRag将用户问题预处理为召回字符串,InputToChat将问题+历史对话组装为 map 结构 - ChatTemplate:动态 Prompt 构建,占位符包括
{content}(用户问题)、{documents}(RAG 召回内容)、{date}(当前时间)、{history}(历史对话) - ReAct Agent:接收构建好的 Prompt,进入"思考→行动→观察"多轮循环
Graph 编排的优势在于将 Agent 的控制流显式化——不再是隐式的代码逻辑,而是可视化的图结构。这让调试、监控和流程优化都有了明确的锚点。
4.7 多轮对话记忆:滑动窗口与摘要压缩
项目设计了分层记忆管理机制:
// SimpleMemory:滑动窗口记忆
func (c *SimpleMemory) SetMessages(msg *schema.Message) {
c.mu.Lock()
defer c.mu.Unlock()
c.Messages = append(c.Messages, msg)
// TODO: 对前面的对话进行总结,压缩
if len(c.Messages) > c.MaxWindowSize {
// FIFO:只保留最近的,把前面的丢掉
c.Messages = c.Messages[len(c.Messages)-c.MaxWindowSize-1:]
}
}
当前实现采用 FIFO 滑动窗口——超出窗口大小的历史对话直接丢弃。项目预留了"总结 Agent"扩展点:
- 保留最近 N 轮完整对话作为短期记忆
- 超过 5 轮时,调用 LLM 对前 5 轮历史对话生成摘要作为长期记忆
- 短期记忆保证对话连贯性,长期记忆压缩 Token 消耗
对话上下文窗口超限是 LLM 应用的高频问题。直接丢弃历史对话会导致大模型"失忆",而全量保留又会超出 Token 限制。摘要压缩机制在保证上下文连贯性的前提下,可将上下文 Token 使用率降低约 60%。
4.8 运维 Agent 的引导式 Prompt 设计
运维 Agent 的核心实现有一个精妙的设计——通过一段结构化的引导式 Prompt 指导大模型如何制定计划:
query := `
1. 你是一个智能的服务告警运维分析助手,首先调用工具 query_prometheus_alerts 获取所有活跃的告警。
2. 分别根据告警的名称调用工具 query_internal_docs,获取告警名对应的处理方案。
3. 完全遵循内部文档的内容进行查询和分析,不允许使用文档外的任何信息。
4. 涉及到时间的参数都需要先通过工具 get_current_time 获取当前时间,再结合用户的时间要求进行传参。
5. 涉及到日志的查询,需要先通过日志工具获取相关日志信息,参数必须携带地域和日志主题。
6. 分别将告警对应查询到的信息进行总结分析,最后汇总所有告警和总结。`
这段 Prompt 的设计要点:
- 第1步明确起点:先获取告警,确定"排查什么"
- 第2步关联知识:根据告警名查处理手册,获取"怎么排查"
- 第3步约束边界:只遵循文档内容,禁止编造——防止 LLM 幻觉
- 第4步时间校准:大模型不能精准知道当前时间,必须先调用工具获取
- 第5步参数规范:日志查询必须携带地域和日志主题,避免无效查询
- 第6步输出格式:分别总结 + 汇总,确保报告结构化
五、核心源码片段解读
5.1 知识库 Agent:文档上传与增量索引(Go)
func (c *ControllerV1) FileUpload(ctx context.Context, req *v1.FileUploadReq) (*v1.FileUploadRes, error) {
r := g.RequestFromCtx(ctx)
uploadFile := r.GetUploadFile("file")
// 1. 保存文件到本地
savePath := filepath.Join(common.FileDir)
uploadFile.Save(savePath, false)
// 2. 构建知识库索引(含增量更新逻辑)
err = buildIntoIndex(ctx, common.FileDir+"/"+newFileName)
return res, nil
}
func buildIntoIndex(ctx context.Context, path string) error {
// 创建知识库 Agent 执行器
r, _ := knowledge_index_pipeline.BuildKnowledgeIndexing(ctx)
loader, _ := loader2.NewFileLoader(ctx)
// 加载文件到内存
docs, _ := loader.Load(ctx, document.Source{URI: path})
cli, _ := client.NewMilvusClient(ctx)
// 查询 metadata 中 _source 相同的旧数据(文档更新场景)
expr := fmt.Sprintf(`metadata["_source"] == "%s"`, docs[0].MetaData["_source"])
queryResult, _ := cli.Query(ctx, common.MilvusCollectionName, []string{}, expr, []string{"id"})
if len(queryResult) > 0 {
// 提取需要删除的旧 ID
var idsToDelete []string
for _, column := range queryResult {
if column.Name() == "id" {
for i := 0; i < column.Len(); i++ {
id, _ := column.GetAsString(i)
idsToDelete = append(idsToDelete, id)
}
}
}
// 删除旧数据,避免新旧文档同时被检索到
deleteExpr := fmt.Sprintf(`id in ["%s"]`, strings.Join(idsToDelete, `","`))
cli.Delete(ctx, common.MilvusCollectionName, "", deleteExpr)
}
// 重新构建索引(分块 → 向量化 → 存入 Milvus)
ids, err := r.Invoke(ctx, document.Source{URI: path},
compose.WithCallbacks(log_call_back.LogCallback(nil)))
return nil
}
踩坑记录:未做文档版本管理时,新旧手册同时被检索到,LLM 返回矛盾答案。解决方案:通过
_source元数据(文件路径)关联同一文档的新旧版本,上传新文档时先删除旧文档的所有向量记录,再重建索引。
5.2 对话 Agent:ReAct + SSE 流式输出(Go)
func (c *ControllerV1) ChatStream(ctx context.Context, req *v1.ChatStreamReq) (*v1.ChatStreamRes, error) {
id := req.Id
msg := req.Question
// 1. 创建 SSE 客户端
client, _ := c.service.Create(ctx, g.RequestFromCtx(ctx))
// 2. 构建对话消息(含历史记忆)
userMessage := &chat_pipeline.UserMessage{
ID: id,
Query: msg,
History: mem.GetSimpleMemory(id).GetMessages(),
}
// 3. 创建对话 Agent 执行器(ReAct 模式)
runner, _ := chat_pipeline.BuildChatAgent(ctx)
// 4. 使用 stream 流式输出模式
sr, _ := runner.Stream(ctx, userMessage,
compose.WithCallbacks(log_call_back.LogCallback(nil)))
defer sr.Close()
// 5. 从流中逐块读取,通过 SSE 推送给前端
for {
chunk, err := sr.Recv()
if errors.Is(err, io.EOF) {
client.SendToClient("done", "Stream completed")
return &v1.ChatStreamRes{}, nil
}
if err != nil {
client.SendToClient("error", err.Error())
return &v1.ChatStreamRes{}, nil
}
// 逐 Token 推送
client.SendToClient("message", chunk.Content)
}
}
5.3 运维 Agent:引导式 Prompt + Plan-Execute-Replan(Go)
func (c *ControllerV1) AIOps(ctx context.Context, req *v1.AIOpsReq) (*v1.AIOpsRes, error) {
// 引导式 Prompt:指导大模型如何制定排查计划
query := `
1. 你是一个智能的服务告警运维分析助手,首先调用工具 query_prometheus_alerts 获取所有活跃的告警。
2. 分别根据告警的名称调用工具 query_internal_docs,获取告警名对应的处理方案。
3. 完全遵循内部文档的内容进行查询和分析,不允许使用文档外的任何信息。
4. 涉及到时间的参数都需要先通过工具 get_current_time 获取当前时间,再结合用户的时间要求进行传参。
5. 涉及到日志的查询,需要先通过日志工具获取相关日志信息,参数必须携带地域和日志主题。
6. 分别将告警对应查询到的信息进行总结分析,最后汇总所有告警和总结。`
// 构建 Plan-Execute-Replan Agent 并执行
resp, detail, err := plan_execute_replan.BuildPlanAgent(ctx, query)
if err != nil {
return nil, err
}
res := &v1.AIOpsRes{
Result: resp, // 汇总的分析结果
Detail: detail, // 详细执行步骤列表
}
return res, nil
}
5.4 Java 版对话 Agent:ReactAgent + SSE(Java)
@PostMapping(value = "/chat_stream", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chatStream(@RequestBody ChatRequest request) {
SseEmitter emitter = new SseEmitter(300000L);
executor.execute(() -> {
// 1. 获取历史消息
SessionInfo session = getOrCreateSession(request.getId());
List<Map<String, String>> history = session.getHistory();
// 2. 构建系统提示词(含 RAG 召回内容)
String systemPrompt = chatService.buildSystemPrompt(history);
// 3. 创建 ReactAgent(绑定工具:日志查询、告警查询、时间查询等)
ReactAgent agent = chatService.createReactAgent(chatModel, systemPrompt);
// 4. 流式执行
StringBuilder fullAnswerBuilder = new StringBuilder();
Flux<NodeOutput> stream = agent.stream(request.getQuestion());
stream.subscribe(
output -> {
if (output instanceof StreamingOutput streamingOutput) {
if (streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING) {
String chunk = streamingOutput.message().getText();
if (chunk != null && !chunk.isEmpty()) {
fullAnswerBuilder.append(chunk);
// 实时推送到前端
emitter.send(SseEmitter.event()
.name("message")
.data(SseMessage.content(chunk), MediaType.APPLICATION_JSON));
}
}
}
},
error -> emitter.completeWithError(error),
() -> {
// 完成:更新会话历史 + 发送 done 标记
session.addMessage(request.getQuestion(), fullAnswerBuilder.toString());
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.done(), MediaType.APPLICATION_JSON));
emitter.complete();
}
);
});
return emitter;
}
5.5 工具绑定:让大模型拥有工具能力
func main() {
ctx := context.Background()
// 1. 创建 ChatModel
config := &openai.ChatModelConfig{
APIKey: "xxx",
Model: "deepseek-v3-1-terminus",
BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
}
chatModel, _ := openai.NewChatModel(ctx, config)
// 2. 获取工具列表(MCP 工具 + 本地 Tool 混合)
toolList, _ := tools2.GetLogMcpTool() // MCP 日志查询工具
toolList = append(toolList, tools2.NewGetCurrentTimeTool()) // 本地时间查询工具
// 3. 提取工具信息
toolInfos := make([]*schema.ToolInfo, 0)
for _, todoTool := range toolList {
info, _ := todoTool.Info(ctx)
toolInfos = append(toolInfos, info)
}
// 4. 将工具绑定到 ChatModel
chatModel.BindTools(toolInfos)
// 5. 创建处理链并编译运行
chain := compose.NewChain[[]*schema.Message, *schema.Message]()
chain.AppendChatModel(chatModel, compose.WithNodeName("chat_model"))
agent, _ := chain.Compile(ctx)
resp, _ := agent.Invoke(ctx, []*schema.Message{
{Role: schema.User, Content: "告诉我你有哪些工具可以使用"},
})
fmt.Println(resp.Content)
}
MCP 工具和本地 Tool 对大模型完全透明——大模型不需要知道某个工具是本地函数还是远程 MCP 服务,它只看到统一的 ToolInfo 结构。这种设计让工具的"本地/远程"切换对 Agent 透明,极大降低了维护成本。
六、性能边界、局限与工程化优化
6.1 性能基线
以下为系统在实际部署中的性能数据(qwen3-max(通义千问)作为主模型,text-embedding-v4 作为 Embedding 模型):
| 指标 | 数值 | 说明 |
|---|---|---|
| 文档上传延迟 | ~3s/100KB | 包含解析 + 分块 + 向量化 + 入库 |
| 快速对话响应 | 1-3s | 单轮无工具调用 |
| 流式对话首 Token | <1s | 用户感知"开始回答"的时间 |
| 知识库检索(召回+重排) | ~120ms | 向量召回 50ms + 重排 70ms |
| 运维 Agent 诊断耗时 | 2-5min | 取决于 Plan-Execute-Replan 迭代轮数 |
| 知识检索准确率 | 85%+ | 按语义段落分块 + TopK=3 |
6.2 业务价值量化
| 维度 | Agent 前 | Agent 后 | 改善幅度 |
|---|---|---|---|
| 重复咨询响应时间 | 5-10 分钟(人工) | 秒级(AI 自动回复) | 99%+ |
| 故障排查时间 | 1-4 小时(人工全链路) | 2-5 分钟(Agent 自动) | 90%+ |
| 知识检索方式 | 手动翻文档目录 | 自然语言语义检索 | 质变 |
6.3 当前局限
- 文档质量依赖:RAG 的召回质量高度依赖原始文档的规范性。如果文档随心所欲地写、语义相关信息散落各处,再好的分块策略也无法保证召回准确率
- 多轮对话状态依赖内存:当前 Memory 存储在进程内存中,服务重启会导致对话上下文丢失。需要引入 Redis 等外部存储做持久化
- Agent 间通信尚未实现:当前三个 Agent 独立运行,复杂场景需要人工编排。如"对话 Agent 发现用户描述的是告警 → 自动转交运维 Agent"的联动尚需开发
- 流式对话记忆缺失:Go 版流式对话接口暂未接入记忆功能(项目预留了 TODO 挑战)
- 总结 Agent 未实现:多轮对话超出窗口时直接 FIFO 丢弃,摘要压缩机制尚未落地
6.4 工程化优化方案
| 优化方向 | 具体措施 | 预期收益 |
|---|---|---|
| 对话记忆持久化 | Memory 从内存迁移到 Redis,支持服务重启后恢复 | 消除状态丢失风险 |
| 对话摘要压缩 | 超 5 轮时调用 LLM 生成历史摘要,替换原文 | Token 使用率降低 60% |
| 多用户会话隔离 | 用户 ID 作为会话标识,独立 Memory 实例 | 支持万级并发用户 |
| Agent 联动 | 对话 Agent 识别告警意图后自动转交运维 Agent | 无需人工切换 |
| 知识自动沉淀 | 工单处理结果自动总结入库 | 形成处理-沉淀-复用闭环 |
| 部署容器化 | Docker 部署 Milvus + 前端 + 后端 | 环境一致性,快速部署 |
6.5 部署架构
项目部署在办公环境开发机中,用 Docker 启动三个容器(Milvus + 后端 + 前端),办公网可直接访问,不必为线上网络和办公网络打通而烦恼。
性能基线可视化对比:
| 颜色通道 | 含义 | 关键瓶颈 |
|---|---|---|
| 文档上传 3s | 解析+分块+向量化+入库 | Embedding API 调用为主要耗时 |
| 知识库检索 120ms | 向量召回 50ms + 重排 70ms | Cross Encoder 逐对计算 |
| 快速对话 2s | 单轮无工具调用 | LLM 推理延迟 |
| 流式首 Token <1s | SSE 逐 Token 推送 | 用户感知快于实际完成 |
| 运维诊断 2-5min | Plan-Execute-Replan 多轮迭代 | 取决于工具调用次数与 LLM 推理 |
运维诊断耗时(2-5min)是系统的性能天花板——每次 Replan 都需要调用 LLM 重新规划,而 LLM 推理延迟不可控。优化方向:缓存常见告警类型的排查计划模板,减少 Planner 的实时推理次数;对 Executor 的工具调用做并行化处理(无关工具并发执行)。
七、未来迭代路线
短期目标——完善对话记忆机制。将 Memory 从内存迁移到 Redis,实现服务重启后对话恢复。同时实现"总结 Agent"——超过 5 轮时自动对历史对话生成摘要,在保证上下文连贯性的前提下将 Token 使用率降低 60%。
中期目标——打通 Agent 联动链路。用户在对话中描述异常症状时,对话 Agent 自动识别告警意图,将上下文无缝转交运维 Agent 启动诊断流程。同时接入群聊,将工单问题和解决方案的聊天记录自动总结沉淀为知识库文档。
长期目标——构建强化学习反馈闭环。每次运维诊断后,系统跟踪"告警是否真正被解决"的反馈信号(如告警是否在 30 分钟内未复现),用这个信号持续优化 Planner 的策略质量。这将使系统从"基于规则的智能"进化为"基于经验的智能"——越用越聪明,正是 Agent 系统的终极愿景。
回溯整个系统,有以下几个值得在面试或技术分享中重点展开的设计决策:
- RAG 两阶段检索(召回+重排):向量召回 Top-10 保证速度,Cross Encoder 重排 Top-3 保证精度,“先广撒网再精挑细选”。分块参数通过数据驱动的方式调优——从历史工单整理测试集,交叉验证不同分块策略和 TopK 组合,最终达到 85%+ 检索准确率
- ReAct vs Plan-Execute-Replan 场景化选型:对话场景用 ReAct(灵活实时决策,适合开放式的多轮业务咨询),运维场景用 Plan-Execute-Replan(结构化规划+动态纠偏,适合多步骤故障排查)。两种模式共享同一套工具集和知识库,但工作模式完全不同
- MCP 协议即插即用:将通用 Tool 变成独立服务统一托管,Agent 通过 MCP 协议远程调用。本地 Tool 和 MCP Tool 对大模型完全透明,工具的"本地/远程"切换零代码改动
- SSE 流式输出:基于 HTTP 的单向推送协议,比 WebSocket 更轻量。四种事件类型(connected/message/error/done)覆盖完整的流式对话生命周期
- Eino Graph 编排:用图定义 Agent 工作流,节点是原子能力,边是数据流向。Lambda Node 做数据转换,ChatTemplate 做 Prompt 构建,ReAct Agent 做多轮推理——整个流程可视化、可调试
- 引导式 Prompt 设计:运维 Agent 通过 6 步结构化 Prompt 指导大模型制定排查计划,从"先获取告警"到"最后汇总分析",每一步都有明确的工具调用指引和约束条件
- 文档版本增量管理:上传新文档时通过
_source元数据查询并删除旧文档的所有向量记录,避免新旧文档同时被检索到导致 LLM 返回矛盾答案 - 知识闭环沉淀:每次运维诊断的结果自动更新知识库,形成"诊断 → 沉淀 → 下次更快诊断"的正循环。这是系统"越用越聪明"的工程化保障
在代码构筑的数字秩序中,没有银弹,只有不断迭代的工程智慧。智能 OnCall Agent 不是终点,而是运维自治化的第一块基石。当 Agent 学会"预见故障"而非仅仅"响应告警"时,那才是数字基建真正成熟的那一刻。
项目资源链接:智能 OnCall Agent 项目资源
更多推荐



所有评论(0)