不要把 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 回答是否有据、防幻觉
口诀一裁判一维度;要不要参考答案,决定 evaluator 从哪里取数据

③ 把裁判接进 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