三个月前我的 AI Agent 在凌晨 2 点挂了。它负责每天抓取数据、生成报告、推送给下游系统。挂了之后什么都没发生——没有报错没有告警下游系统只是静静地不再收到数据。直到第二天早上用户问昨天的报告怎么没出来我才发现。当时的状态监控就是每小时ps aux | grep agent。这是我犯的第一个根本性错误把进程存活当成Agent 正常运行。为什么 Agent 的可观察性比普通服务更难普通服务挂了你看 HTTP 5xx 就知道了。Agent 不一样它可以是「活着但卡住」进程在跑但 LLM 调用卡在 rate limit retry 里三小时没有实质进展它可以是「活着但走错路」任务执行了但每一步都在做错误决策直到资源耗尽才崩它可以是「静默失败」工具调用返回空数组Agent 认为没有数据正常退出但实际上是查询条件写错了传统的进程是否存活检测对这三种情况全部失效。你需要的是语义级别的健康检测。第一层心跳 ≠ 进程探活我用 OpenClaw 跑 Agent它有内置的 heartbeat 机制。但我最初配错了方向// 错误配置只检测进程{heartbeat:{interval:30m,check:process}}进程活着 ≠ Agent 在干活。正确的做法是让 Agent主动写入心跳时间戳// agent/main.js — 每完成一个工作单元就更新asyncfunctionprocessTask(task){awaitupdateHeartbeat({task_id:task.id,step:started,timestamp:Date.now()});constresultawaitllm.call(task.prompt);awaitupdateHeartbeat({task_id:task.id,step:llm_done,tokens_used:result.usage.total_tokens,timestamp:Date.now()});// ... 后续步骤}然后有个独立的 watchdog 进程检查心跳是否超时// watchdog.jsasyncfunctioncheckHeartbeat(){constlastBeatawaitdb.get(agent:heartbeat:last);constageDate.now()-lastBeat.timestamp;if(age10*60*1000){// 10 分钟没心跳awaitalert.send(Agent 疑似卡死上次心跳${Math.round(age/60000)}分钟前步骤${lastBeat.step});}}关键点watchdog 必须是独立进程不能跟 Agent 在同一个进程里——否则 Agent 崩了 watchdog 也跟着没了。第二层状态快照与检查点Agent 执行到一半挂了最难处理重启后不知道跑到哪里了从头跑可能重复操作不跑又丢数据。我现在的做法是每个不可逆操作前都写检查点classAgentCheckpoint{constructor(runId,storage){this.runIdrunId;this.storagestorage;// Redis / 本地 SQLite 均可}asyncsave(step,state){awaitthis.storage.set(checkpoint:${this.runId}:${step},{step,state,saved_at:Date.now()});console.log([checkpoint] saved step${step});}asyncload(step){returnthis.storage.get(checkpoint:${this.runId}:${step});}asynchasCompleted(step){constcpawaitthis.load(step);returncp!null;}}// 使用asyncfunctionrunPipeline(runId){constcpnewAgentCheckpoint(runId,redis);// 步骤 1拉数据幂等可重跑letrawData;if(awaitcp.hasCompleted(fetch)){rawData(awaitcp.load(fetch)).state.data;console.log([resume] skipping fetch, loaded from checkpoint);}else{rawDataawaitfetchData();awaitcp.save(fetch,{data:rawData});}// 步骤 2LLM 处理有成本不可随意重跑letanalysis;if(awaitcp.hasCompleted(analyze)){analysis(awaitcp.load(analyze)).state.result;}else{analysisawaitllm.analyze(rawData);awaitcp.save(analyze,{result:analysis});}// 步骤 3写入下游只跑一次if(!awaitcp.hasCompleted(push)){awaitpushToDownstream(analysis);awaitcp.save(push,{pushed_at:Date.now()});}}这段代码做到了重启后从上次成功的步骤继续不重复 LLM 调用不重复写入下游。第三层语义健康检查心跳告诉你 Agent 在跑但不告诉你跑得对不对。我加了一个每 5 分钟跑一次的语义探针asyncfunctionsemanticHealthCheck(agent){// 发一个有已知答案的探针问题constPROBE{input:22等于多少,expected_pattern:/4/};conststartDate.now();constresultawaitagent.run(PROBE.input,{timeout:30_000});constlatencyDate.now()-start;constmetrics{latency_ms:latency,responded:result!null,correct:PROBE.expected_pattern.test(result?.output||),timestamp:Date.now()};awaitmetrics.record(agent.health,metrics);if(!metrics.correct){awaitalert.critical(语义健康检查失败探针问题回答异常latency${latency}ms);}if(latency20_000){awaitalert.warn(Agent 响应过慢${latency}ms);}returnmetrics;}真实生产中探针问题可以更复杂——比如处理一条测试数据验证输出格式正确。核心是有输入、有期望输出、机器可判断对错。第四层故障恢复自动化上面三层都是发现问题。发现之后呢我之前的流程是收到告警 → 手动 SSH → 查日志 → 重启。这在凌晨 3 点不现实。现在的做法是把恢复动作编成代码classAgentSupervisor{constructor(agentFactory,options{}){this.agentFactoryagentFactory;this.maxRestartsoptions.maxRestarts??3;this.restartWindowoptions.restartWindow??3600_000;// 1h 内最多 N 次this.restartHistory[];this.agentnull;}asyncstart(task){this.agentawaitthis.agentFactory();try{returnawaitthis.agent.run(task);}catch(err){returnthis.handleFailure(err,task);}}asynchandleFailure(err,task){constnowDate.now();this.restartHistorythis.restartHistory.filter(tnow-tthis.restartWindow);if(this.restartHistory.lengththis.maxRestarts){// 超过重启次数上限人工介入awaitalert.critical(Agent 在${this.restartWindow/60000}分钟内重启了${this.maxRestarts}次停止自动恢复等待人工处理,{error:err.message,last_checkpoint:awaitthis.getLastCheckpoint()});throwerr;}this.restartHistory.push(now);constdelayMath.min(1000*2**this.restartHistory.length,60_000);awaitalert.warn(Agent 崩溃${delay/1000}s 后自动重启第${this.restartHistory.length}次,{error:err.message});awaitsleep(delay);// 重启并从检查点恢复this.agentawaitthis.agentFactory();returnthis.agent.resumeFrom(task,awaitthis.getLastCheckpoint());}}重点设硬上限。自动恢复很好但无限重启会掩盖真正的 bug还会烧钱LLM 调用是有成本的。现在的监控架构三个月踩坑下来我的 Agent 监控长这样┌─────────────────────────────────────────┐ │ Agent 主进程 │ │ ┌─────────┐ ┌──────────┐ ┌────────┐ │ │ │ 心跳写入 │ │ 检查点存储│ │ 指标上报│ │ │ └────┬────┘ └────┬─────┘ └───┬────┘ │ └───────┼─────────────┼────────────┼───────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Redis │ │ SQLite │ │ InfluxDB│ └────┬────┘ └─────────┘ └────┬────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │Watchdog │ │ Grafana │ │(独立进程)│ │(告警规则)│ └────┬────┘ └────┬────┘ │ │ └──────────┬──────────────┘ ▼ ┌──────────┐ │ 告警通知 │ │(TG/邮件) │ └──────────┘四层加一起从发现凌晨挂机要到早上变成了5 分钟内自动告警、30 分钟内自动恢复或人工接管。踩坑总结不要用进程活着当健康指标——用语义心跳watchdog 必须独立于 Agent 进程——否则 Agent 崩了什么都不知道每个不可逆操作前存检查点——幂等重跑比重来成本低很多自动恢复要设上限——无限重启无限烧钱而且掩盖真实问题语义探针比日志更早发现问题——日志记录的是发生了什么探针检测的是能不能正常工作如果你的 Agent 现在也只有进程监控这篇文章里的代码可以直接拿去用。有问题欢迎评论区交流。
Agent 挂了我怎么知道?自主Agent 的可观察性工程实践
三个月前我的 AI Agent 在凌晨 2 点挂了。它负责每天抓取数据、生成报告、推送给下游系统。挂了之后什么都没发生——没有报错没有告警下游系统只是静静地不再收到数据。直到第二天早上用户问昨天的报告怎么没出来我才发现。当时的状态监控就是每小时ps aux | grep agent。这是我犯的第一个根本性错误把进程存活当成Agent 正常运行。为什么 Agent 的可观察性比普通服务更难普通服务挂了你看 HTTP 5xx 就知道了。Agent 不一样它可以是「活着但卡住」进程在跑但 LLM 调用卡在 rate limit retry 里三小时没有实质进展它可以是「活着但走错路」任务执行了但每一步都在做错误决策直到资源耗尽才崩它可以是「静默失败」工具调用返回空数组Agent 认为没有数据正常退出但实际上是查询条件写错了传统的进程是否存活检测对这三种情况全部失效。你需要的是语义级别的健康检测。第一层心跳 ≠ 进程探活我用 OpenClaw 跑 Agent它有内置的 heartbeat 机制。但我最初配错了方向// 错误配置只检测进程{heartbeat:{interval:30m,check:process}}进程活着 ≠ Agent 在干活。正确的做法是让 Agent主动写入心跳时间戳// agent/main.js — 每完成一个工作单元就更新asyncfunctionprocessTask(task){awaitupdateHeartbeat({task_id:task.id,step:started,timestamp:Date.now()});constresultawaitllm.call(task.prompt);awaitupdateHeartbeat({task_id:task.id,step:llm_done,tokens_used:result.usage.total_tokens,timestamp:Date.now()});// ... 后续步骤}然后有个独立的 watchdog 进程检查心跳是否超时// watchdog.jsasyncfunctioncheckHeartbeat(){constlastBeatawaitdb.get(agent:heartbeat:last);constageDate.now()-lastBeat.timestamp;if(age10*60*1000){// 10 分钟没心跳awaitalert.send(Agent 疑似卡死上次心跳${Math.round(age/60000)}分钟前步骤${lastBeat.step});}}关键点watchdog 必须是独立进程不能跟 Agent 在同一个进程里——否则 Agent 崩了 watchdog 也跟着没了。第二层状态快照与检查点Agent 执行到一半挂了最难处理重启后不知道跑到哪里了从头跑可能重复操作不跑又丢数据。我现在的做法是每个不可逆操作前都写检查点classAgentCheckpoint{constructor(runId,storage){this.runIdrunId;this.storagestorage;// Redis / 本地 SQLite 均可}asyncsave(step,state){awaitthis.storage.set(checkpoint:${this.runId}:${step},{step,state,saved_at:Date.now()});console.log([checkpoint] saved step${step});}asyncload(step){returnthis.storage.get(checkpoint:${this.runId}:${step});}asynchasCompleted(step){constcpawaitthis.load(step);returncp!null;}}// 使用asyncfunctionrunPipeline(runId){constcpnewAgentCheckpoint(runId,redis);// 步骤 1拉数据幂等可重跑letrawData;if(awaitcp.hasCompleted(fetch)){rawData(awaitcp.load(fetch)).state.data;console.log([resume] skipping fetch, loaded from checkpoint);}else{rawDataawaitfetchData();awaitcp.save(fetch,{data:rawData});}// 步骤 2LLM 处理有成本不可随意重跑letanalysis;if(awaitcp.hasCompleted(analyze)){analysis(awaitcp.load(analyze)).state.result;}else{analysisawaitllm.analyze(rawData);awaitcp.save(analyze,{result:analysis});}// 步骤 3写入下游只跑一次if(!awaitcp.hasCompleted(push)){awaitpushToDownstream(analysis);awaitcp.save(push,{pushed_at:Date.now()});}}这段代码做到了重启后从上次成功的步骤继续不重复 LLM 调用不重复写入下游。第三层语义健康检查心跳告诉你 Agent 在跑但不告诉你跑得对不对。我加了一个每 5 分钟跑一次的语义探针asyncfunctionsemanticHealthCheck(agent){// 发一个有已知答案的探针问题constPROBE{input:22等于多少,expected_pattern:/4/};conststartDate.now();constresultawaitagent.run(PROBE.input,{timeout:30_000});constlatencyDate.now()-start;constmetrics{latency_ms:latency,responded:result!null,correct:PROBE.expected_pattern.test(result?.output||),timestamp:Date.now()};awaitmetrics.record(agent.health,metrics);if(!metrics.correct){awaitalert.critical(语义健康检查失败探针问题回答异常latency${latency}ms);}if(latency20_000){awaitalert.warn(Agent 响应过慢${latency}ms);}returnmetrics;}真实生产中探针问题可以更复杂——比如处理一条测试数据验证输出格式正确。核心是有输入、有期望输出、机器可判断对错。第四层故障恢复自动化上面三层都是发现问题。发现之后呢我之前的流程是收到告警 → 手动 SSH → 查日志 → 重启。这在凌晨 3 点不现实。现在的做法是把恢复动作编成代码classAgentSupervisor{constructor(agentFactory,options{}){this.agentFactoryagentFactory;this.maxRestartsoptions.maxRestarts??3;this.restartWindowoptions.restartWindow??3600_000;// 1h 内最多 N 次this.restartHistory[];this.agentnull;}asyncstart(task){this.agentawaitthis.agentFactory();try{returnawaitthis.agent.run(task);}catch(err){returnthis.handleFailure(err,task);}}asynchandleFailure(err,task){constnowDate.now();this.restartHistorythis.restartHistory.filter(tnow-tthis.restartWindow);if(this.restartHistory.lengththis.maxRestarts){// 超过重启次数上限人工介入awaitalert.critical(Agent 在${this.restartWindow/60000}分钟内重启了${this.maxRestarts}次停止自动恢复等待人工处理,{error:err.message,last_checkpoint:awaitthis.getLastCheckpoint()});throwerr;}this.restartHistory.push(now);constdelayMath.min(1000*2**this.restartHistory.length,60_000);awaitalert.warn(Agent 崩溃${delay/1000}s 后自动重启第${this.restartHistory.length}次,{error:err.message});awaitsleep(delay);// 重启并从检查点恢复this.agentawaitthis.agentFactory();returnthis.agent.resumeFrom(task,awaitthis.getLastCheckpoint());}}重点设硬上限。自动恢复很好但无限重启会掩盖真正的 bug还会烧钱LLM 调用是有成本的。现在的监控架构三个月踩坑下来我的 Agent 监控长这样┌─────────────────────────────────────────┐ │ Agent 主进程 │ │ ┌─────────┐ ┌──────────┐ ┌────────┐ │ │ │ 心跳写入 │ │ 检查点存储│ │ 指标上报│ │ │ └────┬────┘ └────┬─────┘ └───┬────┘ │ └───────┼─────────────┼────────────┼───────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Redis │ │ SQLite │ │ InfluxDB│ └────┬────┘ └─────────┘ └────┬────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │Watchdog │ │ Grafana │ │(独立进程)│ │(告警规则)│ └────┬────┘ └────┬────┘ │ │ └──────────┬──────────────┘ ▼ ┌──────────┐ │ 告警通知 │ │(TG/邮件) │ └──────────┘四层加一起从发现凌晨挂机要到早上变成了5 分钟内自动告警、30 分钟内自动恢复或人工接管。踩坑总结不要用进程活着当健康指标——用语义心跳watchdog 必须独立于 Agent 进程——否则 Agent 崩了什么都不知道每个不可逆操作前存检查点——幂等重跑比重来成本低很多自动恢复要设上限——无限重启无限烧钱而且掩盖真实问题语义探针比日志更早发现问题——日志记录的是发生了什么探针检测的是能不能正常工作如果你的 Agent 现在也只有进程监控这篇文章里的代码可以直接拿去用。有问题欢迎评论区交流。