














本文以一个完整的代码示例,演示如何在 LangChain / LangGraph 项目中集成 LangFuse,实现对 LLM 调用的可观测性。全文不讲空泛理论,只看代码和效果。
当你把 LLM 应用从 Demo 推向生产,一定会遇到这几个问题:
LangFuse 就是解决这些问题的。 它是一个开源的 LLM 可观测性平台(由德国团队开发),你可以用它的 SaaS 服务(cloud.langfuse.com),也可以自部署。
它的核心价值:给你的 LLM 应用加上"全链路追踪"能力,让你在控制台里看到每一次调用的完整细节。
pip install langfuse langgraph langchain langchain-community
# LangFuse 密钥(在 LangFuse 控制台 → Settings → API Keys 中获取)
$env:LANGFUSE_PUBLIC_KEY="pk-lf-xxx"
$env:LANGFUSE_SECRET_KEY="sk-lf-xxx"
$env:LANGFUSE_BASE_URL="https://cloud.langfuse.com"
# LLM API 密钥(本文使用通义千问)
$env:DASHSCOPE_API_KEY="sk-xxx"
提示: LangFuse 云服务部署在海外,国内访问可能有延迟。如果遇到 OpenTelemetry 超时报刷屏,可以加一行代码抑制:
logging.getLogger("opentelemetry").setLevel(logging.CRITICAL)
这是 LangFuse 最推荐的集成方式。核心思想:给函数加个装饰器,追踪就自动完成了。
from langfuse import observe, get_client
from langchain_community.llms import Tongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = Tongyi(model_name="qwen-turbo-latest", dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"))
@observe(as_type="generation")
def call_llm(prompt_text: str) -> str:
prompt = ChatPromptTemplate.from_template("{text}")
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"text": prompt_text})
return result
@observe()
def qa(question: str) -> str:
return call_llm(f"请用一句话回答:{question}")
@observe(name="my_agent")
def agent_entry(user_input: str):
return qa(user_input)
# 直接调用,无需任何其他配置
result = agent_entry("什么是大语言模型?")
就这么多。不需要创建 handler,不需要传 config,不需要手动 flush。程序退出时数据自动上报。
| 装饰器写法 | 记录类型 | 适用场景 |
|---|---|---|
@observe(name="xxx") |
Trace(顶层入口) | 标记整个请求的入口,name 是你在控制台看到的 trace 名称 |
@observe() |
Span(中间步骤) | 标记业务流程中的子步骤,如翻译、摘要、分类 |
@observe(as_type="generation") |
Generation(LLM 调用) | 标记实际的 LLM 调用,会自动记录 input/output/耗时 |
当被 @observe 装饰的函数互相调用时,LangFuse 会自动构建父子关系。在我们的示例中:
@observe(name="multi_tool_agent")
def agent_entry(user_input: str):
answer = qa(user_input) # 问答
summary = summarize(answer) # 摘要
translated = translate(summary) # 翻译
return {"answer": answer, "summary": summary, "translated": translated}
在 LangFuse 控制台中,你会看到这样的追踪结构:
└─ multi_tool_agent (trace)
├─ qa (span)
│ └─ call_llm (generation) ← 自动记录 input/output/耗时
├─ summarize (span)
│ └─ call_llm (generation)
└─ translate (span)
└─ call_llm (generation)
一次调用,三层层级关系,全自动生成。 你不需要写任何追踪逻辑。
如果你想让 LangFuse 控制台显示更完整的 LLM 调用信息(模型名称、完整 prompt 等),可以用 update_current_generation():
@observe(as_type="generation")
def call_llm(prompt_text: str, model_name: str = "qwen-turbo-latest") -> str:
prompt = ChatPromptTemplate.from_template("{text}")
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"text": prompt_text})
# 显式上报 model 和 prompt,控制台中能看到完整的 LLM 请求上下文
get_client().update_current_generation(
model=model_name,
input=prompt_text,
output=result,
)
return result
在实际生产环境中,你通常需要:
这就需要动态注入上下文信息。
from opentelemetry import trace as otel_trace
@observe(as_type="chain")
def traced_graph_invoke(question, user_id, session_id, tags=None, metadata=None):
# 通过 OpenTelemetry span 属性注入用户上下文
span = otel_trace.get_current_span()
if span.is_recording():
span.set_attribute("langfuse.user.id", user_id)
span.set_attribute("langfuse.session.id", session_id)
if tags:
span.set_attribute("langfuse.tags", tags)
# 附加自定义元数据
if metadata:
get_client().update_current_span(metadata=metadata)
# 执行业务逻辑
app = build_graph()
return app.invoke({"question": question})
关键点:LangFuse 基于 OpenTelemetry 架构,所以它能自动识别 OTEL span 中的特定属性:
| Span 属性 | 作用 |
|---|---|
langfuse.user.id |
关联到 LangFuse 的 Users 视图 |
langfuse.session.id |
同一 session_id 的 trace 会被归为一组 |
langfuse.tags |
用于筛选和过滤 |
session_id = f"session-{uuid.uuid4().hex[:8]}"
questions = [
("user_001", "什么是 Python?"),
("user_001", "它和 Java 有什么区别?"), # 同一用户,同一会话
("user_002", "推荐一个入门编程语言"), # 不同用户
]
for user_id, question in questions:
result = traced_graph_invoke(
question=question,
user_id=user_id,
session_id=session_id,
tags=["langgraph", "multi-turn"],
metadata={"app_version": "1.0.0"},
)
在 LangFuse 控制台中:
user_001 / user_002 筛选不同用户的追踪session_id 而归为同一会话langgraph 标签快速过滤出这批请求上面的方式2已经展示了 LangGraph 的集成。核心模式很简单:
@observe 装饰外层函数
└─ 内部调用 graph.invoke()
└─ graph 的节点调用 @observe 标记的函数
└─ 自动形成完整的追踪链
在我们的示例中,LangGraph 图的结构是:
class State(TypedDict):
question: str
answer: Optional[str]
def chat_node(state: State) -> dict:
answer = call_llm(f"请用一句话回答:{state['question']}")
return {"answer": answer}
def build_graph():
graph = StateGraph(State)
graph.add_node("chat", chat_node)
graph.set_entry_point("chat")
graph.add_edge("chat", END)
return graph.compile()
chat_node 内部调用了 call_llm,而 call_llm 被 @observe(as_type="generation") 装饰。因此 LangGraph 执行图的时候,LLM 调用会自动被追踪到,不需要对 LangGraph 本身做任何修改。
LangFuse 的数据上报是异步批量的。关于何时需要手动 flush:
| 场景 | 是否需要手动 flush |
|---|---|
| 脚本执行完自然退出 | 不需要,程序退出时自动 flush |
| 多轮对话,想实时看到每轮数据 | 每轮结束后调用 get_client().flush() |
| 长时间运行的服务(如 Web 服务) | 建议在请求结束时 flush |
# 手动 flush
get_client().flush()
| 对比维度 | 方式1:@observe 装饰器 | 方式2:@observe + 动态上下文 |
|---|---|---|
| 代码侵入性 | 极低,加装饰器即可 | 低,需在入口函数注入属性 |
| user_id / session_id | 不支持动态传入 | 支持,通过 OTEL span 属性 |
| tags / metadata | 不支持动态传入 | 支持,灵活设置 |
| 适用场景 | 简单应用、快速验证、脚本 | 生产环境、多用户多会话 |
| 与 LangGraph 配合 | 节点函数加 @observe | 外层包装 + 图内 @observe |
日常开发推荐方式1,够简单够快。上生产需要按用户/会话追踪时,切换到方式2。
以下是可直接运行的完整代码,复制到本地 .py 文件即可执行。
运行前确保设置好环境变量,然后:python 文件名.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
LangGraph + LangFuse 集成演示
LangFuse 基于 OpenTelemetry 架构,提供两种追踪方式:
方式1(推荐):@observe 装饰器
- 最简洁,直接修饰函数
- 自动追踪函数内的所有 LLM 调用
- 支持嵌套:被装饰的函数互相调用时,自动形成父子 span
方式2:@observe + OTEL span 属性
- 适用于需要动态传入 user_id / session_id / tags 的场景
- 通过 OpenTelemetry span 属性注入用户上下文
环境变量配置(Windows PowerShell):
$env:DASHSCOPE_API_KEY="sk-xxx"
$env:LANGFUSE_PUBLIC_KEY="pk-lf-xxx"
$env:LANGFUSE_SECRET_KEY="sk-lf-xxx"
$env:LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 可选
"""
import os
import uuid
import logging
from typing import TypedDict, Optional
import warnings
warnings.filterwarnings("ignore")
# 修复 langchain 版本兼容性
import langchain
for attr in ('verbose', 'debug', 'llm_cache'):
if not hasattr(langchain, attr):
setattr(langchain, attr, False if attr != 'llm_cache' else None)
from langchain_community.llms import Tongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langgraph.graph import StateGraph, END
# LangFuse 导入
from langfuse import observe, get_client
# 抑制 OpenTelemetry 网络超时日志
logging.getLogger("opentelemetry").setLevel(logging.CRITICAL)
# ==================== 全局配置 ====================
LANGFUSE_ENABLED = bool(
os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY")
)
llm = Tongyi(
model_name="qwen-turbo-latest",
dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"),
)
# ====== 方式1:@observe 装饰器(推荐) ======
@observe(as_type="generation")
def call_llm(prompt_text: str, model_name: str = "qwen-turbo-latest") -> str:
prompt = ChatPromptTemplate.from_template("{text}")
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"text": prompt_text})
get_client().update_current_generation(
model=model_name, input=prompt_text, output=result,
)
return result
@observe()
def translate(text: str) -> str:
return call_llm(f"请将以下内容翻译成英文,只返回译文:\n{text}")
@observe()
def summarize(text: str) -> str:
return call_llm(f"请用一句话总结以下内容:\n{text}")
@observe()
def qa(question: str) -> str:
return call_llm(f"请用一句话回答:{question}")
@observe(name="multi_tool_agent")
def agent_entry(user_input: str):
answer = qa(user_input)
summary = summarize(answer)
translated = translate(summary)
return {"answer": answer, "summary": summary, "translated": translated}
# ====== 方式2:@observe + 动态设置 trace 属性 ======
class State(TypedDict):
question: str
answer: Optional[str]
def chat_node(state: State) -> dict:
answer = call_llm(f"请用一句话回答:{state['question']}")
return {"answer": answer}
def build_graph():
graph = StateGraph(State)
graph.add_node("chat", chat_node)
graph.set_entry_point("chat")
graph.add_edge("chat", END)
return graph.compile()
@observe(as_type="chain")
def traced_graph_invoke(question, user_id, session_id, tags=None, metadata=None):
from opentelemetry import trace as otel_trace
span = otel_trace.get_current_span()
if span.is_recording():
span.set_attribute("langfuse.user.id", user_id)
span.set_attribute("langfuse.session.id", session_id)
if tags:
span.set_attribute("langfuse.tags", tags)
if metadata:
get_client().update_current_span(metadata=metadata)
app = build_graph()
return app.invoke({"question": question})
# ====== 运行演示 ======
def demo_observe_decorator():
print("\n" + "=" * 60)
print("方式1:@observe 装饰器")
print("=" * 60)
result = agent_entry("什么是大语言模型?")
print(f" 回答: {result['answer']}")
print(f" 摘要: {result['summary']}")
print(f" 译文: {result['translated']}")
def demo_observe_with_context():
print("\n" + "=" * 60)
print("方式2:@observe + 动态上下文")
print("=" * 60)
session_id = f"session-{uuid.uuid4().hex[:8]}"
questions = [
("user_001", "什么是 Python?"),
("user_001", "它和 Java 有什么区别?"),
("user_002", "推荐一个入门编程语言"),
]
for user_id, question in questions:
print(f"\n [{user_id}] {question}")
result = traced_graph_invoke(
question=question, user_id=user_id,
session_id=session_id,
tags=["langgraph", "multi-turn"],
metadata={"app_version": "1.0.0"},
)
print(f" 助手: {result['answer']}")
if LANGFUSE_ENABLED:
get_client().flush()
if __name__ == "__main__":
print(f"LangFuse: {'已启用' if LANGFUSE_ENABLED else '未配置(跳过追踪)'}")
demo_observe_decorator()
demo_observe_with_context()
if LANGFUSE_ENABLED:
get_client().flush()
print("\n运行完毕。在 LangFuse 控制台可查看追踪数据。")
在 LangFuse 控制台(Traces 页面)即可看到完整的追踪数据,包括每次 LLM 调用的 prompt、输出、耗时、token 用量等信息。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。