LLM 应用上线后出了 bug,你怎么查?聊聊 Langfuse 全链路追踪的接入和踩坑

LLM 应用上线后出了 bug,你怎么查?聊聊 Langfuse 全链路追踪的接入和踩坑

线上跑着一个 LLM Agent,用户反馈"回答不对"。你打开日志,看到一堆 INFO: request sent、INFO: response received。然后呢?

你不知道那次请求用了多少 token,不知道中间调了几次工具,不知道哪一步的 prompt 拼错了导致最终输出跑偏。传统日志系统是为 HTTP 请求设计的,一个请求对应一个响应。但 Agent 不是这样------一次用户提问可能触发 5 次 LLM 调用、3 次工具执行、2 次重试,整条链路散落在日志里,根本串不起来。

这就是 LLM 可观测性要解决的问题。我最近在生产环境接入了 Langfuse,踩了不少坑,这篇把接入过程和实际遇到的问题都记录下来。

Langfuse 是什么,不是什么

先说清楚边界。Langfuse 是一个开源的 LLM 可观测性平台,核心能力是 trace------把一次 Agent 执行的完整链路记录下来,包括每一步的输入、输出、耗时、token 用量和成本。

它不是 APM(Application Performance Monitoring),不替代 Datadog 或 Prometheus。它也不做模型训练或微调。把它理解成"LLM 专用的分布式追踪系统"就行。

架构上 Langfuse 分三层:

  • SDK 层:Python/JS SDK 嵌入你的应用代码,采集 trace 数据
  • 后端服务:接收 trace 数据,存入 PostgreSQL + ClickHouse
  • Web UI:可视化 trace 链路,做成本分析和评分

部署:Docker Compose 一键起

Langfuse 支持 SaaS(cloud.langfuse.com)和自部署。生产环境我选了自部署,原因很简单------prompt 里有业务数据,不想过第三方。

bash 复制代码
# 拉取 docker-compose 配置
git clone https://github.com/langfuse/langfuse.git
cd langfuse

# 创建环境变量文件
cat > .env << 'EOF'
DATABASE_URL=postgresql://postgres:postgres@db:5432/langfuse
NEXTAUTH_SECRET=your-secret-key-change-this
NEXTAUTH_URL=http://localhost:3000
SALT=your-salt-change-this
TELEMETRY_ENABLED=false
EOF

# 启动
docker compose up -d

启动后访问 localhost:3000,注册账号,在 Settings 里创建 API Key。拿到 LANGFUSE_PUBLIC_KEY 和 LANGFUSE_SECRET_KEY。

踩坑 #1:默认的 docker-compose.yml 里 PostgreSQL 没有持久化 volume。我第一次部署时忘了加,Docker 重启后数据全丢了。改成这样:

yaml 复制代码
services:
  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data   # 加这一行
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: langfuse

volumes:
  pgdata:    # 声明 volume

最简接入:改一行 import

Langfuse 对 OpenAI SDK 做了 drop-in replacement。如果你的代码用的是 openai 库,接入成本低到离谱:

python 复制代码
# 改之前
from openai import OpenAI
client = OpenAI()

# 改之后
from langfuse.openai import OpenAI
client = OpenAI()

只改了 import 路径。之后所有通过这个 client 发出的请求,Langfuse 会自动记录 trace,包括 model、messages、completion、token 用量、耗时。

环境变量里配好 Key:

bash 复制代码
export LANGFUSE_PUBLIC_KEY="pk-..."
export LANGFUSE_SECRET_KEY="sk-..."
export LANGFUSE_HOST="http://localhost:3000"  # 自部署地址

跑一个简单测试:

python 复制代码
from langfuse.openai import OpenAI

client = OpenAI()
resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "用一句话解释什么是 KV Cache"}],
)
print(resp.choices[0].message.content)

去 Langfuse Web UI 的 Traces 页面,能看到这次调用的完整记录:输入 messages、输出 content、model 名称、prompt_tokens、completion_tokens、total_cost、latency。

这对单次调用已经够用了。但 Agent 不是单次调用。

Agent 多步追踪:@observe 装饰器

拿一个 RAG Agent 举例。用户提问后,Agent 先做向量检索,拿到相关文档,拼好 prompt,调 LLM 生成回答,最后可能还有格式化和安全检查。五六步串在一起,哪步出问题都可能导致最终回答跑偏。

用 Langfuse 的 @observe() 装饰器可以把这些步骤串成一条完整的 trace:

python 复制代码
from langfuse.decorators import observe, langfuse_context
from langfuse.openai import OpenAI
client = OpenAI()

@observe()
def retrieve_docs(query: str) -> list[str]:
    """模拟向量检索"""
    # 实际项目里这里会调 Milvus/Qdrant/Chroma
    docs = [
        "KV Cache 是 Transformer 推理时缓存已计算的 Key 和 Value 矩阵",
        "PagedAttention 把 KV Cache 分页管理,减少显存碎片",
        "vLLM 用 PagedAttention 实现了接近零浪费的显存分配",
    ]
    return docs

