首页
/ DeerFlow 可观测性实战:基于 Langfuse 与 LangSmith 双 Provider 的 Tracing 接入方案

DeerFlow 可观测性实战:基于 Langfuse 与 LangSmith 双 Provider 的 Tracing 接入方案

2026-09-04 09:28:08作者:鲍丁臣Ursa

本文基于 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)可归纳为三步:

  1. 扩展追踪配置:从"仅 LangSmith 的单 Provider 形状"扩展为多 Provider 配置结构;
  2. 引入追踪回调工厂:根据环境变量构建 0 个、1 个或 2 个回调(callback);
  3. 改造模型创建路径:在创建模型实例时挂接这些回调。若某个 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.pyget_tracing_config() 的实现):

环境变量 兼容别名 用途 默认值
LANGSMITH_TRACING LANGCHAIN_TRACING_V2LANGCHAIN_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_configuredFalseexplicitly_enabled_providers["langfuse"],且 validate_enabled_tracing_providers() 抛出匹配 LANGFUSE_PUBLIC_KEYValueError
  • 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

这段实现精确对应了计划中的两条架构决策:

  1. "构建 0、1 或 2 个回调":无 Provider 启用时直接返回空列表,不产生任何副作用;
  2. "显式启用但初始化失败的 Provider 必须报出指名该 Provider 的明确错误"validate_enabled_tracing_providers() 负责配置缺失场景(抛 ValueError 并列出缺失的变量名),而每个 Provider 的初始化异常都被包装成带 Provider 名称的 RuntimeErrorLangfuse 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)}")

这里有两点值得注意:

  1. 追加而非覆盖[*existing_callbacks, *callbacks] 保证调用方已挂的回调不被抹掉,双 Provider 的多个回调共存于同一模型实例;
  2. 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.mdbackend/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.pybuild_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.pyrun_agent)与内嵌客户端路径(client.pyDeerFlowClient.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_modelattach_tracing)、依赖与文档(pyproject.toml + 两份 README)。仓库源码显示该计划已完整落地:多 Provider 配置支持 LangSmith 与 Langfuse 同时启用且互不干扰;显式启用但配置缺失的 Provider 会在模型创建阶段抛出指名具体变量名或 Provider 名的错误,避免"追踪静默消失"这类最难排查的问题。对于要在 DeerFlow 上搭建自托管追踪的运维者,只需按上文环境变量表配齐 .env;对于要接入新追踪 Provider 的开发者,则可以 MonocleTracingConfigbuild_tracing_callbacks() 的 Provider 分派结构作为扩展参照——需要注意的是 Monocle 属于进程级 OTel 插桩而非 LangChain 回调,其初始化路径(Gateway lifespan)与回调类 Provider 不同,源码中已用注释明确区分。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384