一次 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_match0/1 命中langchain
允许拼写/格式微差string_distance1=完全一致,越小越远langchain
开放式回答、语义对就算对embedding_distance语义距离,越小越像langchain-openai 等 embedding
规则简单但字段特殊自定义函数你自己定义无额外依赖
要主观质量/有理由的判分LLM-as-judge见下一章见下一章
口诀硬匹配用 exact,软匹配用 distance,语义用 embedding,规则怪自己写,要解释找 judge。

3. 自定义 evaluator:吃透 (run, example) -> dict 契约

自定义 evaluator 是评估系统里你写得最多的东西,必须把契约背下来。框架对每个 example 调用你的函数,约定如下:

  1. 入参 run:本次 target 跑出来的 Run 对象。run.outputs 是 target 的返回 dict;run.inputs 是喂给 target 的输入;run.error 非空表示 target 这条挂了。
  2. 入参 example:数据集里这一行。example.inputs 是输入 dict,example.outputs 是参考答案(reference),可能为 None(无参考的纯生成任务)。
  3. 返回值:最简形式是 dict {"key": 指标名, "score": 数值}key 决定 UI 里这一列叫什么名字,score 是该列的分数。
  4. 也可返回 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 带上判分理由。