@observe()
def build_prompt(query: str, docs: list[str]) -> list[dict]:
    """拼装 prompt"""
    context = "\n".join(f"- {d}" for d in docs)
    return [
        {"role": "system", "content": f"根据以下资料回答问题:\n{context}"},
        {"role": "user", "content": query},
    ]

@observe()
def generate_answer(messages: list[dict]) -> str:
    """调用 LLM"""
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        temperature=0.3,
    )
    return resp.choices[0].message.content

@observe()
def rag_agent(query: str) -> str:
    """完整的 RAG Agent 入口"""
    # 给这条 trace 加上用户 ID 和 session ID,方便后续按用户筛选
    langfuse_context.update_current_trace(
        user_id="user_12345",
        session_id="session_abc",
        tags=["rag", "production"],
    )
    
    docs = retrieve_docs(query)
    messages = build_prompt(query, docs)
    answer = generate_answer(messages)
    return answer

# 运行
result = rag_agent("vLLM 的 PagedAttention 怎么减少显存浪费的?")
print(result)

在 Langfuse UI 里看到的是一棵树状结构:

scss 复制代码
rag_agent (trace)
├── retrieve_docs (span, 12ms)
├── build_prompt (span, 0.3ms)
└── generate_answer (span, 1.2s)
    └── OpenAI Chat Completion (generation)
        ├── model: gpt-4o
        ├── prompt_tokens: 187
        ├── completion_tokens: 92
        └── total_cost: $0.0023

每一步的输入输出、耗时都能点开看。当某个用户反馈"回答不对"时,拿 session_id 一搜,整条链路一目了然------是检索没检到相关文档,还是 prompt 拼装的格式有问题,还是模型本身生成了错误内容。

踩坑 #2 :@observe() 装饰器在异步函数上的行为跟同步不一样。如果你用 async def,必须用 @observe() 而不是手动创建 span。我一开始在异步代码里手动调 langfuse.trace() 创建 trace,发现子 span 关联不上父 trace,因为 Langfuse 用的是 contextvars 做上下文传递,手动创建会丢失上下文链。

python 复制代码
# 错误写法------异步函数里手动创建 trace
async def rag_agent(query: str):
    trace = langfuse.trace(name="rag_agent")  # 这样子 span 串不起来
    docs = await retrieve_docs(query)
    ...

# 正确写法------用装饰器
@observe()
async def rag_agent(query: str):
    docs = await retrieve_docs(query)  # 子函数也要有 @observe()
    ...

成本追踪:每个月到底花了多少钱

Langfuse 自动根据 model 名称和 token 数量计算成本。内置了主流模型的价格表(GPT-4o、Claude 3.5 Sonnet、Gemini 等),更新挺及时的。

但如果你用的是代理 API 或者私有部署的模型,内置价格表就对不上了。这时候需要手动设置:

python 复制代码
from langfuse import Langfuse

langfuse = Langfuse()

# 自定义模型的价格(每 1000 tokens 的美元价格)
langfuse.create_model(
    model_name="my-fine-tuned-qwen",
    input_price=0.002,     # $0.002 / 1K input tokens
    output_price=0.006,    # $0.006 / 1K output tokens
    unit="TOKENS",
)

配好之后,Dashboard 里能看到按天、按模型、按用户的成本聚合。我们上线第一周就发现一个问题:有个内部工具每次查询都拼了完整的上下文历史,导致单次请求的 prompt_tokens 平均 8000+,一天下来光这一个接口就烧了 40 。改成只带最近 5 轮对话后,成本降到 40。改成只带最近 5 轮对话后,成本降到 40。改成只带最近5轮对话后,成本降到6/天。

踩坑 #3 :Langfuse SDK 默认是异步上报 trace 的,程序退出时如果不调 langfuse.flush(),最后几条 trace 会丢。在 Web 服务里问题不大(进程一直跑着),但在脚本和 Lambda 函数里必须显式 flush:

python 复制代码
from langfuse.decorators import langfuse_context

def main():
    result = rag_agent("some question")
    print(result)
    # 脚本结束前必须 flush,否则最后几条 trace 丢失
    langfuse_context.flush()

if __name__ == "__main__":
    main()

评分(Scoring):不只是记录,还能打分

trace 记录下来只是第一步。生产环境里你还想知道:这些回答的质量怎么样?

Langfuse 支持给 trace 打分,分三种方式:

1. 代码里自动打分

python 复制代码
from langfuse.decorators import langfuse_context

@observe()
def rag_agent(query: str) -> str:
    answer = generate_answer(...)
    
    # 简单的自动评分:回答长度是否合理
    score = 1.0 if 50 < len(answer) < 2000 else 0.5
    langfuse_context.score_current_trace(
        name="answer_quality",
        value=score,
        comment="基于长度的粗粒度评分",
    )
    return answer

2. 用 LLM 做评估(LLM-as-Judge)

python 复制代码
@observe()
def evaluate_answer(question: str, answer: str, reference: str) -> float:
    """用另一个 LLM 评估回答质量"""
    eval_prompt = f"""评估以下回答的质量,打分 0-1:
问题:{question}
参考答案:{reference}
待评估答案:{answer}

只输出一个 0 到 1 之间的数字。"""
    
    resp = client.chat.completions.create(
        model="gpt-4o-mini",  # 评估用便宜模型就行
        messages=[{"role": "user", "content": eval_prompt}],
        temperature=0,
    )
    return float(resp.choices[0].message.content.strip())

