程云来 / 杭州
Back to Blog·LangGraph
·About 12 min

LangGraph 过程监控(二):先让 Phoenix 留下可解释的证据

从本地部署 Phoenix 开始,理解 OpenTelemetry、OpenInference 和 LangGraph 的接入边界,并建立一套可查询、可脱敏的 Trace 与指标。


“这次很慢”需要一棵证据树

本地 graph.invoke() 返回结果时,打印一行成功日志也许够用。设想开发者在 Phoenix 里看到一条运行耗时 28 秒:他需要继续定位,究竟是检索节点、模型请求还是重试退避占用了时间;检索为空是上游没有数据,还是过滤条件变化;恢复之后的发布是否真的只发生了一次。

单条日志适合描述一个事实,Trace 适合串起一次运行的父子调用、耗时和属性。对于 LangGraph,还需要把节点、模型、检索器、工具和外部 HTTP 请求放进同一条关联链,否则只能看到许多孤立的“成功 / 失败”。

Phoenix 处在执行器之外

Phoenix1 是 Trace 的接收、存储、查询和可视化后端。它不执行 LangGraph,不保存图的恢复点,也不负责决定用户能否继续审核。数据链路可以画成:

正在绘制图表…
查看 Mermaid 源码
flowchart LR
    A[LangGraph 图] --> B[LangChain / OpenInference 语义]
    B --> C[OpenTelemetry SDK]
    C -->|OTLP| D[Phoenix]
    D --> E[Trace 查询]
    D --> F[评估与指标]
    A --> G[Checkpointer]

Mermaid 大图

可滚动查看图表,点击缩放比例可恢复 100%。按 Esc 关闭。

Trace 经 OpenInference 和 OpenTelemetry 送入 Phoenix;Checkpointer 仍由应用运行时负责,不能把 Trace 当作恢复状态。

OpenTelemetry(OTel)2 提供 Trace、Span、导出器和传输的厂商无关能力;OpenInference3 在其上补充 LLM、Agent、Tool 和 Retriever 的语义属性。一次调研运行在 Trace 中应类似这样:research.run 是根 Span,下面有 split_questionscollect_sourcesllm.generatepublish_article;每个子 Span 带耗时、错误和外部请求 ID。Phoenix 消费这些标准化数据并提供 AI 调用的查询界面。将来更换支持 OTLP 的后端时,应用侧的观测边界不必整体重写。

先在本地启动一个可验证的 Phoenix

开发环境可以用 Docker 快速确认数据链路。latest 只适合实验;生产镜像应锁定实际验证过的版本,并单独配置认证、持久化和网络入口:

bash
docker pull arizephoenix/phoenix:latest
docker run --rm \
  -p 6006:6006 \
  -p 4317:4317 \
  arizephoenix/phoenix:latest

打开 http://localhost:6006,确认页面能访问后再启动应用。6006 是常用的 Web / collector HTTP 入口,4317 是 OTLP gRPC 入口;真实部署要以所用 Phoenix 版本的端口和协议配置为准。

如果团队希望把生命周期写入项目,可以使用 Compose:

yaml
services:
  phoenix:
    image: arizephoenix/phoenix:latest
    ports:
      - "6006:6006"
      - "4317:4317"
    environment:
      PHOENIX_SQL_DATABASE_URL: postgresql://postgres:postgres@db:5432/postgres
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    volumes:
      - phoenix-db:/var/lib/postgresql/data

volumes:
  phoenix-db:
bash
docker compose up -d

Compose 示例里的数据库凭据只适合本地开发。线上至少要替换凭据、限制 6006 的暴露范围、使用持久卷和备份,并设定 Trace 保留期。提示词、用户问题和检索片段可能含个人信息,Phoenix 的访问控制不能被当成“调试工具例外”。

用 Phoenix OTel 接入应用入口

按 2026-08-24 Phoenix 文档的 Python 示例,先安装 OTel 封装和需要的 instrumentor:

bash
pip install "arize-phoenix-otel>=0.16.0"
pip install openinference-instrumentation-langchain
pip install openinference-instrumentation-openai

本地默认可以连接 http://localhost:6006;远程实例再注入 endpoint 和 API key:

bash
export PHOENIX_COLLECTOR_ENDPOINT="http://localhost:6006"
# 远程实例按部署文档设置:
# export PHOENIX_API_KEY="..."

初始化集中放在应用入口,一次完成,不要在每个节点里重复初始化:

python
# observability.py
from phoenix.otel import register


def setup_tracing():
    return register(
        project_name="langgraph-research",
        auto_instrument=True,
        batch=True,
    )

auto_instrument=True 会根据当前环境启用受支持的集成。安装 LangChain 和 OpenAI 的 instrumentor 后,LangGraph 中由 LangChain 驱动的链和模型调用有机会出现在同一条 Trace 中。接入后不要只确认“列表里出现了一条 Trace”,而要展开一次运行检查:根 Span 是否包含 run_id,模型 Span 是否挂在正确的节点下,超时是否记录了错误类型和供应商请求 ID。

