定位:专业复盘体 + 分点 + 图。原稿:《运维诊断 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

另外两个坑更简单,也更典型:

  1. 把基础能力做成了节点:我们特意加了一个 Composer 节点,负责把证据汇总成报告。后来发现,总结归纳本来就是大模型自带的能力,在 ReAct 循环末尾通过 Prompt 要求它按格式输出即可,单独建模反而多花一次 API 调用、多一次延迟和 token 消耗。
  2. 把代码的活交给了 LLM:Gatekeeper 校验证据 ID 是否合法,VerifiedInput 做数据清洗,本质是参数校验、格式转换、字段过滤,几行正则或 if 就能完成,结果还是确定的。交给 LLM 成本更高、还可能漏判,等于把确定性任务交给了不确定的东西。

重构:推理交给 ReAct,校验交给代码

踩完这三个坑,我们把架构拆了重来。核心思路一句话:推理交给 ReAct,校验交给代码。具体做了三件事:

  1. 合并节点:把 Planner、Executor、Composer 合并成一个 DiagnosisAgent。它内部是一个 while 循环,持续「理解 → 决策 → 执行 → 观察」,直到得出结论。所有推理都在同一个上下文里完成,连贯性自然恢复。
  2. 代码层守门:把原来的 Gatekeeper 和 VerifiedInput 下沉为工具前后的拦截器。工具执行前由代码校验入参(时间格式、ID 合法性),执行后由代码清洗返回(上千行日志只保留 ERROR),最终输出用 JSON Schema 校验器强制检查格式,不合格就重试,全程不调用 LLM。这样 LLM 只负责思考,确定性的工作由代码完成,可靠且成本低。
  3. 验收异步化:如果业务确实需要二次验证,保留一个独立的验收 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 没有用,关键是优化上下文管理,主要做了三件事:

  1. 卸载原始数据:工具返回的大段日志或监控数据,直接写入外部文件(如 refs/xxx.md),上下文里只保留摘要和文件路径,需要细节时按需读取。
  2. 返回前先压缩:工具结果返回给 Agent 之前,由 Harness 代码先过滤,比如从上千行日志里只挑出 5 条 ERROR 再交给模型。
  3. 早期历史折叠:多轮对话中,把前几轮的完整返回压缩成一句话摘要,只保留最近两三轮的详细内容,上下文窗口始终保持可控。

这个滑窗机制可以这样理解:

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:

  1. 工具数量超过 10 个,模型选择准确率明显下降。
  2. 压缩卸载之后,上下文仍然装不下必需信息。

总结

一个成熟的 Agent 系统,核心在于把推理收敛到 ReAct 循环、把确定性逻辑下沉到代码层,组件数量和流程图复杂度反而是次要的。用 while 驱动思考,用 Harness 管住边界,系统才能既清爽又稳定。

重构之后,平均响应时间和 token 消耗都显著降低,因为省去了多 Agent 之间的状态同步,也不再需要维护多套 Prompt 和 JSON 结构。


附:设计前的检查顺序(自检清单)

  1. 能不能合并节点:相邻节点之间没有人工审批或长时等待,就合并成一次 ReAct 动作。
  2. 上下文会不会膨胀:跑一次任务,观察历史是否全量累加,是的话就做卸载和压缩。
  3. 工具职责是否单一:一个 Agent 不要同时持有跨域工具(如既读文件又写数据库),按安全域拆分。
  4. 任务依赖是否线性:线性依赖不上多 Agent;只有真正可并行的耗时子任务才考虑 DAG。
  5. 能否写成代码校验:能写代码校验的,就不要只写在 Prompt 里,更不要做成 Graph 节点。