3. 在 Web UI 里人工标注

Langfuse 的 UI 支持直接在 trace 上打分和写标注。我们的做法是每周抽样 50 条 trace,让团队里的人花半小时标注,主要标"回答准确性"和"是否产生幻觉"两个维度。这些标注数据后续可以导出,用来做 fine-tuning 的训练集。

生产环境的几个实际问题

性能开销:Langfuse SDK 的 trace 上报是异步的,对主流程几乎没影响。我们实测加了 Langfuse 后,P99 延迟增加不到 2ms。但注意别在高频循环里创建 trace------比如批处理 1000 条数据时,别每条都开一个 trace,而是把批处理作为一个 trace,每条数据作为子 span。

数据量:trace 数据量增长很快。我们每天大概 5 万次 LLM 调用,一个月下来 ClickHouse 里存了约 80GB 数据。Langfuse 有数据保留策略设置,建议生产环境配好自动清理,比如只保留最近 90 天。

采样:不是每条请求都需要记录 trace。Langfuse 支持采样率设置:

python 复制代码
from langfuse.decorators import observe

@observe(sample_rate=0.1)  # 只记录 10% 的请求
def rag_agent(query: str) -> str:
    ...

开发环境用 100%,预发布用 50%,生产环境看流量大小调整,一般 10%-30% 够用了。

踩坑 #4 :Langfuse 的 Python SDK 和 LangChain 集成时有个版本兼容问题。langfuse>=2.40 要配合 langchain>=0.2,如果你还在用 langchain 0.1.x,trace 里的 chain 步骤会显示为空。没有报错,就是悄悄地不记录。排查了两个小时才发现是版本问题。建议锁死依赖版本:

txt 复制代码
# requirements.txt
langfuse==2.51.3
langchain==0.3.14
langchain-openai==0.3.6

跟其他方案对比

说几个常拿来对比的工具:

LangSmith:LangChain 官方出的,跟 LangChain 深度绑定。如果你项目全栈用 LangChain,LangSmith 的集成体验确实更顺滑。但 LangSmith 不开源,数据存在 LangChain 的服务器上。

OpenTelemetry GenAI Semantic Conventions :2025 年底 OTEL 社区发布了 GenAI 语义规范,定义了 gen_ai.request.model、gen_ai.usage.prompt_tokens 等标准 attribute。Langfuse 已经支持通过 OpenLLMetry 对接 OTEL,走标准协议。如果你的公司已经有 OTEL 的基础设施(Jaeger、Grafana Tempo),可以考虑直接用 OTEL 而不是单独部署 Langfuse。

Helicone:专注于 API 代理模式,在你的 LLM API 请求前面加一层代理来记录。接入更简单(只改 base_url),但功能没有 Langfuse 全。

我的建议:小团队、刚起步用 Langfuse(开源免费、功能全);中大团队、已有 OTEL 体系用 OpenTelemetry + 自己的后端;深度依赖 LangChain 的用 LangSmith。

小结

传统 APM 能告诉你"请求 500 了",但 LLM 应用的问题往往不是 500------是回答错了、花太多钱了、中间某一步的 prompt 拼炸了。这类问题只靠日志排查效率很低。

Langfuse 开源、自部署友好、SDK 设计简洁,改一行 import 就能跑起来。但真要在生产环境用好,trace 粒度、异步上报、采样率、数据保留都得提前想清楚。

上面提到的四个坑------Docker volume 忘挂、异步上下文丢失、flush 遗漏、版本兼容------都是我实际碰到的,记录在这里供参考。

相关推荐
SL_staff2 小时前
企业AI落地难?JVS-AI渐进式实践:安全可控、小切口验证、标准化沉淀与规模化复制
java·开源·aigc
小虎AI生活2 小时前
Anatomy 接入 WorkBuddy 实操:一句话生成 12 零件可交互科普模型(附2组提示词)
aigc·ai编程
长沙三为智能科技6 小时前
GEO与传统SEO的5个关键差异:排名与被引用、关键词与答案型的全维度对比
python·aigc·seo·ai搜索·geo优化
threerocks7 小时前
一个人指挥一支 AI 剧组:Gemini 4 + Omni 1.2 剧情短片制作保姆级教程
aigc·openai·ai编程
西安小哥9 小时前
大厂把大模型做成一层网关之后,我先把三件事重新想了
aigc
小和尚同志9 小时前
阿里大佬做的一款轻量化 Token 监控工具—知度 Tokei
人工智能·aigc·监控
小虎AI生活19 小时前
WorkBuddy 积分不够烧?我找到了每天白送 5000 的平台
aigc·ai编程
Csvn20 小时前
模型挂了全站 502?降级链让它先保住“有响应”(R04)
人工智能·aigc·agent
狼爷1 天前
AI 时代学习真相:答案唾手可得,思考才是稀缺品
aigc