传统接口测试很好办:发请求、看状态码、断言返回体里的字段值。但换成大模型接口就乱套了——同一个 prompt,两次返回的文字不一样;状态码是 200,不代表答案是对的;线上用户抱怨"答非所问",你回去翻日志,只能看到一句孤零零的 completion,中间经历了几次工具调用、检索了哪些片段、花了多少 token,全都是黑盒。
更麻烦的是回归。改了一行 system prompt,到底是变好了还是变坏了?靠人逐条看答案?一次跑几十条 case 还能扛,上百条就废了。
Langfuse 就是冲着这个问题来的。它是一个开源的 LLM 可观测与评估平台,干两件事:把每次 LLM 调用的全过程录下来(trace),再把"拿一批测试用例跑一遍、自动打分"这件事产品化(dataset + experiment)。
Langfuse 官网给自己的定位是 open-source LLM engineering platform,开源协议是 MIT,核心功能不收费。它有云版本(cloud.langfuse.com,每月 5 万次 observations 免费额度),也支持自托管,SDK 两边完全通用。
对测试同学来说,不用记它官网上那一长串功能名词,先把它对应到我们熟悉的概念上:
Trace / Observation:一次请求的完整录像。用户问了什么、模型回了什么、中间调了几次工具、检索召回了哪几段、每一步延迟和 token 消耗,全挂在一棵树上。出问题顺着树查,不用再翻日志。
Dataset:测试用例集。每条 case 有输入、有期望输出(也可以没有,靠规则判断)。可以手工录、可以从线上 trace 一键转成 case,也支持 CSV 导入。
Experiment / Run:一次测试执行。把 dataset 里的每条 case 喂给你的应用,跑完出一份报告。改 prompt、换模型、改 system message 之后重新跑一次,两次结果并排比。
Evaluator / Score:断言。可以是代码写的精确匹配、包含校验、正则;也可以是"再调一个模型当裁判"(LLM-as-a-judge);还支持人工标注队列。
Prompt Management:prompt 的版本管理和灰度发布。prompt 不进代码库,改完一键回滚,这对需要反复调 prompt 的测试和产品同学很实用。
一句话总结:Datadog 是给后端服务做监控的,Langfuse 就是给 LLM 应用做监控 + 测试的。它底层用 ClickHouse 存 trace,官方说月处理 observations 已经到百亿级别,Fortune 50 里有 21 家在用。
官网地址:
https://langfuse.com/
想快速体验直接用云版就行,注册个账号拿 API key 就能连 SDK。但测试同学一般要在自己环境里玩,数据不出内网,这里走自托管的 docker compose 路径,这也是官方文档里最省事的方式。
环境准备
一台装了 git 和 docker(带 compose 插件)的机器就行。本地 Mac/Windows 用 Docker Desktop,服务器建议 4 核 16G 内存起步,磁盘 100G 左右——trace 数据攒起来很快。
拉代码并启动
git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up
官方仓库里带了一份 docker-compose.yml,它会一起拉起 Postgres、ClickHouse、Redis、MinIO 这几个依赖组件,不用你额外搭。启动前打开 yml,把里面所有标了 # CHANGEME 的密钥项改成自己的随机串,正式环境别偷懒。
起来大概两到三分钟,等 langfuse-web-1 这个容器日志打出 Ready,浏览器访问 http://localhost:3000 就能看到登录页。服务器部署的话记得在安全组放行 3000 端口,或者直接 SSH 端口转发。
建项目并拿 API Key
第一次访问会让你注册管理员账号(自托管模式下这个账号只在你自己实例里)。登录进去之后:
新建一个 Organization,再在下面建一个 Project,比如叫 llm-app-test。
进 Project 的 Settings → API Keys,新建一对 key:一个 pk-lf-...(public),一个 sk-lf-...(secret)。后面 SDK 要用到。
到这里 Langfuse 本身就装好了。接下来是把我们自己的应用接进去。
这一节分两步走:先给一次 OpenAI 调用接上 trace,确认数据能在 Langfuse 里看到;再搭一个最小的离线评估,也就是把"测试用例集 → 执行 → 断言打分 → 版本对比"跑通。
第一步:给一次 LLM 调用加 trace
装 SDK:
pip install langfuse openai
配环境变量。自托管的话 LANGFUSE_BASE_URL 指向你自己的实例:
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="http://localhost:3000"
export OPENAI_API_KEY="sk-..."
最省事的接法是用 Langfuse 包装过的 OpenAI 客户端——只改一行 import,其他代码完全不动:
from langfuse.openai import openai
completion = openai.chat.completions.create(
name="test-chat",
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一个简洁的助手。"},
{"role": "user", "content": "1 + 1 等于几?"}
],
)
print(completion.choices[0].message.content)
跑一下这个脚本,然后回到 Langfuse 后台的 Tracing 页面,刷新就能看到这条 trace。点进去可以看到:完整的 prompt 和 completion、模型名、token 用量、延迟,全部自动记录,不用你手动埋点。短脚本记得最后调一下 langfuse.flush(),不然进程退出前 SDK 还没把数据发完就结束了。
如果你用的是 LangChain、LlamaIndex、Vercel AI SDK、OpenAI Agents SDK,官方都有对应的集成,基本也是改个 import 或传个 callback handler 的事。
第二步:搭一个最小离线评估(重点)
trace 解决的是"线上出问题怎么查",评估解决的是"改了东西怎么知道没改坏"。这部分才是测试同学真正花时间的地方。Langfuse 把它拆成三块:测试用例集(dataset)、被测应用函数(task)、断言函数(evaluator)。
1. 建测试用例集。下面这段脚本建了一个叫 sf-sites 的 dataset,塞了 5 条关于旧金山景点的问答题,每条都带期望输出。这就是我们的 golden set:
from langfuse import get_client
langfuse = get_client()
dataset_name = "sf-sites"
langfuse.create_dataset(
name=dataset_name,
description="旧金山景点问答 golden set",
)
items = [
{"input": {"q": "连接旧金山和马林县的红橙色悬索桥叫什么?"},
"expected_output": "Golden Gate Bridge"},
{"input": {"q": "旧金山湾里那座 former 联邦监狱所在的岛叫什么?"},
"expected_output": "Alcatraz Island"},
{"input": {"q": "电报山上那座装饰艺术风格的塔叫什么?"},
"expected_output": "Coit Tower"},
]
for it in items:
langfuse.create_dataset_item(dataset_name=dataset_name, **it)
print("dataset ready")
跑完之后去 Langfuse 后台的 Datasets 页面,能看到刚建的这个用例集,每条 case 的输入和期望输出都列在那里。可以手工加、可以 CSV 导,也可以直接把线上某条 trace "转正"成一条回归 case——这招特别实用,线上用户报过的问题,一键收进 golden set,下次改 prompt 自动重跑。

