bug(sdk-python): LangchainCallbackHandler attributes only the root span in async runs — child observations lose user_id/session_id/tags

该问题发生在 Langfuse Python SDK 的异步 LangChain/LangGraph 调用中,导致只有根 span 被正确写入 user_id 、 session_id 、 tags ,所有子 observation(尤其是产生费用的 GENERATION)都丢失这些属性。优先排查是

快速结论:该问题发生在 Langfuse Python SDK 的异步 LangChain/LangGraph 调用中,导致只有根 span 被正确写入 user_idsession_idtags,所有子 observation(尤其是产生费用的 GENERATION)都丢失这些属性。优先排查是否在异步链路中使用了 propagate_attributes 包裹整个应用调用,这是目前唯一确认有效的绕行方案。

适用环境:Langfuse Python SDK(v4 及以上版本影响更明显),LangChain/LangGraph 异步运行路径,Python 3.x(具体版本未在 Issue 中标注),不依赖特定显卡或 CUDA 环境。

最快修复方案:暂无确认的一步修复方案。可从两条路径选择:一是使用 propagate_attributes 上下文管理器包裹整个异步调用流;二是给 CallbackHandler 设置 run_inline = True,但后者会阻塞事件循环,未在实际生产中得到充分验证。

注意事项:propagate_attributes 绕行方案已被确认有效,但需要修改应用代码;run_inline = True 属于可优先尝试的方案,但可能因序列化回调导致事件循环阻塞,需根据实际耗时 I/O 评估风险。

问题场景

在 LangGraph 或 LangChain 的异步调用链路(如 astream_eventsainvoke)中使用 Langfuse 的 LangchainCallbackHandler 时,用户在每个 on_chain_start 回调中通过 propagate_attributes(...) 传入 user_idsession_idtags 等元数据。结果是根 span(root span)能正确携带这些属性,但所有子 observation(包括承载调用成本的 GENERATION)均无法继承,导致 Langfuse v4 的控制台按用户统计费用时出现大量“未知用户”费用。

报错原文

bug(sdk-python): LangchainCallbackHandler attributes only the root span in async runs — child observations lose user_id/session_id/tags

In an **async** LangChain/LangGraph run, LangchainCallbackHandler lands user_id / session_id / tags / trace_name on the **root span only**. Every child observation — including the GENERATIONs that carry the cost — is exported with no user.id, no session.id and no langfuse.trace.tags.

原因分析

根本原因在 LangChain 的 AsyncCallbackManager 调度机制。Langfuse 的 LangchainCallbackHandler 是同步处理器且未设置 run_inline,因此 LangChain 将每个回调通过 run_in_executor 分发到线程池执行,并传入调用方上下文的 副本。回调内部执行的 otel_context_api.attach() 只修改了这份被丢弃的副本,后续回调(如 on_chat_model_starton_tool_start)拿到的仍是未包含属性的新副本,导致子 span 全部丢失元数据。这是异步回调用 copy_context()run_in_executor 组合的机制性缺陷,已由 Langfuse 维护方确认分析正确。

环境排查

  • 确认 Langfuse Python SDK 版本(v4 及以上时下游影响更明显)
  • 确认 LangChain / LangGraph 的异步调用路径(astream_eventsainvokeastream
  • 确认回调管理器是 AsyncCallbackManager(非同步路径,同步调用无此问题)
  • 排查是否已在应用层统一使用 propagate_attributes 包裹异步迭代器

解决步骤

  1. 首选绕行方案(已验证有效):在应用层用 propagate_attributes 上下文管理器包裹整个异步调用流,而非仅在 on_chain_start 中设置。示例代码:
    from langfuse import propagate_attributes
    
    with propagate_attributes(user_id=..., session_id=..., tags=..., trace_name=...):
        async for event in graph.astream_events(..., config={"callbacks": [handler]}):
            ...

    这样 LangChain 在每次 executor 分发时都会将应用层上下文复制进回调,所有子 span 自然继承属性。

  2. 可优先尝试的 SDK 层修复方向:CallbackHandler 设置 run_inline = True,让异步回调直接在当前事件循环上下文中执行,使 attach() 立即可见。但需承担事件循环被回调阻塞的风险。
  3. 备选修复方向(未实现,仅供维护者参考):_RootRunState 中存储解析后的属性,在每个子回调中直接用 span.set_attribute 重复写入,绕过 OTel 上下文传播机制。该方案在 Issue 中被提出但尚未落地。

验证方法

用 Issue 提供的 InMemorySpanExporter 复现代码跑一次异步链路,检查所有导出的 span(尤其是 GENERATION 类型)是否都包含 user.idsession.idlangfuse.trace.tags。同时对比同步运行与异步运行的输出差异——同步路径若有属性,异步路径缺失即可复现。修复后异步路径应与同步输出一致。

参考来源

langfuse/langfuse #16177

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 18799

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注