一次 evaluate = 一个 experiment。 target 负责「产出」,evaluator 负责「判分」,两者职责分离:target 不该自己打分,evaluator 不该自己造数据。这种分离让你能换 target 不换评分标准(对比模型/Prompt),也能换评分标准不换 target(对比评分口径)。
1. 端到端最小可运行示例
先跑通主路径。下面这段代码假设你已经按 setup-and-environment 配好 LANGSMITH_API_KEY,并按 datasets 章节建好了一个数据集。它演示:建一个 target 函数 → 用内置评分逻辑 + 自定义 evaluator → 一次 evaluate() 跑出 experiment。
# 安装:langsmith 自带 evaluate;用到 openai 做 target 时再装 openai
pip install -U "langsmith[openai]" openai
# 环境变量(也可写进 .env)
export LANGSMITH_API_KEY="lsv2_pt_xxx"
export LANGSMITH_TRACING="true" # 让 experiment 的每条 run 都带 trace
export OPENAI_API_KEY="sk-xxx"
from langsmith import Client, wrappers
client = Client()
# 0) 准备数据集(幂等:已存在就复用)。生产中通常已在 datasets 章节建好。
dataset_name = "qa-capitals"
if not client.has_dataset(dataset_name=dataset_name):
ds = client.create_dataset(dataset_name=dataset_name)
client.create_examples(
dataset_id=ds.id,
examples=[
{"inputs": {"question": "中国的首都是哪里?"}, "outputs": {"answer": "北京"}},
{"inputs": {"question": "法国的首都是哪里?"}, "outputs": {"answer": "巴黎"}},
{"inputs": {"question": "日本的首都是哪里?"}, "outputs": {"answer": "东京"}},
],
)
# 1) target:被评估的系统。入参恒为 example.inputs(dict),返回值会成为 run.outputs。
openai_client = wrappers.wrap_openai(__import__("openai").OpenAI()) # 自动 trace 每次调用
def target(inputs: dict) -> dict:
resp = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "只回答城市名,不要多余文字。"},
{"role": "user", "content": inputs["question"]},
],
temperature=0,
)
return {"answer": resp.choices[0].message.content.strip()}
# 2) 自定义 evaluator:契约 (run, example) -> dict
# run.outputs 来自 target 的返回;example.outputs 是数据集里的参考答案。
def exact_match(run, example) -> dict:
predicted = (run.outputs or {}).get("answer", "")
reference = (example.outputs or {}).get("answer", "")
return {"key": "exact_match", "score": int(predicted == reference)}
# 3) 一次调用 = 一个 experiment
results = client.evaluate(
target, # 也可以传一个已有 experiment 的名字来复评
data=dataset_name, # 数据集名 / id / examples 可迭代对象都行
evaluators=[exact_match], # 评分器列表,每个 example 都会被每个评分器打分
experiment_prefix="qa-gpt4o-mini", # UI 里 experiment 名的前缀
max_concurrency=4, # 并发跑 example,加速
)
# results 可迭代,本地也能消费
for r in results:
print(r["example"].inputs, "->", r["run"].outputs, r["evaluation_results"])
2. 内置 evaluator:少写代码拿到标准指标
不是所有评分都要手写。langsmith 内置了一批开箱即用的评分逻辑,最常用的两类是精确/模糊字符串匹配与嵌入相似度。它们封装在 langsmith.evaluation 下,用 helper 构造后直接塞进 evaluators。
from langsmith import Client
from langsmith.evaluation import LangChainStringEvaluator
client = Client()
# A) 精确匹配 / 模糊字符串距离(基于 LangChain 字符串评估器)
# 需要 langchain:pip install -U langchain
exact = LangChainStringEvaluator("exact_match") # 0/1 完全相等
fuzzy = LangChainStringEvaluator("string_distance") # 归一化编辑距离
# B) 嵌入相似度:语义接近就给高分,适合开放式回答
# 需要 embedding 后端:pip install -U langchain-openai
embedding_dist = LangChainStringEvaluator("embedding_distance")
# 这些内置 evaluator 默认从 example.outputs 里找参考答案、从 run.outputs 找预测,
# 字段名不匹配时用 prepare_data 做字段映射:
fuzzy_mapped = LangChainStringEvaluator(
"string_distance",
prepare_data=lambda run, example: {
"prediction": (run.outputs or {}).get("answer", ""),
"reference": (example.outputs or {}).get("answer", ""),
"input": example.inputs["question"],
},
)
client.evaluate(
target, # 复用第 1 节的 target
data="qa-capitals",
evaluators=[exact, fuzzy_mapped, embedding_dist],
experiment_prefix="qa-builtin-metrics",
)
| 场景 | 选哪个 evaluator | 返回分语义 | 依赖 |
|---|---|---|---|
| 答案有唯一标准(城市、枚举、ID) | exact_match | 0/1 命中 | langchain |
| 允许拼写/格式微差 | string_distance | 1=完全一致,越小越远 | langchain |
| 开放式回答、语义对就算对 | embedding_distance | 语义距离,越小越像 | langchain-openai 等 embedding |
| 规则简单但字段特殊 | 自定义函数 | 你自己定义 | 无额外依赖 |
| 要主观质量/有理由的判分 | LLM-as-judge | 见下一章 | 见下一章 |
3. 自定义 evaluator:吃透 (run, example) -> dict 契约
自定义 evaluator 是评估系统里你写得最多的东西,必须把契约背下来。框架对每个 example 调用你的函数,约定如下:
- 入参 run:本次 target 跑出来的 Run 对象。
run.outputs是 target 的返回 dict;run.inputs是喂给 target 的输入;run.error非空表示 target 这条挂了。 - 入参 example:数据集里这一行。
example.inputs是输入 dict,example.outputs是参考答案(reference),可能为 None(无参考的纯生成任务)。 - 返回值:最简形式是 dict
{"key": 指标名, "score": 数值}。key决定 UI 里这一列叫什么名字,score是该列的分数。 - 也可返回
EvaluationResult、布尔/数值(会被包装),或 多个结果的列表 /EvaluationResults——一个函数一次性产出多个指标列。
from langsmith.schemas import Run, Example
from langsmith.evaluation import EvaluationResult, EvaluationResults
# 形式一:最简 dict —— 一个 evaluator 出一个指标
def has_answer(run: Run, example: Example) -> dict:
out = (run.outputs or {}).get("answer", "")
return {"key": "non_empty", "score": int(bool(out.strip()))}
# 形式二:返回 EvaluationResult —— 可带 comment(写回 UI 的说明),便于排查
def length_penalty(run: Run, example: Example) -> EvaluationResult:
ans = (run.outputs or {}).get("answer", "")
too_long = len(ans) > 10
return EvaluationResult(
key="concise",
score=0 if too_long else 1,
comment=f"len={len(ans)}; 期望城市名应很短",
)
# 形式三:一个函数返回多个指标 —— 返回 EvaluationResults(results 列表)
def multi_metric(run: Run, example: Example) -> EvaluationResults:
pred = (run.outputs or {}).get("answer", "")
ref = (example.outputs or {}).get("answer", "")
return EvaluationResults(results=[
EvaluationResult(key="exact", score=int(pred == ref)),
EvaluationResult(key="contains_ref", score=int(ref in pred)),
])
# 处理 target 报错的那条:run.error 非空时给 0 分而不是抛异常
def robust_match(run: Run, example: Example) -> dict:
if run.error: # target 这条挂了
return {"key": "exact_match", "score": 0, "comment": run.error[:200]}
pred = (run.outputs or {}).get("answer", "")
ref = (example.outputs or {}).get("answer", "")
return {"key": "exact_match", "score": int(pred == ref)}
client.evaluate(
target,
data="qa-capitals",
evaluators=[has_answer, length_penalty, multi_metric, robust_match],
experiment_prefix="qa-custom-evaluators",
)
3.1 score 的类型与 key 的命名
✓推荐做法
- score 用数值(0/1 或 0~1 连续分),方便 dashboard 聚合成平均分与趋势
- key 用稳定、语义清晰的名字(如 exact_match / faithfulness),跨 experiment 保持一致才能横向对比
- 需要解释判分时在 EvaluationResult 里写 comment,排查时一眼看到原因
✗不推荐
- 不要在不同 experiment 里给同一指标换 key 名(exact vs exact_match),UI 会当成两列对不齐
- 不要返回裸字符串当分数,分数列无法聚合
- 不要让 evaluator 有副作用(写库、改全局),它应当是纯函数
⚠常见误区
- 布尔型 score 在 UI 里按 0/1 聚合;想看「通过率」就保持 0/1,别混入 None
- 同一函数返回多指标时,每个 EvaluationResult 的 key 必须各不相同,否则后者覆盖前者
换一个 target 重跑后,UI 里能按相同的指标列名横向对齐看到每行分数变化,即为契约用对了。
4. 评分回写与多实验横向对比
evaluate() 跑完后,每个 example 的得分会以 feedback 形式回写到对应 run 上,并聚合到这个 experiment。关键点:只要多次 evaluate 跑在同一个数据集上,LangSmith 就能按 example 行对齐,把多个 experiment 并排成一张对比表——这就是回归测试和 A/B 选型的核心视图。
from langsmith import Client
client = Client()
# 同一数据集、同一评分器,换不同 target,跑成多个 experiment
def target_v1(inputs: dict) -> dict:
# ... gpt-4o-mini
return {"answer": "北京"}
def target_v2(inputs: dict) -> dict:
# ... 换 prompt / 换模型
return {"answer": "北京市"}
def exact_match(run, example) -> dict:
pred = (run.outputs or {}).get("answer", "")
ref = (example.outputs or {}).get("answer", "")
return {"key": "exact_match", "score": int(pred == ref)}
exp1 = client.evaluate(target_v1, data="qa-capitals",
evaluators=[exact_match], experiment_prefix="v1")
exp2 = client.evaluate(target_v2, data="qa-capitals",
evaluators=[exact_match], experiment_prefix="v2")
# 程序化读回某个 experiment 的聚合分(用于 CI 里做阈值断言)
project = client.read_project(project_name=exp2.experiment_name)
stats = project.feedback_stats or {}
print("v2 exact_match avg =", stats.get("exact_match", {}).get("avg"))
# 在 UI 里:进入数据集 -> Experiments 标签,勾选 v1 与 v2,
# 系统按 example 逐行并排显示两边 outputs 与 exact_match 分数,标红回归行。
评分列在 UI 里全空白
- 典型表现
- experiment 跑完了,run 都有 output,但某个指标整列没有分数
- 判断标准
- 该列 evaluator 对每行都成功返回了带 key/score 的结果
- 解决方向
- 多半是 evaluator 内部抛异常(如 example.outputs 为 None 取下标)。改用 (example.outputs or {}).get(...),并对 run.error 显式返回 0 分。
两个 experiment 对不齐
- 典型表现
- 勾选两个实验想横向对比,却各看各的、行对不上
- 判断标准
- 两次 evaluate 跑在同一 data 上,且指标 key 完全一致
- 解决方向
- 确认 data 传的是同一个数据集(名字/id 一致),并统一 evaluator 的 key 命名;换了数据集或改了 key 名都会导致无法对齐。
target 返回字符串导致 evaluator 取不到字段
- 典型表现
- run.outputs 不是预期的 dict,evaluator 里 .get 报错或恒为空
- 判断标准
- target 始终返回 dict,evaluator 按约定字段名读取
- 解决方向
- target 必须 return dict(如 {"answer": ...})。若历史上返回字符串,run.outputs 会被包成 {"output": "..."},按该键读取或修正 target。
embedding_distance 趋势看反
- 典型表现
- dashboard 上『质量』随版本越跑越高却其实变差
- 判断标准
- 明确该指标是距离(越小越好)还是相似度(越大越好)
- 解决方向
- 距离类指标值越小越好。要『越大越好』就自定义 evaluator 返回 1 - distance,并在 key 命名上体现(如 embedding_similarity)。
下一章进入 LLM-as-judge:当答案是开放式、没有唯一参考、需要「有理由的主观打分」时,用大模型当裁判来写 evaluator。它本质上还是这一章的 (run, example) -> dict 契约,只是 score 由模型给出、comment 带上判分理由。