第 11 / 15 章 约 7 分钟
Observability
还原任务执行链路,定位延迟、成本和错误。
内容同步:2026年10月25日 · 文档版本 7
这个模块在 Agent 里的位置
可观测性不是“多打日志”,而是让一次 Agent run:
- 可还原。
- 可定位。
- 可比对。
- 可治理。
它要回答:
- 哪个 run 失败。
- 失败在模型、工具、数据还是执行器。
- 调用了多少工具。
- 上下文为什么膨胀。
- 成本来自哪个阶段。
- 压缩后为什么重复查询。
没有观测,prompt 优化和预算治理都是猜。
先记住几个概念
| 概念 | 含义 | 为什么重要 |
|---|---|---|
| runId | 一次逻辑任务 ID | 贯穿全链路 |
| seq | 事件顺序 | 支持回放 |
| Event stream | append-only 事件流 | 还原执行过程 |
| Usage | token 和 cost | 成本归因 |
| Tool metrics | 耗时、大小、状态 | 定位工具瓶颈 |
| Sampling | 高频成功事件采样 | 控制观测成本 |
| Privacy boundary | 不记录正文和密钥 | 避免泄漏 |
从一个真实问题开始:用户说“结果错了”,但没人知道错在哪
用户反馈:
这份报告引用了一个不存在的风险窗口。
排查时发现问题:
- 模型日志没有 runId。
- 工具日志只有“查询成功”。
- 上下文压缩没有记录前后大小。
- 输出校验失败被吞掉。
- 日志里没有引用 ID。
结果只能看到:
ERROR agent failed
这行日志无法回答:
- 是模型编了引用?
- 是工具返回错?
- 是压缩丢了引用?
- 是 validator 没校验?
- 是外部数据缺失?
这不是日志量不够,而是事件模型不对。
最小事件集
一个 run 至少要有这些事件:
| 事件 | 关键字段 |
|---|---|
| run.started | runId、sessionId、userId、tenantId |
| prompt.resolved | promptVersionId |
| model.started | modelKind、turnId |
| model.completed | usage、cost、latency |
| model.failed | provider error、latency |
| tool.started | toolName、toolCallId、input summary |
| tool.completed | duration、outputBytes、rows |
| tool.failed | errorCode、retryable |
| compaction.started | reason、beforeTokens |
| compaction.ended | afterTokens、summary refs |
| subtask.started | childAgent、parentRunId |
| subtask.ended | status、duration |
| action.proposed | proposalId、actionType、risk |
| action.approved | approver、decision |
| action.executed | terminal status |
| run.finished | status、duration、totalCost |
这些事件共同构成一次 run 的完整轨迹。
事件必须有序
只记录事件还不够,必须有顺序。
create table agent_run_events (
id bigint,
run_id bigint not null,
tenant_id bigint not null,
seq int not null,
event_type text not null,
payload jsonb not null,
occurred_at timestamptz not null,
primary key (id, occurred_at)
) partition by range (occurred_at);
create index agent_run_events_lookup_idx
on agent_run_events (tenant_id, run_id, seq);
seq 的价值:
- 回放时不会乱序。
- 并发写入时可检测重复。
- 可以判断事件是否缺失。
事件示例
[
{
"runId": "run-9f27",
"seq": 1,
"type": "run.started",
"context": {
"tenantId": 7,
"sessionId": "session-42"
}
},
{
"runId": "run-9f27",
"seq": 2,
"type": "tool.completed",
"context": {
"toolName": "query_top_risk_entities",
"toolCallId": "call-551",
"durationMs": 213,
"inputBytes": 512,
"outputBytes": 2048,
"status": "success"
}
},
{
"runId": "run-9f27",
"seq": 3,
"type": "model.completed",
"context": {
"turnId": "turn-103",
"inputTokens": 48200,
"outputTokens": 1800,
"latencyMs": 4200
}
}
]
这能回答:
- 哪个工具慢。
- 哪个 turn 贵。
- 事件先后顺序。
- 是否有异常输出大小。
隐私边界
不要记录:
- 完整 prompt。
- 完整回复。
- raw payload。
- 图片正文。
- 密钥。
可以记录:
- 大小。
- hash。
- 字段名。
- ID。
- 摘要。
例如:
{
"toolName": "query_events",
"inputSummary": {
"startTs": 1790000000000,
"endTs": 1790086400000,
"topK": 10
},
"inputBytes": 512,
"outputBytes": 2048,
"outputHash": "sha256:..."
}
这足以排障,又不暴露正文。
成本归因
一次 run 的成本不止模型 token,还包括:
- 模型 usage。
- 工具执行成本。
- 沙箱资源。
- 子 Agent 调用。
- 重试成本。
一个汇总事件:
{
"runId": "run-9f27",
"totalCost": 0.092,
"modelCost": 0.071,
"toolCalls": 12,
"subagentRuns": 3,
"durationMs": 84000
}
没有这个数据,就无法判断:
- prompt 变更是否降低成本。
- 工具瘦身是否有效。
- compaction 是否减少重复查询。
- 子 Agent 是否值得。
采样策略
在 10 万 run / 天、每 run 72 个事件的规模下,不能所有事件全量上传。
建议:
| 事件类型 | 策略 |
|---|---|
| run started / finished | 全量 |
| model usage | 全量 |
| tool failed | 全量 |
| action executed | 全量 |
| compaction | 全量 |
| tool success | 可采样 |
| 高频 debug | 可采样 |
失败和成本事件优先保留,成功事件可以采样。
和传统后端的关系
Agent 可观测性并没有脱离传统后端的三大件:
| Agent 里的概念 | 传统后端里的对应物 |
|---|---|
| runId | traceId / requestId |
| Event stream | 结构化日志 |
| Usage / cost | 业务指标 |
| Tool duration | 下游依赖耗时 |
| 失败率 | error rate |
| 采样 | 日志采样 |
| 告警 | SLO / alerting |
区别在于,Agent 还要额外记录模型 token、上下文大小、压缩次数和工具输出大小。
常见工程错误
| 错误 | 实际后果 | 正确做法 |
|---|---|---|
| 无 runId | 无法还原链路 | 所有事件带 runId |
| 无 seq | 回放乱序 | (runId, seq) |
| 记录全文 | 成本和隐私风险 | 摘要 + hash |
| 只记失败 | 无法优化成本 | 成功也记录 |
| 同步上传 | 拖垮业务 | 异步 / 采样 |
| 错误无 code | 无法分类 | stable errorCode |
| 事件分散无关联 | 无法定位 | 统一事件流 |
上线前检查
- runId 贯穿。
- 事件有序。
- usage 可归因。
- tool 有耗时和大小。
- 失败有稳定错误码。
- 不记录敏感正文。
- 失败可重放。
- 高频事件有采样策略。