☰
Langfuse 实战:LLM 应用 trace 与离线评估体系搭建
2026/10/2 11:06:17 网站建设 项目流程

1. 为什么 LLM 应用需要 trace 与离线评估

1.1 从"能跑通"到"跑得稳"之间的鸿沟

我最早接触 LLM 应用开发的时候,和大多数人一样,注意力全在 prompt 上。调通了 OpenAI 的接口,拿到返回结果,往页面上一贴,感觉这事就成了。但真正把应用放到线上跑一段时间之后,问题就一个接一个冒出来:用户反馈回答质量忽高忽低,有时候答非所问,有时候干脆胡编;某个 prompt 改了一版,感觉好像好了,但过两天又觉得不如上一版;token 消耗莫名其妙涨了一截,账单出来才发现成本翻倍。

这些问题的共同点是——你根本不知道每一次请求到底发生了什么。传统后端服务有日志、有链路追踪、有指标监控,出了问题可以顺着调用链一层层往下查。但 LLM 应用不一样,它的核心逻辑藏在自然语言里,输入是一段话,输出是另一段话,中间还夹着检索、工具调用、多轮对话状态管理。你光看最终输出,根本判断不了是检索环节召回错了文档,还是 prompt 模板拼接出了问题,还是模型本身在这个场景下就是不稳定。

这就是trace要解决的事情。Trace 这个词在分布式系统里早就有了,指的是把一次请求经过的所有环节串成一条完整的链路。放到 LLM 应用里,trace 记录的是:用户输入了什么、系统拼了什么样的 prompt、检索到了哪些上下文、调用了哪个模型、消耗了多少 token、返回了什么、耗时多久。有了这条链路,你才能定位问题到底出在哪一环。

而离线评估解决的是另一个维度的问题。线上 trace 让你看到"发生了什么",离线评估让你回答"这样到底好不好"。你不可能每次改 prompt 都靠肉眼一条条看结果,那效率太低了。你需要一套可重复运行的评估集,用固定的测试用例去跑你的应用,用可量化的指标去打分,这样才能在迭代过程中知道自己是进步了还是退步了。

Langfuse 这个工具,恰好把这两件事都覆盖了。它本身是一个开源的 LLM 工程平台,核心能力就是 trace 记录加上评估体系。我用了大概几个月,从最开始只是拿它看请求日志,到后来把离线评估也接进 CI 流程,中间踩了不少坑,也积累了一些比较实用的经验。这篇文章就把整个上手过程拆开讲清楚,包括为什么这么设计、具体怎么配、哪些地方容易出问题。

1.2 Langfuse 到底解决什么问题,适合谁用

先说清楚 Langfuse 的定位。它不是模型,不是框架,也不是 prompt 管理工具。它更像是一个"可观测性 + 评估"的基础设施层,架在你的应用和模型之间。你的应用照常调用 OpenAI 或者其他模型,只是在调用的前后,把关键信息上报给 Langfuse,它帮你存下来、可视化、做分析。

具体来说,它能做这几件事:

  • Trace 记录:把一次完整的请求链路记录下来,包括嵌套的 span(比如检索是一个 span,模型调用是另一个 span),每个 span 的输入输出、耗时、token 用量都能看到。
  • Session 追踪:多轮对话场景下,把同一个用户的多次交互归到一个 session 里,方便看整体对话质量。
  • Prompt 管理:把 prompt 模板集中管理,支持版本控制,改 prompt 不用改代码。
  • 评估体系:支持在线评估(比如用 LLM as judge 自动打分)和离线评估(用数据集批量跑分)。
  • 成本与延迟监控:按模型、按时间段统计 token 消耗和响应延迟。

适合谁来用?我的判断是:只要你的 LLM 应用开始有真实用户,或者你开始认真迭代 prompt 和检索策略,就该考虑接入了。如果只是本地跑个 demo 玩玩,那确实没必要。但一旦进入"改一版测一版"的循环,没有 trace 和评估体系,你的迭代基本靠玄学。

有一点要提前说明:Langfuse 有云端版和自托管版。云端版注册就能用,省事;自托管版需要自己部署,但数据完全在自己手里。我两种都用过,后面会讲怎么选。

2. 核心概念拆解与整体架构设计

2.1 Trace、Span、Generation 三层结构怎么理解

Langfuse 的数据模型其实不复杂,但第一次接触容易绕晕。我用一个生活化的类比来解释:把一次 LLM 请求想象成你去餐厅吃饭。

