首页
/ DeerFlow 全链路追踪体系深度解析:LangSmith / Langfuse / Monocle 三层可观测性架构与配置实战

DeerFlow 全链路追踪体系深度解析:LangSmith / Langfuse / Monocle 三层可观测性架构与配置实战

2026-09-07 17:16:32作者:翟萌耘Ralph

导读:本文以 DeerFlow 仓库中 deerflow.tracing 模块(backend/packages/harness/deerflow/tracing/AGENTS.md)为骨架,完整拆解其三层追踪方案——作为 LangChain CallbackHandler 接入的 LangSmith 与 Langfuse,以及基于 OpenTelemetry 的进程级遥测 Provider Monocle。你将掌握:三类 Provider 在代码中的接线位置与挂载时机、Langfuse 保留字段(session / user / trace_name / tags)如何从 LangGraph 配置注入根 Trace、为什么回调必须挂在"图根"而非模型上、以及 MONOCLE_TRACING / MONOCLE_EXPORTERS 等全部环境变量的含义与部署边界。读完即可在自己的 DeerFlow 网关或嵌入式客户端中正确开启全链路追踪,并理解每条 Trace 与 X-Trace-Id、日志、用户、会话之间的关联关系。

一、追踪体系总览:两类架构范式并存

DeerFlow 是一个多 Agent、可编排长时任务(research / code / create)的 SuperAgent 运行时。正因为一次任务会穿越 lead agent、子代理、模型调用、工具执行与各类中间件,DeerFlow 需要一套能把这整条链路收束为"一次运行一条 Trace"的可观测性方案。

deerflow.tracing 模块同时支持三类可观测性后端,但它们在架构上分属两种完全不同的范式:

Provider 机制 生命周期 覆盖路径
LangSmith LangChain CallbackHandler,随图运行逐次挂载 每次 run 内生效 所有从图根发起的路径 + 图外独立模型调用
Langfuse LangChain CallbackHandler(v4)+ RunnableConfig.metadata 保留字段 每次 run 内生效 同上,且把 trace 提升为带 session/user 语义的根 Trace
Monocle OpenTelemetry TracerProvider 进程级全局探针(非 Callback) 进程启动时一次性初始化 仅 Gateway lifespan;嵌入式 DeerFlowClient / TUI 需手动开启

这一"两套范式"的区分是整个模块设计的出发点,后续所有接线位置、初始化时机、配置项都能由此推导。

二、核心接线:factory.py 与 metadata.py 的分层职责

官方 AGENTS 文档明确指出,wiring 代码集中在两个文件、两个函数:

这两个文件的定位差异值得注意:factory.py 管"Handler 挂在哪",metadata.py 管"Langfuse 把元数据提升到根 Trace"。实现上:

# factory.py 精简骨架(真实源码见 factory.py#L37-L66)
def build_tracing_callbacks() -> list[Any]:
    validate_enabled_tracing_providers()          # 显式启用但凭据缺失时快速失败
    enabled_providers = get_enabled_tracing_providers()
    if not enabled_providers:
        return []
    tracing_config = get_tracing_config()
    callbacks: list[Any] = []
    for provider in enabled_providers:
        if provider == "langsmith":
            callbacks.append(_create_langsmith_tracer(tracing_config.langsmith))
        elif provider == "langfuse":
            callbacks.append(_create_langfuse_handler(tracing_config.langfuse))
    return callbacks

几点实现细节来自源码:

  • LangSmith 使用 LangChainTracer(project_name=config.project)
  • Langfuse 在 v4 中凭据通过 Langfuse(secret_key=…, public_key=…, host=…) 客户端单例配置,随后 LangfuseCallbackHandler 挂到该已配置 client 上;
  • 若某个 Provider 显式启用(env 为真)但初始化抛异常,build_tracing_callbacks() 会抛出 RuntimeError("… tracing initialization failed: …") 而非静默禁用——这是 README 与源码共同约定的 fail-fast 行为;
  • Monocle 不是回调型 Provider,因此 build_tracing_callbacks() 完全不会初始化它,仅当检测到 MONOCLE_TRACING 已设置但本进程尚未完成 setup 时输出一条 debug 日志,提示嵌入式/TUI 调用方需要自行调用 setup_monocle_tracing_if_enabled()factory.py#L42-L45)。

