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