Trace就是你这次用餐的完整经历,从进门到出门。它是最外层的容器,代表一次完整的用户请求。比如用户问了一个问题,从收到问题到返回答案,整个过程就是一个 trace。

Span是这次用餐里的各个步骤。比如"看菜单"是一个 span,"点菜"是一个 span,"等上菜"是一个 span。在 LLM 应用里,检索文档是一个 span,调用工具是一个 span,后处理是一个 span。Span 可以嵌套,比如"点菜"里面还包含"询问推荐"和"确认菜品"两个子步骤。

Generation是一种特殊的 span,专门用来记录模型调用。它比普通 span 多了几个关键字段:用的哪个模型、输入 prompt 是什么、输出是什么、消耗了多少 input token 和 output token、花了多少钱。之所以单独拎出来,是因为模型调用是 LLM 应用里最核心也最贵的环节,需要单独统计。

这三层结构的好处是,你既能看到整体(trace 级别的总耗时、总成本),也能下钻到细节(某个 span 为什么慢,某次 generation 为什么 token 用得多)。我实际用下来,最常看的还是 generation 级别,因为大部分问题都出在 prompt 或者模型选择上。

2.2 为什么选 Langfuse 而不是自己写日志

有人可能会想,我直接往数据库里写日志不就行了,为什么要引入一个额外的平台?我一开始也这么想,后来发现自建日志有几个绕不过去的坑。

第一是可视化。日志是线性的文本,但 LLM 请求是树状的。你从一堆日志里想还原出"这次请求先检索了什么、再拼了什么 prompt、最后模型返回了什么",得自己写解析脚本,费时费力。Langfuse 直接给你一棵可展开的树,点一下就能看到每一层的输入输出。

第二是评估集成。自己写日志的话,评估得另起一套系统,数据集管理、打分逻辑、结果对比都要自己搞。Langfuse 把 trace 和评估打通了,你可以直接从线上 trace 里挑一条觉得有问题的,一键加到数据集里,下次离线评估就能覆盖这个 case。

第三是成本统计。不同模型的 token 计价不一样,还有缓存命中、批量折扣这些细节。自己算容易出错,Langfuse 内置了主流模型的价格表,自动帮你算。

当然,Langfuse 也不是没有代价。引入它意味着多一个依赖,多一层网络调用。如果你的应用对延迟极其敏感,上报数据这一步可能会带来额外开销。不过它支持异步上报和批量发送,实际影响很小,后面会讲怎么配。

2.3 整体接入架构:SDK 埋点 + 异步上报

Langfuse 的接入方式很直接:在你的应用代码里引入它的 SDK,在关键位置调用几个方法,SDK 会把数据异步发到 Langfuse 服务端。

整体架构大概是这样:

用户请求 → 你的应用 → [Langfuse SDK 埋点] → 调用 OpenAI ↓ 异步上报到 Langfuse 服务端 ↓ 存储 + 可视化 + 评估

关键设计点是异步。SDK 默认不会阻塞你的主流程,它把数据放进队列,后台批量发送。这样即使 Langfuse 服务端挂了,也不会影响你的应用正常返回。这一点很重要,我见过有人把上报做成同步的,结果 Langfuse 一抖动,整个应用跟着卡。

另一个设计点是采样。如果你的请求量很大,全量上报成本太高,可以配置采样率,比如只上报 10% 的请求。但要注意,采样会影响评估的准确性,如果要做严格的离线评估,最好保证评估相关的请求全量上报。

3. 环境准备与 SDK 接入实操

3.1 部署方式选择:云端还是自托管

先说部署。Langfuse 云端版注册就能用,免费额度对个人开发者和小团队够用。自托管版需要用 Docker 部署,官方提供了 docker-compose 配置,基本是开箱即用。

我的建议是:先用云端版跑通流程,等确实有数据合规需求或者用量大了再考虑自托管。自托管虽然数据在自己手里,但运维成本不低,要管数据库、要管升级、要管备份。我自托管那套跑在 2 核 4G 的机器上,跑是能跑,但数据量大了之后查询会变慢,后来还是迁回了云端。

如果决定自托管,官方的最小配置大概是这样:

# docker-compose.yml 核心部分 services: langfuse-server: image: langfuse/langfuse:latest ports: - "3000:3000" environment: - DATABASE_URL=postgresql://postgres:postgres@db:5432/langfuse - NEXTAUTH_SECRET=your-secret-here - SALT=your-salt-here - NEXTAUTH_URL=http://localhost:3000 depends_on: - db db: image: postgres:15 environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=langfuse

注意:NEXTAUTH_SECRET和SALT一定要换成随机字符串,不要用默认值。这两个是安全相关的配置,用默认值等于门没锁。

