不要把 LLM-as-judge 想成新机制。在 LangSmith 里它和上一章的规则 evaluator 是同一个接口:一个函数,入参
run(被测对象,run.outputs是应用输出)+example(数据集样例,example.inputs/example.outputs是输入与可选参照),返回一个dict,必须含key(指标名,会成为 UI 里的列名)和score(数值,建议归一到 0~1),可选comment(裁判理由,UI 可点开复核)。唯一的不同是函数体里发生了一次 LLM 调用。把它当 evaluator 而不是「魔法」,你才会自然地去给它写测试、做对齐、控版本。
# LangSmith + LangChain 裁判模型(以 Anthropic 为例,OpenAI 同理换包)
pip install -U langsmith langchain langchain-anthropic
# 三个环境变量:开启 tracing + 鉴权(评估结果会自动上报到 LangSmith)
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="lsv2_pt_..." # 平台 Settings 里生成
# 裁判模型的 key(按你选的 provider 配;这里用 Anthropic)
export ANTHROPIC_API_KEY="sk-ant-..."
① 用 with_structured_output 让裁判吐结构化的 score + reasoning
裁判最大的工程风险是输出不可解析:你让它返回 JSON,它给你包了一层 markdown 代码围栏,或者多说了一句「这是我的评分:」,正则当场崩。正确做法是用 LangChain 的 with_structured_output(Schema)——把一个 Pydantic 模型交给它,模型会用底层的 tool-calling / JSON 模式直接产出结构化对象,你拿到的就是带类型的 Grade 实例,不碰一行解析代码。下面是一个 correctness(正确性) 裁判的完整实现:它对照 example 里的参考答案,判断应用输出是否事实正确。
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
# 1) 用 Pydantic 定义裁判必须返回的结构:分数 + 理由
class CorrectnessGrade(BaseModel):
"""对一个回答的正确性评分。"""
reasoning: str = Field(description="先逐步说明判断依据,再给分(强制先想后打分)")
score: float = Field(description="0.0 到 1.0,1.0 表示与参考答案完全事实一致", ge=0.0, le=1.0)
# 2) 裁判模型:temperature=0 让评分尽量可复现
judge_llm = init_chat_model(
"anthropic:claude-sonnet-4-5", # 也可换 "openai:gpt-4o" 等
temperature=0,
)
# with_structured_output 直接返回 CorrectnessGrade 实例,无需手动解析 JSON
correctness_judge = judge_llm.with_structured_output(CorrectnessGrade)
CORRECTNESS_INSTRUCTIONS = """你是一位严格的评分员,判断【学生回答】相对【参考答案】是否事实正确。
评分锚点(rubric):
- 1.0:与参考答案的所有关键事实一致,无错误。
- 0.5:大体正确,但遗漏或弄错了次要事实。
- 0.0:与参考答案矛盾,或包含关键事实错误。
只评正确性,不要因为措辞啰嗦或风格扣分。"""
# 3) 这就是一个标准 LangSmith evaluator:吃 run/example,吐 dict
def correctness_evaluator(run, example) -> dict:
question = example.inputs["question"]
reference = example.outputs["answer"] # 数据集里的参考答案
prediction = run.outputs["answer"] # 被测应用的输出
grade: CorrectnessGrade = correctness_judge.invoke([
{"role": "system", "content": CORRECTNESS_INSTRUCTIONS},
{"role": "user", "content":
f"问题:{question}\n\n参考答案:{reference}\n\n学生回答:{prediction}"},
])
# 把裁判的 reasoning 放进 comment,UI 里点开就能看到「为什么打这个分」
return {"key": "correctness", "score": grade.score, "comment": grade.reasoning}
② relevance / conciseness:单维度裁判与 reference-free 评估
correctness 需要参考答案,但很多维度是 reference-free(无参照)的——relevance(回答切不切题)只看问题和回答的关系,conciseness(简不简洁)只看回答本身。关键纪律是:一个 evaluator 只评一个维度。别让一个裁判同时打 relevance + conciseness 三个分,那样模型会互相干扰、分数纠缠,UI 里也聚合不清。下面两个裁判各管一摊,且都不需要 gold 答案。
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
judge_llm = init_chat_model("anthropic:claude-sonnet-4-5", temperature=0)
# ---------- relevance:回答是否切题(reference-free) ----------
class RelevanceGrade(BaseModel):
reasoning: str = Field(description="判断回答是否针对问题、有无答非所问")
score: float = Field(description="0~1,1=完全切题,0=完全答非所问", ge=0.0, le=1.0)
relevance_judge = judge_llm.with_structured_output(RelevanceGrade)
RELEVANCE_INSTRUCTIONS = """判断【回答】是否切合【问题】。只看相关性,不判断对错。
1.0=直接回答了问题;0.5=部分相关、夹带无关内容;0.0=答非所问。"""
def relevance_evaluator(run, example) -> dict:
g: RelevanceGrade = relevance_judge.invoke([
{"role": "system", "content": RELEVANCE_INSTRUCTIONS},
{"role": "user", "content":
f"问题:{example.inputs['question']}\n\n回答:{run.outputs['answer']}"},
])
return {"key": "relevance", "score": g.score, "comment": g.reasoning}
# ---------- conciseness:是否简洁、无冗余(reference-free) ----------
class ConcisenessGrade(BaseModel):
reasoning: str = Field(description="指出有无冗余、重复、废话")
score: float = Field(description="0~1,1=精炼无废话,0=大量冗余", ge=0.0, le=1.0)
conciseness_judge = judge_llm.with_structured_output(ConcisenessGrade)
CONCISENESS_INSTRUCTIONS = """判断【回答】是否简洁。只看是否有冗余/重复/废话,不判断对错或相关性。
1.0=精炼、每句都有信息量;0.5=有些啰嗦;0.0=大量重复或注水。"""
def conciseness_evaluator(run, example) -> dict:
g: ConcisenessGrade = conciseness_judge.invoke([
{"role": "system", "content": CONCISENESS_INSTRUCTIONS},
{"role": "user", "content": f"回答:{run.outputs['answer']}"},
])
return {"key": "conciseness", "score": g.score, "comment": g.reasoning}
| 维度 | 需要参考答案? | 裁判只看什么 | 典型用途 |
|---|---|---|---|
| correctness | 需要 (reference-based) | 问题 + 参考答案 + 回答 | 事实问答、知识库 QA 的对错 |
| relevance | 不需要 (reference-free) | 问题 + 回答 | 回答是否答非所问、跑题 |
| conciseness | 不需要 (reference-free) | 仅回答本身 | 摘要/客服回复是否啰嗦注水 |
| faithfulness/groundedness | 需要 (检索上下文) | 上下文 + 回答 | RAG 回答是否有据、防幻觉 |
③ 把裁判接进 evaluate() 跑全量数据集
三个裁判写好后,用上一章的 evaluate() 一把跑完:第一个参数是被测应用(接收 example.inputs、返回输出 dict),data 指向数据集,evaluators 列表里把三个裁判一起塞进去。LangSmith 会对每个样例分别跑三个裁判,结果作为三列 score 上报,可在 UI 里逐 example 下钻看每个裁判的 comment。
from langsmith import Client
from langchain.chat_models import init_chat_model
client = Client()
# 被测应用:真实里是你的链/agent,这里用一个模型直接回答做示例
app_llm = init_chat_model("anthropic:claude-sonnet-4-5", temperature=0)
def my_app(inputs: dict) -> dict:
"""被测目标:吃 example.inputs,返回输出 dict(key 与 evaluator 里取的对应)"""
resp = app_llm.invoke([
{"role": "user", "content": inputs["question"]},
])
return {"answer": resp.content}
# 数据集需提前建好(见上一章 datasets):每个 example
# inputs = {"question": "..."}
# outputs = {"answer": "<参考答案>"} # correctness 裁判会用到
results = client.evaluate(
my_app,
data="qa-eval-set", # 数据集名或 examples 列表
evaluators=[
correctness_evaluator, # 上文 ① 定义
relevance_evaluator, # 上文 ② 定义
conciseness_evaluator, # 上文 ② 定义
],
experiment_prefix="llm-judge-baseline", # 实验前缀,便于在 UI 里区分 run
max_concurrency=4, # 裁判调用是 IO 密集,适度并发提速
)
# 打印每个样例的三项裁判分
for r in results:
ex_id = r["example"].id
scores = {er["key"]: er["score"] for er in r["evaluation_results"]["results"]}
print(ex_id, scores)
可靠性:裁判本身是个需要被评估的系统
最危险的错觉是把裁判的分当成真理。LLM 裁判有系统性偏差:偏爱更长的回答(length bias)、偏爱排在前面的候选(position bias)、偏爱自家模型的风格(self-enhancement bias)、对自信措辞给高分而不管对错。再加上若 temperature 没调零,同一份输入多次评分还会抖动。所以上线前必须先回答一个问题:我的裁判到底有多准? 答案只能来自和人类的对齐校准。
评分抖动 / 不可复现
- 典型表现
- 同一条输入跑两次裁判,分数不一样;实验之间分数飘
- 判断标准
- 同输入重复评 5 次,score 方差应接近 0
- 解决方向
- 裁判模型显式 temperature=0;用 with_structured_output 固定输出结构,去掉自由文本里的随机表述;锁定裁判模型版本号,别用会滚动更新的别名
长度偏差 (length bias)
- 典型表现
- 啰嗦但空洞的回答 score 反而比精炼准确的高
- 判断标准
- 构造一对「长而水」vs「短而准」样例,裁判应给后者更高分
- 解决方向
- 在 rubric 里显式写明「长度不是优点,冗余要扣分」;把 conciseness 拆成独立裁判,不要和 correctness 混在一个 prompt 里互相干扰
维度纠缠
- 典型表现
- 一个裁判同时打 relevance+correctness+conciseness,分数互相污染、说不清扣在哪
- 判断标准
- 每个指标在 UI 里是独立一列、能单独追因
- 解决方向
- 一个 evaluator 只评一个维度,各写各的 system prompt 和 Schema;需要多维就并列多个 evaluator 传进 evaluators 列表
裁判与人类不一致
- 典型表现
- 裁判打高分的回答,人工复核觉得很差(反之亦然)
- 判断标准
- 在一批人工标注集上,裁判分与人工分的一致率 / 相关系数达到你设定的阈值(如一致率 ≥ 0.8)
- 解决方向
- 见下方对齐校准:用人工标注的 meta-dataset 反过来评估裁判,迭代 rubric 直到对齐,再上线
对齐校准:用人工标注反过来评估你的裁判
校准的核心思路是元评估(meta-evaluation):人工先给一批样例打金标分(human_score),再让裁判对同一批样例打分,最后量裁判分和人工分的一致程度。一致率达标才信这个裁判,否则改 rubric 重来。下面把人工分存进数据集的 example metadata,跑一次裁判,直接算出一致率。
from langsmith import Client
client = Client()
# 1) 一个「校准集」:每个 example 带人工金标分(0/1,或 0~1)
# 这里假设建数据集时把人工分写进了 example.outputs["human_score"]
# 例如 outputs = {"answer": "...", "human_score": 1.0}
# 2) 元评估器:比较裁判分与人工分是否一致(阈值 0.5 二值化后比对)
def judge_agreement(run, example) -> dict:
judge_score = run.outputs["answer_score"] # 被测「应用」其实是裁判,输出它的打分
human_score = example.outputs["human_score"]
agree = 1.0 if (judge_score >= 0.5) == (human_score >= 0.5) else 0.0
return {"key": "judge_human_agreement", "score": agree}
# 3) 这里「被测应用」就是裁判本身:吃问题+回答,输出裁判分
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Grade(BaseModel):
reasoning: str
score: float = Field(ge=0.0, le=1.0)
judge = init_chat_model("anthropic:claude-sonnet-4-5", temperature=0).with_structured_output(Grade)
def judge_as_app(inputs: dict) -> dict:
g = judge.invoke([
{"role": "system", "content": "判断回答相对参考答案是否正确,0~1 打分。"},
{"role": "user", "content":
f"问题:{inputs['question']}\n参考:{inputs['reference']}\n回答:{inputs['answer']}"},
])
return {"answer_score": g.score}
# 4) 跑元评估:结果里 judge_human_agreement 的均值就是「裁判与人类一致率」
results = client.evaluate(
judge_as_app,
data="judge-calibration-set", # 带 human_score 的校准集
evaluators=[judge_agreement],
experiment_prefix="judge-calibration",
)
agrees = [er["score"]
for r in results
for er in r["evaluation_results"]["results"]
if er["key"] == "judge_human_agreement"]
print("裁判与人工一致率:", sum(agrees) / len(agrees))
✓推荐做法
- 裁判 temperature=0,并锁定具体模型版本号,保证可复现
- 用 with_structured_output(PydanticSchema) 直接拿结构化的 score+reasoning,永不手写 JSON 解析
- 一个 evaluator 只评一个维度,多维就并列多个 evaluator
- rubric 里给清晰评分锚点(1.0/0.5/0.0 各代表什么),并显式说明「不看哪些方面」
- 上线前用人工标注集做对齐校准,量出一致率达标再信任裁判分
✗不推荐
- 不要让一个裁判 prompt 同时打 relevance+correctness+conciseness 三种分
- 不要让裁判自由输出文本再用正则抠分数——围栏、多余话会让解析崩
- 不要默认裁判的分是真理,不校准就拿去做选型决策
- 不要用会滚动更新的模型别名当裁判,版本一变评分基准就漂
- 不要忽视长度/位置偏差——必须用对抗样例主动测出来
⚠常见误区
- temperature 没显式设 0,导致同输入评分抖动、实验间不可比
- Schema 把 score 放在 reasoning 前面,丢掉了「先想后打分」的 CoT 收益
- 评估器返回 dict 漏了 key 字段,UI 里聚合不出列
- 校准集太小(<30 条)或本身标注有噪声,一致率不可信
三个单维度裁判用 with_structured_output 各自吐 score+reasoning,通过 evaluate() 在数据集上出三列分;且裁判在人工校准集上的一致率达到预设阈值——即视为 LLM-as-judge 闭环可信可上线。
瓶颈是验证,不是生成。LLM 裁判把「输出好不好」变成可度量,但裁判自己也得先被度量——没和人类对齐过的裁判,只是另一个未经验证的生成器。
— Karpathy Lens