Agent 挂了我怎么知道?自主 Agent 的可观察性工程实践

三个月前,我的 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 分钟内自动恢复或人工接管"。


踩坑总结

  1. 不要用进程活着当健康指标------用语义心跳
  2. watchdog 必须独立于 Agent 进程------否则 Agent 崩了什么都不知道
  3. 每个不可逆操作前存检查点------幂等重跑比重来成本低很多
  4. 自动恢复要设上限------无限重启=无限烧钱,而且掩盖真实问题
  5. 语义探针比日志更早发现问题------日志记录的是发生了什么,探针检测的是能不能正常工作

如果你的 Agent 现在也只有"进程监控",这篇文章里的代码可以直接拿去用。有问题欢迎评论区交流。

相关推荐
姓洪的1 小时前
我把AI从"聊天框"变成了"函数":学会用代码调用AI
openai·deepseek
机器之心4 小时前
Speech LLM 的下一个突破口:你的语音大模型可以是个「带韵律的文本模型」
人工智能·openai
Artech4 小时前
[MAF的Agent管道详解-03]连接LLM的IChatClient对象
ai·llm·openai·agent·maf·agent管道·ichatclient
niaonao5 小时前
我把 Codex 的底座模型换成了 DeepSeek V4
openai·agent·deepseek
灵感__idea16 小时前
《AI工程》:大语言模型,到底是什么?
aigc·openai·ai编程
机器之心21 小时前
DeepSeek陈德里与两个AI,合写了一篇论文
人工智能·openai
Roc-xb1 天前
Codex桌面版接入deepseek-v4-pro详细教程
openai·codex·deepseek
147API1 天前
OpenAI 兼容客户端通用教程:API 地址、密钥与模型名
android·数据库·redis·openai