部署起来之后,访问对应端口,注册一个账号,创建一个 project,就能拿到 public key 和 secret key。这两个 key 后面接入 SDK 要用。

3.2 Python SDK 安装与初始化

Python 是目前 LLM 应用最主流的语言,Langfuse 的 Python SDK 也最成熟。安装很简单:

pip install langfuse

如果你用的是 OpenAI,建议把 openai 也一起装了,Langfuse 提供了对 OpenAI SDK 的封装,可以自动记录调用:

pip install openai

初始化的时候,推荐用环境变量传 key,不要硬编码在代码里:

export LANGFUSE_PUBLIC_KEY="pk-lf-..." export LANGFUSE_SECRET_KEY="sk-lf-..." export LANGFUSE_HOST="https://cloud.langfuse.com" # 自托管改成自己的地址

然后在代码里初始化客户端:

from langfuse import Langfuse langfuse = Langfuse()

这里有个细节:Langfuse()不传参数的话,会自动读环境变量。如果你有多个 project,可以显式传参。我建议在应用启动的时候初始化一次,全局复用这个客户端,不要每次请求都新建,那样会浪费连接资源。

3.3 用 OpenAI 封装自动记录调用

Langfuse 提供了一个很省事的方式:直接替换 OpenAI 的客户端。原来你是这样调用的:

from openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "你好"}] )

换成 Langfuse 封装之后:

from langfuse.openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "你好"}] )

就改了一个 import,其他代码完全不用动。Langfuse 会自动把这次调用记录成一个 generation,包括模型名、输入消息、输出内容、token 用量。这个封装是我最喜欢的功能之一,接入成本几乎为零。

但要注意,自动记录只覆盖了模型调用这一层。如果你的应用里有检索、有工具调用、有多步推理,这些还是需要手动埋点,否则 trace 里只有孤零零一个 generation,看不到完整链路。

3.4 手动埋点:把检索和工具调用串进 trace

手动埋点用的是@observe()装饰器,或者上下文管理器。装饰器方式最简洁:

from langfuse import observe @observe() def retrieve_documents(query: str): # 模拟检索 docs = vector_store.search(query, top_k=5) return docs @observe() def generate_answer(query: str, docs: list): context = "\n".join([d.text for d in docs]) prompt = f"根据以下资料回答问题:\n{context}\n\n问题:{query}" response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}] ) return response.choices[0].message.content

这样retrieve_documents和generate_answer都会各自成为一个 span,而且如果它们被同一个 trace 调用,会自动嵌套进去。Langfuse 靠上下文来关联,同一个请求链路里的 span 会自动归到同一个 trace 下。

如果你需要更细的控制,比如给 span 加自定义的 metadata,可以用上下文管理器:

with langfuse.start_as_current_span(name="rerank") as span: span.update(input={"candidates": len(docs)}) reranked = rerank(docs) span.update(output={"top": reranked[0].text})

实操心得:span 的命名很重要。我一开始随便起名,后来 trace 多了之后根本分不清哪个是哪个。建议用"动词+对象"的格式,比如retrieve_documents、rerank_results、format_prompt,一看就知道这一步在干嘛。

4. 离线评估体系搭建与实操

4.1 为什么离线评估比在线看日志更重要

Trace 解决的是"事后排查",但如果你每次改 prompt 都要等线上出问题才发现,那迭代效率太低了。离线评估的核心价值是把验证提前。

具体做法是:准备一批有标准答案或者有明确质量标准的测试用例,每次改动之后,用这批用例跑一遍,看指标是涨了还是跌了。这样你改 prompt 的时候心里有底,不会出现"感觉好像好了但不确定"的情况。

Langfuse 的评估体系分两块:数据集(Dataset)和评估运行(Dataset Run)。数据集就是你的测试用例集合,每条用例包含输入和期望输出(或者评估标准)。评估运行就是用某个版本的代码去跑这个数据集,得到一批结果,然后打分。

打分方式有两种:人工打分和自动打分。人工打分准确但慢,适合小规模精标;自动打分快但需要设计好评判逻辑,常见的是用 LLM as judge,让一个模型去评判另一个模型的输出。

4.2 构建评估数据集:从线上 trace 反哺

数据集不用从零开始造。最实用的做法是从线上 trace 里挑。你在 Langfuse 界面里翻 trace,看到哪条回答得不好,直接点"Add to dataset",就把这条请求的输入加进去了。然后你手动补一个期望输出,或者标注一个评分标准。