三、回调挂载点:为什么必须挂在"图根"而不是模型上

LangChain CallbackHandler 可以挂在很多层级:模型、链、图。DeerFlow 的原则是——图内运行一律挂在 LangGraph 调用根上,让一次 run 产生一条完整 Trace,所有 node / LLM / tool 调用都成为其子 span。

3.1 图根的四处接线

  • lead agent 路径agents/lead_agent/agent.pymake_lead_agent 在调用图前把 build_tracing_callbacks() 返回的 handler 追加进 config["callbacks"]
  • 网关 worker 路径runtime/runs/worker.py::run_agent 在构造图配置时注入元数据;
  • 嵌入式客户端路径client.py::DeerFlowClient.streamstream() 中做同样的事——注释明确写着"让嵌入式客户端与网关 worker 行为一致";
  • 子代理路径subagents/executor.py::_aexecute 为每次子代理 run 重复同一套"图根挂 handler + 注入元数据"的模式,使子代理 Trace 归组到父线程的 session 卡片下。

3.2 挂在图根的技术原因

Langfuse 有一个关键行为:它的 v4 langchain.CallbackHandler 只有看到 on_chain_start(parent_run_id=None)(即根节点)时,才会把 RunnableConfig.metadata 中的保留字段(langfuse_session_idlangfuse_user_id 等)通过内部 _parse_langfuse_trace_attributes 提升到根 Trace 上。

如果回调挂在模型上,模型只是图中嵌套的一层 observation,Langfuse handler 会丢弃这些 langfuse_* 字段——这正是 lead agent 源码注释中反复强调"root-level attachment"的原因(agent.py#L983-L988)。

3.3 图外独立调用的兜底

图外还有些不经过 LangGraph 的模型调用(例如记忆模块 MemoryUpdater 这类 standalone 调用方)。它们没有图根可用,DeerFlow 的兜底策略是:

  • models/factory.py::create_chat_modelattach_tracing=True 为默认值;
  • attach_tracing=True 时,模型创建完成即把 build_tracing_callbacks() 直接附到 model_instance.callbacks 上(factory.py#L334-L339),退化为"模型级回调挂载";
  • 反之,图内运行(如 TitleMiddleware 等在图内发起的模型调用)必须传 attach_tracing=False,否则会与图根的 handler 产生双重计数。

四、Langfuse Trace 元数据注入:字段映射与三条注入路径

4.1 注入点与 setdefault 语义

metadata.py::inject_langfuse_metadata() 被设计为"网关 worker 与嵌入式客户端共享的同一入口,两条路径永不漂移"。它的合并语义是 setdefault调用方已提供的 key 优先生效(例如前端外部传入的 session_id 覆盖不会被内部覆盖),并通过就地修改 config["metadata"] 完成注入。

三个核心注入点:

  1. 网关路径 worker.py::run_agentL1057-L1065),在安装 runtime context 之后、真正构建图之前执行;
  2. 嵌入式路径 client.py::DeerFlowClient.streamL908-L916),使用 ensure_trace_id() 保证与请求侧一致;
  3. 子代理路径 executor.py::_aexecuteL1459-L1467)。

此外,utils/oneshot_llm.pyagents/memory/manager.pyskills/security_scanner.pyruntime/goal.py 等图外调用方也会注入同一份 Langfuse 元数据,使它们也能作为独立 Trace 被 Langfuse 采集。

4.2 字段映射总表(AGENTS 文档原表)

