Langfuse 是目前开源生态里最顺手的 LLM 应用可观测平台。当你的 RAG 或 Agent 服务上线后,最怕两件事:用户说「答得不对」,你却不知道错在检索、提示词还是模型;月底账单突然翻倍,你也说不清钱花在哪个功能上。传统监控看不到这两层,Langfuse 正是补这块缺口的工具。
本文不谈概念堆砌,直接给出可落地的路径:自托管部署、Python SDK 埋点、LangChain 一行接入、Token 成本归因、提示词版本管理、线上打分,以及我在生产环境踩过的五个坑。
一、传统监控为什么看不住 LLM 应用
如果你已经用 Prometheus 看指标、用 OpenTelemetry 串链路,会发现它们在 LLM 场景下并非无效,而是「粒度不对」。一次问答请求在 APM 里可能只是一个耗时 4.2 秒的 HTTP span,但业务上它其实包含:改写用户问题、向量库召回 8 个片段、重排序、拼装提示词、调用模型、解析 JSON、失败后重试一次。APM 只告诉你「慢」,不会告诉你「召回的 8 个片段里有 6 个是噪声」。
1.1 三件传统 APM 看不见的事
第一是输入输出内容本身。排查 LLM 问题必须看到当时那一版提示词的完整文本、模型返回的原始字符串。指标系统天生不存内容,日志系统存了但没法按会话串起来看。
第二是Token 与花费。CPU 和内存是包月资源,Token 是按次计费的变动成本。哪个功能、哪个用户、哪个模型吃掉了预算,需要一套按调用维度累加的账本,而不是一条 QPS 曲线。
第三是质量。接口返回 200 不代表答对了。LLM 应用唯一有意义的健康指标是「回答有没有用」,这需要人工标注、用户点赞点踩、模型自动评分三种信号回流到同一条链路上。
换个说法:Prometheus 与 Grafana 管的是「机器还活着吗」,OpenTelemetry 链路追踪 管的是「请求走了哪些服务」,而 Langfuse 管的是「这次回答是怎么生成的、值多少钱、好不好」。三者互补而非替代。
二、四个核心概念:Trace / Observation / Session / Score
Langfuse 的数据模型很轻,理解四个词就够用。埋点前先把它们和自己的业务对上号,后面查数据会顺畅很多。
| 概念 | 含义 | 对应业务 | 常犯的错 |
|---|---|---|---|
| Trace | 一次完整的业务请求 | 用户问一个问题 | 把每次模型调用当成一个 Trace,链路碎成一片 |
| Observation | Trace 内的一个步骤,分 span / generation / event | 召回、重排、模型调用 | 只埋模型调用,检索环节成黑盒 |
| Session | 多轮对话的串联 | 一个会话窗口 | 不传 session_id,多轮上下文无法回溯 |
| Score | 挂在 Trace 或 Observation 上的评分 | 点赞、人工标注、自动评测 | 只看耗时不回流质量,无法做版本对比 |
其中 generation 是专门给模型调用用的 Observation 类型,它额外带 model、input、output、usage 字段,成本核算全靠它。span 用于普通步骤,event 用于打一个瞬时标记(比如「命中缓存」)。
三、自托管部署:Docker Compose 起一套
Langfuse 有云版,但国内团队更常见的是自托管——数据不出内网,提示词和用户问题都算敏感资产。新版本依赖 PostgreSQL(业务数据)、ClickHouse(分析型存储)、Redis(队列)和 S3 兼容对象存储(大字段),因此不要试图只起一个容器。
# 官方仓库自带完整编排,直接用最省事
git clone https://github.com/langfuse/langfuse.git
cd langfuse
# 生产环境务必改掉默认密钥
cp .env.example .env
# 三个必须自己生成的变量
openssl rand -hex 32 # NEXTAUTH_SECRET
openssl rand -hex 32 # SALT
openssl rand -hex 32 # ENCRYPTION_KEY
docker compose up -d
# 观察启动,web 容器 healthy 后访问 3000 端口
docker compose ps
docker compose logs -f langfuse-web --tail 50
首次访问 http://server-ip:3000 注册第一个账号即成为实例管理员,然后新建 Organization 与 Project,在项目设置里拿到 pk-lf- 与 sk-lf- 一对密钥。如果对外暴露,前面挂一层反向代理并配好 HTTPS,做法可参考站内的 Nginx 反向代理实战。
3.1 资源与保留策略
一台 4C8G 的机器足够支撑日均几万条 Trace。真正膨胀的是 ClickHouse 与对象存储里的原始输入输出——一条 RAG Trace 带十几个片段,单条几十 KB 很常见。上线第一天就把保留期定下来(例如明细留 30 天、聚合指标留一年),别等磁盘告警才处理。磁盘被写满的连锁反应有多难看,可以看 磁盘 I/O 打满导致服务雪崩 那一篇。
四、Python SDK 埋点:装饰器 + 手动 generation
最省力的接法是 @observe 装饰器:函数进出自动生成 Observation,嵌套调用自动形成父子关系。真正需要精细控制的只有模型调用那一层,因为要把 model、usage、成本喂进去。
import os
from langfuse import observe, get_client
from openai import OpenAI
os.environ['LANGFUSE_PUBLIC_KEY'] = 'pk-lf-xxx'
os.environ['LANGFUSE_SECRET_KEY'] = 'sk-lf-xxx'
os.environ['LANGFUSE_HOST'] = 'http://langfuse.internal:3000'
lf = get_client()
client = OpenAI()
@observe(name='retrieve')
def retrieve(question: str):
# 这里写你的向量召回;返回值会作为 Observation 的 output 被记录
docs = vector_store.search(question, top_k=8)
return [d.text for d in docs]
@observe(name='answer', as_type='generation')
def answer(question: str, docs: list):
prompt = '根据资料回答问题。\n资料:\n' + '\n---\n'.join(docs) + '\n问题:' + question
resp = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
temperature=0.2,
)
# 把用量与模型名回填到当前 generation,成本才能算准
lf.update_current_generation(
model='gpt-4o-mini',
usage_details={
'input': resp.usage.prompt_tokens,
'output': resp.usage.completion_tokens,
},
)
return resp.choices[0].message.content
@observe(name='rag-qa')
def rag_qa(question: str, user_id: str, session_id: str):
lf.update_current_trace(user_id=user_id, session_id=session_id,
tags=['rag', 'prod'], metadata={'tenant': 'acme'})
docs = retrieve(question)
return answer(question, docs)
if __name__ == '__main__':
print(rag_qa('报销单多久到账', user_id='u_1024', session_id='s_77'))
lf.flush() # 短生命周期进程必须 flush,否则数据留在缓冲区
三个细节值得强调。其一,user_id 与 session_id 必须在入口就写进 Trace,事后补不回来;后续做用户级成本分摊、会话级质量分析全靠它们。其二,tags 用来区分环境与功能模块,是后台过滤最高频的维度,建议统一约定命名。其三,脚本、Celery 任务、Serverless 这类短生命周期进程一定要 flush(),SDK 是异步批量上报的,进程退出太快会丢数据。
4.1 LangChain / LlamaIndex 一行接入
如果应用是用 LangChain 搭的,不必逐个函数改造,用回调处理器即可把整条 Chain 的每一步自动上报,包括工具调用与中间思考步骤。这对调试 Agent 尤其有用——工具选错、参数拼错都能一眼看到。相关编排写法可回看 LangChain Agent 实战。
from langfuse.langchain import CallbackHandler
handler = CallbackHandler()
result = agent_executor.invoke(
{'input': '统计上周订单金额 TOP5 的门店'},
config={
'callbacks': [handler],
'metadata': {
'langfuse_user_id': 'u_1024',
'langfuse_session_id': 's_77',
'langfuse_tags': ['agent', 'sql-tool'],
},
},
)
让 Agent 直接生成 SQL 查数据库的场景,链路可观测性几乎是刚需——生成的语句到底动了哪张表、跑了多久、是否被安全闸拦下,全要留痕。这套安全执行的做法在 Text2SQL 落地实战 里有完整拆解。
五、Token 成本归因:把调用折算成钱
Langfuse 内置了主流模型的价目表,只要 generation 里的 model 名能匹配上,成本自动算出来。真正的问题出在两类情况:一是走了自建网关,model 名被改成了 internal-chat-v2 这类别名;二是用了自部署的开源模型,官方价目表里根本没有。
解法是在项目设置里新建自定义模型定义:配置匹配用的正则、输入输出单价与计价单位。自部署模型没有账单,但强烈建议按 GPU 摊销折算出一个「影子单价」录进去——否则做自建与调用 API 的比价时无据可依。这块的算法逻辑可以参考 大模型推理成本优化。
# 自建网关场景:显式上报用量与实际模型,避免成本落到 unknown
lf.start_as_current_generation(
name='gateway-call',
model='internal-chat-v2', # 与自定义模型定义的正则匹配
model_parameters={'temperature': 0.2, 'max_tokens': 1024},
input=messages,
usage_details={'input': 1820, 'output': 356, 'cache_read_input': 900},
cost_details={'total': 0.0043}, # 也可直接上报金额,优先级高于价目表
)
成本看板真正有价值的是拆维度看。下面这张表是我们做预算复盘时固定看的四个切面,每次都能发现一两处意料之外的开销。
| 切面 | 怎么看 | 典型发现 |
|---|---|---|
| 按 tag | 按功能模块聚合花费 | 一个边缘的摘要功能吃掉三成预算 |
| 按 user_id | 找出重度用户 | 少数账号被脚本刷接口,需要限流 |
| 按 model | 对比不同模型单位成本 | 简单意图分类还在用旗舰模型 |
| 按输入输出比 | 看提示词是否过胖 | 历史对话全量拼接,输入 Token 是输出的 12 倍 |
最后一行是最常见也最好治的浪费。把全量历史改成滑动窗口加摘要,把重复的系统提示词抽出来利用缓存,输入 Token 常能直接砍掉一半。提示词组织的取舍在 大模型上下文工程实战 里讲得更细。
六、提示词版本管理与线上打分
把提示词硬编码在代码里,意味着每次改一个字都要走发版流程,运营和产品同学永远插不上手。Langfuse 的 Prompt Management 把提示词变成带版本与标签的资源:代码按标签拉取,改文案不发版,且每条 Trace 会记录当时用的是哪个版本——这才让「新版提示词是不是更好」变成可验证的问题。
// 拉取带 production 标签的提示词并编译变量
const prompt = await langfuse.getPrompt('rag-answer', undefined, {
label: 'production',
cacheTtlSeconds: 60, // 本地缓存,避免每次请求都打网络
});
const compiled = prompt.compile({ question, context });
// 用户点踩时回流一个分数,与 traceId 绑定
await langfuse.score({
traceId,
name: 'user-feedback',
value: 0, // 1 有用 / 0 没用
comment: '答案与实际报销政策不符',
});
分数的来源建议同时铺三条:用户显式反馈(点赞点踩,量小但最准)、模型自动评分(用一个模型判断回答是否有事实依据,覆盖面大但有偏差)、人工抽检(每天抽 20 条标注,作为校准另外两条的基准)。三者对齐后,才能在灰度两个提示词版本时用数据决策,而不是靠感觉。
需要区分清楚的是:Langfuse 的 Score 面向线上真实流量,衡量的是「这次回答好不好」;而离线基准测试衡量的是「这套系统的能力上限在哪」,两者搭配才完整。离线评测怎么设计,参考 AI Agent 评测:如何量化智能体真实能力;检索环节的质量指标则在 RAG 进阶实战 里有专门讨论。
七、生产落地的五个坑
下面五条都是真踩过的,按发生频率排序。
| 坑 | 表现 | 处置 |
|---|---|---|
| 数据丢失 | 本地跑有数据,容器里没有 | 进程退出前 flush;Web 服务在 shutdown 钩子里调用 |
| 敏感信息入库 | 身份证号、手机号被完整记录 | 上报前脱敏,或对特定字段关闭 input/output 记录 |
| 成本落到 unknown | 看板有调用无花费 | 补自定义模型定义,或直接上报 cost_details |
| Trace 粒度过碎 | 一次问答变成七条互不相关的 Trace | 入口统一开 Trace,子步骤用 Observation 嵌套 |
| 存储膨胀 | ClickHouse 与对象存储月增几百 GB | 设保留期,超长输入输出截断后再上报 |
脱敏这条尤其要提前想清楚。可观测的价值来自记录内容,而合规要求恰好是限制记录内容,两者天然冲突。务实的做法是:在 SDK 上报前做一层正则脱敏(手机号、邮箱、证件号),并把「是否记录完整 input」做成按环境开关——开发环境全记,生产环境只记结构化摘要加长度。
八、与既有可观测体系怎么拼
不要把 Langfuse 当成第二套 APM。合理的分工是:机器与接口层面的指标告警继续留在 Prometheus 与 Alertmanager;跨服务的请求链路继续走 OpenTelemetry;Langfuse 只负责 LLM 语义层。做关联的关键是把两侧的 ID 互相带上——在 Trace 的 metadata 里写入 OTel 的 trace_id,在 OTel span 的属性里写入 Langfuse 的 trace_id,排查时就能双向跳转。
告警侧也别重复建设。LLM 场景值得单独设的告警只有三类:单位时间成本突增(防刷与死循环)、平均评分下滑(质量退化)、生成失败率上升(模型或网关异常)。其余沿用现有告警体系即可,避免又造出一堆没人看的告警——告警治理的方法论见 On-call 值班与告警治理实战。
小结
LLM 应用的可观测不是把日志再打一遍,而是补上三样传统监控天生缺失的东西:完整的输入输出、按调用累计的钱、可回流的质量分。Langfuse 用 Trace / Observation / Session / Score 四个概念把它们串在一起,自托管一套 Compose、埋一个装饰器、接一个回调,一天内就能跑通最小闭环。
落地顺序上我的建议是:先接入拿到链路,再补 user_id 与 tags 拿到成本归因,最后铺 Score 做质量闭环。三步走完,提示词改动与模型替换才第一次有了可验证的依据,而不是继续靠拍脑袋上线。




