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

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

线上跑着一个 LLM Agent,用户反馈"回答不对"。你打开日志,看到一堆 INFO: request sentINFO: 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_KEYLANGFUSE_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+,一天下来光这一个接口就烧了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 40 。改成只带最近 5 轮对话后,成本降到 40。改成只带最近 5 轮对话后,成本降到 </math>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.modelgen_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 遗漏、版本兼容------都是我实际碰到的,记录在这里供参考。

相关推荐
Awu12272 小时前
🍎Claude Code Playground:我愿称之为「前端调参神器」
前端·人工智能·aigc
爱吃的小肥羊2 小时前
从注册到订阅再到防封号,国内用 Claude 的完整避坑手册(2026 最新)
aigc·ai编程
Hommy887 小时前
【开源剪映小助手】项目介绍
开源·github·aigc·剪映小助手
DanCheOo8 小时前
我开源了一个工具,把 Cursor 37 个对话提取成 519 条结构化记忆——再也不用给 AI 重复解释背景了
aigc·ai编程·cursor
杀生丸学AI8 小时前
【动态重建】SparseCam4D:基于稀疏相机实现时空一致的4D重建技术
数码相机·aigc·扩散模型·图像编辑·视觉大模型·点云分割
InfiniSynapse9 小时前
minimax_image插件基本上能做到无AI感出图了
人工智能·ai作画·数据分析·aigc
明天有专业课9 小时前
RAG-向量数据库Milvus
面试·aigc
向量引擎9 小时前
向量引擎×GPT Image 2×deepseek v4实战全解析:API调用、Key管理和高并发的新潮玩法!
gpt·aigc·api·ai编程·ai写作·key
阿杰学AI9 小时前
AI核心知识143—大语言模型之 奖励作弊(简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·aigc·reward hacking·奖励作弊