这种方式的好处是,你的数据集天然覆盖了真实用户会遇到的情况,比拍脑袋想测试用例靠谱得多。我一般会定期翻一遍最近的 trace,把有问题的挑出来,积累到一定数量就跑一次评估。

数据集的结构大概是这样:

dataset = langfuse.create_dataset(name="qa_eval_v1") dataset.create_item( input={"question": "Langfuse 支持哪些评估方式?"}, expected_output="支持人工打分和自动打分,自动打分常用 LLM as judge。", metadata={"source": "线上trace", "difficulty": "easy"} )

input是喂给应用的输入,expected_output是期望输出,metadata可以放一些辅助信息,比如难度、来源、标签。metadata 在后续分析的时候很有用,比如你可以单独看"困难"类用例的得分。

4.3 用 LLM as judge 做自动打分

自动打分最常用的方式是 LLM as judge。思路很简单:让一个模型(通常是能力比较强的,比如 GPT-4o)去看应用的输出,对照期望输出或者评分标准,给一个分数。

Langfuse 提供了几种内置的评估器,也支持自定义。内置的比如faithfulness(回答是否忠于上下文)、relevance(回答是否切题)。自定义的话,你可以写一个评估函数:

from langfuse import Langfuse langfuse = Langfuse() def llm_judge(input_question, expected, actual): judge_prompt = f"""你是一个评估专家。请对比以下两个回答,给出 0-1 的分数。 问题:{input_question} 期望回答:{expected} 实际回答:{actual} 评分标准:语义一致得 1 分,部分一致得 0.5 分,不一致得 0 分。 只输出分数,不要解释。""" response = judge_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": judge_prompt}] ) return float(response.choices[0].message.content.strip())

然后在评估运行的时候,对每条结果调用这个函数,把分数写回 Langfuse:

for item in dataset.items: # 跑你的应用 output = my_app(item.input["question"]) # 打分 score = llm_judge(item.input["question"], item.expected_output, output) # 写回 langfuse.score( trace_id=item.trace_id, name="llm_judge_score", value=score )

注意:LLM as judge 本身也有不确定性。同一个回答,judge 模型可能这次给 0.8,下次给 0.7。所以不要指望分数精确到小数点后两位,看趋势就行。如果两次评估分数差在 0.1 以内,基本可以认为是噪声,不用太在意。

4.4 评估结果对比与迭代决策

跑完评估之后,Langfuse 会把结果按 dataset run 组织起来,你可以对比不同 run 的得分。比如你改了 prompt,跑了一个新 run,和上一个 run 对比,看平均分是涨了还是跌了。

这里有个经验:不要只看平均分。平均分涨了不代表所有用例都变好了,可能是一部分涨了很多,另一部分跌了。我一般会看分布,把得分下降的用例单独拎出来分析,看看是不是某类问题被改坏了。

Langfuse 的界面上可以按 score 排序,也可以按 metadata 分组。我习惯按难度分组看,如果简单题得分高但难题得分低,说明应用在处理复杂问题上还有短板。

还有一个实用技巧:把评估运行和代码版本关联起来。每次跑评估的时候,在 metadata 里记一下 git commit hash 或者版本号,这样回头看的时候能对应上是哪版代码跑出来的结果。

5. 常见问题与排查技巧实录

5.1 Trace 丢失或不完整怎么办

这是最常见的问题。表现是:明明调用了,但 Langfuse 上看不到,或者只看到一部分 span。

排查思路按顺序来:

第一,检查 key 和 host 配对了没有。云端版和自托管版的 host 不一样,配错了数据发到别的地方去了。我见过有人自托管部署了,但 SDK 还指着云端地址,结果数据全发云端了。

第二,检查是不是异步上报还没发出去。SDK 是批量发送的,默认有个缓冲时间。如果你程序跑完立刻退出,缓冲区里的数据可能还没发出去。解决办法是在程序结束前调用langfuse.flush():

langfuse.flush()

第三,检查采样率。如果你配了采样,那本来就只有一部分请求会被记录。看看环境变量里有没有LANGFUSE_SAMPLING_RATE之类的配置。

第四,检查网络。自托管的话,确认应用能访问到 Langfuse 服务端。云端的话,确认没有网络策略拦截。

5.2 Token 统计对不上

有时候 Langfuse 显示的 token 用量和你自己算的对不上。原因通常有几个:

一是模型价格表没更新。Langfuse 内置的价格表可能滞后于模型更新,新模型或者调价之后,统计就不准了。这种情况可以手动在设置里覆盖价格。

二是流式响应的统计方式。流式返回的时候,token 是边生成边统计的,如果中途断了,统计可能不完整。

