个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain 效果评测:没有评测,优化就是在瞎调](#LangChain 效果评测:没有评测,优化就是在瞎调)
-
- 一、为什么要先做评测
- 二、评测什么:三层指标
- [三、从 20 条测试集开始](#三、从 20 条测试集开始)
- 四、检索层评测
- 五、生成层评测
-
- [5.1 自己实现一个 LLM 裁判](#5.1 自己实现一个 LLM 裁判)
- [5.2 拒答率:别忘了测"不该答的"](#5.2 拒答率:别忘了测"不该答的")
- [5.3 用 RAGAs](#5.3 用 RAGAs)
- 六、系统层指标
- [七、放进 CI:防止改崩](#七、放进 CI:防止改崩)
- 八、常见误区
- 九、小结
LangChain 效果评测:没有评测,优化就是在瞎调
改了切分参数,感觉"好像好了一点";
换了 embedding 模型,觉得"似乎更准了"。
这些"感觉"是不可靠的,而且每一次改动都可能悄悄让另一类问题变差。
这篇讲怎么用最小的成本搭一套能用的评测:
从 20 条测试集开始,到能放进 CI 的回归测试。
一、为什么要先做评测
一个真实的场景:你发现"召回不全",把 chunk_size 从 500 降到 300。
召回率确实上去了,但每个 chunk 太短缺上下文,
生成质量反而下降 ------而你只测了召回率,没测最终答案质量,
于是高高兴兴上线了。
RAG 各环节是互相牵制的:
| 改动 | 可能变好 | 可能变差 |
|---|---|---|
| chunk_size ↓ | 召回更聚焦 | 上下文缺失,答案不完整 |
| chunk_overlap ↑ | 边界信息完整 | 冗余,重复召回 |
| top_k ↑ | 召回率↑ | 噪声↑,成本↑ |
| 加 rerank | 排序更准 | 延迟↑,成本↑ |
没有量化指标,你根本不知道自己是在优化还是在拆东墙补西墙。
二、评测什么:三层指标
| 层次 | 指标 | 回答的问题 |
|---|---|---|
| 检索层 | 召回率、MRR、NDCG | "相关的文档找出来了吗、排得对吗" |
| 生成层 | 忠实度、相关性 | "答案是否基于上下文、是否答对" |
| 系统层 | 延迟、成本、拒答率 | "能不能上线" |
三层都要看。 只看生成层,你不知道是检索还是生成的锅;
只看检索层,可能出现"检索全对但答案很烂"。
三、从 20 条测试集开始
不要一上来就想搞几百条,20~30 条就能开始,而且质量比数量重要。
python
test_set = [
{
"question": "二线城市住宿标准是多少?",
"ground_truth": "450 元",
"relevant_chunk_ids": ["handbook.md#c17"],
},
{
"question": "报销需要几个工作日?",
"ground_truth": "审核 5 个工作日,到账 3 个工作日",
"relevant_chunk_ids": ["handbook.md#c42"],
},
# ... 凑够 20~30 条
]
建测试集的三条原则:
- 覆盖典型场景 ------别全挑简单的。要包含:
能直接答的、需要跨 chunk 综合的、库里根本没有的(测拒答); - ground_truth 要人工写 ------别用 LLM 生成答案当标准答案,
否则你在用模型评测模型,测出来的分数没有意义; - 标注 relevant_chunk_ids ------否则只能测生成层,
没法定位是检索的锅还是生成的锅。
四、检索层评测
python
def eval_retrieval(retriever, test_set, k=5):
recall_sum, mrr_sum = 0.0, 0.0
for case in test_set:
got_ids = [d.metadata["chunk_id"] for d in retriever.invoke(case["question"])[:k]]
expect = set(case["relevant_chunk_ids"])
# 召回率:相关的召回了多少
hit = len(expect & set(got_ids))
recall_sum += hit / len(expect) if expect else 0
# MRR:第一个相关结果排在第几位
for rank, cid in enumerate(got_ids, 1):
if cid in expect:
mrr_sum += 1 / rank
break
n = len(test_set)
return {"recall@k": recall_sum / n, "mrr": mrr_sum / n}
两个指标的含义:
- Recall@k :正确答案在 top-k 里的比例。
这个数字决定重排有没有意义 ------如果 top-10 召回率只有 50%,
重排再强也只能救回一半; - MRR(Mean Reciprocal Rank) :第一个正确结果排名的倒数均值。
平均排第 1 → 1.0,平均排第 2 → 0.5。这个数字反映"排得准不准"。
用法:先看 top-10 的 Recall,再看 top-3 的 MRR。
前者低说明召回有问题(改切分/embedding),后者低说明排序有问题(上 rerank)。
五、生成层评测
生成层没有标准答案可比(答案是一段话),主流做法是用 LLM 当裁判。
5.1 自己实现一个 LLM 裁判
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
judge = ChatOpenAI(model="gpt-4o-mini", temperature=0)
FAITHFULNESS = ChatPromptTemplate.from_template("""
判断【回答】中的内容是否全部能在【上下文】中找到依据。
评分标准:
2 = 全部内容都有依据,没有编造
1 = 大部分有依据,但有少量推断或补充
0 = 包含明显编造的内容,或上下文里根本没有答案却给出了答案
只输出一个数字(0/1/2),不要解释。
【上下文】
{context}
【回答】
{answer}
""")
def eval_faithfulness(chain, test_set):
scores = []
for case in test_set:
# 需要同时拿到答案和它用的上下文
out = chain_with_source.invoke(case["question"])
s = (FAITHFULNESS | judge | StrOutputParser()).invoke({
"context": format_docs(out["context"]),
"answer": out["answer"],
})
scores.append(int(s.strip()))
return sum(scores) / len(scores) / 2 # 归一化到 0~1
忠实度(Faithfulness)是最重要的一个指标 ------
它直接反映"模型有没有编"。RAG 最危险的失效就是一本正经地胡说。
5.2 拒答率:别忘了测"不该答的"
python
UNANSWERABLE = [
{"question": "公司明年的上市计划是什么?", "expect": "拒答"},
{"question": "CEO 的私人手机号是多少?", "expect": "拒答"},
]
def eval_refusal(chain):
ok = 0
for case in UNANSWERABLE:
ans = chain.invoke(case["question"])
if "不知道" in ans or "没有找到" in ans or "无法回答" in ans:
ok += 1
else:
print("未拒答:", case["question"], "->", ans[:60])
return ok / len(UNANSWERABLE)
这个指标比忠实度更容易被忽略,但上线后用户投诉往往来自这里。
测试集里一定要有 3~5 条"库里没有"的问题。
5.3 用 RAGAs
不想自己写裁判,可以用现成的库:
bash
pip install ragas
python
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
from datasets import Dataset
data = Dataset.from_list([
{"question": q, "answer": a, "contexts": ctxs, "ground_truth": gt}
for q, a, ctxs, gt in results
])
score = evaluate(data, metrics=[faithfulness, answer_relevancy, context_precision])
print(score)
RAGAs 提供四个指标:
faithfulness(忠实度)、answer_relevancy(答案相关性)、
context_precision(上下文精确率)、context_recall(上下文召回率)。
优点是开箱即用,缺点是它需要调用 LLM(有成本),
而且对中文的支持取决于你配的 judge 模型 ------
用 GPT-4 系列判中文效果尚可,用小模型判中文会明显不准。
六、系统层指标
python
import time
def eval_system(chain, test_set):
latencies = []
for case in test_set:
t0 = time.time()
chain.invoke(case["question"])
latencies.append(time.time() - t0)
latencies.sort()
return {
"p50": latencies[len(latencies) // 2],
"p95": latencies[int(len(latencies) * 0.95)],
}
看 p95 而不是平均值 ------用户投诉来自那些慢的 5%,
平均值会把它们掩盖掉。
成本就靠前面讲的 CostTracker 回调统计,
目标是算出"单次问答平均花多少钱"。
七、放进 CI:防止改崩
评测最大的价值不是"知道自己现在多少分",
而是每次改动后能立刻发现退步。
python
# eval_run.py
BASELINE = {
"recall@10": 0.85,
"mrr@3": 0.62,
"faithfulness": 0.90,
"refusal_rate": 1.00,
"p95_latency": 8.0,
}
TOLERANCE = 0.05 # 允许 5% 的波动
def main():
current = run_all_metrics()
failed = []
for k, base in BASELINE.items():
cur = current[k]
if k == "p95_latency":
ok = cur <= base * (1 + TOLERANCE)
else:
ok = cur >= base * (1 - TOLERANCE)
print(f"{k}: {cur:.3f} (基线 {base}) {'✅' if ok else '❌'}")
if not ok:
failed.append(k)
if failed:
raise SystemExit(f"评测未通过: {failed}")
留 5% 的容差很重要 ------LLM 输出有随机性,
同一份代码跑两次分数也会有小幅波动,不留容差会天天误报。
放进 GitHub Actions:
yaml
- name: Run RAG eval
run: python eval_run.py
改动 PR 时自动跑一遍,退步就卡住。
八、常见误区
| 误区 | 问题 | 正确做法 |
|---|---|---|
| 用 LLM 生成标准答案 | 用模型评模型,分数虚高 | 人工写 ground_truth |
| 测试集全是简单题 | 上线才发现复杂场景崩 | 覆盖难例和拒答题 |
| 只看总分 | 掩盖了局部退步 | 分层看 + 逐条看失败case |
| 跑一次就下结论 | LLM 有随机性 | 跑 2~3 次取平均 |
| 不留容差 | CI 天天误报,最后被关掉 | 留 5% |
| 只测检索 | 检索对了但答案很烂 | 三层都测 |
九、小结
- 没有评测的优化就是瞎调,RAG 各环节互相牵制,改一处会影响另一处;
- 三层指标:检索层(Recall@k、MRR)、生成层(忠实度、拒答率)、
系统层(p95 延迟、成本); - 测试集 20~30 条就够开始 ,但必须人工写答案 、
必须包含拒答类问题 、必须标注相关 chunk; - Recall 低 → 改切分/embedding;MRR 低 → 上 rerank;
- 忠实度是生成层最重要的指标,拒答率最容易被忽略但最招投诉;
- 放进 CI 并留 5% 容差,让"改崩了"在合并前被发现。
到这里,LangChain 从入门(LCEL、Model I/O)到 RAG(加载、切分、向量化、
检索、进阶)再到 Agent(工具调用、结构化输出、多 Agent、State、持久化)
最后是部署与评测,30 篇的完整链路就走完了。