ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

三AI考官协作打分:让RAG评估从拍脑袋到有数可依

三AI考官协作打分:让RAG评估从拍脑袋到有数可依

1. 为什么单考官打分不靠谱

做过 RAG 评估的人大概都有这种体验:你精心搭了一套检索增强生成流程,召回率看着不错,生成的内容也像模像样,但真到了要判断“这次改动到底有没有让系统变好”的时候,就抓瞎了。上一版回答和这一版回答摆在一起,肉眼看着都挺对,可到底哪个更准、哪个更全、哪个更少胡说八道,谁也说不清。

这就是 RAG 评估最核心的痛点:生成质量是一个多维度的模糊概念,单一指标或者单一评判者根本覆盖不了。你用 BLEU、ROUGE 这类传统指标去算,它们只能衡量字面重叠度,对于“意思对了但措辞不同”的情况直接判低分,误杀率极高。你换用单个 LLM 当裁判,又会遇到另一个问题——每个模型都有自己的偏好和盲区。有的模型偏爱长回答,看到字数多就给高分;有的模型对格式敏感,回答里没有分点列举就扣分;还有的模型在特定领域知识上存在系统性偏差,你让它评判它不熟悉的领域,它给出的分数基本是噪声。

我在实际项目里就踩过这个坑。当时用单个模型给一批问答对打分,跑出来的分数分布特别集中,几乎全在 7 到 9 分之间,区分度极差。后来换了另一个模型重新评,同一批数据的分差直接拉大到 3 到 9 分。这说明什么?说明单考官打出来的分数,很大程度上反映的是考官的偏好,而不是被评估系统的真实水平。

所以这一篇要解决的问题很明确:怎么用三个 AI 考官协作打分,把 RAG 评估从“拍脑袋”变成“有数可依”。我会把整套方案的选型逻辑、提示词设计、分数聚合方法、以及实操中遇到的坑全部拆开讲清楚。适合正在做 RAG 系统迭代、需要量化评估来指导优化的朋友参考,也适合对 LLM 评估方法论感兴趣的同学。

2. 三考官评估体系的整体设计

2.1 为什么是三个而不是两个或五个

三个考官这个数字不是拍脑袋定的。我从两个角度来论证这个选择。

从统计学角度看,评估的本质是在估计一个“真实质量分数”。单个考官的评分可以看作一次带噪声的观测,多个考官的评分取平均可以降低噪声。但考官数量不是越多越好——每增加一个考官,成本线性增长,而噪声降低的边际收益递减。两个考官的问题在于,当两人打分出现分歧时,你没有第三方来打破平局,最终还是要靠人来裁决,自动化程度不够。三个考官则可以在多数情况下通过多数表决或者中位数聚合得到稳定结果,同时在成本上仍然可控。

从评估维度覆盖角度看,RAG 生成质量至少包含三个核心维度:忠实度(回答是否基于检索到的上下文,有没有编造)、相关性(回答是否切题,有没有答非所问)、完整性(回答是否覆盖了问题的所有关键点)。让一个考官同时评三个维度,它很容易顾此失彼,或者把多个维度混在一起给一个笼统的分数。让三个考官各自聚焦一个维度,每个考官的评判标准更清晰,打分也更稳定。

注意:三个考官不一定是三个不同的模型。你也可以用同一个模型配合三套不同的提示词,分别引导它从忠实度、相关性、完整性三个角度评判。但实测下来,用不同模型做考官,评分多样性更好,能捕捉到单一模型忽略的问题。

2.2 考官角色的分工与提示词设计

三个考官的分工如下:

考官编号角色定位核心评判维度推荐模型类型
考官 A事实核查员忠实度:回答是否完全基于检索上下文推理能力强的模型
考官 B用户代言人相关性:回答是否直接解决了用户问题指令遵循好的模型
考官 C质量审计员完整性:回答是否覆盖所有关键信息点知识面广的模型

每个考官的提示词设计遵循同一个骨架:角色设定 + 评分标准 + 评分范围 + 输出格式约束。以考官 A 为例,提示词的核心部分是这样的:

EXAMINER_A_PROMPT = """你是一个严格的事实核查员。你的任务是判断【回答】中的每一句话是否都能在【检索上下文】中找到依据。 评分标准: - 10分:回答中所有事实性陈述都能在上下文中找到直接依据,无任何编造。 - 7-9分:回答基本忠实,但有1-2处细节与上下文有轻微出入。 - 4-6分:回答中有明显编造内容,但核心信息仍来自上下文。 - 1-3分:回答大量编造,或与上下文矛盾。 - 0分:回答完全脱离上下文。 请先逐句分析回答中的事实性陈述,然后给出最终分数。输出格式: 分析过程:... 最终分数:X """