Langfuse 字段 取值来源
langfuse_session_id LangGraph thread_id
langfuse_user_id get_effective_user_id()(无认证场景回落到 default);子代理场景在 task_tool 时刻通过 resolve_runtime_user_id()runtime.context 捕获
langfuse_trace_name RunRecord.assistant_id / 客户端 agent_name(默认 lead-agent);子代理为 subagent:<name>(转小写、_-
langfuse_tags env:<DEER_FLOW_ENV> + model:<model_name>
deerflow_trace_id 当前入口 trace id,来自 deerflow.trace_context始终写入,且恒等于同一 Gateway 请求返回的 X-Trace-Id,不受任何配置开关门控

4.3 deerflow_trace_id:一条贯穿日志、响应头与 Trace 的关联主键

deerflow_trace_id 值得单独说明。它被无条件写入(Langfuse 未启用时 build_langfuse_trace_metadata 返回 {},不影响 LangSmith 单跑场景),并且其值不是 Langfuse 自己的 trace id,也不是 DeerFlow 的 run id,而是 DeerFlow 请求级关联 id(trace_context.py):

  • 对 HTTP 路径,由 Gateway 的 TraceMiddleware 绑定,并作为 X-Trace-Id 响应头返回;
  • 对非 ASGI 入口(定时调度、MCP 任务通知、IM 消息、嵌入式 DeerFlowClient),由 ensure_trace_context 先绑定;
  • logging.enhance.enabled 开启时,日志记录携带同一 trace_id 字段。

因此,拿着任意一条日志/响应头的 X-Trace-Id,就能在 Langfuse 中找到对应的那条 Trace。trace_context.py 还特别强调:metadata.deerflow_trace_id 属于派生输出,永远不会被读回作为输入——外部调用方要固定关联 id 必须发送 X-Trace-Id 头,而不是在请求 metadata 里塞 deerflow_trace_id。这个约定是为了防止持久化的 run 与同请求已返回的响应头互相矛盾。

4.4 子代理的归组语义

executor.py 可以看到子代理 Trace 的完整设计:

  • assistant_id 规范化为 subagent:<normalized-name>(小写、下划线转连字符),归入父线程的 Langfuse session 卡片;
  • 携带父 thread_id → langfuse_session_id
  • 携带在 task_tool 时刻从 runtime.context 捕获的 langfuse_user_id
  • 同步注入与父 run 相同的 deerflow_trace_id,让子代理 Trace 与父请求日志对齐。

五、配置解析与全部相关环境变量

追踪配置全部由环境变量驱动,统一在 config/tracing_config.pyget_tracing_config() 中解析,且结果带进程级缓存(_tracing_config,可用 reset_tracing_config() 丢弃缓存以支持测试)。

5.1 完整环境变量清单

Provider 环境变量 默认值 / 说明
LangSmith LANGSMITH_TRACING 布尔开关;兼容 LANGCHAIN_TRACING_V2 / LANGCHAIN_TRACING
LangSmith LANGSMITH_API_KEY 兼容 LANGCHAIN_API_KEY
LangSmith LANGSMITH_PROJECT 默认 deer-flow(兼容 LANGCHAIN_PROJECT
LangSmith LANGSMITH_ENDPOINT 默认 https://api.smith.langchain.com
Langfuse LANGFUSE_TRACING 布尔开关
Langfuse LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY 凭据,两者缺一即视为"显式启用但未配置完全"
Langfuse LANGFUSE_BASE_URL 默认 https://cloud.langfuse.com;自托管时改为你的 host
Monocle MONOCLE_TRACING 布尔开关,默认关闭
Monocle MONOCLE_EXPORTERS 默认 file;逗号分隔,可选 file / console / okahu / s3 / blob / gcs
Monocle OKAHU_API_KEY okahu exporter 需要
环境标签 DEER_FLOW_ENV(或 ENVIRONMENT 用于给 trace 打 env:<…> 标签,区分部署环境

两点解析细节:

  • 布尔取值_env_flag_preferred 只在环境变量"存在且非空"时才生效,真值集合为 {1, true, yes, on}tracing_config.py#L122-L131);
  • 取值优先级_first_env_value 按候选顺序取第一个非空值,因此 LANGSMITH_* 优先于旧式 LANGCHAIN_*,README 明确说明"同时设置时 LANGSMITH_* 胜出"。

5.2 enabled 与 explicitly_enabled 的区别

源码刻意区分了两个概念(tracing_config.py#L96-L112):

  • enabled_providers:真正启用且凭据齐全的 Provider(LangSmith 需 api_key,Langfuse 需 public+secret 双 key),由 get_enabled_tracing_providers() 返回;
  • explicitly_enabled_providers:只要 env 显式打开就算(无论凭据是否齐全)。

build_tracing_callbacks() 首先调用 validate_enabled_tracing_providers():若某 Provider 显式启用但缺凭据,会直接抛出带具体缺失变量名的 ValueError(如 Langfuse tracing is enabled but required settings are missing: LANGFUSE_PUBLIC_KEY),在初始化阶段就暴露配置问题,而不是运行中静默丢 Trace。

5.3 LangSmith 与 Langfuse 的双 Provider 并存

backend/README.md 给出了两套可直接粘贴进 .env 的配置:

# LangSmith
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=lsv2_pt_xxxxxxxxxxxxxxxx
LANGSMITH_PROJECT=xxx

# Langfuse(可同时开启)
LANGFUSE_TRACING=true
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_BASE_URL=https://cloud.langfuse.com

两者同时启用时,build_tracing_callbacks() 会同时初始化两个 handler 并追加到同一回调列表——同一次 run 的数据会被同时上报到两套系统。Docker 部署默认关闭(LANGSMITH_TRACING=false),需要时在 .env 中打开并补齐凭据即可。另外注意:Monocle 走纯环境变量(MONOCLE_TRACING 等),不会出现在 config.yaml / config.example.yaml 的键位中(config.example.yaml#L58-L64 中只有一段注释说明)。

六、Monocle:结构完全不同的进程级 OTel 遥测

Monocle 与 LangSmith/Langfuse 最大的不同在于它不是 LangChain callback,而是 OpenTelemetry 生态的进程级遥测:

  • monocle.py::setup_monocle_tracing_if_enabled() 调用 monocle_apptrace.setup_monocle_telemetry()
  • 该调用一次性安装进程全局的 OTel TracerProvider、patch span 序列化,并自动插桩 openai / langchain / langgraph 客户端。

6.1 唯一初始化点:Gateway lifespan

正因为这是一次性、进程级副作用(而非每次 run 一个回调),Monocle 只允许从 Gateway lifespan 初始化

  • 唯一调用点位于 app/gateway/app.py#L241 的 lifespan 内,由测试 test_gateway_lifespan_initializes_monocle 钉死"sole call site";
  • setup 调用被刻意移出 agents/__init__.py,保证 import deerflow.agents 绝不会触发追踪(测试 test_no_import_time_setup 钉死该约束);
  • build_tracing_callbacks()永远不会初始化 Monocle。

由此带来的边界是:与"图根挂载、覆盖所有路径"的 LangSmith/Langfuse 不同,嵌入式 DeerFlowClient 与 TUI 默认不被 Monocle 插桩。嵌入式用户若需要 Monocle Trace,必须在运行 agent 前自行调用一次:

from deerflow.tracing import setup_monocle_tracing_if_enabled

setup_monocle_tracing_if_enabled()  # MONOCLE_TRACING 未开启时是安全的 no-op

6.2 薄封装的设计取舍

setup_monocle_tracing_if_enabled() 故意保持"薄":

  • 先经 monocle.validate() 快速失败——未知 exporter 或选中 okahu 却缺 OKAHU_API_KEY 会在启动时报错(MONOCLE_EXPORTERS has unknown exporter(s): … / Monocle 'okahu' exporter is selected but OKAHU_API_KEY is not set),避免配置拼写错误破坏正常 agent 运行;
  • monocle_apptrace 自身已做重复 setup 防护(instrumentor.py::check_duplicate_setup),且从不强制覆盖已存在的全局 Provider,因此封装层只需门控配置即可;
  • monocle_apptrace 未安装,抛出带安装指引的 RuntimeErroruv sync --extra monocle(backend 目录)或 pip install 'deerflow-harness[monocle]'
  • 当选择了远程 exporter 时(非 file/console),会发出警告日志,提醒 trace 数据(prompt、工具输入输出、补全内容)正在离开本机,且若 Langfuse 同时启用共享全局 Provider,Langfuse 的 span 也会一并被这些 exporter 导出,务必确保目标可信。

6.3 导出目标

MONOCLE_EXPORTERS 支持逗号分隔多选,默认 file,把 trace 数据以 JSON 形式写入本地 .monocle/ 目录;其余为 console(输出到 stdout)、okahu(需 OKAHU_API_KEY)、s3blobgcs。DeerFlow 在 tracing_config.py#L53 中本地维护了一份支持的 exporter 白名单镜像,专门为了让拼写错误在启动时以清晰信息失败,而不是透传上游晦涩报错。

6.4 与 Langfuse 的共存已被验证

Langfuse v4 本身也是 OTel 基础的。DeerFlow 验证了两者共存的安全性:谁后初始化谁就复用已有的全局 TracerProvider 并追加自己的 span processor,任何一方都不会丢失 span——两个 processor 都能看到全部 span,因此同时开启时 Monocle 的 exporter 也会捕获 Langfuse 的 span(测试 test_coexists_with_langfuse 钉死)。LangSmith 是纯回调,天然共存。

6.5 DeerFlow 不为 Monocle 注入额外字段

与上文 Langfuse metadata 大量注入不同,DeerFlow 不向 Monocle trace 注入任何 per-run 字段——它设置的唯一属性是 workflow_name="deer-flow"。其余所有 span 属性(span.typeentity.*、token 用量、span 输入输出、scope.agentic.session)全部由 Monocle 自身的 metamodel 与自动插桩生成,因此这里不存在需要维护的 DeerFlow trace 属性层。

七、测试保障:行为契约的"钉子"

该模块的行为大多有对应测试钉死,可作为阅读与二次开发的契约参考(均在 backend/tests 下):

八、配置清单:从零开启可观测性

最后按部署形态给出完整操作清单:

1. 网关部署(默认覆盖所有路径)

# Langfuse(或 LangSmith,可并存)
export LANGFUSE_TRACING=true
export LANGFUSE_PUBLIC_KEY=pk-lf-xxxx
export LANGFUSE_SECRET_KEY=sk-lf-xxxx
export LANGFUSE_BASE_URL=https://cloud.langfuse.com

# 可选:Monocle(进程级 OTel)
export MONOCLE_TRACING=true
export MONOCLE_EXPORTERS=file            # 或 console / okahu / s3 / blob / gcs
export OKAHU_API_KEY=...                 # 仅 okahu 需要

# 环境标签(Langfuse tags = env:<value>)
export DEER_FLOW_ENV=production

2. 嵌入式 / TUI 部署:LangSmith/Langfuse 会在 DeerFlowClient.stream() 内自动挂载;Monocle 需在运行 agent 前手动执行一次 setup_monocle_tracing_if_enabled()(见 tracing/init.py 导出的公共 API)。

3. 验证锚点

  • X-Trace-Id 回查日志与 Langfuse 中的同一条 Trace(deerflow_trace_id 恒等);
  • 子代理 Trace 应出现在父线程 session 下,名为 subagent:<name>
  • 打开 MONOCLE_TRACING 后查看 .monocle/ 目录是否产出 trace JSON;
  • 若同时启用 Langfuse 与 Monocle,两侧都不应丢 span。

三个易踩的边界请牢记:其一,图内模型必须 attach_tracing=False,否则双重计数;其二,显式启用却缺凭据会导致启动失败而非静默——这是刻意的 fail-fast;其三,Monocle 的覆盖范围仅限 Gateway 进程,嵌入式路径必须手动开启。理解了这些边界,DeerFlow 的三层追踪体系就能成为一条真正端到端可对齐、可排查、可审计的观测链路。

登录后查看全文
热门项目推荐
相关项目推荐