跳到主要内容《Agent Runtime 工程化》第九章 可观测与 Trace | 极客日志编程语言Agent Runtime工程化可观测性陈堂会
《Agent Runtime 工程化》第九章 可观测与 Trace
Agent 出错后,最怕只剩一句“模型没做好”。这句话既不能定位问题,也不能指导修复。可观测系统的任务,是把一次 Agent run 拆成可以追踪、可以复盘、可以转化为评测用例的证据链。 OpenTelemetry 的公开文档把 span 视为 trace 的基本操作单元:span 有名称、父 span、开始和结束时间、属性、事件、状态等信息。本书不要求读者
站点编辑3 浏览

书名:《Agent Runtime 工程化:从工具调用循环到可恢复执行系统》
作者:陈堂会
平台:极客日志(https://zeeklog.com)
X:@Megick_com
联系邮箱:[email protected]
章节:第九章 可观测与 Trace
第九章 可观测与 Trace
Agent 出错后,最怕只剩一句'模型没做好'。这句话既不能定位问题,也不能指导修复。可观测系统的任务,是把一次 Agent run 拆成可以追踪、可以复盘、可以转化为评测用例的证据链。
OpenTelemetry 的公开文档把 span 视为 trace 的基本操作单元:span 有名称、父 span、开始和结束时间、属性、事件、状态等信息。本书不要求读者一上来接完整 OpenTelemetry SDK,但建议借用这套语义。对 Agent Runtime 来说,一次 run 可以是一条 trace;一次模型调用、工具调用、权限审批、上下文构建、checkpoint 写入、eval replay,都可以是 span 或 span event。

图 9-1:Trace 记录真实运行,Eval 把可复现问题变成回归用例。没有 trace,eval 很难知道要复现什么;没有 eval,trace 只能停留在事后追责。

图 9-2:一次 Agent run 的 trace 剖面。Root span 描述整体执行,子 span 描述模型、工具、权限、上下文和 checkpoint;属性保存稳定元数据,事件记录关键时间点,敏感内容只保存摘要或引用。
9.1 聊天记录不是 trace
聊天记录回答'用户和模型说了什么'。Trace 回答'系统实际做了什么'。这两个问题不能混在一起。
一个常见失败场景是:用户要求修复测试,Agent 最终说'已经修好了',但测试仍然失败。如果只有聊天记录,你只能看到模型怎样解释自己;如果有 trace,你可以继续追问:
| 问题 | trace 里的证据 |
|---|
| 模型看见了哪些上下文 | context.build span 的 included、dropped、summarized |
| 模型为什么调用某个工具 | model.generate span 的 tool call 数量和 finish reason |
| 工具是否真的执行 | tool.run span 的 status、duration、exit code |
| 用户是否批准写入 | permission.request span 的 decision、actor、reason |
| 文件是否发生变化 | checkpoint.write span 的 changed files、patch hash |
| 测试为何失败 | tool.run_command span 的 stderr artifact、exit code |
这才是 Runtime 工程师需要的材料。模型输出可以错,工具输出也可以错,但 trace 要尽量诚实。
9.2 Trace 的最小数据模型
入门阶段不必先接 Jaeger、Tempo、Datadog 或 LangSmith。先定义本地 trace 数据结构,把语义写对。
{
"traceId": "trace_01HZYQ4H7X9VN",
"runId": "run_20260814_001",
"sessionId": "session_local_mathlib",
"turnId": "turn_12",
"startedAt": "2026-08-14T09:30:00+08:00",
"endedAt": "2026-08-14T09:30:42+08:00",
"status": "error",
"failureKind": "tool",
"spans": []
}
{
"spanId": "span_tool_07",
"parentSpanId": "span_run_root",
"name": "tool.run_command",
"startedAt": "2026-08-14T09:30:15+08:00",
"endedAt": "2026-08-14T09:30:23+08:00",
"durationMs": 8120,
"status": "error",
"attributes": {
"tool.name": "run_command",
"tool.risk": "write",
"command.kind": "test",
"exit_code": 1,
"output.truncated": true
},
"events": [
{
"name": "approval.accepted",
"time": "2026-08-14T09:30:14+08:00",
"attributes": {
"approval.id": "appr_42",
"actor": "user"
}
}
]
}
第一,ID 要能串起来。traceId 串一次 run,turnId 串一次用户请求,toolCallId 串模型输出、工具执行、工具结果回灌。没有关联 ID,日志会变成散落的碎片。
第二,持续时间用单调时钟计算,展示时间用 wall clock。系统时间可能被用户或容器环境调整,持续时间最好从 performance.now() 一类单调时钟得出;ISO 时间用于排序和阅读。
第三,trace schema 要版本化。Agent Runtime 会不断增加字段,旧 trace 仍要能被读取。给 root 加 schemaVersion,比后面写迁移脚本省心。
9.3 Span、attribute、event 怎么分
很多初学者会把所有东西都塞进 attributes,最后每个 span 像一个杂物箱。更稳的分法是:
| 类型 | 适合记录什么 | 例子 |
|---|
| span | 有开始和结束的操作 | 一次模型调用、一次工具执行、一次上下文构建 |
| attribute | 操作的稳定元数据 | model、provider、tool name、token、risk level |
| event | 某个明确时间点发生的事 | 用户批准、开始重试、输出被截断 |
| link | 跨 trace 的因果关系 | eval replay 链接到原始失败 trace |
| artifact ref | 大内容或敏感内容的外部引用 | 命令完整输出、diff、上下文 debug report |
示例:一次 run_command 执行了 8 秒,它本身应该是 span;执行中用户批准,是 event;命令类型、退出码、耗时,是 attribute;完整 stdout/stderr 太长,应保存为 artifact,然后在 span 里写引用。
不要把完整 prompt、完整文件内容、完整终端输出直接塞进 trace。Trace 是索引和证据,不是内容仓库。
9.4 Agent Runtime 专用 span
通用 Web 服务里,常见 span 是 HTTP、DB、queue。Agent Runtime 里,建议先定义以下 span 名称:
| span 名称 | 责任 |
|---|
run.start | 一次 run 的根节点 |
turn.prepare | turn 前置处理、用户消息清洗、session 写入 |
context.build | 构建模型上下文和预算报告 |
model.generate | 调用 provider,接收文本和 tool calls |
tool.plan | 对模型提出的工具调用做校验、分段、并发判断 |
permission.request | 生成预览、请求用户或策略决策 |
tool.execute | 执行单个工具,记录结果和错误 |
checkpoint.write | 保存文件状态、ledger 或 session 快照 |
resume.load | 从 checkpoint 或 session 恢复 |
eval.replay | 用 trace 或 golden task 复现一次运行 |
这些名字不必成为行业标准,但在一个项目里必须稳定。名称稳定,dashboard、查询、CI 报告和事故复盘才能建立在同一套语言上。
9.5 模型 span:记录能力边界,而不是崇拜模型
- provider;
- model;
- request id 或 response id;
- prompt tokens;
- completion tokens;
- latency;
- finish reason;
- tool call 数量;
- 是否流式;
- 是否触发 retry;
- 是否触发上下文降级;
- estimated cost。
{
"name": "model.generate",
"status": "ok",
"attributes": {
"llm.provider": "openai",
"llm.model": "example-model",
"llm.prompt_tokens": 18422,
"llm.completion_tokens": 712,
"llm.finish_reason": "tool_calls",
"agent.tool_call_count": 3,
"context.degraded": false,
"cost.estimated_usd": 0.048
}
}
注意,trace 里不要记录未脱敏的完整 prompt。调试时确实会想看完整输入,但生产系统应把完整模型输入放在受控 artifact 中,并且有访问控制、保留期限和删除机制。
9.6 工具 span:把'调用过'变成可审计
工具 span 要比模型 span更细。因为真正改变世界的,通常是工具。
tool.name;
tool.version;
toolCallId;
- 参数摘要;
- risk level;
- approval id;
- timeout;
- retry count;
- status;
- error kind;
- output size;
- truncated;
- artifact ref;
- idempotency key;
- checkpoint id。
工具错误不要只写 error: true。建议至少分成:
| 错误类型 | 含义 | runtime 处理 |
|---|
validation_error | 模型参数不符合 schema | 转 observation,让模型修正 |
permission_denied | 策略或用户拒绝 | 不重试,停止或请求替代方案 |
timeout | 工具超过时间限制 | 视幂等性决定是否重试 |
process_error | 命令退出非零或工具内部异常 | 返回摘要和 artifact |
output_overflow | 输出过大,被截断 | 保存 artifact,给模型摘要 |
external_unavailable | MCP Server、网络或 API 不可用 | 降级、跳过或稍后重试 |
有了错误分类,失败复盘才不会停在'工具失败'四个字。
9.7 权限 span:用户决定也是事实
权限审批不是 UI 小插曲,而是执行历史的一部分。它必须进 trace。
{
"name": "permission.request",
"status": "ok",
"attributes": {
"approval.id": "appr_42",
"policy": "interactive",
"risk.level": "write",
"decision": "approved",
"actor": "user",
"preview.kind": "command",
"preview.hash": "sha256:8cbd..."
}
}
这里的关键是 preview.hash。用户批准的不是抽象的 run_command,而是某个具体命令、某个 diff、某个目标路径。恢复执行时,如果命令或 diff 改了,就不能复用旧批准。
非交互模式下同样要记录决策。比如策略拒绝了 npm install,trace 要说明是 policy=never_ask 下的自动拒绝,而不是工具崩溃。
9.8 Context Builder span:解释模型为什么'没看见'
许多 Agent 失败不是推理失败,而是输入失败。模型没有看见关键文件、用户约束被旧摘要稀释、工具结果被截断后丢掉关键信息,这些都属于上下文问题。
- total budget;
- used tokens;
- reserved tokens;
- system/developer/user/tool/history 各占多少;
- must keep items;
- included files;
- dropped files;
- summarized messages;
- truncated tool results;
- repo map version;
- debug report path。
{
"name": "context.build",
"status": "ok",
"attributes": {
"context.budget": 64000,
"context.used": 58840,
"context.files_included": 6,
"context.files_dropped": 3,
"context.tool_results_truncated": 2,
"context.summary_count": 4,
"context.debug_report": ".agent/context/run_123_turn_12.json"
}
}
将来排查'模型为什么改错文件'时,第一眼就该看这段。
9.9 脱敏、采样和保留期限
Trace 很敏感。它可能包含用户代码、命令输出、环境变量、文件路径、API 响应、模型输入输出、审批理由。生产系统的 trace 设计必须从第一天就处理隐私和合规。
| 层级 | 内容 | 默认策略 |
|---|
| trace index | ID、span、状态、耗时、大小、哈希 | 默认保存 |
| artifact | 完整输出、上下文报告、diff、截图 | 受控保存,有保留期限 |
| secret-bearing content | token、cookie、私钥、PII | 默认不保存,发现即脱敏 |
脱敏不应只靠正则。正则能挡住常见 API key、Bearer token、私钥头、邮箱和手机号,但企业场景还需要 allowlist、字段级标记和工具侧输出策略。比如工具定义可以声明 sensitiveArgs: ["token", "password"],trace writer 在接收参数摘要时强制替换。
采样也不能简单随机。成功的只读任务可以低采样,失败任务、高风险工具、权限拒绝、恢复执行、eval replay 应尽量全量保留索引。这样既控制存储,又保留最有价值的证据。
9.10 本地 TraceWriter 实现路径
先写一个本地 JSONL writer。它不漂亮,但可靠、可 diff、可进入 eval。
type SpanStatus = "ok" | "error" | "cancelled";
type SpanHandle = {
spanId: string;
traceId: string;
name: string;
startedAt: string;
startMs: number;
};
type TraceWriter = {
startSpan(
name: string,
attrs?: Record<string, unknown>,
parent?: SpanHandle,
): SpanHandle;
addEvent(
span: SpanHandle,
name: string,
attrs?: Record<string, unknown>,
): void;
endSpan(
span: SpanHandle,
status: SpanStatus,
attrs?: Record<string, unknown>,
): void;
};
第一步,所有事件先写到内存数组,run 结束后落盘。这样实现简单,但进程崩溃时会丢最后一段。
第二步,改成 JSONL append。每个 start、event、end 都单独写一行,崩溃后仍能看到最后写到哪里。
第三步,把 traceId、runId、turnId 注入 runtime context,让工具、provider、permission gate 都能拿到。
第四步,为长输出生成 artifact ref。不要把大内容直接写进 JSONL。
第五步,再考虑 OpenTelemetry export。此时你已经有了清晰语义,接 OTLP 只是导出格式问题。
9.11 从 trace 到事故复盘
| 段落 | 要回答的问题 |
|---|
| 用户目标 | 用户真正要完成什么 |
| 执行路径 | 模型做了几步、调用了哪些工具 |
| 失败点 | 第一个错误 span 在哪里 |
| 根因 | 模型、工具、权限、上下文、runtime 哪一类 |
| 回归方案 | 新增什么 eval,防止再次发生 |
用户目标:把 add 改成支持 BigInt,并补测试。
失败点:span_tool_07,run_command npm test 失败。
直接原因:测试期望仍使用 number,BigInt 断言未更新。
根因分类:上下文问题。context.build 未包含 test/math.test.ts。
修复:Context Builder 在编辑 src/math.ts 时优先检索同名测试文件。
回归:新增 eval edit-bigint-001,断言 src 与 test 同时修改,npm test 通过。
这份复盘比'模型没写好测试'更有用,因为它指向了 runtime 的改造点。
9.12 动手任务
为 mini-agent 加入 trace 系统:
- 每次 run 生成一个
traceId;
- 记录
context.build、model.generate、tool.plan、permission.request、tool.execute、checkpoint.write;
- 所有 tool call 都有
toolCallId;
- 大于 8KB 的工具输出保存为 artifact,只在 trace 里保存摘要;
- secret、token、cookie、private key 默认脱敏;
- 失败 run 自动生成复盘草稿;
- eval harness 能读取 trace 并生成 replay case 草稿。
.agent/
traces/
run_20260814_001.jsonl
artifacts/
run_20260814_001_tool_07.stderr.txt
incidents/
run_20260814_001.md
9.13 验收标准
- 一次失败任务可以从 trace 中定位第一个错误 span;
- 能区分模型问题、工具问题、权限问题、上下文问题和 runtime 问题;
- trace 中没有明文 secret;
- 工具输出过大时不会撑爆 trace;
- 权限审批和 checkpoint 能被同一条执行链路串起来;
- 每个重要失败都能转化为一个潜在 eval case。
到这里,Agent Runtime 不再是'黑盒加聊天记录'。它开始具备工程组织可以讨论、审计和持续改进的证据基础。
系列文章目录
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online