定位:专业复盘体 + 分点 + 图。原稿:《运维诊断 Agent 重构:如何避免过度设计》
过度设计是 AI Agent 落地过程中很常见的一个坑。一开始的需求其实很简单:用户提问 → 查询知识库或日志 → 输出诊断报告。
但当时为了让方案「更专业」,我们设计了一条六节点的流水线:Planner → Executor → Gatekeeper → VerifiedInput → Verifier → Composer。每个节点是一次独立的 LLM 调用或处理函数,状态在 Graph 里传递。上线之后,问题接连出现。
踩到的三个坑
先说我踩的第一个坑:把推理动作拆成了两个节点。
我们把「规划要查什么」(Planner)和「执行查询」(Executor)拆成了两个节点。但这两步本质上属于同一个 ReAct 循环里交替发生的动作:想一步、做一步、看结果、再想下一步。拆开之后,状态需要在节点间来回搬运,推理链条被切断,模型前后不一致的情况明显增多。
flowchart LR
subgraph 过度设计
P[Planner] --> E[Executor]
E -->|状态返回| P
end
subgraph 正确做法
R[ReAct循环] --> T[思考]
T --> A[行动]
A --> O[观察]
O -->|未完成| T
O -->|完成| 结束
end
另外两个坑更简单,也更典型:
- 把基础能力做成了节点:我们特意加了一个 Composer 节点,负责把证据汇总成报告。后来发现,总结归纳本来就是大模型自带的能力,在 ReAct 循环末尾通过 Prompt 要求它按格式输出即可,单独建模反而多花一次 API 调用、多一次延迟和 token 消耗。
- 把代码的活交给了 LLM:Gatekeeper 校验证据 ID 是否合法,VerifiedInput 做数据清洗,本质是参数校验、格式转换、字段过滤,几行正则或 if 就能完成,结果还是确定的。交给 LLM 成本更高、还可能漏判,等于把确定性任务交给了不确定的东西。
重构:推理交给 ReAct,校验交给代码
踩完这三个坑,我们把架构拆了重来。核心思路一句话:推理交给 ReAct,校验交给代码。具体做了三件事:
- 合并节点:把 Planner、Executor、Composer 合并成一个 DiagnosisAgent。它内部是一个 while 循环,持续「理解 → 决策 → 执行 → 观察」,直到得出结论。所有推理都在同一个上下文里完成,连贯性自然恢复。
- 代码层守门:把原来的 Gatekeeper 和 VerifiedInput 下沉为工具前后的拦截器。工具执行前由代码校验入参(时间格式、ID 合法性),执行后由代码清洗返回(上千行日志只保留 ERROR),最终输出用 JSON Schema 校验器强制检查格式,不合格就重试,全程不调用 LLM。这样 LLM 只负责思考,确定性的工作由代码完成,可靠且成本低。
- 验收异步化:如果业务确实需要二次验证,保留一个独立的验收 Agent,但不要挂在主链路上。让它异步执行交叉检查(如时间线是否对齐),不影响实时响应。
重构后的流程:
flowchart TD
Start[用户提问] --> Agent[DiagnosisAgent]
subgraph ReAct循环
Think[思考/规划] --> Act[工具调用]
Act --> Observe[观察结果]
Observe -->|证据不足| Think
Observe -->|证据充分| Output[生成报告]
end
Agent --> Harness[Harness代码层<br>校验入参 / 压缩返回]
Harness --> Linter[JSON Schema Linter<br>0次LLM调用]
Linter --> End[诊断报告]
单 Agent 带来的新问题:上下文腐烂
合并之后很快发现一个新问题:工具调用超过 7 次后,上下文越来越长,模型开始重复信息、乱编内容。这就是上下文腐烂。
要解决上下文腐烂,退回多 Agent 没有用,关键是优化上下文管理,主要做了三件事:
- 卸载原始数据:工具返回的大段日志或监控数据,直接写入外部文件(如 refs/xxx.md),上下文里只保留摘要和文件路径,需要细节时按需读取。
- 返回前先压缩:工具结果返回给 Agent 之前,由 Harness 代码先过滤,比如从上千行日志里只挑出 5 条 ERROR 再交给模型。
- 早期历史折叠:多轮对话中,把前几轮的完整返回压缩成一句话摘要,只保留最近两三轮的详细内容,上下文窗口始终保持可控。
这个滑窗机制可以这样理解:
flowchart LR
subgraph 早期轮次
E1[第1轮完整返回] -->|压缩| E2[摘要]
E3[第2轮完整返回] -->|压缩| E4[摘要]
end
subgraph 近期轮次
R1[第3轮详细返回]
R2[第4轮详细返回]
end
早期 --> 近期
Note[上下文窗口保持恒定大小]
单 Agent 还需要 Graph 吗
单 Agent 不需要那个重编排的 Graph,控制流就是一个 while 循环。只有需要状态持久化和断点续跑时(任务跑到一半存档、等人工审批后继续),才会用到 LangGraph 这类框架的检查点功能,此时用到的是它的 checkpoint 能力,与多节点编排无关。
什么时候升级到多 Agent
看任务拓扑。线性深挖(一个结论决定下一步查什么,一路追下去)适合单 Agent,拆开反而丢上下文。扇出并行(同时查 DB、Redis、网关等互不依赖的数据源)才适合用 Supervisor 派多个子 Agent 并行执行,各自返回摘要,主控汇总。
flowchart LR
subgraph 线性深挖_单Agent
L1[查日志] --> L2[查数据库] --> L3[查配置] --> L4[结论]
end
subgraph 扇出汇聚_Supervisor多Agent
S[Supervisor] --> D1[查DB Agent]
S --> D2[查Redis Agent]
S --> D3[查网关 Agent]
D1 -->|摘要| S
D2 -->|摘要| S
D3 -->|摘要| S
S --> 汇总结论
end
两个硬指标,满足任一就考虑多 Agent:
- 工具数量超过 10 个,模型选择准确率明显下降。
- 压缩卸载之后,上下文仍然装不下必需信息。
总结
一个成熟的 Agent 系统,核心在于把推理收敛到 ReAct 循环、把确定性逻辑下沉到代码层,组件数量和流程图复杂度反而是次要的。用 while 驱动思考,用 Harness 管住边界,系统才能既清爽又稳定。
重构之后,平均响应时间和 token 消耗都显著降低,因为省去了多 Agent 之间的状态同步,也不再需要维护多套 Prompt 和 JSON 结构。
附:设计前的检查顺序(自检清单)
- 能不能合并节点:相邻节点之间没有人工审批或长时等待,就合并成一次 ReAct 动作。
- 上下文会不会膨胀:跑一次任务,观察历史是否全量累加,是的话就做卸载和压缩。
- 工具职责是否单一:一个 Agent 不要同时持有跨域工具(如既读文件又写数据库),按安全域拆分。
- 任务依赖是否线性:线性依赖不上多 Agent;只有真正可并行的耗时子任务才考虑 DAG。
- 能否写成代码校验:能写代码校验的,就不要只写在 Prompt 里,更不要做成 Graph 节点。