DeerFlow 全链路追踪体系深度解析:LangSmith / Langfuse / Monocle 三层可观测性架构与配置实战
导读:本文以 DeerFlow 仓库中
deerflow.tracing模块(backend/packages/harness/deerflow/tracing/AGENTS.md)为骨架,完整拆解其三层追踪方案——作为 LangChainCallbackHandler接入的 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::build_tracing_callbacks()——根据当前启用的 Provider 环境变量,返回 LangChain
CallbackHandler列表; - metadata.py::build_langfuse_trace_metadata()——构造 Langfuse 保留的 trace 属性,写入
RunnableConfig.metadata。
这两个文件的定位差异值得注意: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.py 的
make_lead_agent在调用图前把build_tracing_callbacks()返回的 handler 追加进config["callbacks"]; - 网关 worker 路径:runtime/runs/worker.py::run_agent 在构造图配置时注入元数据;
- 嵌入式客户端路径:client.py::DeerFlowClient.stream 在
stream()中做同样的事——注释明确写着"让嵌入式客户端与网关 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_id、langfuse_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_model 的
attach_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"] 完成注入。
三个核心注入点:
- 网关路径
worker.py::run_agent(L1057-L1065),在安装 runtime context 之后、真正构建图之前执行; - 嵌入式路径
client.py::DeerFlowClient.stream(L908-L916),使用ensure_trace_id()保证与请求侧一致; - 子代理路径
executor.py::_aexecute(L1459-L1467)。
此外,utils/oneshot_llm.py、agents/memory/manager.py、skills/security_scanner.py、runtime/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.py 的 get_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未安装,抛出带安装指引的RuntimeError:uv 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)、s3、blob、gcs。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.type、entity.*、token 用量、span 输入输出、scope.agentic.session)全部由 Monocle 自身的 metamodel 与自动插桩生成,因此这里不存在需要维护的 DeerFlow trace 属性层。
七、测试保障:行为契约的"钉子"
该模块的行为大多有对应测试钉死,可作为阅读与二次开发的契约参考(均在 backend/tests 下):
- test_tracing_factory.py:
build_tracing_callbacks()的 Provider 选择、启停与 fail-fast 行为; - test_tracing_metadata.py:Langfuse trace metadata 构造与字段映射;
- test_worker_langfuse_metadata.py 与 test_client_langfuse_metadata.py:网关路径与嵌入式路径的元数据注入;
- test_subagent_executor.py::TestSubagentTracingWiring:子代理的 tracing 接线(命名规范化、session/user 携带);
- test_monocle_tracing.py:
test_no_import_time_setup(import 不触发插桩)、test_gateway_lifespan_initializes_monocle(lifespan 是唯一调用点)、test_coexists_with_langfuse(与 Langfuse 共存)等。
八、配置清单:从零开启可观测性
最后按部署形态给出完整操作清单:
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 的三层追踪体系就能成为一条真正端到端可对齐、可排查、可审计的观测链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00