
前言
前两篇分别解决了 Agent 的工具接入和知识检索:
系统跑起来以后,新的问题很快出现:为什么这次调用了错误工具?慢在模型、检索还是业务接口?Token 为什么突然翻倍?用户看到失败时,Agent 已经执行到哪一步?日志里能不能安全记录 Prompt 和工具参数?
普通 Web 接口往往只有一次请求和若干数据库调用,Agent 却是一个动态循环:模型决定下一步、工具返回结果、模型继续推理,直到结束或触发人工确认。只保存最终答案,会丢失最重要的执行上下文。
本文使用 OpenTelemetry、OpenInference 和 Phoenix 为 LangGraph 增加 Trace,并建立 Token、延迟、错误率和成本指标。示例基于 Python 3.12、LangGraph 1.2.9、openinference-instrumentation-langchain 0.1.67 与 arize-phoenix-otel 0.16.1。
一、先定义需要回答的问题
可观测性不是“日志越多越好”。先明确线上排查需要回答什么:
一次用户请求经过了哪些 Agent 节点?
模型每轮输入、输出和耗时是多少?
调用了哪个工具,参数是否通过校验?
检索返回了哪些 Chunk,分数和引用是什么?
错误发生在哪个 Span,是否自动重试过?
用户是否拒绝了确认,还是系统超时退出?
哪个版本的 Prompt、模型和工具契约产生了结果?
Trace 中是否意外写入了 API Key、身份证号或完整文档?
围绕这些问题设计数据,比直接打印完整 Messages 更有价值。
二、Trace、Metric 和 Log 各自负责什么
三类信号不要互相替代:
本文的遥测链路如下:
LangGraph、模型、MCP 工具和 Retriever 生成 Span;OpenInference 使用生成式 AI 语义补充模型、Token 和工具属性;OpenTelemetry SDK 负责采样、处理和导出;Phoenix 接收 OTLP Trace 并提供可视化分析。
三、Span 应如何划分
不要为每一行代码创建 Span。一个 Span 应对应能够独立判断成败和耗时的业务步骤。
推荐层级:
agent.run CHAIN
├─ model.plan LLM
├─ retriever.search RETRIEVER
│ └─ pgvector.query DB
├─ tool.query_order TOOL
│ └─ http.order_service CLIENT
├─ approval.wait HUMAN_IN_THE_LOOP
└─ model.answer LLM
根 Span 代表一次 Agent 任务,而不是一次模型请求。这样才能看到完整轮数、工具调用和最终状态。
每个 Span 至少记录:
trace_id、span_id、父子关系。节点名称、类型、开始时间和耗时。
状态:
OK、ERROR、CANCELLED或WAITING_CONFIRMATION。模型名称、Prompt 版本、工具契约版本。
输入输出 Token、重试次数和错误码。
经过脱敏的业务关联 ID。
四、启动本地 Phoenix
安装依赖:
pip install \
arize-phoenix \
arize-phoenix-otel==0.16.1 \
openinference-instrumentation-langchain==0.1.67 \
opentelemetry-sdk \
opentelemetry-exporter-otlp
启动本地 Collector 和界面:
python -m phoenix.server.main serve
默认页面位于 http://127.0.0.1:6006,Trace 接收端点是:
http://127.0.0.1:6006/v1/traces
本地模式适合开发调试。生产环境应把 Collector 独立部署,并配置认证、TLS、保留周期和访问控制。
五、配置 OpenTelemetry 与 OpenInference
官方 OpenInference LangChain Instrumentation 通过 langchain-core 的公共基础层捕获 LangChain 1.x 和 LangGraph 调用。
from openinference.instrumentation.langchain import LangChainInstrumentor
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource.create({
"service.name": "order-agent",
"service.version": "2026.07.1",
"deployment.environment": "local",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://127.0.0.1:6006/v1/traces")
))
trace.set_tracer_provider(provider)
LangChainInstrumentor().instrument(tracer_provider=provider)
tracer = trace.get_tracer("order-agent")
生产环境使用 BatchSpanProcessor,避免每个 Span 都同步发送。应用退出时需要给 Exporter 留出 flush 时间,否则最后一批 Trace 可能丢失。
六、为业务节点补充自定义 Span
自动埋点能捕获模型与 Chain,但它不知道“等待用户确认”或“订单查询”这些业务语义。关键节点仍需手动补充。
from opentelemetry.trace import Status, StatusCode
def guarded_tool_node(state):
tool_call = state["messages"][-1].tool_calls[0]
with tracer.start_as_current_span(
f"tool.{tool_call['name']}"
) as span:
span.set_attribute("gen_ai.tool.name", tool_call["name"])
span.set_attribute("agent.step", state["step_count"])
span.set_attribute("tool.call_id", tool_call["id"])
try:
safe_args = sanitize_tool_arguments(tool_call["args"])
span.set_attribute("tool.arguments.summary", safe_args)
result = execute_tool(tool_call)
span.set_attribute("tool.result.status", "success")
return {"messages": [to_tool_message(result, tool_call)]}
except ConfirmationRequired:
span.set_attribute("agent.status", "waiting_confirmation")
span.set_status(Status(StatusCode.UNSET))
raise
except Exception as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR, type(exc).__name__))
raise
确认等待不是系统错误,不能和真正异常混在一起。建议把它标记为明确业务状态,避免错误率告警被正常人工流程污染。
七、关联一次完整会话
一个用户会话可能包含多个 Agent 任务,一次任务又包含多轮模型调用。建议区分三个 ID:
session.id 用户会话,例如浏览器会话或工单会话
agent.run_id 一次 Agent 任务
thread_id LangGraph Checkpointer 使用的线程 ID
这些 ID 可以写入 Span 属性,但不要直接使用手机号、邮箱或身份证号。使用随机 ID,必要时在受控数据库中单独维护映射。
config = {
"configurable": {"thread_id": thread_id},
"metadata": {
"session_id": session_id,
"agent_run_id": run_id,
"prompt_version": "[email protected]",
},
}
result = graph.invoke(initial_state, config=config)
Prompt 版本必须进入 Trace。否则同一个问题昨天正常、今天异常时,无法判断是模型变化、Prompt 变化还是工具结果变化。
八、Token 和成本指标
模型响应通常包含 usage_metadata:
response = model.invoke(messages)
usage = response.usage_metadata or {}
input_tokens = usage.get("input_tokens", 0)
output_tokens = usage.get("output_tokens", 0)
total_tokens = usage.get("total_tokens", input_tokens + output_tokens)
建议至少统计:
每次 Agent 任务的模型调用轮数。
输入、输出和总 Token。
每个模型的估算成本。
模型、工具、检索和人工等待分别耗时。
因重试额外消耗的 Token。
成本估算不要写死在业务代码中。模型价格会变化,应使用独立配置表并记录价格版本:
def estimate_cost(model, input_tokens, output_tokens, price_table):
price = price_table[model]
return (
input_tokens / 1_000_000 * price.input_per_million
+ output_tokens / 1_000_000 * price.output_per_million
)
九、敏感数据脱敏
Trace 的调试价值很高,也最容易变成敏感数据仓库。Prompt、Header、工具参数、检索片段和模型输出都不能默认完整记录。
推荐使用字段白名单,而不是不断补充黑名单:
SAFE_TOOL_FIELDS = {
"query_order": {"order_id"},
"create_ticket": {"order_id", "reason_category"},
}
def sanitize_tool_arguments(tool_name, arguments):
allowed = SAFE_TOOL_FIELDS.get(tool_name, set())
return {
key: redact(value)
for key, value in arguments.items()
if key in allowed
}
def redact(value):
if not isinstance(value, str):
return value
value = API_KEY_RE.sub("[REDACTED]", value)
value = BEARER_RE.sub("Bearer [REDACTED]", value)
value = PHONE_RE.sub("[PHONE]", value)
return value
对“reason”这类自由文本尤其谨慎。它可能包含手机号、地址或用户原始对话,生产环境更适合记录分类和长度,而不是完整内容。
下面是本文本地执行导出的真实 Span 数据。authorization 和 api_key 在写入前已经替换为 [REDACTED]。