这个提示词的关键设计点在于:要求考官先分析再打分。我试过直接让模型输出分数,结果发现模型经常“偷懒”,给一个中间分就完事。强制它先逐句分析,相当于让它把推理过程展开,分数就有了依据,可解释性也强很多。

考官 B 的提示词侧重相关性,核心问题是“这个回答如果给到提问的用户,用户会觉得被回答了还是被敷衍了”。考官 C 的提示词侧重完整性,要求它先列出问题涉及的关键信息点,再逐一检查回答是否覆盖。

2.3 分数聚合策略:从三个分数到一个决策

三个考官各自打分之后,怎么合成一个最终结论?这里有好几种策略,我逐一分析利弊。

策略一:简单平均。把三个分数加起来除以三。优点是简单,缺点是当三个考官分歧很大时(比如 3、6、9),平均值 6 分完全掩盖了分歧本身。而分歧大恰恰说明这个样本值得人工复核。

策略二:中位数。取三个分数的中间值。这个策略对异常值更鲁棒,比如一个考官给了极端低分,中位数能把它拉回来。但缺点是丢失了“分歧程度”这个信息。

策略三:加权平均 + 分歧标记。给每个考官设一个权重(比如事实核查员权重 0.5,另外两个各 0.25),同时计算三个分数的标准差。标准差超过阈值就标记为“需人工复核”。这是我目前最推荐的策略,既给出了综合分,又保留了分歧信号。

import numpy as np def aggregate_scores(score_a, score_b, score_c, weights=(0.5, 0.25, 0.25)): scores = np.array([score_a, score_b, score_c]) weighted_avg = np.average(scores, weights=weights) std = np.std(scores) needs_review = std > 2.0 return { "final_score": round(weighted_avg, 2), "std": round(std, 2), "needs_review": needs_review }

这个聚合函数跑下来,大部分样本的标准差在 1 以内,说明三个考官的一致性还不错。标准差超过 2 的样本大约占 10% 到 15%,这些就是最值得人工去看的“争议样本”。

3. 实操环境搭建与核心代码实现

3.1 环境准备与依赖安装

整套评估流程基于 Python 生态,核心依赖包括 LangChain 用于编排评估链、openevals 用于标准化评估接口、以及至少两个不同厂商的 LLM API 用于充当考官。

pip install langchain langchain-openai openevals pandas numpy tqdm

如果你用的是本地模型,可以通过 Ollama 或者 vLLM 起一个兼容 OpenAI 接口的服务,然后在 LangChain 里用ChatOpenAI指向本地地址即可。我实测下来,本地 7B 级别的模型做考官,在忠实度判断上已经够用,但在完整性判断上跟大模型还有明显差距。如果预算允许,建议至少有一个考官用能力更强的模型。

3.2 评估数据集的准备

评估数据集的质量直接决定评估结果的可信度。一个合格的 RAG 评估集至少包含以下字段:

字段名说明示例
question用户原始问题“LangChain 的 Memory 组件有哪些类型?”
context检索到的上下文从知识库召回的文档片段
answer待评估的回答RAG 系统生成的回答
reference参考答案(可选)人工撰写的标准答案

我建议评估集规模至少 50 条,覆盖不同类型的问题:事实型、对比型、推理型、多跳型。如果评估集太小,分数波动会很大,今天跑 7.2 明天跑 7.5,你根本分不清是系统改进了还是随机波动。

import pandas as pd eval_data = pd.read_json("eval_dataset.jsonl", lines=True) print(f"评估集大小:{len(eval_data)}") print(eval_data[["question", "answer"]].head())

3.3 三考官并行评估的实现

串行调用三个考官太慢,50 条数据乘以 3 个考官就是 150 次 API 调用,按每次 3 秒算要跑将近 8 分钟。用并发可以把时间压到 3 分钟左右。

from concurrent.futures import ThreadPoolExecutor from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate def create_examiner(model_name, prompt_template): llm = ChatOpenAI(model=model_name, temperature=0) prompt = ChatPromptTemplate.from_template(prompt_template) return prompt | llm examiner_a = create_examiner("gpt-4o", EXAMINER_A_PROMPT) examiner_b = create_examiner("claude-3-5-sonnet", EXAMINER_B_PROMPT) examiner_c = create_examiner("gpt-4o-mini", EXAMINER_C_PROMPT) def evaluate_single(row): inputs = { "question": row["question"], "context": row["context"], "answer": row["answer"] } result_a = examiner_a.invoke(inputs) result_b = examiner_b.invoke(inputs) result_c = examiner_c.invoke(inputs) return { "score_a": parse_score(result_a.content), "score_b": parse_score(result_b.content), "score_c": parse_score(result_c.content), "raw_a": result_a.content, "raw_b": result_b.content, "raw_c": result_c.content } with ThreadPoolExecutor(max_workers=5) as executor: results = list(executor.map(evaluate_single, eval_data.to_dict("records")))

