在 LangGraph 项目中集成 Langfuse
在 Agent / 多节点图里,可观测性最怕两件事:树形结构断裂、同一轮对话被拆成多条互不关联的 Trace。Langfuse 通过 LangChain 的 CallbackHandler 把 LLM / Chain 的回调聚合成 Span / Generation;回调挂在哪里,直接决定 Trace 是否是一整棵树状结构还是被拆的七零八落。
正解
推荐做法:在「编译后的图」上统一挂 Callback
更稳妥的方式是:在 compile() 得到的 Runnable 上使用 with_config({"callbacks": [langfuse_handler]}),让整次 invoke / ainvoke / stream 共享同一套回调与同一运行上下文。
一个常见的项目中的模式大致是:
- 先根据环境变量决定是否启用;
- 先构造
Langfuse(public_key=..., secret_key=...)再构造CallbackHandler(见下文 SDK 版本要点); - 对编译后的图:
graph = _compiled.with_config({"callbacks": [_langfuse_cb]})。
这样 LangGraph 作为顶层 Runnable 驱动各节点,一次图执行对应一条主 Trace,其下的 Router / Generator / Responder 等节点里的 ChatOpenAI.ainvoke 会作为子观测嵌套进来,结构清晰。
反例
在
ChatOpenAI(或每个模型调用)上单独塞 CallbackHandler
若在 每一个 ChatOpenAI 实例或 每一次 ainvoke(..., config={"callbacks": [...]}) 上单独挂 CallbackHandler,常见后果包括:
- 多段根 Span:每个模型调用各自拉起一套回调上下文,Langfuse 里看起来像多条独立 Trace 或难以合并的树;
- 与 LangGraph 的并行 /
Send等机制叠加时,父子关系、顺序更难保证一致; - 重复上报或嵌套过深:调试时会觉得「记录被拆得七零八落」。
因此:把 Callback 放在「图的入口 Runnable」一侧,而不是「每个 LLM 客户端」一侧,更符合「一次用户请求 = 一次图运行 = 一条 Trace」的心智模型。
Langfuse v4坑
1
在较新的 SDK(如 v4)里,CallbackHandler(public_key=...) 内部会通过 get_client(public_key=...) 查找 已注册的 Langfuse 实例。若从未执行过 Langfuse(...),可能拿到 静默禁用的占位客户端,界面里看不到数据。
实践要点:在创建 CallbackHandler 之前,用与环境一致的 public_key / secret_key(以及 LANGFUSE_BASE_URL / LANGFUSE_HOST)先执行一次 Langfuse(...),再创建 CallbackHandler。
2
自建 Langfuse 版本偏旧:auth_check() 失败未必代表不能上报
langfuse.auth_check() 往往依赖 服务端某版本才提供的健康检查 / 鉴权探测接口。若自建实例版本 低于 Python SDK 假设的能力,可能出现:
auth_check()报错或返回失败;- 但 Trace 上报路径(如基于 OTEL 的导出或兼容的 Ingest API)仍然可用,界面上能陆续看到 Trace。
因此:不要把 auth_check() 当作「能否写数据」的唯一判据。更可靠的是:
- 看 UI 是否出现新 Trace;
- 必要时开
LANGFUSE_DEBUG=true看客户端日志; - 长期仍建议 升级服务端 与 SDK 文档推荐版本对齐,避免其它 API(Prompt、Score 等)隐性不兼容。
- 本文标题:在 LangGraph 项目中集成 Langfuse
- 本文作者:uygnil
- 本文链接:https://blog.zhoulingyu.net/index.php/archives/29/
- 版权声明:本文采用 CC BY 4.0 协议进行许可
标签:无