本文是「Agent 开发实践与思考」系列第 13 篇。系列目录:
- 2024
- 2025
从一个同步问答工具说起
第一版巡检助手是同步的。我们把历史故障复盘、预案和排查手册整理成知识库,线上出了问题,值班同学把现象发给 Agent,Agent 自己检索知识库,定位问题并给出排查建议,最后由人执行处理。它的定位能力确实有用,但运行一段时间后暴露的瓶颈不在定位质量,而在协作方式:告警先到达人,人再转述给 Agent,Agent 的结论再由人执行。发现和处理这两环都靠人,响应速度的上限是值班同学看到消息的速度,人不在场时 Agent 完全不工作。
改造的目标是把发现和处理两环也从人手里拿过来,定位环节不再依赖人的转述。监控平台的接口异常、JS Error 告警通过 webhook 直接进入系统;cron 定时触发例行巡检和每周性能分析;Agent 拿到原始告警自己定位、自己处理,处理不了或不该自动处理的转人工。运行形态从”人问一句答一句”变成常驻进程,监听消息队列和 HTTP 回调。
架构上动的其实不多:一个事件队列,一个带检查点的循环,一套状态外置的约定。真正变化的是触发权和责任边界。下面按改造顺序记录,每节先写巡检系统里实际发生的事,再写抽象出来的结论。
事件先统一成队列
改造的第一步不是动 Agent 的循环,而是把”什么能叫醒 Agent”统一掉。巡检系统接入的事件源有四类:
- 监控告警 webhook。接口错误率越限、JS Error 上报激增、性能指标异常。
- HTTP 回调。工单状态变更、人工审批结果返回。
- cron 定时事件。每小时的例行巡检,每周一次的性能分析。
- IM 人工消息。人直接发来的问题,只是事件类型的一种,不再有特殊地位。
四类来源协议不同、触发方式不同,但对 Agent 来说是同一样东西:一条带着类型、负载和时间戳的消息。所以我在 Agent 前面加了一个事件队列,所有来源先写队列,Agent 只从队列里取。Agent 循环从”等待用户输入”改成”等待事件”。
事件定义成统一结构:类型、优先级、负载、时间戳,外加一个去重键和事件 ID。去重键很快证明是必要的。一次接口超时故障,监控平台一分钟能发五十条告警;某个前端版本发布后,同一个 JS Error 几千条上报涌进来。没有去重,Agent 就被同一件事叫醒几千次。队列侧按去重键合并时,要先明确语义。对状态型告警,窗口内相同键的事件可以只保留最新一条;对审批结果、工单回调这类不可丢的事实,不能简单覆盖,应该全部保留并靠消费幂等处理。
这个改动带来的简化很实际:事件可以持久化,可以重放,Agent 挂掉期间到达的告警积压在队列里不丢,调试时还能往队列里手动塞一条消息复现问题。消费时要有明确的 ack 语义。我的做法是先持久化事件,再在完成状态更新和必要副作用记录后 ack;处理失败保留重试信息,超过次数转人工,而不是无条件重放。
定位数据通过 MCP 工具层接入
阶段一的 Agent 只有知识库可以查,定位深度受限于历史预案的覆盖面。阶段二要自动定位,Agent 得自己取到原始数据:git 仓库里的代码和最近提交、变更平台上的发版和配置修改记录、日志系统的异常堆栈、trace 的调用链、指标平台的时序数据。这些数据分散在不同系统里,每个系统一套认证方式和返回格式。
最早我给每个系统写一个专用工具函数。接到第三个月问题就很明显:工具数量膨胀,每个工具的描述、参数 schema、错误返回格式都要单独维护,新接一个系统就要改 Agent 的代码重新部署。换成 MCP 之后,每个外部系统对应一个 MCP server,工具发现、参数描述和错误格式统一,新增数据源变成挂一个新 server 的配置变更,Agent 循环不用动。这个变化的影响在 MCP 用了三个月 里写过,这里只补一条巡检场景特有的要求:诊断类 server 只暴露只读工具。
只读边界放在工具层是有意为之。后面会讲到动作分级,其中”只读诊断全自动”这一条,保证不来自提示词里的一句”你只能查询不能修改”,而来自 server 根本没有写工具。提示词约束可能被注入绕过,工具列表里不存在的操作模型编不出来。诊断 server 用只读凭据,处置 server 用受限凭据,两类凭据分开签发,任一类泄露时破坏范围都有上限。
一次完整的自动定位长这样。接口 5xx 告警进队列,Agent 先查变更记录 server,发现告警前八分钟有一次服务发版;调 git server 看这次提交的 diff,改动集中在一个下游调用;再查日志 server,异常堆栈恰好落在新代码路径上;结论是”发布引入,建议回滚”,连同证据一起写进待办。整个链条没有人工参与,每个判断后面都挂着从对应系统取到的原始证据,早上值班同学确认时不需要重新查一遍。
打断的是计划,不是请求
改造中最关键的一个认识,值得单独说:异步 Agent 的”打断”,打断的是计划,不是 HTTP 请求。
LLM 的 API 调用是阻塞的一来一回。请求发出去,模型开始生成,调用方只能等它吐完。生成一段要十几秒,一次工具调用再走一轮又是十几秒,一个多步任务跑几分钟很正常。每周性能分析这种任务要遍历二十几个核心页面、拉取一周的指标再生成报告,跑二十几分钟。如果这二十分钟里队列进来一条高优先级事件,比如接口 5xx 告警说线上服务挂了,怎么办?
直觉方案是去”中断”那个进行中的 LLM 调用。这在技术上没有意义。HTTP 请求已经发出去了,模型在服务端继续算,本地取消连接,服务端未必停,而且已经收到的半截输出也没法变成”模型此刻的完整想法”。
可行的做法是把步骤拆成尽可能小的单元。一个 LLM 调用通常不能由本地可靠地中途撤销,一个工具调用则要看工具是否支持取消,所以不能笼统地把所有步骤都当成不可打断。对下载、等待、批处理这类可取消步骤,要传递取消信号并在工具侧确认;对确实不可取消的步骤,只能在步骤边界检查新事件。高优先级事件到达时,Agent 放弃当前计划,把进度存下来,转头处理新事件,处理完再决定要不要回来。伪代码大概是这样:
loop:
event = queue.peek_high_priority()
if event and event.priority > current_task.priority:
checkpoint(current_task) # 存下当前进度
current_task = handle(event)
continue
step = plan_next_step(current_task)
result = execute(step) # 可取消步骤响应取消;其他步骤在边界完成
update(current_task, result)也就是说,响应新事件的延迟上限是一个步骤的时长,而不是整个任务的时长。性能分析任务要按页面拆步骤,每分析完一个页面是一个检查点。如果让模型一口气生成整份周报,单个步骤长达二十分钟,“打断”名义上存在,实际上永远等不到检查点。
举个实际发生的例子。Agent 跑性能分析到第十二个页面,队列进来接口 5xx 告警。检查点存下进度,Agent 转去查日志、看 trace,确认是某个下游服务超时,按预案自动摘流重启对应实例,指标恢复;处理完回来从第十三个页面继续分析,而不是从头再跑。放弃当前计划不等于丢掉当前计划,checkpoint 的意义就在这里。另外注意伪代码里比较的是任务优先级,不是所有新事件都能插队,一个普通事件来了,当前任务该跑完还是跑完。
这个认识也解释了为什么流式输出帮不上忙。流式让你更早看到 token,但不改变”模型生成期间不可对话”的事实。可打断性来自步骤切分,不来自传输方式。
自动处理的边界由动作可逆性决定
同步场景里,Agent 每个危险动作之前理论上都有人在场,审批弹窗有人点。巡检 Agent 没有人看着,凌晨三点被告警触发,做什么都不会有人当场确认,而且它的处置动作直接作用在线上环境。权限必须比同步场景更细。
我们按动作的可逆性分三级:
- 只读诊断全自动。查日志、trace、指标、近期变更记录、检索知识库预案,全部走只读 MCP server,不产生副作用,随便跑。
- 可逆的低风险动作自动执行。重启实例、摘流、清缓存、临时限流。出问题可以手动恢复,所以允许自动做,但每个动作必须写审计日志:哪个实例在什么时间因为什么事件执行了什么动作,事后要能完整还原一次任务的来龙去脉。
- 不可逆或影响面大的动作转人工待办。回滚版本、改线上配置、写业务库、发版。Agent 把诊断结论、建议动作和回滚方案写好,等人确认才执行。
两种真实的夜间场景。一种是已知模式:告警触发,Agent 定位到某个实例内存泄漏,按预案自动摘流重启,指标恢复,全程没人参与,结果进日报。另一种是未知模式:Agent 怀疑是一次发布引入的问题,开好回滚单、附上诊断证据,早上值班同学确认后执行。异步不等于全自动,更准确的说法是把判断前移,把放行留给人的时间表。MTTR 里被压缩的是定位时间,放行时间仍然由人控制。
提示注入的风险在这个场景也放大了。巡检 Agent 处理的事件来自外部:JS Error 堆栈、告警文本、用户反馈内容都是不可信输入,里面完全可以藏一句”忽略之前的指令”。事件进入队列时就该被标记为数据,提示词里显式声明这一点,但这只能减少误解,不能当作防护本身。拼接上下文时要区分系统指令、任务指令和外部数据,外部文本不能改变工具权限或审批状态;工具层还要校验参数和调用者身份,高风险副作用走独立审批,审计保留原始事件和最终动作。
执行环境同样按无人值守的标准来。执行环境沙箱化,Agent 能碰的网络范围收在容器里;凭据按最小权限发,巡检 Agent 只拿监控和日志系统的只读账号,加处置接口的受限凭据,哪怕它被提示注入骗了,能造成的破坏也有上限。这些原则在拆解 Coding Agent里聊过,同步场景里是最佳实践,异步场景里是底线。
主动找用户说话是产品设计
Agent 会自己醒来了,下一个问题是:它什么时候有资格打扰我?
这是产品设计问题,不是技术问题。技术上发一条消息没有任何难度,难的是界定边界。第一版我们没想清楚,例行巡检每次跑完都发一条”巡检正常”,三天之后所有人开始无视这个群,等于白做。
后来定的规则分三层。按结果分级:自动处理成功的事件不即时打扰,攒进日报;处理失败或需要审批的即时推送。按渠道分级:紧急的走 IM 强提醒,不紧急的走邮件或看板,渠道本身就在传达紧急程度。按内容分级:每条主动消息必须带”所以呢”,只发”接口 A 错误率越限”没用,要带上”已定位为下游 B 超时,建议摘流重启,或回复忽略”。每条主动打扰都应该让接收者能用一个动作闭环,否则就是在制造通知噪音。巡检正常这种事属于”没有消息就是好消息”,根本不该发。
每周性能报告也按这个原则改:固定时间发,只列指标变化超过阈值的页面和具体数值;没有异常就一句话带过,不贴几十页完整数据。
最后加了一条兜底:任何 Agent 对任何渠道有频率上限,一小时最多几条。告警风暴时宁可合并成一条”过去一小时共 23 条告警,已合并为 3 个事件,摘要如下”,也不能让 Agent 把群刷爆。被用户屏蔽的 Agent,技术再对也等于不存在。
状态外置,防僵尸 Agent
同步 Agent 的生命周期是一次会话,结束即销毁。巡检 Agent 是长生命周期的,可能连续跑几个月,中间经历部署、重启、崩溃。结论很直接:状态必须放在进程外面。
任务进度、当前计划、待处理事件,全部落在数据库或队列里,进程本身无状态。进程挂了,换一个实例捞起来,从上次 checkpoint 继续。这和 Coding Agent 的 sessionless 设计是同一个思路,只是动机从”方便人重开会话”变成了”系统必须能自愈”。
这条是用一次事故换来的。改造早期我把当前计划存在进程内存里,一次常规部署重启,跑了四十分钟的性能周报从头再来,更糟的是它已经发出去一半的通知又发了一遍。那次之后定了规矩:凡是跨步骤的状态,写库;进程内只允许存在当前步骤的临时变量。
长生命周期还带来一个同步场景没有的问题:僵尸 Agent。进程没死透,心跳还在,但卡在一个永远返回不了的工具调用上;或者两个实例都以为自己是活的,同一个告警被处理两遍,摘流执行两次。标准解法是心跳加租约:Agent 周期性续租,租约过期就视为死亡,由调度方回收任务重新分配。任务的领取除了记录租约,还要带一个单调递增的 fencing token。实例每次更新状态或调用支持版本条件的副作用接口时带上这个 token,旧实例即使心跳恢复,也不能覆盖新实例的结果。重复执行仍无法完全杜绝时,写操作要做幂等,使用事件 ID 或业务幂等键去重,不能只依赖租约。
下一步:自动化测试接入巡检闭环
现在的系统是响应式的:监控报什么,Agent 处理什么。但监控覆盖有盲区。页面样式错乱、核心流程的按钮点不动、接口正常但渲染白屏,这类问题不一定触发任何告警,往往等用户反馈才发现。响应式巡检的上限是监控体系的覆盖率。
下一步是打通自动化测试,从响应异常走向主动验证:定时拉起无头浏览器,真实访问线上核心页面,跑关键交互路径,采集截图、DOM 快照、网络请求和性能指标;与基线做视觉对比和断言,比如截图差异超过阈值、关键元素不存在、核心接口调用失败;检测到问题就生成一条标准事件写回队列。发现问题之后的定位、处置、通知,完全复用现有闭环。
这件事在架构上的意义大于功能本身:新能力以新事件源的形式接入,Agent 循环、打断逻辑、权限分级、通知规则全都不用改。统一事件抽象的回报在这里兑现,接入主动验证和当初接入告警 webhook 是同一个动作。
新成本也要提前算清楚。页面正常改版会触发视觉差异,基线截图必须随发版更新,否则误报会淹没队列;自动化用例本身的稳定性需要维护,不稳定用例产生的噪音和监控告警风暴是同一种问题,药方也一样:去重、合并、阈值。主动验证的产出质量,取决于基线维护和用例质量,这两件事目前没有 Agent 能替人做完。
收尾
巡检系统的事实讲完,把可复用的结论集中在这里:
- 触发权转移是根本变化。同步 Agent 的触发权在人,异步 Agent 的触发权在事件。统一队列、检查点、打扰规则、权限分级,都是从这一条长出来的。
- 统一事件抽象决定扩展性。所有来源收敛为”类型、优先级、负载、去重键”的消息之后,接入新能力就是接入新事件源,Agent 循环本身不动。
- 可打断性来自步骤切分,不来自传输方式。流式输出、取消 HTTP 请求都不解决打断问题,把任务拆成可在边界放弃的小步骤才解决。
- 打扰是预算。每条主动消息消耗用户的注意力配额,分级、汇总、频率上限需要像功能一样设计和评审。
- 自动化程度由动作可逆性决定。只读操作全自动,可逆动作自动执行加审计,不可逆动作转人工待办。
- 状态外置和租约是长生命周期的前提。进程无状态、checkpoint 落库、fencing token 防并发实例、写操作幂等,这一套缺一个,都会在某次部署或宕机之后补课。
还没解决的问题也明确:自动处理的正确率怎么持续评估,该发现而没发现的漏报怎么记账,处置经验回写知识库的质量谁来把关。巡检 Agent 跑起来之后,怎么评估一个长期运行的 Agent,是接下来要补的课。