这里有个细节要注意:temperature=0是必须的。评估任务需要的是稳定、可复现的判断,不是创意写作。温度调高会让同一个样本在不同运行中拿到不同分数,评估就失去了意义。

3.4 分数解析与结果落盘

LLM 输出的分数格式可能五花八门,有的写“最终分数:8”,有的写“Final Score: 8/10”,还有的会在分析过程中提到其他数字。解析函数要足够健壮。

import re def parse_score(text): patterns = [ r"最终分数[::]\s*(\d+(?:\.\d+)?)", r"Final Score[::]\s*(\d+(?:\.\d+)?)", r"分数[::]\s*(\d+(?:\.\d+)?)" ] for pattern in patterns: match = re.search(pattern, text) if match: return float(match.group(1)) return None

解析失败的样本要单独记录下来,不能直接丢弃。我遇到过大约 3% 的样本解析失败,检查后发现是模型在输出里用了全角冒号或者加了额外空格。把解析规则补全之后,失败率降到 0.5% 以下。

结果落盘用 JSONL 格式,每行一条记录,包含原始输入、三个考官的原始输出、解析后的分数、聚合分数和分歧标记。这样后续做错误分析的时候,可以随时回溯到原始输出。

4. 评估结果的分析与问题排查

4.1 分数分布分析与系统诊断

拿到评估结果之后,第一步是看分数分布。如果所有样本的分数都集中在 8 到 10 分,说明评估集太简单,或者考官太宽松,需要调整。如果分数分布呈现双峰,说明系统在某些类型的问题上表现好,在另一些类型上表现差,需要进一步拆解。

import matplotlib.pyplot as plt df = pd.DataFrame(results) df["final_score"] = df.apply(lambda r: aggregate_scores( r["score_a"], r["score_b"], r["score_c"])["final_score"], axis=1) df["final_score"].hist(bins=10, range=(0, 10)) plt.xlabel("综合评分") plt.ylabel("样本数量") plt.title("RAG 系统综合评分分布") plt.show()

我最近一次评估的结果是:平均分 7.3,标准差 1.8,分布大致呈正态但左尾略长。左尾那些低分样本,我逐条看了原始输出,发现主要问题集中在两类:一是检索上下文本身不包含答案,模型硬编了一个;二是问题涉及多个文档的交叉信息,模型只用了其中一个文档就作答。

4.2 考官分歧的典型模式与处理

三个考官打分不一致的情况,我总结了几种典型模式:

分歧模式表现可能原因处理建议
A低B高C高事实核查员给低分,另两个给高分回答流畅但存在编造优先信任考官A,人工复核
A高B低C高用户代言人给低分回答事实正确但答非所问检查问题理解是否有偏差
A高B高C低质量审计员给低分回答正确但遗漏关键点检查检索召回是否充分
三者都低一致低分系统在该样本上确实失败作为 bad case 重点分析

这个表格是我踩了很多坑之后总结出来的。最开始我不理解为什么考官A和考官B会给出截然不同的分数,后来把原始输出调出来看才明白:考官A关注的是“说的对不对”,考官B关注的是“说的有没有用”,这两个维度确实可能背离。一个回答可以做到每句话都有依据但完全没回答用户的问题,也可以做到切题但夹杂编造。

4.3 常见报错与排查速查表

在实际跑评估的过程中,我遇到过的报错和解决方法整理如下:

报错信息可能原因解决方法
RateLimitErrorAPI 调用频率超限降低并发数,加指数退避重试
ContextLengthExceeded检索上下文太长截断上下文或换用长上下文模型
JSONDecodeError模型输出不是合法 JSON改用正则解析,或在提示词中强制 JSON 格式
分数解析为 None输出格式不符合预期补充解析规则,记录原始输出人工检查
评估结果波动大temperature 未设为 0检查所有考官的温度参数

提示:如果评估结果在不同运行之间波动超过 0.5 分,优先检查 temperature 设置和模型版本是否一致。我有一次评估结果突然整体下降 1 分,排查半天发现是 API 提供方悄悄更新了模型版本。

4.4 从评估结果到系统优化的闭环

评估本身不是目的,驱动优化才是。我的做法是:每次评估后,把低分样本和争议样本单独导出,按问题类型归类,然后针对性地调整检索策略或生成提示词。

比如有一轮评估发现“多跳问题”的完整性分数普遍偏低,原因是检索只召回了单跳相关的文档。后来在检索阶段加了查询改写,把多跳问题拆成多个子查询分别检索再合并,完整性分数从 5.8 提升到 7.4。这个提升是实打实测出来的,不是感觉出来的。