三是缓存命中。有些模型对重复的 prompt 有缓存,缓存命中的部分不计费,但 Langfuse 可能还是按全量算。这个要看具体模型的计费规则。

5.3 评估分数波动大怎么处理

前面提过,LLM as judge 本身有噪声。如果分数波动大,可以试试这几个办法:

  • 多次打分取平均:同一条结果让 judge 打 3 次,取平均分,能降低随机性。
  • 用更强的 judge 模型:judge 模型能力越强,打分越稳定。用 GPT-4o 比用 GPT-3.5 稳定得多。
  • 细化评分标准:不要用"好/中/差"这种模糊标准,改成具体的、可判断的维度。比如"回答是否包含关键信息点 A、B、C",这样 judge 更容易给出一致的判断。
  • 增加评估集规模:单条用例波动大,但 100 条用例的平均分就稳定多了。

5.4 常见问题速查表

问题现象可能原因排查方法
Trace 完全看不到key/host 配错检查环境变量,确认 host 地址
Trace 不完整异步未 flush程序结束前调用 flush()
只有部分请求有记录采样率配置检查采样相关环境变量
Token 统计偏差价格表滞后手动覆盖模型价格
评估分数波动大judge 噪声多次打分取平均,细化标准
自托管查询慢数据量大加索引,或迁移到云端
上报影响主流程延迟同步上报确认使用异步模式

5.5 几个我踩过的坑

第一个坑是在循环里创建 Langfuse 客户端。我一开始图省事,在每个请求处理函数里都Langfuse()一下,结果连接数暴涨,性能反而下降了。正确做法是全局初始化一次。

第二个坑是span 嵌套层级太深。有次我把每个小函数都加了@observe(),结果 trace 树深得没法看,找个关键信息要展开七八层。后来我改成只对关键环节埋点,比如检索、模型调用、后处理,中间的工具函数不埋。

第三个坑是评估集和线上数据分布不一致。我早期评估集全是简单问题,跑出来分数很高,但线上用户问的很多是复杂问题,实际体验差很多。后来我刻意往评估集里加难题,分数虽然降了,但更接近真实情况。

第四个坑是忘了给评估运行打标签。跑了几十次评估之后,回头看根本分不清哪次是哪次。后来我养成习惯,每次跑评估都在 metadata 里记上版本号和改动说明。

6. 把 trace 和评估接进日常开发流程

6.1 开发阶段的快速验证

开发阶段我一般会开一个本地 Langfuse 实例,或者直接用云端的一个测试 project。每次改完 prompt,先在本地跑几条用例,看看 trace 里的 prompt 拼接对不对、模型返回符不符合预期。这个阶段不用太正式,主要是快速看效果。

有个小技巧:在开发环境把采样率设成 100%,保证每条请求都记录。生产环境再调低采样率控制成本。

6.2 上线前的回归评估

上线前跑一次完整的离线评估,对比上一个版本的分数。如果分数明显下降,就要查原因。我一般会设一个阈值,比如平均分下降超过 5% 就 block 上线。

这一步最好能自动化。Langfuse 有 API,可以写个脚本,在 CI 里跑评估,把结果拉出来对比。如果你们用 GitHub Actions 之类的,可以配一个 workflow,每次合并到主分支之前自动跑。

6.3 线上问题的快速定位

线上出问题的时候,trace 是第一手资料。用户反馈某条回答有问题,你直接按时间或者按用户 ID 搜 trace,找到那条请求,展开看每一层的输入输出。大部分问题能直接定位到是检索召回错了,还是 prompt 拼接有问题,还是模型本身抽风。

如果发现是某类问题反复出现,就把这条 trace 加到评估集里,下次迭代的时候就能覆盖到。

6.4 持续迭代的节奏

我的节奏大概是:每周翻一次线上 trace,挑出有问题的加到评估集;每两周跑一次完整评估,看整体趋势;每次改 prompt 或者换模型,必跑评估。这样下来,迭代是有数据支撑的,不是凭感觉。

Langfuse 本身也在持续更新,新功能不少。我建议定期看看它的 changelog,有些新特性确实能省事。比如最近加的 prompt 版本对比功能,改 prompt 的时候能直接看 diff,比手动对比方便多了。

最后分享一个我个人的习惯:我会在 Langfuse 里给每条重要的 trace 加一个 tag,比如bug、good_case、edge_case。这样后面筛选的时候很方便,想找所有出过问题的 case,直接按 tag 过滤就行。这个习惯看起来小,但积累下来,你的评估集质量会高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询