DeerFlow 可观测性实战:基于 Langfuse 与 LangSmith 双 Provider 的 Tracing 接入方案
本文基于 DeerFlow 仓库中 Langfuse Tracing 实施计划,完整拆解该方案的设计目标、多 Provider 追踪配置模型、回调工厂与模型挂接机制,并逐任务对照仓库中已落地的源码实现与测试用例。读完本文,你可以掌握:如何在 DeerFlow 中同时启用 Langfuse 与 LangSmith 双追踪通道、各环境变量与默认值的准确含义,以及"显式启用但配置缺失时快速失败(fail-fast)"这一行为在代码层的实现原理与验证方法。
背景与目标:从单一 LangSmith 到多 Provider 追踪
DeerFlow 是一个面向长周期任务的 SuperAgent 运行框架,其内部的 LLM 调用链路复杂(Lead Agent、子 Agent、中间件层层调用),因此可观测性是定位问题、评估 token 消耗的关键手段。早期版本中,DeerFlow 的追踪配置只支持 LangSmith 一种 Provider。
该计划文档开篇明确了改造目标(Goal):
Goal: Add optional Langfuse observability support to DeerFlow while preserving existing LangSmith tracing and allowing both providers to be enabled at the same time.
即在保留现有 LangSmith 追踪能力的前提下,新增可选的 Langfuse 支持,并允许两者同时启用。技术栈为:Python 3.12、Pydantic、LangChain callbacks、LangSmith、Langfuse、pytest。
计划采用的总体架构(Architecture)可归纳为三步:
- 扩展追踪配置:从"仅 LangSmith 的单 Provider 形状"扩展为多 Provider 配置结构;
- 引入追踪回调工厂:根据环境变量构建 0 个、1 个或 2 个回调(callback);
- 改造模型创建路径:在创建模型实例时挂接这些回调。若某个 Provider 被显式启用但配置不完整或初始化失败,追踪初始化必须在模型创建阶段抛出明确指名该 Provider 的错误,而不是静默降级。
该方案采用测试驱动(TDD)的四个任务推进:先写失败测试,再写最小实现,最后接线依赖与文档。下文按任务顺序展开,每一步都给出仓库中已实现的对应源码证据。
Task 1:多 Provider 追踪配置模型
计划中的 Task 1 要求修改 test_tracing_config.py,先写覆盖以下场景的失败测试:
- 仅 Langfuse 的配置解析(Langfuse-only config parsing)
- 双 Provider 解析(dual-provider parsing)
- 显式启用但缺少 Langfuse 必填字段的场景
- 不依赖 LangSmith 专属辅助函数的 Provider 启用检测
然后运行 cd backend && uv run pytest tests/test_tracing_config.py -q 确认失败,再写最小实现使测试通过。
落地实现:tracing_config.py
当前仓库中的实现位于 tracing_config.py,使用 Pydantic 模型分层描述每个 Provider 的配置:
class LangSmithTracingConfig(BaseModel):
"""Configuration for LangSmith tracing."""
enabled: bool = Field(...)
api_key: str | None = Field(...)
project: str = Field(...)
endpoint: str = Field(...)
@property
def is_configured(self) -> bool:
return self.enabled and bool(self.api_key)
def validate(self) -> None:
if self.enabled and not self.api_key:
raise ValueError("LangSmith tracing is enabled but LANGSMITH_API_KEY (or LANGCHAIN_API_KEY) is not set.")
class LangfuseTracingConfig(BaseModel):
"""Configuration for Langfuse tracing."""
enabled: bool = Field(...)
public_key: str | None = Field(...)
secret_key: str | None = Field(...)
host: str = Field(...)
@property
def is_configured(self) -> bool:
return self.enabled and bool(self.public_key) and bool(self.secret_key)
def validate(self) -> None:
if not self.enabled:
return
missing: list[str] = []
if not self.public_key:
missing.append("LANGFUSE_PUBLIC_KEY")
if not self.secret_key:
missing.append("LANGFUSE_SECRET_KEY")
if missing:
raise ValueError(f"Langfuse tracing is enabled but required settings are missing: {', '.join(missing)}")
顶层 TracingConfig 将各 Provider 聚合在一起,并提供两个关键属性来区分"显式启用"与"完整配置"——这是实现 fail-fast 语义的核心:
class TracingConfig(BaseModel):
"""Tracing configuration for supported providers."""
langsmith: LangSmithTracingConfig = Field(...)
langfuse: LangfuseTracingConfig = Field(...)
monocle: MonocleTracingConfig = Field(...)
@property
def explicitly_enabled_providers(self) -> list[str]:
# 只看 enabled 标志,不看凭据是否齐全
...
@property
def enabled_providers(self) -> list[str]:
# 只看 is_configured(enabled 且凭据齐全)
...
从源码结构看,这一"双属性"设计对应了计划中的两条行为要求:explicitly_enabled_providers 用于检测"用户主动打开但没配齐"的情形(应报错),enabled_providers 用于实际构建回调(只挂配置完整的通道)。
环境变量解析与优先级
配置从环境变量惰性构建并缓存(get_tracing_config() 使用模块级单例加 threading.Lock 双重检查,另提供 reset_tracing_config() 供测试重置缓存)。环境变量解析有两个辅助函数:
_env_flag_preferred(*names):取第一个存在且非空的开关变量,判定其是否为真值(真值集合为{"1", "true", "yes", "on"});_first_env_value(*names):取第一个非空的值变量。
由此形成的完整环境变量清单与默认值如下(均出自 tracing_config.py 中 get_tracing_config() 的实现):
| 环境变量 | 兼容别名 | 用途 | 默认值 |
|---|---|---|---|
LANGSMITH_TRACING |
LANGCHAIN_TRACING_V2、LANGCHAIN_TRACING |
启用 LangSmith 追踪的开关 | 关闭 |
LANGSMITH_API_KEY |
LANGCHAIN_API_KEY |
LangSmith API 密钥 | 无(启用时必填) |
LANGSMITH_PROJECT |
LANGCHAIN_PROJECT |
追踪项目名 | deer-flow |
LANGSMITH_ENDPOINT |
LANGCHAIN_ENDPOINT |
LangSmith API 端点 | https://api.smith.langchain.com |
LANGFUSE_TRACING |
— | 启用 Langfuse 追踪的开关 | 关闭 |
LANGFUSE_PUBLIC_KEY |
— | Langfuse 公钥 | 无(启用时必填) |
LANGFUSE_SECRET_KEY |
— | Langfuse 密钥 | 无(启用时必填) |
LANGFUSE_BASE_URL |
— | Langfuse 服务地址(自托管时改为部署地址) | https://cloud.langfuse.com |
需要注意的优先级规则(由 _env_flag_preferred 的"取第一个存在且非空"语义决定,并有专门测试锁定):LANGSMITH_* 变量优先于 LANGCHAIN_* 遗留变量;例如即使设置了 LANGCHAIN_TRACING_V2=true,只要显式设置了 LANGSMITH_TRACING=false,LangSmith 追踪仍为关闭。
测试证据
test_tracing_config.py 中的测试与计划要求逐条对应:
test_langfuse_config_is_loaded:验证仅 Langfuse 配置解析,且get_enabled_tracing_providers() == ["langfuse"];test_dual_provider_config_is_loaded:双 Provider 同时启用时返回["langsmith", "langfuse"];test_langfuse_enabled_requires_public_and_secret_keys:显式启用但缺LANGFUSE_PUBLIC_KEY时,is_configured为False、explicitly_enabled_providers为["langfuse"],且validate_enabled_tracing_providers()抛出匹配LANGFUSE_PUBLIC_KEY的ValueError;test_langsmith_tracing_false_overrides_langchain_tracing_v2_true:锁定上文所述的开关优先级。
该测试文件还通过一个 autouse fixture 在每个用例前后统一清理全部追踪相关环境变量并调用 reset_tracing_config(),保证用例间互不污染——这也体现了为何 reset_tracing_config() 被设计为公开 API 而非让测试直接改动私有模块属性。
Task 2:回调工厂与模型挂接
计划中的 Task 2 要求修改 test_model_factory.py 并新建 test_tracing_factory.py,先写覆盖以下场景的失败测试:LangSmith 回调创建、Langfuse 回调创建、双回调创建、"显式启用的 Provider 初始化失败时的启动失败"、模型工厂将所有追踪回调追加到模型回调中。由于当时不存在 Provider 工厂、模型创建只挂 LangSmith,测试预期失败;随后创建工厂模块并改造模型工厂使测试通过。
落地实现:tracing/factory.py
仓库中的工厂模块位于 factory.py,核心函数为 build_tracing_callbacks():
def build_tracing_callbacks() -> list[Any]:
"""Build callbacks for all explicitly enabled tracing providers."""
validate_enabled_tracing_providers() # 先校验:显式启用但缺凭据 → ValueError
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":
try:
callbacks.append(_create_langsmith_tracer(tracing_config.langsmith))
except Exception as exc:
raise RuntimeError(f"LangSmith tracing initialization failed: {exc}") from exc
elif provider == "langfuse":
try:
callbacks.append(_create_langfuse_handler(tracing_config.langfuse))
except Exception as exc:
raise RuntimeError(f"Langfuse tracing initialization failed: {exc}") from exc
return callbacks
这段实现精确对应了计划中的两条架构决策:
- "构建 0、1 或 2 个回调":无 Provider 启用时直接返回空列表,不产生任何副作用;
- "显式启用但初始化失败的 Provider 必须报出指名该 Provider 的明确错误":
validate_enabled_tracing_providers()负责配置缺失场景(抛ValueError并列出缺失的变量名),而每个 Provider 的初始化异常都被包装成带 Provider 名称的RuntimeError(Langfuse tracing initialization failed: .../LangSmith tracing initialization failed: ...)。
两个 Provider 的创建函数各有细节。LangSmith 侧直接构造 LangChain 自带的 LangChainTracer 并传入项目名;Langfuse 侧则有一个版本适配要点——源码注释指出 langfuse>=4 通过 client 单例初始化项目级凭据,LangChain 回调随后挂接到该已配置的单例,因此工厂先构造 Langfuse(secret_key=..., public_key=..., host=...) 客户端、再返回 LangfuseCallbackHandler(public_key=...)。这一"客户端先行"的顺序有专门测试锁定(见下文)。
测试证据
test_tracing_factory.py 中的用例与计划要求一一对应:
test_build_tracing_callbacks_returns_empty_list_when_disabled:无启用 Provider 时返回[];test_build_tracing_callbacks_creates_langsmith_and_langfuse:双 Provider 场景下返回两个回调,且顺序为 LangSmith 在前、Langfuse 在后;test_build_tracing_callbacks_raises_when_enabled_provider_fails:monkeypatch 让 Langfuse handler 构造抛错,断言异常消息匹配Langfuse tracing initialization failed;test_build_tracing_callbacks_raises_for_explicitly_enabled_misconfigured_provider:只设LANGFUSE_TRACING=true而删掉LANGFUSE_PUBLIC_KEY,调用build_tracing_callbacks()直接抛ValueError(匹配LANGFUSE_PUBLIC_KEY),验证 fail-fast 发生在构建入口而非静默跳过;test_create_langfuse_handler_initializes_client_before_handler:通过伪造sys.modules中的langfuse模块记录构造调用序列,断言Langfuse客户端的初始化严格先于CallbackHandler的构造,防止未来重构破坏 v4 客户端单例语义。
模型挂接:create_chat_model 的 attach_tracing
计划要求"更新模型创建流程以挂接这些回调",落地实现见 models/factory.py 中的 create_chat_model()。模型实例构造完成后:
if attach_tracing:
callbacks = build_tracing_callbacks()
if callbacks:
existing_callbacks = model_instance.callbacks or []
model_instance.callbacks = [*existing_callbacks, *callbacks]
logger.debug(f"Tracing attached to model '{name}' with providers={len(callbacks)}")
这里有两点值得注意:
- 追加而非覆盖:
[*existing_callbacks, *callbacks]保证调用方已挂的回调不被抹掉,双 Provider 的多个回调共存于同一模型实例; attach_tracing开关:create_chat_model()的attach_tracing参数默认为True——独立调用者(如MemoryUpdater、临时工具类脚本)依赖模型级回调产生 trace;但已在图根节点挂接追踪的调用方(make_lead_agent、图内TitleMiddleware等)必须传attach_tracing=False。源码注释解释了原因:否则同一次 LLM 调用会发出重复 span(一个根在 graph、一个根在 model),且模型退化为嵌套 observation 后其langfuse_*元数据键会被剥离,导致session_id/user_id无法到达 trace。这是双 Provider 追踪中"回调挂在哪一层"这一常见陷阱的显式约定。
由于 build_tracing_callbacks() 在模型创建时被调用,"配置缺失即抛错"的行为也自然传导到模型构建阶段——这正是计划文档 Architecture 一节所要求的"tracing initialization during model creation should fail with a clear error naming that provider"。
Task 3:依赖接线与文档
计划的 Task 3 包含两步:把 langfuse 加入 harness 依赖;在文档中记录 Langfuse 环境变量、双 Provider 行为以及显式启用 Provider 的失败行为。
依赖声明
pyproject.toml 的依赖中已包含:
"langfuse>=3.4.1",
而 factory.py 中的注释进一步说明:langfuse>=4 的 API 形态(client 单例 + 按 public_key 挂接的 callback)是当前实现适配的版本,_create_langfuse_handler 的"先建客户端再建 handler"顺序即为此设计。
用户侧配置方式
README.md 与 backend/README.md 中的 "Langfuse Tracing" 章节给出了面向用户的配置指引。启用方式为在 .env 文件中添加:
LANGFUSE_TRACING=true
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_BASE_URL=https://cloud.langfuse.com
使用自托管 Langfuse 时,把 LANGFUSE_BASE_URL 指向自己的部署地址即可。
README 还专门记录了双 Provider 行为(Dual Provider Behavior):
- 若 LangSmith 与 Langfuse 同时启用,DeerFlow 会初始化并挂接两者回调,同一次运行数据会同时上报到两个系统;
- 若某 Provider 被显式启用但凭据缺失、或回调初始化失败,DeerFlow 会在模型创建过程中的追踪初始化阶段抛出错误,而不是静默禁用追踪;
- Docker 部署:在 docker-compose.yaml 中追踪默认关闭(
LANGSMITH_TRACING=false),需要在.env中显式设置LANGSMITH_TRACING=true和/或LANGFUSE_TRACING=true并配齐凭据,才能在容器化部署中启用追踪。
追踪关联字段
README.md 还说明了每次运行注入的 Langfuse 保留追踪属性,使 Sessions 与 Users 页面自动可用:
session_id= LangGraph 的thread_id,把同一会话的所有 trace 分组;user_id= 生效用户(无鉴权模式下回退为default);trace_name= assistant id(默认lead-agent);tags=[env:<DEER_FLOW_ENV>, model:<model_name>](未设置时省略);metadata.deerflow_trace_id= DeerFlow 请求关联 id,与请求头X-Trace-Id对应。
从源码结构看,这些属性的构造集中在 metadata.py 的 build_langfuse_trace_metadata() 中:它把 thread_id 映射为 langfuse_session_id、把 assistant id 映射为 langfuse_trace_name、把环境与模型名拼装进 langfuse_tags,并始终注入 deerflow_trace_id 以把 Langfuse trace 与日志行、X-Trace-Id 关联起来。inject_langfuse_metadata() 以 setdefault 方式合并进 RunnableConfig.metadata(上游已设置的值优先),并在 Langfuse 未启用时返回空操作——因此调用方可以无条件合并而不影响 LangSmith 通道。README 指出该注入同时发生在网关路径(runtime/runs/worker.py 的 run_agent)与内嵌客户端路径(client.py 的 DeerFlowClient.stream)两处图调用根节点,与前述 attach_tracing 的"图根挂接优先"约定相衔接。
Task 4:回归验证与 Lint
计划的 Task 4 不要求代码改动,只要求运行回归与静态检查。计划给出的验证命令(在当前仓库中可直接复现):
# 目标测试集(三个文件:配置解析 + 模型工厂 + 回调工厂)
cd backend && uv run pytest tests/test_tracing_config.py tests/test_model_factory.py tests/test_tracing_factory.py -q
# 静态检查,覆盖本次改动涉及的三个位置
cd backend && uv run ruff check packages/harness/deerflow/config/tracing_config.py packages/harness/deerflow/models/factory.py packages/harness/deerflow/tracing
# 复查改动面
git diff -- backend/packages/harness backend/tests README.md backend/README.md
前两条命令对应的正是该功能的完整测试边界:test_tracing_config.py 覆盖配置解析与 fail-fast 语义,test_model_factory.py 覆盖模型创建时的回调挂接,test_tracing_factory.py 覆盖工厂本身的回调构建与异常包装。若需要单独快速冒烟,只跑 test_tracing_factory.py 即可覆盖双 Provider、初始化失败、缺凭据三类核心路径。
小结
这份 实施计划 的价值在于它把"加一个可观测性 Provider"拆成了可独立验证的四层:配置模型(TracingConfig 及两个 Provider 子模型)、回调工厂(build_tracing_callbacks)、模型挂接(create_chat_model 的 attach_tracing)、依赖与文档(pyproject.toml + 两份 README)。仓库源码显示该计划已完整落地:多 Provider 配置支持 LangSmith 与 Langfuse 同时启用且互不干扰;显式启用但配置缺失的 Provider 会在模型创建阶段抛出指名具体变量名或 Provider 名的错误,避免"追踪静默消失"这类最难排查的问题。对于要在 DeerFlow 上搭建自托管追踪的运维者,只需按上文环境变量表配齐 .env;对于要接入新追踪 Provider 的开发者,则可以 MonocleTracingConfig 与 build_tracing_callbacks() 的 Provider 分派结构作为扩展参照——需要注意的是 Monocle 属于进程级 OTel 插桩而非 LangChain 回调,其初始化路径(Gateway lifespan)与回调类 Provider 不同,源码中已用注释明确区分。
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 StartedRust0623
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