十、从 Trace 聚合 Metric
Trace 用来排查单次请求,Metric 用来发现趋势。本文示例的一次任务包含两次模型调用、两个工具调用,其中一个写工具因缺少确认被安全阻断。

图中数值来自确定性测试数据:输入 Token 1039、输出 Token 204、总耗时 438ms,两个工具调用中一个进入错误状态,因此工具错误率为 50%。这只是用于验证采集和展示链路,不代表生产性能基准。
生产环境建议建立这些面板:
十一、采样策略
全量记录所有 Trace 成本很高,也会扩大数据风险。建议组合使用:
错误、超时和用户投诉关联 Trace 全量保留。
高风险写工具和权限拒绝全量保留。
正常请求按比例采样,例如 5% 或 10%。
只保留结构化摘要,默认不保存完整 Prompt 和文档内容。
对 Trace 设置保留周期,并支持按用户请求删除关联数据。
Head Sampling 在请求开始时决定是否采样,开销低,但可能漏掉后续错误。Tail Sampling 可以在看到完整结果后保留异常 Trace,更适合 Agent 这种动态长链路,但需要 Collector 具备缓冲能力。
十二、告警如何设置
不要为单次工具失败直接报警。网络抖动和参数错误本来就是正常运行的一部分。
更有意义的告警:
5 分钟窗口内 Agent 成功率低于 SLO。
某工具 P95 延迟较基线翻倍。
单任务 Token P95 突然上涨。
平均执行步数持续增加,可能出现循环。
PERMISSION_DENIED或PROMPT_INJECTION_BLOCKED异常升高。OTLP Exporter 持续丢弃 Span。
告警中携带 Trace 链接和版本信息,值班人员才能从聚合指标快速跳到具体请求。
十三、失败回放与评估
Trace 可以用于离线回归,但不能直接把完整生产请求复制到测试环境。先脱敏并提取最小重现输入:
{
"question": "我的订单为什么还没有退款?",
"expected_tools": ["query_order"],
"forbidden_tools": ["create_ticket"],
"expected_status": "needs_more_information",
"source_trace_id": "masked-trace-id"
}
每次修改 Prompt、模型、工具描述或检索策略后,运行这批回归用例,比较:工具选择、参数正确率、最终状态、Token 和延迟。Agent 的质量不能只看最终文字“像不像”,还要验证执行轨迹是否合理。
十四、常见踩坑
14.1 把完整思维过程写入日志
可观测性需要记录模型输入输出和工具决策,不代表要存储隐藏推理内容。记录可解释的决策摘要和实际动作即可。
14.2 Span 名称包含用户输入
tool.query_order.ORD-1001 会造成高基数,还可能泄露业务数据。Span 名称保持稳定,把经过处理的业务 ID 放在属性中。
14.3 只记录成功请求
失败、取消和用户拒绝才是排查最需要的数据。必须在异常路径设置状态并确保 Exporter flush。
14.4 把确认等待计入模型延迟
人工确认可能等待几分钟。如果不单独建 Span,会让 Agent P95 延迟完全失真。
14.5 Trace 和业务日志没有关联
日志中应包含 trace_id,业务响应也可以返回安全的请求 ID。否则用户反馈问题时很难找到对应链路。
十五、生产检查清单
根 Span 覆盖完整 Agent 任务,而不是只覆盖单次模型调用。
模型、工具、检索和人工确认具有独立 Span。
记录 Prompt、模型、工具契约和知识库版本。
Token、成本、执行步数、错误率和延迟形成 Metric。
Span 名称低基数,敏感字段使用白名单与脱敏。
错误和高风险操作全量保留,正常请求按策略采样。
告警关联 Trace,Trace 关联脱敏后的回归用例。
Collector 配置认证、TLS、保留周期和删除机制。
应用关闭前 flush,监控 Exporter 丢弃和发送失败。
十六、总结
Agent 可观测性不是给现有日志加几个 Token 字段,而是重新描述一次动态执行任务:模型如何决策、工具如何执行、知识如何检索、用户在哪里确认、系统如何结束。
OpenInference 提供生成式 AI 语义,OpenTelemetry 提供标准 Trace 管道,Phoenix 提供本地和生产分析界面,LangGraph 则给每个节点提供了清晰的埋点边界。
当工具、知识和遥测三部分都具备明确契约后,Agent 才真正从“能演示”进入“可上线、可排查、可持续改进”的阶段。