自动埋点之后,还要补业务语义

自动埋点通常能告诉你某个库调用了多久,却不一定知道这次调用属于哪个租户、哪个业务运行或哪个草稿版本。应用入口应把这些关联信息放进 Trace context 或受控属性:

python
from opentelemetry import trace

tracer = trace.get_tracer("research-workflow")


def run_research(graph, state, config, run_id: str):
    thread_id = config["configurable"]["thread_id"]
    with tracer.start_as_current_span("research.run") as span:
        span.set_attribute("app.run_id", run_id)
        span.set_attribute("app.thread_id", thread_id)
        span.set_attribute("app.workflow", "research")
        return graph.invoke(state, config=config)

属性名称是项目契约的一部分;不要把整段 prompt、用户原文和工具参数无差别写入 Span。建议记录模板版本、输入哈希、租户级匿名 ID、草稿 revision、错误分类和外部请求 ID。这样开发者点开某个 llm.generate Span 时,能知道它属于哪次运行和哪个版本,却不会在 Trace 页面直接暴露用户原文。原始内容如确实需要复现,应放在受控存储,并在 Trace 中只保留引用。

节点级手工 Span 也要有边界:只有当自动埋点不能解释业务步骤时才补,不要为每一行 Python 代码创建 Span。一个“检索资料” Span 应包含开始 / 结束时间、来源数量、重试次数和结果分类,而不是完整网页正文。

一次 LangGraph 运行至少观察什么

可以按四个问题组织字段:

问题关键字段用途
运行走到哪里run_idthread_id、节点名、状态还原控制流和恢复关系
为什么变慢节点 / 模型耗时、排队时间、重试次数定位延迟来源
为什么失败错误类型、供应商请求 ID、是否可重试判断处理路径
为什么变贵或变差token、模型版本、工具次数、反馈标签成本和质量回归

从 Trace 聚合出的指标可以包括:按节点和模型的 p50 / p95 延迟、失败率、重试率、空检索率、每次运行 token 和费用估算、人工暂停时长。比如 p95 只在 collect_sources 升高,说明先查检索上游;如果所有节点都升高,才需要检查整体排队或 Phoenix exporter。指标只保留聚合事实;不要为了方便统计而把敏感原文复制到指标标签,基数过高的 thread_id 也不适合作为 Prometheus label。

两条 Phoenix 接入失败路径

页面能打开,但 Trace 列表为空

先看应用的 PHOENIX_COLLECTOR_ENDPOINT、协议配置和 exporter 错误,再用一个确定性节点发起最小运行。如果本地页面正常而 exporter 没有数据,通常是 endpoint、端口或批处理进程退出时没有 flush;处理方式是按照目标版本确认 HTTP / gRPC 入口,并在短脚本结束前关闭 tracer provider。验证时应能在 Phoenix 中按项目名看到一条运行和至少一个模型 / 节点 Span。

Trace 有模型 Span,却看不到业务节点

先检查自动 instrumentor 是否覆盖了 LangGraph 当前版本,以及应用是否只把模型调用包进了 Span。处理方式是在应用入口补一个 research.run Span,必要时为检索和发布节点增加少量手工 Span,并用 run_id / thread_id 关联。验证时展开 Trace,节点、模型和外部请求应形成可解释的父子关系,而不是一组孤立调用。

Phoenix 不能替代什么

Phoenix 能解释“发生了什么”,但它不能替代:

  • Checkpointer:保存图 State 和恢复点;
  • 业务数据库:保存发布、审核、权限和幂等事实;
  • 用户事件协议:决定浏览器应该显示哪些进度;
  • 评估集:判断改动是否改善了答案质量;
  • 告警系统:把失败率、延迟或成本异常通知到值班流程。

把这些职责都塞进 Trace,会导致 Trace 既无法作为可靠业务记录,也无法作为轻量用户事件。它只负责留下可查询的运行证据,业务事实和调优结论要在各自的系统中保存。

最小上线检查清单

  • Phoenix 版本、端口、存储和认证方式已固定并验证。
  • OTel / OpenInference 初始化只发生在应用入口。
  • run_idthread_id、Trace ID 的关联关系已写入接口约定。
  • LangGraph、模型、检索和工具的 Span 层级能在页面中解释。
  • 节点耗时、错误分类、重试、token 和外部请求 ID 可查询。
  • prompt、用户内容、工具参数和检索片段有脱敏与访问控制。
  • Trace 数据有保留期、删除和备份策略。
  • 自动埋点失败时,应用仍能完成核心业务或进入明确降级路径。

参考资料

Footnotes

  1. Phoenix 官方文档:用于接收、查询和可视化 LLM、Agent 与工具调用的 Trace。

  2. OpenTelemetry 官方文档:厂商无关的观测数据采集与传输标准。

  3. OpenInference 官方文档:为 AI 调用补充统一语义属性的规范与工具。