第 8 篇把本地笔记问答从文件一路跑通到答案,收尾却留了个悬案:检索三板斧调完到底有没有变好,肉眼问三五条问题说了不算。本篇是《LangChain 从入门到实战》第 9 篇,专讲评测:给第 6 篇的 agent、第 7 篇的记忆会话、第 8 篇的 RAG 问答装上"改配置就能看分数"的回归护栏。评测驱动开发(EDD,Evaluation-Driven Development)的完整方法论本站已有专文,本篇只管工具层落地:先手写一个不依赖任何平台的最小评测,再把黄金集托管到 LangSmith、用 evaluate 跑批、用 LLM as judge 打开放式分,最后立下回归纪律,并给第 8 篇的笔记问答做一次检索质量评测。
1、无评测的调优是玄学
大模型应用有三个高权重变量:提示词、模型版本、检索参数。第 2 篇说提示词是效果的核心变量,第 8 篇的 k、MMR、chunk_size 同样是——任何一处改动都会让输出漂移。没有评测时,验证改动的唯一手段是肉眼:自己想几个问题,跑一遍,看着"好像更好了"就合入。这套流程有三个系统性漏洞。样本随机:三五条问题是随手想的,覆盖不了真实分布,改动在没问到的地方劣化了你也不知道。记忆偏差:人只记得住改好的案例,改坏的两次被"可能是波动"解释掉了。基线缺失:上个版本多少分、这个版本多少分,没有任何可对比的数字,两周后连"当初为什么这么改"都说不清。
评测的解法是把这三件事全部机械化:固定的输入集(黄金集)消灭样本随机,自动打分器(评估器)消灭记忆偏差,留档的分数(实验)消灭基线缺失。一句话立论:无评测的调优是玄学,有评测的调优才是工程。类比传统的单元测试会更好懂:黄金集就是大模型应用的测试用例,评估器就是断言,跑批工具就是测试运行器——连"先红后绿"的老规矩都照搬:先让黄金集里出现 FAIL,再动手改,全绿的集子查不出问题。
黄金集怎么抽才有代表性、指标怎么选才不跑偏,是方法论问题,EDD 系列专文讲过;本篇往下只讲一件事:这套东西在 LangChain 生态里怎么落地。
2、最小评测手写黄金集
先别急着上平台——评测的最小形态十几行就能写,不依赖任何服务。黄金集就是一个列表:每条存问题、期望答案里必须出现的关键词、答案应当来自的笔记。断言循环如下:
golden = [ # 黄金集:三要素齐备
{"q": "部署前要做哪些检查?", "want": ["端口", "首页"], "src": "deploy.md"},
{"q": "chunk_overlap 是什么?", "want": ["重叠"], "src": "rag.md"},
{"q": "checkpointer 存什么?", "want": ["历史"], "src": "memory.md"},
]
hits = 0
for case in golden:
answer = qa.invoke(case["q"]) # 第 8 篇的问答链
ok = all(k in answer for k in case["want"])
hits += ok
print("PASS" if ok else "FAIL", case["q"])
print(f"通过率 {hits}/{len(golden)}")
一次典型运行的输出如下:
PASS 部署前要做哪些检查?
PASS chunk_overlap 是什么?
FAIL checkpointer 存什么?
通过率 2/3
这个循环已经具备评测的全部骨架:黄金集、被测对象、断言、分数。FAIL 那条暴露的正是肉眼调优的盲区——它可能是检索没召回 memory.md 的块,也可能是答案换了个措辞没写"历史"二字,断言循环只能告诉你"有病",定位"病在哪"是第 5 节 judge 与第 7 节命中率指标的事。关键词断言的适用边界也要认清:答案形态确定(分类、抽取、检索来源)时它是利器,开放式回答时太脆——问"为什么",期望关键词没法穷举。
黄金集的起步规模给个工程参考:十到二十例就能立住基线,覆盖上讲究三条——每篇笔记至少一问、高频主题多配几问、必含两三条笔记里没有答案的越界问题(考拒答护栏,第 8 篇的第二问就是这类)。例子最值钱的来源不是坐在屋里想,而是真实问答日志:用户问过什么、答错过什么,按主题分层抽出来,就是第一版黄金集。
3、LangSmith 数据集托管
手写黄金集的短板在时间维度:脚本里的列表没有版本、没有历史,第 6 次调参时你已经找不回第 1 次的分数。LangSmith 的数据集(dataset)就是托管的黄金集:示例存在云端,每次跑批自动生成一条带分数的实验记录,前后对比一目了然。SDK 独立发布(见 PyPI 主页),安装如下:
pip install -U langsmith
装好后设置环境变量 LANGCHAIN_API_KEY(详见评估快速入门),把第 2 节的黄金集传上去:
from langsmith import Client
client = Client()
dataset = client.create_dataset(
dataset_name="笔记问答黄金集", # 数据集名,全局可见
description="本地笔记 RAG 的回归测试集",
)
client.create_examples( # 批量写入示例
inputs=[{"question": c["q"]} for c in golden],
outputs=[{"keywords": c["want"],
"source": c["src"]} for c in golden],
dataset_id=dataset.id,
)
读代码注意两个约定。inputs 是喂给应用的输入,outputs 是期望输出(参考答案)——评估器拿它当参考打分,所以关键词和期望来源放这边。create_examples 是批量版,单条加用 create_example;数据集在 LangSmith 界面上可以直接增删改示例,黄金集从此有了单独的家,换机器、换脚本都不丢。
4、用 evaluate 跑批
数据集就位,跑批入口是 langsmith.evaluation 的 evaluate 函数——它接收被测目标(target)、按数据集逐条调用、再把评估器的分数汇总成一次实验。目标函数与评估器的写法如下:
from langsmith.evaluation import evaluate
def qa_target(inputs: dict) -> dict: # 目标:吃输入、吐输出
return {"answer": qa.invoke(inputs["question"])}
def keyword_hit(run, example) -> dict: # 评估器:实际对期望
want = example.outputs["keywords"] # 黄金集的期望关键词
answer = run.outputs["answer"] # 目标的实际输出
score = int(all(k in answer for k in want))
return {"key": "keyword_hit", "score": score}
evaluate(qa_target, data="笔记问答黄金集", # 数据集名直取
evaluators=[keyword_hit],
experiment_prefix="notes-qa") # 实验名前缀
解读三点。目标函数的形状是"字典进字典出":输入是数据集示例的 inputs,返回值进入 run.outputs;被测对象不必是链——第 6 篇的 agent、第 7 篇带 thread_id 的多轮会话都能包成这样的函数,官方评估文档(LangSmith 评估指南)另有 agent 评测专篇。评估器用 run 加 example 双参签名:run.outputs 是实际输出,example.outputs 是黄金集期望,比较二者即可;只看输入输出、不需要参考答案的评估器也支持更简的 inputs 加 outputs 签名。确定性任务还有现成轮子:exact_match 从 langsmith.evaluation 导入即用,输出与参考完全一致才得分,适合分类与抽取。
跑批结束控制台会打印实验链接,浏览器里每条例子的输入、输出、得分逐条可查;同一数据集跑多次,实验在界面上并排成表——第 7 节那张参数对比表的真实来源就是它。基线实验自此立住:之后的每次改动,都与它比分数。
pytest 风格的
ls.test装饰器已进入新版 SDK 文档,但 2026 年 10 月仍标注 beta——生产入口继续用 evaluate,等它定版再迁不迟。
5、LLM as judge 评分
关键词断言到开放式回答就失灵了:"解释一下 MMR 的原理"的正确答案有无数种写法。LLM as judge 的思路是让另一个模型按评分标准打分:给它问题、参考答案、实际输出,让它裁决实际输出是否正确。官方把现成封装放在独立的 openevals 包(openevals 仓库),安装与造一个正确性 judge 如下:
pip install openevals
from openevals.llm import create_llm_as_judge
from openevals.prompts import CORRECTNESS_PROMPT
correctness_judge = create_llm_as_judge(
prompt=CORRECTNESS_PROMPT, # 官方预置的正确性评分模板
feedback_key="correctness", # 分数在报告里的键名
judge=init_chat_model("openai:gpt-6.1-terra", temperature=0),
)
judge 造好后直接放进 evaluators 列表。它的输出是 pydantic 结构约束的"理由加分数",不是裸文本——强制模型先写推理再下结论,分数稳定性显著好于让它直接吐数字。不想要现成模板,也可以手写函数评估器:内部用 with_structured_output 定义自己的裁决结构(reasoning 加 score),官方 LLM as judge 指南(judge 用法文档)现行示例正是这种写法,与 openevals 殊途同归。无论哪条路,评分 prompt 的要点如下:
| 要点 | 说明 |
|---|---|
| 维度单一 | 一个 judge 只量一件事,正确性与简洁度分开建 |
| 给参考答案 | 有参考的裁决更稳,黄金集的 outputs 直接复用 |
| 先理由后分数 | 结构化输出强制推理在前,抑制随手打分 |
| judge 换模型 | 裁决者与被测者分离,避开自我偏爱 |
四条里最值钱的是"给参考答案":有参考的 judge 分数与人工标注的一致性系统性高于无参考版本,黄金集在这一层再度变现——同一份数据,喂给第 2 节的断言、第 4 节的 exact_match、第 5 节的 judge,只是打分器换了。
还有一步别省:judge 上岗前先校准。挑二十例人工判一遍,再让 judge 判一遍,两份结论的一致率达到八成才值得信任;不达标的先改评分模板(维度拆细、参考答案补全),而不是急着上线。分数日后漂移,也先怀疑 judge——被测应用没动、judge 模型悄悄升了版,是这类事故的高发原因。
6、回归纪律
工具齐了,让它们产生复利的是纪律:任何改动,跑一遍评测、对比分数再合入。完整闭环如下:
图里最容易被跳过的是"对比基线"那个菱形——不比分数的跑批只是花哨的日志。三条纪律配这张图:改动必跑,改一个字也算改动;下降必回滚,"可能是波动"不是合入理由,想清楚再改;黄金集只增不改,修期望输出等于换考卷,必须单独提交并说明理由。跑批成本高的项目可以分层:日常改动跑二十例冒烟集,发版前跑全量。这套纪律落到操作上就是一句话:分数不涨的改动不合入。
7、实战检索质量评测
回到第 2 节那个 FAIL:checkpointer 那问没过,是检索没召回还是措辞没对上?答案质量的瓶颈几乎总在检索不在生成——第 8 篇的判断需要数据来验收。给检索单独建目标函数,只测"期望来源的笔记有没有出现在检索结果里",这就是检索评测最常用的命中率(hit rate)指标:
def retrieve_target(inputs: dict) -> dict: # 只测检索不测生成
docs = retriever.invoke(inputs["question"])
return {"sources": [d.metadata["source"] for d in docs]}
def retrieval_hit(run, example) -> dict: # 命中率评估器
want = example.outputs["source"] # 期望来源笔记
return {"key": "retrieval_hit",
"score": int(want in run.outputs["sources"])}
evaluate(retrieve_target, data="笔记问答黄金集",
evaluators=[retrieval_hit], experiment_prefix="retrieval")
命中率把"答案好不好"降解成"该来的块来没来",FAIL 的归因立刻清晰。拿它跑第 8 篇三板斧的参数对比:黄金集扩到 12 例——扩的九例按第 2 节的规矩从问答日志里分层抽,三种检索配置各跑一轮,一次典型运行的结果如下:
| 实验 | 检索配置 | 命中率 |
|---|---|---|
| retrieval-a1 | 相似度,k 取 4 | 9 比 12 |
| retrieval-a2 | MMR,k 取 4 | 10 比 12 |
| retrieval-a3 | 相似度,k 取 8 | 11 比 12 |
三条实验一排,决策不再靠手感:MMR 比纯相似度多命中一条,去重收益为正;k 调到 8 又多中一条,但上下文多喂一倍的块、token 成本与噪声同涨——多出的 2 条命中值不值,产品口径说了算,评测的职责是把这笔账摆上桌。顺带一提,checkpointer 那问在 a3 才命中:期望的块排在第 5 位开外,k 取 4 时天然够不着——第 2 节的悬案至此告破。
8、总结与练习
回顾全文:评测三件套——黄金集管输入、评估器管打分、实验管对比;最小形态是手写断言循环,十几行不依赖平台;LangSmith 数据集托管黄金集,evaluate 以"目标函数加评估器列表"跑批并留档实验;确定性断言用关键词或 exact_match,开放式回答交给 LLM as judge,openevals 包的 create_llm_as_judge 三行起步;回归纪律一句话——分数不涨的改动不合入;检索质量用命中率单测,把生成问题与检索问题分开归因。对于正在迭代的应用,建议今天就动手:先给第 8 篇的问答链手写十条例子的黄金集跑通断言循环,再迁上 LangSmith 立住基线实验——从下一次改提示起,让每个决定都有分数背书。
练习两道:
- 给黄金集补三条越界问题,评估器改为断言拒答,验证第 8 篇的护栏
- 用
create_llm_as_judge替换keyword_hit重跑一轮,找出结论相反的例子