low_score_samples = df[df["final_score"] < 6.0] disputed_samples = df[df["std"] > 2.0] print(f"低分样本数:{len(low_score_samples)}") print(f"争议样本数:{len(disputed_samples)}") low_score_samples.to_json("low_score_cases.jsonl", orient="records", lines=True) disputed_samples.to_json("disputed_cases.jsonl", orient="records", lines=True)

这两个文件就是下一轮优化的输入。低分样本告诉你系统在哪里失败,争议样本告诉你评估体系本身在哪里需要校准。

5. 成本控制与评估频率的平衡

5.1 三考官方案的成本估算

三个考官意味着三倍的 API 调用成本。以 50 条评估集为例,每条数据三个考官各调用一次,总共 150 次调用。如果每次调用平均消耗 2000 token(输入加输出),总 token 消耗约 30 万。按主流模型的价格算,一轮评估的成本在几块钱到几十块钱之间,取决于你用的模型档次。

这个成本对于迭代期的项目来说完全可以接受。但如果你每天跑十轮评估,成本就会累积。我的建议是:全量评估每周跑一到两次,日常迭代用抽样评估。抽样评估从评估集里随机抽 20 条,成本降到全量的 40%,但分数趋势仍然能看出来。

5.2 考官模型的选型权衡

不是所有考官都需要用最贵的模型。我的实测经验是:

  • 考官 A(事实核查):需要较强的推理能力,建议用能力强的模型。忠实度判断涉及逐句比对,弱模型容易漏判。
  • 考官 B(相关性):对模型能力要求相对低,中等模型即可胜任。相关性判断更多是语义匹配,不需要复杂推理。
  • 考官 C(完整性):需要一定的知识面,建议用中等偏上的模型。太弱的模型自己都不知道问题涉及哪些关键点,没法判断回答是否完整。

这样搭配下来,成本比三个考官都用最强模型降低约 40%,而评估结果的一致性(用标准差衡量)只下降了不到 10%。

5.3 评估流水线的自动化集成

评估不应该是一个手动触发的脚本,而应该集成到 CI/CD 流程里。我的做法是:每次 RAG 系统的提示词或检索策略有变更,自动触发一轮抽样评估,评估结果写入数据库,并在变更记录里附上分数对比。

def run_evaluation_and_log(config_version): results = run_full_evaluation() summary = { "version": config_version, "timestamp": datetime.now().isoformat(), "avg_score": results["final_score"].mean(), "std": results["final_score"].std(), "low_score_count": len(results[results["final_score"] < 6.0]) } log_to_database(summary) return summary

这样每次改动都有据可查,回滚的时候也知道回滚到哪个版本分数最高。我吃过一次亏:改了一版提示词,感觉回答变好了,直接上线,结果一周后用户反馈变差。回头想对比改之前的分数,发现根本没记录,只能凭记忆。从那以后评估结果全部落库。

6. 几个容易踩的坑和实操心得

第一个坑是评估集泄漏。如果你用评估集里的样本去调提示词,调完之后再拿同一个评估集去评,分数肯定虚高。正确的做法是评估集分两份:一份用于日常迭代参考,一份作为“测试集”只在最终验收时用。测试集不能用来调参。

第二个坑是考官提示词过拟合。我一开始为了让考官给出符合预期的分数,反复调整提示词里的评分标准描述,结果考官确实按我想要的分布打分了,但那个分布是我主观期望的,不是真实质量分布。后来我把提示词固定下来,不再根据评估结果去调考官,评估才变得可信。

第三个坑是忽略检索质量对生成评估的干扰。RAG 系统的最终回答质量受检索和生成两个环节影响。如果检索没召回相关文档,生成环节再强也答不好。所以在评估生成质量的同时,要单独评估检索的召回率和准确率。否则你看到生成分数低,以为是生成模型的问题,拼命调生成提示词,实际上问题出在检索。

提示:建议把检索评估和生成评估分开跑。检索评估用召回率、MRR、NDCG 这些指标,生成评估用三考官方案。两者结合才能定位问题到底出在哪个环节。

第四个心得是保留原始输出。聚合分数只是最终结论,但排查问题时你需要看考官的分析过程。我遇到过好几次聚合分数正常但考官分析过程里已经指出了潜在问题的情况。如果只存分数不存原始输出,这些信号就丢了。

最后分享一个实用技巧:给考官加一个“不确定”选项。当考官觉得信息不足以判断时,允许它输出“无法判断”而不是强行给一个中间分。这样可以把真正模糊的样本筛出来,而不是让它们污染分数分布。我加了“不确定”选项之后,大约 5% 的样本被标记为不确定,这些样本人工复核后发现确实大部分是边界情况,强行打分没有意义。

返回列表