让值班 Agent 记住失败的处置
作者让故障响应 Agent 同时记录成功与失败的操作,生成简报时引用历史事件及处置结果,避免重复采取恶化故障的措施。系统以 Spring Boot 编排流程、PostgreSQL 保存原始记录,并使用 Hindsight 提供记忆能力。
AI 深度解读
值班 Agent 最有用的回答,有时是「先别重启」。作者在一次凌晨 2 点的支付故障预演中,遇到了这样的提醒:几个月前,相似操作曾让连接池从退化变成不可用,Agent 找出了事故记录,解释了原因,还给出了另一条处理路径。
这套设计的关键,是把失败操作和成功修复同等存入记忆。更值得程序员借鉴的是后半段:检索负责找证据,模型负责组织简报,应用代码负责检查警告有没有依据。
复盘只记最终修复,为什么不够用
作者做的是一个事故响应 Agent:告警触发后,查找公司过去遇到过的问题,告诉值班工程师哪些事故相似、当时尝试过什么、每次操作产生了什么结果。
它面向的环境并不庞大:支付、结账、库存同步、通知等几个微服务,加上数年的事故历史。Spring Boot 接收告警并编排简报流程;PostgreSQL 保存事故、服务、告警和操作,作为可以重建记忆的数据源;开源记忆系统 Hindsight 提供长期记忆;Gemini 把检索结果整理成可读简报。
传统复盘常写「原因是什么,最后如何解决」。但值班工程师正在处理事故时,通常还不知道根因,只知道症状,需要决定下一步做什么。如果记录只剩「增加连接池容量后恢复」,前面两次让情况恶化的尝试就消失了。
事故记忆应该保存操作过程,以及每一步的结果。
作者把结果分成四类,并要求操作携带原因解释:
public enum ActionOutcome {
RESOLVED, // 操作后事故解除
MITIGATED, // 症状减轻,但根因仍然存在
NO_EFFECT, // 没有观察到变化
WORSENED // 操作后指标或用户影响恶化
}
public record IncidentAction(
String incidentId, // 例如 "INC-2041"
String service, // 例如 "payments-api"
String symptom, // 告警呈现的症状
String action, // 工程师执行的操作
ActionOutcome outcome,
String causalExplanation // 为什么产生这种效果
) {}
这里最该关注的是 causalExplanation。单独的 WORSENED 只能告诉人「以前失败过」,解释才能帮助人判断当前是否适用。
「重启让情况变差」容易变成机械禁令;「所有 Pod 同时重连 Postgres,耗尽 max_connections」则提供了可以检查的条件。当前连接数、重连方式是否相同,会影响这条警告的参考价值。
为什么同一份结果要同时写进文本和元数据
作者不想自己实现语义检索,于是使用 Hindsight 的 retain 和 recall 操作保存、召回记忆。
写入时,每条操作会被展开成完整文本,包含事故编号、服务、症状、动作、结果和原因,再通过 hindsight.retain 存入指定的 bankId。文本必须能独立阅读,因为召回时,它未必和事故的其他记录一起出现。
结果还会同步写入元数据,字段包括 incident_id、service、outcome。这份重复有明确用途:文本帮助检索理解语义,结构化字段帮助程序判断结果。
例如,查询「部署后支付服务出现连接错误」,不必与历史记录逐字匹配;但判断一条记录能否支撑风险警告,代码直接检查 outcome,不用从自然语言里猜。
这也解释了作者为什么不把全部历史塞进提示词:事故会持续积累,他不想每次告警都重新汇总所有记录。
原文的跨服务案例很直观。INC-2041 最初发生在 payments-api,症状是连接超时。重启所有 Pod 后,连接错误增加,这一步被记为 WORSENED;之后,在单个实例上提高连接池上限并逐步排空流量,被记为 RESOLVED。
几个月后,checkout-service 出现延迟升高和间歇性数据库连接错误。服务名和措辞都变了,Agent 仍能召回 INC-2041,提醒工程师先检查连接数。
它提供的是一种可能适用的失败模式,没有断言两个事故根因相同。
有了历史记录,为什么还要检查模型输出
作者担心的情况是:模型拿到一些材料,就写出一份流畅简报,即使材料并不支持其中的警告。虚构的历史经验尤其危险,因为它听起来像团队已经验证过的知识。
因此,流程分成两步。先从 Hindsight 召回相关记忆,这一步不让 LLM 撰写简报;再把召回内容交给 Gemini,要求引用事故编号。生成后,应用继续校验。
下面这段代码决定警告能否通过:
public Briefing build(Alert alert) {
List<RecalledMemory> recalled = hindsight.recall(bankId, alert.asQuery(), 10);
Set<String> knownIds = recalled.stream()
.map(m -> m.metadata().get("incident_id"))
.collect(Collectors.toSet());
Briefing draft = gemini.brief(alert, recalled);
for (Warning w : draft.warnings()) {
boolean traceable = knownIds.contains(w.incidentId());
boolean backedByFailure = recalled.stream().anyMatch(m ->
m.metadata().get("incident_id").equals(w.incidentId())
&& "WORSENED".equals(m.metadata().get("outcome")));
if (!traceable || !backedByFailure) {
throw new UngroundedWarningException(w);
}
}
return draft;
}
要注意 backedByFailure 的判断:警告引用的事故必须在本次召回结果里,而且其中必须存在 WORSENED 记录。检索调用的数量参数是 10。不满足条件就抛出 UngroundedWarningException,作者描述的后续流程是拒绝并重新生成,但这段代码没有展示重试实现。
界面也承担了一部分核验工作:简报旁边的侧栏原样展示本次召回的记忆,工程师可以直接读来源。
没有相似历史时,系统明确标记「没有先前事故」。作者认为,这个空结果状态比检索调优更费设计功夫。它避免每次告警都得到一段貌似有根据的历史故事。
我的判断
我认同作者对记忆粒度的选择。成功方案告诉人可以尝试什么,失败记录补充了适用条件和风险。对于要辅助决策的 Agent,这比只保存最终答案更有用。
不过,原文给的是作者的实现经历和案例,没有召回准确率、误报率,也没有与「只保存成功方案」的对照数据。开头还是 staging 环境的故障预演,不能据此认定它已经降低了生产事故损失。
代码的保护范围也要看清。它证明的是「引用的事故存在,并有恶化记录」,没有证明「警告中的具体操作和原因都被那条记录支持」。同一事故如果包含多次操作,模型仍可能张冠李戴。更严格的实现可以为操作设置独立编号,让警告引用具体动作,并核对相关字段。
此外,causalExplanation 的可信度取决于复盘质量。操作后指标恶化,未必就是操作导致的;原文没有交代因果解释如何审核,也没有讨论历史配置变化后如何处理旧经验。
和直接检索复盘文档相比,这套方案增加了结构化录入与校验成本,收益是应用更容易执行明确规则。一般团队可以先规范事故操作记录,再判断是否需要引入专门的长期记忆系统。
能拿来做什么
- 给 Node 服务或全栈项目补事故记录时,把每次操作、观察结果和原因分开保存;原因尚未确认就明确标注,别让推测变成后续 Agent 的依据。
- 做内部值班助手时,让告警先触发检索,再生成简报,并给每条风险警告附上可核验的操作记录编号。
- 在前端简报页面展示本次召回原文,同时设计清楚「没有相关历史」的状态,让工程师能判断建议有多少历史依据。
- 检查现有 AI 编程记忆时,可以借鉴这种记录方式:失败方案应带上症状、条件和失败原因。原文没有验证它在 AI 编程中的效果,落地时应先评估记录质量和检索相关性。