核心摘要
Agent 可观测性有三个不同任务:
- Trace 记录足以重建一次运行的有界事件;
- Eval 判断结果是否满足契约;
- Debug 定位导致失败的状态转移、策略决策、依赖或预算。
目标不是捕获每个 Token,而是在最小化敏感数据的同时保留回答运维问题所需的证据,并让质量信号独立于模型自己的解释。
为什么 Agent 可观测性不同
传统 APM 可以把 HTTP 成功作为技术信号,但 Agent 返回 200 时,仍可能选错 Tool、越过租户边界、引用没有依据的资料,或花费超过预算。反过来,下游超时是技术失败,即使 Agent 正确拒绝了危险操作。
| 问题 | 技术遥测 | Agent 证据 |
|---|---|---|
| 请求完成了吗? | 状态码、耗时、异常 | 最终结果与取消原因 |
| 运行时做了什么? | Span 树与依赖调用 | 状态转移、Tool 调用、策略决策 |
| 回答质量如何? | APM 通常不可见 | 契约、证据、人工或模型评估 |
| 系统安全吗? | 认证错误与网络事件 | 拒绝操作、跨租户尝试、外发行为 |
| 花费多少? | CPU、内存 | Token、厂商价格、Tool 与重试预算 |
不要从状态码、模型解释或单一分数推断语义质量。每种决策都应记录对应的独立证据。
先定义事件契约
在选择 LangSmith、Langfuse、Phoenix、其他平台或自建存储之前,先定义由应用拥有的事件契约:
{
"event": "tool_call_completed",
"schema_version": 3,
"run_id": "run_7f...",
"trace_id": "4bf92f...",
"step": 4,
"tenant_hash": "h:...",
"subject_hash": "h:...",
"tool": "invoice.list",
"tool_version": "2026-05-1",
"argument_digest": "sha256:...",
"policy": "allow",
"state_version": 12,
"duration_ms": 184,
"result_class": "bounded_page",
"error_code": null,
"usage": {
"input_tokens": 812,
"output_tokens": 164,
"estimated_cost_usd": 0.0019
}
}
建议的事件包括 run_started、model_call_completed、tool_call_requested、policy_decision、tool_call_completed、state_transition、budget_exhausted、run_completed 和 run_failed。每个事件都应有稳定的 Schema 版本、Run ID 和脱敏策略。
应用事件契约让后端可以迁移。Trace 平台可以负责存储传输元数据,但即使更换 SDK 或模型厂商,业务事件仍然有意义。
Trace 边界与隐私
OpenTelemetry 是厂商无关的 Trace、Metric 和 Log 传输基础。它不替应用决定应该记录什么。使用 Span 表达耗时和因果关系,使用事件或日志表达结构化业务结果。
OpenTelemetry GenAI 当前状态
截至 2026-07-28,OpenTelemetry 的 GenAI 语义约定已从核心 Semantic Conventions 文档迁移到独立的 semantic-conventions-genai 仓库。该仓库覆盖 GenAI Client、Agent、MCP、Span、Metric 和 Event,但尚未发布正式 Release。因此应把 Agent 属性视为持续演进的约定:固定实现所用的 Commit 或生成 Schema,保留应用自己的事件版本,并在升级约定前测试 Exporter。OpenTelemetry 核心 Trace、Metric、Log 与 Context API 可以稳定,而 GenAI 专用属性集仍可能变化。
各类信号的职责
| 信号 | 负责表达 | 不能证明 |
|---|---|---|
| Trace/Span | Agent Run、Model Call、Retrieval、Tool Call 和 Handoff 的因果与耗时 | 任务质量或授权 |
| Span Event/Log | 策略拒绝、重试、缓存决策或状态转移等有界事件 | 时间序列服务目标 |
| Metric | 聚合的速率、延迟、Token、成本、队列、错误与策略计数 | 某次运行为何失败 |
| Evaluation | 对可观察产物做契约、证据、安全和任务质量判断 | 运行时因果或权限 |
整条运行使用统一关联模型:以 AgentRun 为根,为 Model Call、Retrieval、Tool Call 和 Handoff 建立有界子 Span。Evaluation 作为独立版本化记录,通过 run_id、Evaluator 版本、数据切片和证据 ID 关联。生产级 RAG 评估指南定义检索和答案质量门禁;Agent 工具安全指南定义运行时权限,遥测只能观察它,不能替代它。
不要采用以下默认做法:
- 在每个 Span 中写入原始 Prompt 或完整 Tool 参数;
- 保存完整模型回答、隐藏推理或私有检索文档;
- 保存 Bearer Token、API Key、Authorization Header 或原始用户 ID;
- 把无界 Tool Result 放进 Span 属性;
- 用一个
success=true隐藏策略拒绝和语义失败。
只有在存在明确关联目的时才使用 Hash 或稳定伪名。Hash 并不会自动使数据匿名:输入空间很小时,映射可能被反推。遥测存储也必须按租户做访问控制,并将删除请求传播到导出物和评估数据集。
from dataclasses import dataclass
from hashlib import sha256
from typing import Any
from opentelemetry import trace
def digest(value: str) -> str:
return "sha256:" + sha256(value.encode("utf-8")).hexdigest()
@dataclass(frozen=True)
class RunContext:
run_id: str
tenant_id: str
subject_id: str
def record_tool_event(
tracer: trace.Tracer,
context: RunContext,
*,
tool_name: str,
tool_version: str,
arguments: dict[str, Any],
policy_decision: str,
state_version: int,
duration_ms: int,
result_class: str,
error_code: str | None,
) -> None:
with tracer.start_as_current_span("ToolCall") as span:
span.set_attribute("agent.run_id", context.run_id)
span.set_attribute("agent.tenant_hash", digest(context.tenant_id))
span.set_attribute("agent.subject_hash", digest(context.subject_id))
span.set_attribute("agent.tool.name", tool_name)
span.set_attribute("agent.tool.version", tool_version)
span.set_attribute("agent.argument_digest", digest(repr(sorted(arguments.items()))))
span.set_attribute("agent.policy.decision", policy_decision)
span.set_attribute("agent.state.version", state_version)
span.set_attribute("agent.duration_ms", duration_ms)
span.set_attribute("agent.result.class", result_class)
if error_code:
span.set_attribute("agent.error.code", error_code)
该片段记录摘要和结果类别,而不是参数内容。若法律或运维目的确实需要保留少量字段,应放进独立的访问受控审计记录。
Span 命名与上下文传播
使用 AgentRun、ModelCall、ToolCall、PolicyCheck 和 StateTransition 等稳定、低基数 Span 名称。不要把用户输入、资源 ID 或任意 Tool 参数放入名称。跨网关和可信 Tool 服务传播 W3C Trace Context;跨越租户或安全边界时,创建新的内部 Run ID。
区分以下标识:
trace_id:传输层因果关联;run_id:一次 Agent 执行及其重试/回放身份;request_id:外部请求或幂等边界;argument_digest:不暴露内容的精确输入摘要。
Tool 被拒绝时,记录策略结果和原因类别,但不要把 Trace 后端当作授权系统。
质量评估是独立平面
评估必须针对明确问题使用明确 Oracle:
| 评估层 | 示例 Oracle | 适用场景 |
|---|---|---|
| Contract | Schema、枚举、引用、预算、策略 | 每次运行或 CI |
| Scenario | 预期 Tool、Resource、状态和结果 | 回归测试 |
| Replay | 固定模型、Prompt、Fixture 和 Tool Double | 版本比较 |
| Abuse | 注入、跨租户、外泄、重复副作用 | 安全门禁 |
| Human Review | Rubric 与复议流程 | 高影响或歧义场景 |
| Model Judge | 校准 Rubric 与标注集 | 可扩展语义信号 |
不要让被评估的模型给自己的隐藏推理打分。评估可观察的输出、证据支持、策略决策和副作用。Judge 返回的置信度不是校准概率,除非已经用标注样本完成校准。
更安全的 Judge 契约
只传入 Rubric 需要的最少数据。把检索文本和模型回答视为不可信输入,使用清晰边界防止它们改变评估指令:
type JudgeInput = {
question: string;
answer: string;
evidence: Array<{ id: string; text: string }>;
};
type JudgeResult = {
supported: "yes" | "no" | "uncertain";
relevant: "yes" | "no" | "uncertain";
missingEvidence: string[];
rationale: string;
};
function validateJudgeResult(value: unknown): JudgeResult {
if (!value || typeof value !== "object") throw new Error("invalid_judge_result");
const result = value as Record<string, unknown>;
const allowed = new Set(["yes", "no", "uncertain"]);
if (!allowed.has(String(result.supported)) ||
!allowed.has(String(result.relevant)) ||
!Array.isArray(result.missingEvidence) ||
!result.missingEvidence.every((item) => typeof item === "string")) {
throw new Error("invalid_judge_result");
}
return {
supported: result.supported as JudgeResult["supported"],
relevant: result.relevant as JudgeResult["relevant"],
missingEvidence: result.missingEvidence as string[],
rationale: typeof result.rationale === "string" ? result.rationale : "",
};
}
权限、Tool 参数、结构化输出、证据 ID、预算和幂等性使用确定性检查。模型评估分数应与校准集比较,持续监控评估者一致性;高影响分歧进入人工复核。
能解释结果的指标
不要只做平均延迟和一个质量分数的仪表盘。指标应映射到具体动作:
- 运行完成、取消和预算耗尽率;
- 每次运行的 Model Call、Tool Call、重试和状态转移数量;
- 按策略版本统计 allow/confirm/deny,包括跨租户拒绝;
- Tool 参数校验失败和结果超限;
- 抽样评估中的证据覆盖率和无依据声明率;
- 按模型、Tool、租户类别和结果统计 p50/p95/p99 延迟;
- 输入/输出 Token、估算厂商成本和每个成功任务成本;
- 排队时间、下游错误类别和取消传播。
指标标签应使用有界枚举。包含用户问题或任意资源 ID 的标签会造成高基数和隐私问题。
不保存思维链也能调试循环
Agent 循环可以通过状态和动作事件观测:
from collections.abc import Iterable
from dataclasses import dataclass
@dataclass(frozen=True)
class StepEvent:
step: int
tool: str | None
argument_digest: str | None
state_version: int
policy: str
outcome: str
def detect_stall(events: Iterable[StepEvent], window: int = 4) -> bool:
sequence = list(events)
if len(sequence) < window * 2:
return False
recent = sequence[-window:]
previous = sequence[-window * 2:-window]
signature = lambda item: (
item.tool,
item.argument_digest,
item.state_version,
item.policy,
item.outcome,
)
return [signature(item) for item in recent] == [
signature(item) for item in previous
]
重复签名还必须结合最大步数、状态版本不变、重复幂等键和依赖超时。重复调用同一个 Tool 不一定是循环,分页和轮询可能是合法行为。最大步数和取消必须由运行时执行,而不是由遥测后端代替。
回放时保存脱敏事件 Fixture 和确定性 Tool Double,使用固定模型或已记录的模型响应重跑,再比较事件契约和业务结果。如果会再次执行外部副作用,就不能把它称为安全的“时间旅行”;应使用 Dry Run 或补偿语义。
采样与成本控制
采样是策略决策,不存在统一比例:
| 信号 | 可采用的策略 |
|---|---|
| 高影响操作或明确事故 | 保留所需审计字段 |
| 策略拒绝、超时或预算超限 | 保留扩展的脱敏 Trace |
| 普通低风险运行 | 保留元数据和有界样本 |
| 聚合健康指标 | 保留不含内容的计数和直方图 |
按 run_id 做确定性采样,确保子 Span 一致。Tail Sampling 可以等待结果信息后执行,但隐私过滤必须在导出前完成。成本估算应包含事件大小、流量、后端留存、评估调用和删除要求。
如何选择技术栈
按照契约和数据控制选择组件,而不是使用通用排行榜:
| 需求 | 能力候选 |
|---|---|
| 厂商无关传输 | OpenTelemetry SDK 与 Collector |
| Trace 查询 | 兼容 OTel 的后端 |
| Prompt/Eval 工作流 | 满足数据驻留、留存和访问要求的平台 |
| 回放与回归 | 版本化 Fixture、数据集和确定性 Tool Double |
| 看板与告警 | 使用有界标签的指标后端 |
托管和自托管产品的 SDK、数据驻留、删除、导出和价格都会变化。选型前应核对厂商当前文档。某个平台深度集成 LangChain,不代表它支持你的 Agent 运行时或授权模型。
生产控制
访问与留存
- 分离运维、开发、评估和租户对遥测的访问;
- 加密传输和存储,轮转导出器凭证;
- 将原始内容、事件元数据、聚合指标和评估产物放进不同留存类别;
- 将主体删除传播到 Trace 后端、缓存、导出物和评估数据集;
- 记录脱敏版本和策略版本,使 Schema 变更后仍可解释事件。
告警
围绕工作负载基线告警:
- 拒绝或跨租户尝试突然增加;
- 重复状态且没有进展;
- p99 延迟或排队时间超过服务目标;
- 每个成功任务成本超出预算;
- 校准样本中无依据证据或契约失败增加;
- 导出器失败或遥测丢失。
不要把 90%、80%、15% 或固定“生产就绪等级”写成通用阈值。正确阈值取决于影响、流量、模型和业务容忍度。
发布门禁
启用新 Tool 或模型前,至少需要确定性契约测试、场景测试、滥用测试、回放对比和隐私评审。高影响操作需要人工审批或独立授权工作流;Trace 或 Judge 分数不能授予权限。
常见失败模式
| 失败 | 需要的证据 | 修复动作 |
|---|---|---|
| 重复循环 | 状态版本、动作摘要、预算事件 | 停止运行,修复转移或预算 |
| Tool 选错 | 候选集合、选中 Tool、Schema 结果 | 改善边界并增加场景用例 |
| 无依据回答 | 证据 ID 与声明检查 | 要求证据或拒答 |
| 跨租户尝试 | 主体摘要、租户策略结果 | 拒绝、告警并检查调用路径 |
| 成本失控 | Token、重试、模型路由 | 执行预算和路由策略 |
| 遥测泄露 | 脱敏审计和访问日志 | 撤销访问、删除导出物并修过滤器 |
常见问题
Agent Trace 应该记录什么?
记录有界标识、耗时、版本、用量、策略结果、结果类别和最终业务结果。按用途脱敏或省略内容。Trace 不是隐藏推理逐字稿。
OpenTelemetry 是 Agent 专用标准吗?
不是。它是厂商无关的可观测性基础。应用仍需定义 Agent 事件、隐私规则和业务语义,再把事件映射到 Span、Log 和 Metric。
LLM Judge 可以直接作为发布门禁吗?
不能单独使用。确定性检查、校准的模型评估、回放、滥用测试和高影响人工复核应共同组成门禁。
不保存思维链,如何排查 Agent 循环?
使用有界 Step Event、状态版本、参数摘要、策略结果和预算;用脱敏 Fixture 与 Tool Double 回放。
遥测应该保留多久?
根据用途、敏感度、法律义务、事故需求和删除传播选择,不存在统一期限或采样率。
总结
好的 Agent 可观测性是一套受控的证据系统。OpenTelemetry 可以承载因果上下文,但应用事件契约才定义什么重要;评估应检查可观察契约,而不是相信模型解释;调试应重建状态和动作转移,而不是把私有 Prompt 或隐藏思维变成永久日志。把 Trace、评估、策略、隐私和回放放在一起设计,才能回答“发生了什么、应该改变什么”,同时避免制造第二个数据治理问题。
一手来源
- OpenTelemetry Documentation
- OpenTelemetry Trace Specification
- OpenTelemetry GenAI Semantic Conventions Repository,访问于 2026-07-28
- OpenTelemetry GenAI Semantic Conventions Move Notice,访问于 2026-07-28
- W3C Trace Context
- NIST AI Risk Management Framework
- OWASP Top 10 for LLM Applications
- Model Context Protocol Tools