图:Langfuse 的 Datasets 页面,每个 dataset 就是一组测试用例
2. 写被测函数和断言。被测函数就是把 dataset 里的 question 喂给你的应用,返回模型答案。断言这里用最简单的精确匹配:答案和期望完全一致记 1 分,否则 0 分。
from langfuse import Evaluation, get_client
from langfuse.openai import OpenAI
langfuse = get_client()
client = OpenAI()
def answer_question(question: str) -> str:
# 实际项目里这里换成你自己的应用函数
resp = client.responses.create(
model="gpt-4o-mini",
input=[
{"role": "system", "content": "回答关于旧金山景点的问题。"},
{"role": "user", "content": question},
],
)
return resp.output_text
def application_task(*, item, **kwargs):
return answer_question(item.input["q"])
def exact_match(*, output, expected_output, **kwargs):
return Evaluation(
name="exact_match",
value=1.0if output.strip() == expected_output else0.0,
)
3. 跑一轮 experiment。一行 run_experiment,SDK 会自动并发地把 dataset 里每条 case 喂给 task,把每次调用的 trace 存好,再让 evaluator 打分:
dataset = langfuse.get_dataset("sf-sites")
result = dataset.run_experiment(
name="旧金山景点问答",
run_name="v1-原始prompt",
task=application_task,
evaluators=[exact_match],
)
print(result.format())
langfuse.flush()
终端会打印一份汇总,同时 Langfuse 后台的 Experiments 页面会多出一条 run。点进去能看到:每条 case 的输入、模型实际输出、exact_match 得分、以及背后那次 LLM 调用的延迟和 token。哪条 case 挂了,直接点进去看 trace 细节,和 debug 一个接口报错几乎一样。
4. 改 prompt 再跑一次做对比。实际测试中最常见的场景:v1 跑出来准确率不够,怀疑是 system prompt 太松。把 system message 改成"只返回景点官方名称,不要任何额外解释",然后把 run_name 改成 v2-收紧输出 再跑一遍。两次 run 用的是同一个 dataset,在后台可以并排对比平均分和每条 case 的差异——这就是 LLM 时代的回归测试。
精确匹配很严格,模型输出多一句"The answer is"都会判 0。正经项目里通常会叠一个 LLM-as-a-judge:再调一个模型,让它按"答案语义是否正确"打分,和精确匹配互补。也可以把这套脚本挂到 CI 上,prompt 或模型一变就自动跑一遍 golden set,分数掉了直接拦住发布。
小结
到这一步你已经把 Langfuse 的核心链路走通了:docker 起实例 → 改一行 import 收 trace → 建 dataset → 写 task 和 evaluator → 跑 experiment → 版本对比。后面要深入的方向,官方文档都有对应章节:LLM-as-a-judge 配置、人工标注队列(annotation queue)、prompt 灰度发布、把 experiment 接进 CI/CD。
对测试团队来说,它最大的价值不是"又一个监控大屏",而是把 LLM 应用的质量保障从"人肉看输出"往"可重复执行的回归测试"推了一步。这一步现在走起来还比较糙,但方向是对的。

