三、LangGraph Agent 可观测性实战:追踪模型、Token 与工具调用

前言 前两篇分别解决了 Agent 的工具接入和知识检索: LangGraph + MCP 实战:把业务 API 封装成可控的 Agent 工具 LangGraph + PGVector 构建 RAG 知识

LangGraph Agent 全链路追踪封面

前言

前两篇分别解决了 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。

一、先定义需要回答的问题

可观测性不是“日志越多越好”。先明确线上排查需要回答什么:

  1. 一次用户请求经过了哪些 Agent 节点?

  2. 模型每轮输入、输出和耗时是多少?

  3. 调用了哪个工具,参数是否通过校验?

  4. 检索返回了哪些 Chunk,分数和引用是什么?

  5. 错误发生在哪个 Span,是否自动重试过?

  6. 用户是否拒绝了确认,还是系统超时退出?

  7. 哪个版本的 Prompt、模型和工具契约产生了结果?

  8. Trace 中是否意外写入了 API Key、身份证号或完整文档?

围绕这些问题设计数据,比直接打印完整 Messages 更有价值。

二、Trace、Metric 和 Log 各自负责什么

三类信号不要互相替代:

信号

适合回答

示例

Trace

一次请求如何经过多个节点

Agent → Model → Tool → Model

Metric

系统整体趋势是否异常

P95 延迟、Token、工具错误率

Log

某个离散事件的详细上下文

参数校验失败、用户拒绝确认

本文的遥测链路如下:

Agent 遥测数据流

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_idspan_id、父子关系。

  • 节点名称、类型、开始时间和耗时。

  • 状态:OKERRORCANCELLEDWAITING_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 数据。authorizationapi_key 在写入前已经替换为 [REDACTED]

OpenTelemetry Span 实际导出视图

十、从 Trace 聚合 Metric

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

Agent 请求关键指标

图中数值来自确定性测试数据:输入 Token 1039、输出 Token 204、总耗时 438ms,两个工具调用中一个进入错误状态,因此工具错误率为 50%。这只是用于验证采集和展示链路,不代表生产性能基准。

生产环境建议建立这些面板:

指标

建议维度

用途

Agent 成功率

Agent、版本、环境

发现整体回归

P50/P95/P99 延迟

节点类型、模型、工具

定位慢点

Token/任务

模型、Prompt 版本

发现上下文膨胀

工具错误率

工具名、错误码

发现依赖异常

平均执行步数

Agent、任务类型

发现循环或规划问题

人工确认率

工具名、结果

评估流程设计

拒答率

知识库版本、问题类型

评估 RAG 覆盖

十一、采样策略

全量记录所有 Trace 成本很高,也会扩大数据风险。建议组合使用:

  1. 错误、超时和用户投诉关联 Trace 全量保留。

  2. 高风险写工具和权限拒绝全量保留。

  3. 正常请求按比例采样,例如 5% 或 10%。

  4. 只保留结构化摘要,默认不保存完整 Prompt 和文档内容。

  5. 对 Trace 设置保留周期,并支持按用户请求删除关联数据。

Head Sampling 在请求开始时决定是否采样,开销低,但可能漏掉后续错误。Tail Sampling 可以在看到完整结果后保留异常 Trace,更适合 Agent 这种动态长链路,但需要 Collector 具备缓冲能力。

十二、告警如何设置

不要为单次工具失败直接报警。网络抖动和参数错误本来就是正常运行的一部分。

更有意义的告警:

  • 5 分钟窗口内 Agent 成功率低于 SLO。

  • 某工具 P95 延迟较基线翻倍。

  • 单任务 Token P95 突然上涨。

  • 平均执行步数持续增加,可能出现循环。

  • PERMISSION_DENIEDPROMPT_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 才真正从“能演示”进入“可上线、可排查、可持续改进”的阶段。

LICENSED UNDER CC BY-NC-SA 4.0
评论