本文是「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 循环从等待用户输入改为等待事件。
事件采用统一结构:类型、优先级、负载、时间戳、去重键和事件 ID。去重键是必要字段。一次接口超时故障中,监控平台一分钟可发送五十条告警;某个前端版本发布后,同一个 JS Error 可能同时涌入几千条上报。缺少去重时,Agent 会为同一问题重复创建任务。队列按去重键合并前,需要先定义语义:状态型告警在窗口内可只保留相同键的最新事件;审批结果、工单回调等不可丢失的事实不能被覆盖,应全部保留,并通过消费幂等处理。
事件队列支持持久化和重放。Agent 故障期间到达的告警会积压在队列中;调试时也可以手动写入一条消息复现问题。消费需要定义 ack 语义。我的做法是先持久化事件,完成状态更新和必要副作用记录后再 ack;处理失败则保留重试信息,超过次数后转人工,不进行无条件重放。
通过 MCP 工具层读取定位数据
阶段一的 Agent 只能查询知识库,定位深度受历史预案覆盖面限制。自动定位需要读取原始数据,包括 git 仓库中的代码和最近提交、变更平台中的发版和配置修改记录、日志系统的异常堆栈、trace 调用链和指标平台的时序数据。这些数据分散在不同系统,各自使用不同的认证方式和返回格式。
最早我为每个系统编写专用工具函数。到第三个月,维护成本开始显现:工具数量增加后,描述、参数 schema 和错误返回格式都要分别维护;接入一个系统还要修改 Agent 代码并重新部署。改用 MCP 后,每个外部系统对应一个 MCP server,工具发现、参数描述和错误格式保持一致。新增数据源只需增加一个 server 配置,Agent 循环无需修改。MCP 用了三个月讨论过这一变化;巡检场景还有一项要求:诊断类 server 只提供只读工具。
将只读边界放在工具层,是为了约束后文的动作分级。只读诊断能够自动执行,不依赖提示词中的“只能查询不能修改”,而是因为诊断 server 不提供写工具。提示词约束可能被提示注入绕过;工具列表中不存在的操作无法被调用。诊断 server 使用只读凭据,处置 server 使用受限凭据,两类凭据分别签发,任一类泄露时的影响范围受到限制。
一次自动定位过程如下:接口 5xx 告警写入队列后,Agent 先查询变更记录 server,发现告警前八分钟发生过一次服务发版;接着通过 git server 查看该提交的 diff,发现改动集中在一个下游调用;再查询日志 server,异常堆栈落在新代码路径上。Agent 给出“发布引入,建议回滚”的结论,并将证据写入待办。该过程无需人工参与,每个判断都对应从相关系统读取的原始证据;值班同学早上确认时无需再次收集这些数据。
在步骤边界切换任务计划
异步 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,确认某个下游服务超时,按预案自动摘流并重启对应实例,指标恢复;随后从第十三个页面继续分析,无需从头执行。保存检查点可以保留当前计划。伪代码按任务优先级比较,普通事件不会中断正在执行的任务。
流式输出不能改变模型生成期间无法继续对话的限制。可打断性取决于步骤切分,而非传输方式。
按动作可逆性划分自动处理权限
同步场景中,Agent 执行危险动作前通常有人在场处理审批弹窗。巡检 Agent 可在凌晨三点由告警触发,现场可能没有人确认,其处置动作直接作用于线上环境,因此权限需要更细的划分。
我们按动作可逆性分为三级:
- 只读诊断自动执行:查询日志、trace、指标、近期变更记录和知识库预案,均通过只读 MCP server 完成,不产生副作用。
- 可逆的低风险动作自动执行:重启实例、摘流、清缓存、临时限流。此类动作可由人工恢复,因此允许自动执行;每个动作必须写入审计日志,记录哪个实例在何时因何事件执行了什么动作,以便事后还原任务过程。
- 不可逆或影响面大的动作转人工待办:回滚版本、修改线上配置、写业务库、发版。Agent 写入诊断结论、建议动作和回滚方案,等待人工确认后执行。
夜间可能出现两类场景。对于已知模式,告警触发后,Agent 定位某个实例的内存泄漏,按预案自动摘流重启,指标恢复,结果写入日报。对于未知模式,Agent 判断问题可能由一次发布引入,创建回滚单并附上诊断证据,由早上的值班同学确认后执行。异步执行将诊断提前,人工仍控制需要放行的动作。MTTR 被缩短的部分是定位时间,放行时间仍由人控制。
提示注入风险在此场景中也更高。巡检 Agent 处理的事件来自外部,JS Error 堆栈、告警文本和用户反馈都属于不可信输入,其中可能包含“忽略之前的指令”。事件写入队列时应标记为数据,并在提示词中明确这一点;这只能减少误解,不能作为防护措施。拼接上下文时应区分系统指令、任务指令和外部数据,外部文本不得改变工具权限或审批状态;工具层还应校验参数和调用者身份,高风险副作用通过独立审批执行,审计记录原始事件和最终动作。
执行环境也应满足无人值守要求。执行环境需要沙箱化,Agent 可访问的网络范围限制在容器中;凭据按最小权限签发,巡检 Agent 仅使用监控和日志系统的只读账号,以及处置接口的受限凭据。即使提示注入影响了 Agent,潜在影响范围也受到限制。这些原则在拆解 Coding Agent中讨论过:同步场景中属于最佳实践,异步场景中则是必要条件。
主动通知的发送条件和频率
Agent 可以主动处理事件后,还需要定义其发送消息的条件。
这是产品设计问题。发送消息在技术上较简单,难点是界定通知边界。第一版没有明确规则,例行巡检每次完成都发送“巡检正常”;三天后,群成员开始忽略这类消息,通知没有发挥作用。
后续规则分为三层。按结果分级:自动处理成功的事件不即时通知,汇总到日报;处理失败或需要审批的事件即时推送。按渠道分级:紧急事件通过 IM 强提醒发送,不紧急事件通过邮件或看板发送,渠道传达事件紧急程度。按内容分级:每条主动消息都需要说明后续动作。仅发送“接口 A 错误率越限”无法支持处理;消息应包含“已定位为下游 B 超时,建议摘流重启,或回复忽略”。接收者应能通过一个操作完成处理闭环,否则通知会增加噪音。巡检正常属于无需通知的状态。
每周性能报告也按此原则调整:在固定时间发送,只列出指标变化超过阈值的页面和具体数值;没有异常时仅简要说明,不附上几十页完整数据。
还需要设置频率上限:任何 Agent 在任何渠道每小时最多发送几条消息。告警风暴时,应合并为一条“过去一小时共 23 条告警,已合并为 3 个事件,摘要如下”,避免连续推送。用户屏蔽通知后,系统无法再通过该渠道提供信息。
将状态存储在进程外并处理失效实例
同步 Agent 的生命周期是一次会话,结束后销毁。巡检 Agent 生命周期较长,可能连续运行数月,期间会经历部署、重启和崩溃,因此状态必须存储在进程外。
任务进度、当前计划和待处理事件存储在数据库或队列中,进程不保留持久状态。进程故障后,新实例可以从上一个 checkpoint 恢复。这与 Coding Agent 的 sessionless 设计使用相同思路,但目的不同:前者方便人重新打开会话,后者保证系统可恢复。
一次事故说明了这一要求。改造早期,我把当前计划保存在进程内存中;一次常规部署重启后,已运行四十分钟的性能周报需要从头执行,更严重的是已发出的部分通知被再次发送。之后规定:跨步骤状态写入数据库;进程内只保存当前步骤的临时变量。
长生命周期还会出现失效实例问题。进程未退出、心跳仍存在,却卡在一个不会返回的工具调用中;或者两个实例都处于运行状态,使同一个告警被处理两次,摘流重复执行。处理方式是心跳和租约:Agent 周期性续租,租约过期后由调度方视为失效,回收任务并重新分配。领取任务时,除记录租约外还需携带单调递增的 fencing token。实例每次更新状态,或调用支持版本条件的副作用接口时,都带上该 token;旧实例即使恢复心跳,也不能覆盖新实例的结果。租约不能完全防止重复执行时,写操作必须保持幂等,使用事件 ID 或业务幂等键去重,不能只依赖租约。
将自动化测试作为新的巡检事件源
当前系统根据监控事件响应,但监控存在覆盖盲区。页面样式错乱、核心流程按钮无法点击、接口正常但页面白屏等问题,不一定触发告警,通常在用户反馈后才被发现。响应式巡检受监控体系覆盖率限制。
后续计划接入自动化测试,定时拉起无头浏览器访问线上核心页面,执行关键交互路径,采集截图、DOM 快照、网络请求和性能指标;再与基线进行视觉对比和断言,例如截图差异超过阈值、关键元素不存在、核心接口调用失败。检测到问题后,系统生成一条标准事件并写回队列。问题发现后的定位、处置和通知复用现有流程。
从架构看,自动化测试以新的事件源接入,因此 Agent 循环、任务切换逻辑、权限分级和通知规则无需修改。接入主动验证与接入告警 webhook 使用相同的事件接入方式。
还需要评估新增成本。页面正常改版会造成视觉差异,基线截图必须随发版更新,否则误报会积压在队列中;自动化用例的稳定性也需要维护,不稳定用例产生的噪音与监控告警风暴类似,处理手段是去重、合并和阈值。主动验证的产出质量取决于基线维护和用例质量,这两项工作目前仍需人工完成。
异步巡检 Agent 的实现约束
同步 Agent 由人触发,异步 Agent 由事件触发。基于这一变化,巡检系统需要满足以下约束:
- 所有来源应转换为包含类型、优先级、负载和去重键的消息。接入新能力时新增事件源,Agent 循环无需修改。
- 任务应拆分为可在步骤边界保存和恢复的小步骤。流式输出和取消 HTTP 请求无法提供任务切换能力。
- 主动通知需要定义分级、汇总和频率上限,以控制接收者的通知负担。
- 自动化程度由动作可逆性决定:只读操作自动执行;可逆动作自动执行并记录审计;不可逆动作转人工待办。
- 长生命周期服务需要将持久状态存储在进程外,使用 checkpoint、租约、fencing token 和幂等写操作处理重启和并发实例。
仍需解决的问题包括:如何持续评估自动处理的正确率,如何记录应发现但未发现的漏报,以及如何审核回写知识库的处置经验。长期运行的巡检 Agent 还需要相应的评估方法。
