想清楚「分挂在哪、是什么类型」,评估就不会乱。Score 是 Langfuse 唯一的评估数据单位。挂载点决定它回答什么问题:挂 trace 回答「这次完整请求好不好」,挂 observation(span/generation)回答「这一步好不好」,挂 dataset run item 回答「这个测试样例在某个实验里得了几分」。类型由
data_type决定:NUMERIC(连续分,如 0.0–1.0 相关性)、CATEGORICAL(枚举标签,如 good/neutral/bad)、BOOLEAN(true/false,存储为 1/0)。
| 想要的评估 | 挂载点 | dataType | SDK 入口 |
|---|---|---|---|
| 整条请求的总体质量 | trace | NUMERIC | create_score(trace_id=...) |
| 某一步检索/生成的质量 | observation | NUMERIC/CATEGORICAL | create_score(trace_id, observation_id=...) |
| 用户点赞 / 点踩 | trace | BOOLEAN 或 NUMERIC | create_score(name='user_feedback') |
| 人工标注分类 | trace/observation | CATEGORICAL | create_score(value='good') |
| LLM 裁判自动评估 | trace | NUMERIC/CATEGORICAL | UI evaluator 或 SDK judge |
| 离线实验单样例得分 | dataset run item | NUMERIC | item.run() 内 create_score |
① Scores:手动打分与用户反馈
Python SDK v3(基于 OpenTelemetry)写分的核心是 langfuse.create_score(...)。最常见的模式是在 @observe 装饰的函数里,先拿到当前 trace 的 id,再把分写回去。下面是一个覆盖三种 data_type 的最小可运行示例。
# Langfuse Python SDK v3(OTel 架构)
pip install "langfuse>=3.0.0" openai
# 三个环境变量:自托管把 HOST 换成你的地址(如 http://localhost:3000)
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://cloud.langfuse.com" # EU 区;US 区用 us.cloud.langfuse.com
from langfuse import get_client, observe
langfuse = get_client() # 从环境变量读取 keys / host
@observe() # 自动开一条 trace + 一个根 observation
def answer_question(question: str) -> str:
# ... 你的真实 LLM 调用,这里用占位返回 ...
answer = f"回答:{question} 的答案是 42"
# 拿到当前 trace_id(v3 从 active span 上下文获取)
trace_id = langfuse.get_current_trace_id()
# 1) NUMERIC 数值分:0.0 ~ 1.0,常用于相关性/准确度
langfuse.create_score(
trace_id=trace_id,
name="relevance",
value=0.92, # float
data_type="NUMERIC",
comment="答案与问题高度相关",
)
# 2) CATEGORICAL 类别分:value 必须是字符串
langfuse.create_score(
trace_id=trace_id,
name="tone",
value="professional", # str
data_type="CATEGORICAL",
)
# 3) BOOLEAN 布尔分:value 传 0/1,UI 显示 True/False
langfuse.create_score(
trace_id=trace_id,
name="contains_pii",
value=0, # 0=False, 1=True
data_type="BOOLEAN",
)
return answer
answer_question("地球到月球多远?")
langfuse.flush() # 进程退出前 flush,确保异步队列发出
用户反馈分:把前端的 thumbs up/down 写回
生产里最高价值的信号往往是真实用户反馈。模式是:后端接口返回时把 trace_id 一并给前端,用户点赞/点踩后再带着这个 id 回调一个打分接口。下面用 FastAPI 演示这个回写端点。
from fastapi import FastAPI
from pydantic import BaseModel
from langfuse import get_client
app = FastAPI()
langfuse = get_client()
class Feedback(BaseModel):
trace_id: str # 生成回答时返回给前端的 trace id
thumbs_up: bool # True=赞, False=踩
comment: str | None = None
@app.post("/feedback")
def submit_feedback(fb: Feedback):
# 用户反馈用 BOOLEAN:赞=1 踩=0
langfuse.create_score(
trace_id=fb.trace_id,
name="user_feedback",
value=1 if fb.thumbs_up else 0,
data_type="BOOLEAN",
comment=fb.comment,
)
langfuse.flush()
return {"status": "recorded"}
② LLM-as-judge:自动给 trace 打分
人工打分不可规模化,LLM-as-judge 让一个模型去评估另一个模型的输出。Langfuse 提供两条路:UI 配置的 Managed Evaluator(无代码、自动对新 trace 跑、内置 hallucination / relevance / toxicity 等模板),和 SDK 自定义 judge(你自己写裁判 prompt 与解析逻辑,灵活但要自己跑)。
- UI 路径:Evaluators → New evaluator,选模板(或自定义 prompt),用变量
{{input}}{{output}}映射 trace 字段,设采样率与目标 filter,Langfuse 会异步对匹配的 trace 自动打分。 - SDK 路径:自己调用一个评估模型,解析出分值,再 create_score 写回——适合需要私有评分逻辑、或想在离线批处理里集中跑的场景。
- 两者写出的都是同一个 Score 对象,可以在同一个 Dashboard 里和人工分、用户反馈分一起聚合对比。
# SDK 自定义 LLM-as-judge:用一个模型评估另一个模型的回答
# pip install "langfuse>=3.0.0" openai
import json
from openai import OpenAI
from langfuse import get_client
langfuse = get_client()
oai = OpenAI()
JUDGE_PROMPT = """你是严格的评估员。判断【回答】是否忠实于【问题】,
只输出 JSON:{{"score": <0到1的小数>, "reason": "<简短理由>"}}
问题:{question}
回答:{answer}"""
def llm_judge(trace_id: str, question: str, answer: str):
resp = oai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user",
"content": JUDGE_PROMPT.format(question=question, answer=answer)}],
response_format={"type": "json_object"},
temperature=0,
)
verdict = json.loads(resp.choices[0].message.content)
# 把裁判结果写回成 trace 上的 NUMERIC score
langfuse.create_score(
trace_id=trace_id,
name="faithfulness_judge",
value=float(verdict["score"]),
data_type="NUMERIC",
comment=verdict["reason"], # 把裁判理由存进 comment,方便人工复核
)
return verdict
# 用法:对某条已存在的 trace 补打裁判分
llm_judge(
trace_id="abc123...",
question="地球到月球多远?",
answer="约 38.4 万公里",
)
langfuse.flush()
③ Datasets 与实验:离线对比 prompt / 模型版本
Score 解决「线上单条好不好」,Dataset 解决「换 prompt / 换模型后整体是变好还是变差」。流程三步:建数据集与样例 → 遍历样例跑实验并通过 item.run() 链接 trace → 写回 score。每次跑用一个 run name 区分,UI 会自动把同名 run 聚合成一行,多个 run 横向对比就是离线 A/B。
# 第一步:创建 dataset 和 dataset items(只需跑一次)
from langfuse import get_client
langfuse = get_client()
# 创建数据集(幂等:同名重复调用不会重复创建)
langfuse.create_dataset(
name="qa-eval-set",
description="问答质量回归测试集",
metadata={"owner": "ak", "version": 1},
)
# 灌入测试样例:input 是输入,expected_output 是参照答案(gold)
samples = [
{"q": "地球到月球多远?", "gold": "约 38.4 万公里"},
{"q": "水的沸点是多少?", "gold": "标准大气压下 100°C"},
{"q": "光速是多少?", "gold": "约 30 万公里每秒"},
]
for s in samples:
langfuse.create_dataset_item(
dataset_name="qa-eval-set",
input={"question": s["q"]}, # dict,对应你的应用入参
expected_output={"answer": s["gold"]},
)
print("dataset 灌入完成")
# 第二步:遍历 dataset 跑实验,用 item.run() 链接 trace 并写回 score
from langfuse import get_client
from langfuse.openai import openai # 用 drop-in 包装,generation 自动进 trace
langfuse = get_client()
def my_app(question: str, model: str) -> str:
"""被测应用:换 model 参数就是换实验变量"""
resp = openai.chat.completions.create(
model=model,
messages=[{"role": "user", "content": question}],
temperature=0,
)
return resp.choices[0].message.content
def exact_match(output: str, gold: str) -> float:
"""简单评估函数:gold 子串命中算对。真实场景可换成 LLM-as-judge"""
return 1.0 if gold in output else 0.0
def run_experiment(run_name: str, model: str):
dataset = langfuse.get_dataset("qa-eval-set")
for item in dataset.items:
# item.run() 上下文管理器:自动创建一条 trace 并 link 到该 run
with item.run(
run_name=run_name,
run_metadata={"model": model},
) as root_span:
question = item.input["question"]
output = my_app(question, model)
# 把应用输出更新到 trace 上,方便 UI 查看
root_span.update_trace(input=item.input, output={"answer": output})
# 写回 score:用 root_span.score_trace 自动绑定本次 run 的 trace
gold = item.expected_output["answer"]
root_span.score_trace(
name="exact_match",
value=exact_match(output, gold),
data_type="NUMERIC",
)
langfuse.flush()
print(f"实验 {run_name} 完成")
# 跑两个 run:对比两个模型版本
run_experiment(run_name="gpt-4o-mini-baseline", model="gpt-4o-mini")
run_experiment(run_name="gpt-4o-candidate", model="gpt-4o")
✓推荐做法
- 给每个 score 起稳定、语义化的 name(如 relevance / faithfulness_judge),UI 才能跨 trace 聚合
- 实验 run_name 带上变量信息(模型名 + prompt 版本),一眼看出对比的是什么
- 在 UI 建 Score Config 约束 dataType 与取值范围,让 SDK 写入获得服务端校验
- 裁判分把 reason 写进 comment,保留可复核的证据链
✗不推荐
- 不要用同一个 name 混写不同 dataType 的分,会污染聚合
- 不要在 CATEGORICAL 分里传数字 value,UI 会当成无效
- 不要忘了进程退出前 langfuse.flush(),异步队列没发出分就丢了
- 不要用线上业务 trace 直接当回归集——dataset 要可控、可复现
⚠常见误区
- 短脚本里漏 flush() 导致 score / trace 没上报,最常见踩坑
- v2 SDK 的 langfuse.score() 与 v3 的 create_score() 参数不同,照旧文档抄会报错
- item.run() 是上下文管理器,忘了用 with 包住会导致 trace 没 link 到 dataset run
在 Datasets UI 能看到至少两个 run、每个 run 有聚合后的平均 score,且能逐 item 下钻,即视为实验闭环搭成。
无度量、无对照、无回滚 = 无进步。Score 是度量,Dataset run 是对照,prompt 版本是回滚点——评估体系本质是给 LLM 应用装上可验证的反馈回路。
— Karpathy Lens