
01闭环五步,全景一览
先看完整链路,再逐步拆:

图1:评测闭环五步流程图
| Step | 吃什么(输入) | 吐什么(输出) | Ragas扮演什么角色 |
|---|---|---|---|
| 1 | 文档目录 | Document列表 | 无(LangChain加载) |
| 2 | Document列表 | KnowledgeGraph(节点+实体+关系) | 切卡片 → 划重点(NER)→ 拉红线(Jaccard) |
| 3 | KG + Synthesizer + 数量 | testset.json(问题+参考答案+题型标签) | TestsetGenerator + 3种QuerySynthesizer |
| 4 | testset中的问题 | 每条的response + retrieved_contexts | 无(你的RAG系统) |
| 5 | (question, response, contexts, reference) | 6指标×N条的得分矩阵 | 6个Metric scorer |
下面逐步展开。
· · ·
02Step 1:文档加载
最简单的一步,但它的输出格式决定后面所有环节怎么跑。
输入:一个文件夹,里面放 .md/ .txt/ .pdf文档。
输出:一个 Document列表。每个Document两部分——正文(page_content)和来源信息(metadata)。
from langchain_community.document_loaders import DirectoryLoader, TextLoader
loader = DirectoryLoader("./my_docs", glob="**/*.md", loader_cls=TextLoader)
documents = loader.load()
# [Document(page_content="爱因斯坦的相对论...", metadata={"source": "physics.md"}), ...]
这步没Ragas什么事。但输出要存盘——后面可能换不同参数重建知识图谱,不想每次都重新扫文件夹。
· · ·
03Step 2:建证据墙(知识图谱)
这是上一篇讲的"证据墙"在代码层面的落地。回忆一下:切卡片→划重点→拉红线。在Ragas里,这三步对应三个组件:
| 证据墙的步骤 | Ragas组件 | 做了什么 |
|---|---|---|
| 切卡片 | DocumentSplitter | 把Document切成chunk,每个chunk是图上的一个Node |
| 划重点 | NERExtractor / KeyphraseExtractor | 从每个Node提取实体、关键词,挂到Node的properties上 |
| 拉红线 | JaccardSimilarityBuilder | 两个Node共享实体?拉一条边连起来 |
输入:Document列表
输出:KnowledgeGraph对象——节点(文档chunk)+ 边(实体关系)
在代码里,你不需要手动分别调这三步。TestsetGenerator.generate_with_langchain_docs内部会自动走完整个建图流程:
from ragas.testset import TestsetGenerator
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from openai import AsyncOpenAI
# 所有模型调用统一走OpenAI协议
client = AsyncOpenAI(base_url="http://your-api/v1", api_key="sk-xxx")
llm = llm_factory("gpt-4o-mini", client=client)
embeddings = embedding_factory("openai", model="text-embedding-3-small", client=client)
generator = TestsetGenerator(llm=llm, embedding_model=embeddings)
调 generate_with_langchain_docs时,Ragas先建图再出题,是一体完成的。但有个问题:**建图非常贵。**一批文档几十上百个chunk,每个都要调LLM做NER提实体,再两两算Jaccard相似度建边。跑一次可能几分钟+几千次LLM调用。
所以实践中必须把KG持久化。建好一次,存成pickle文件,后面换分布重新出题、或者换RAG系统重新评测,都直接加载这个pkl,不重复花钱。
· · ·
04Step 3:批量造题
知识图谱建好了,接下来从图上"长出"测试题。这对应上一篇讲的"换不同证人来问"——同一张证据墙,按不同题型策略遍历节点,生成不同难度的问题。
输入:KnowledgeGraph + QuerySynthesizer列表 + 生成数量
输出:testset.json,每条样本包含四个字段:
{
"user_input": "停在仓库门口的车主,和谁有过法律纠纷?",
"reference": "李强曾因合同纠纷起诉仓库老板张伟。",
"reference_contexts": ["监控录像显示...", "报案记录显示..."],
"query_type": "multi_hop_specific"
}
Ragas里有三种现成的QuerySynthesizer,对应三种出题策略:
from ragas.testset.synthesizers import (
SingleHopSpecificQuerySynthesizer, # 单张卡片出事实题
MultiHopSpecificQuerySynthesizer, # 多张卡片出事实题
MultiHopAbstractQuerySynthesizer, # 多张卡片出推理题
)
它们的区别在于怎么从图上选节点:
| Synthesizer | 选节点方式 | 对应上一篇的 |
|---|---|---|
| SingleHopSpecificQuerySynthesizer | 取单个Node | 单跳+具体(最简单,一张卡片一句话能答) |
| MultiHopSpecificQuerySynthesizer | 取一条边连接的两个Node | 多跳+具体(得关联两张卡片) |
| MultiHopAbstractQuerySynthesizer | 取一条边连接的两个Node | 多跳+抽象(关联+推理,最难) |
调用时通过 query_distribution参数控制比例。这个参数的类型是 List[Tuple[Synthesizer, float]]——一个synthesizer配一个权重:
query_distribution = [
(SingleHopSpecificQuerySynthesizer(llm=llm), 0.35),
(MultiHopSpecificQuerySynthesizer(llm=llm), 0.35),
(MultiHopAbstractQuerySynthesizer(llm=llm), 0.30),
]
testset = generator.generate_with_langchain_docs(
documents=documents,
testset_size=20,
query_distribution=query_distribution,
)
testset.samples就是生成的样本列表。每个sample有 user_input(问题)、reference(参考答案)、reference_contexts(出处原文)。
怎么给每条题打上类型标签
上面的调用方式有个问题——Ragas不会告诉你哪条样本是哪个Synthesizer生成的。解决办法是分批生成,每次只传一种Synthesizer:
QUERY_TYPE_MAP = {
"single_hop_specific": SingleHopSpecificQuerySynthesizer,
"multi_hop_specific": MultiHopSpecificQuerySynthesizer,
"multi_hop_abstract": MultiHopAbstractQuerySynthesizer,
}
all_samples = []
for query_type, synth_cls in QUERY_TYPE_MAP.items():
synthesizer = synth_cls(llm=llm)
testset = generator.generate_with_langchain_docs(
documents=documents,
testset_size=type_size,
query_distribution=[(synthesizer, 1.0)], # 一次只用一种
)
for sample in testset.samples:
all_samples.append({
"user_input": sample.user_input,
"reference": sample.reference,
"reference_contexts": sample.reference_contexts,
"query_type": query_type, # 打标签
})
这样每条测试题都带了 query_type标签,后面评分时就能按题型分组统计。
这是整条流水线最值钱的中间产物
testset.json生成一次,可以拿去测N个不同的RAG系统——切片策略不同、Embedding模型不同、检索参数不同、甚至完全不同的技术栈——让它们在同一组考题上打分,对比才有意义。
· · ·
05Step 4:RAG系统答题
有了考卷,接下来让"考生"上场答题。
输入:testset.json里的每一条 user_input
输出:每条问题对应两样东西——
-
response
:RAG系统给出的答案文本
-
retrieved_contexts
:RAG系统检索到的原文片段列表
这一步和Ragas无关——它完全取决于你的RAG系统长什么样。你的系统可能是Qdrant+OpenAI,可能是Elasticsearch+Qwen,可能是一个现成的平台API。
为了让评测工具不绑定特定实现,我们定义了一个抽象接口:
class RAGInterface(ABC):
@abstractmethod
async def query(self, question: str, top_k: int = 5) -> RAGResponse:
...
@dataclass
class RAGResponse:
answer: str # LLM生成的回答
retrieved_contexts: list[str] # 检索到的原文片段
不管你的RAG系统底层用什么,只要能实现这个接口——给一个问题,返回答案和检索上下文——就能接入评测。配套项目里实现了一个 QdrantRAGInterface(向量搜索Qdrant + LLM生成答案),作为参考实现。
· · ·
06Step 5:6指标打分
现在手里有了每条问题的完整四元组:

用Ragas的6个Metric逐一打分。回忆第一篇讲的:前三个评检索,中间两个评生成,最后一个评抗压。
输入:四元组
输出:6个分数
Ragas评分统一模式
所有指标的调用方式一致:初始化scorer → 调 .ascore()传入所需字段 → 拿 result.value:
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from ragas.metrics.collections import (
Faithfulness, AnswerRelevancy, ContextPrecision,
ContextRecall, ContextEntityRecall, NoiseSensitivity,
)
client = AsyncOpenAI(base_url="http://your-api/v1", api_key="sk-xxx")
llm = llm_factory("gpt-4o-mini", client=client)
embeddings = embedding_factory("openai", model="text-embedding-3-small", client=client)
每个指标吃什么字段
这里是关键——不是每个指标都需要四元组的全部字段。哪个指标需要什么,严格对应它考察的维度:
检索环节的三个指标:
# 上下文精确率:检索到的内容,相关的排前面了吗?
# 需要:question + reference + contexts
scorer = ContextPrecision(llm=llm)
result = await scorer.ascore(
user_input=question,
reference=reference, # 拿标准答案判断哪个context是相关的
retrieved_contexts=contexts,
)
# 上下文召回率:标准答案里的信息点,检索都覆盖了吗?
# 需要:question + reference + contexts
scorer = ContextRecall(llm=llm)
result = await scorer.ascore(
user_input=question,
retrieved_contexts=contexts,
reference=reference, # 把reference拆成claim,逐条检查context能否支撑
)
# 上下文实体召回:关键实体漏没漏?
# 需要:reference + contexts
scorer = ContextEntityRecall(llm=llm)
result = await scorer.ascore(
reference=reference, # 从reference提取实体集合
retrieved_contexts=contexts, # 从contexts提取实体集合,算交集比例
)
生成环节的两个指标:
# 忠实度:回答有没有瞎编?
# 需要:question + response + contexts(不需要reference!)
scorer = Faithfulness(llm=llm)
result = await scorer.ascore(
user_input=question,
response=response, # 把response拆成claim
retrieved_contexts=contexts, # 逐条检查claim能否被context支撑
)
# 回答相关性:回答跑题了吗?
# 需要:question + response(只比对意图,不需要context也不需要reference)
scorer = AnswerRelevancy(llm=llm, embeddings=embeddings)
result = await scorer.ascore(
user_input=question,
response=response, # 从response反推问题,算和原问题的相似度
)
抗压测试:
# 噪声敏感度:被无关内容带偏了吗?(越低越好)
# 需要:全部四元组
scorer = NoiseSensitivity(llm=llm)
result = await scorer.ascore(
user_input=question,
response=response,
reference=reference, # 判断哪些claim是"错的"
retrieved_contexts=contexts, # 判断错误是否源于噪声context
)
总结成一张表:
| 指标 | question | response | contexts | reference | 考察 |
|---|---|---|---|---|---|
| Context Precision | ✓ | ✓ | ✓ | 检索排序 | |
| Context Recall | ✓ | ✓ | ✓ | 检索覆盖 | |
| Context Entity Recall | ✓ | ✓ | 实体覆盖 | ||
| Faithfulness | ✓ | ✓ | ✓ | 不瞎编 | |
| Answer Relevancy | ✓ | ✓ | 不跑题 | ||
| Noise Sensitivity | ✓ | ✓ | ✓ | ✓ | 抗干扰 |
按题型聚合
逐条打完分后,按 query_type分组取均值,就能看到每种题型下各指标的表现:
# 分组
by_type = {}
for sample in results:
by_type.setdefault(sample.query_type, []).append(sample)
# 每组每指标取均值
for query_type, samples in by_type.items():
for metric in metrics:
scores = [s.scores[metric] for s in samples if s.scores[metric] >= 0]
avg = sum(scores) / len(scores)
这才是有诊断价值的结果。比如你可能看到:
- Single-Hop Specific:忠实度 0.92,召回率 0.85 → 基本功OK
- Multi-Hop Specific:忠实度 0.88,召回率 0.55→ 跨文档检索覆盖不够
- Multi-Hop Abstract:忠实度 0.72,噪声敏感度 0.35 → 复杂场景下容易被带偏
对应第一篇讲的诊断思路:召回低修检索(扩top-k、调chunk),忠实低修生成(调prompt),噪声高加过滤。现在你知道在哪种题型上修。
· · ·
07测试结果长什么样
跑完 rag-evaluator run-all后,evaluation_results.json是结构化数据,rag-evaluator report把它渲染成一份带图表的Markdown报告。下面是一份示例报告的样子(数据为模拟):
总览表
| Metric | Score |
|---|---|
| Answer Relevancy | 0.7800 |
| Context Precision | 0.9200 |
| Context Recall | 0.7100 |
| Entity Recall | 0.6500 |
| Faithfulness | 0.8500 |
| Noise Sensitivity | 0.2300 |
总览图

图2:总览指标柱状图
按题型对比表
| Metric | Multi-Hop Abstract | Multi-Hop Specific | Single-Hop Specific |
|---|---|---|---|
| Answer Relevancy | 0.6800 | 0.8000 | 0.8500 |
| Context Precision | 0.8500 | 0.9300 | 0.9500 |
| Context Recall | 0.5500 | 0.7500 | 0.8200 |
| Entity Recall | 0.5000 | 0.6800 | 0.7500 |
| Faithfulness | 0.7200 | 0.8800 | 0.9200 |
| Noise Sensitivity | 0.3500 | 0.2000 | 0.1500 |
各题型独立图
Single-Hop Specific(7 samples)——基本功表现:

图3:Single-Hop Specific 指标柱状图
Multi-Hop Specific(7 samples)——跨文档关联能力:

图4:Multi-Hop Specific 指标柱状图
Multi-Hop Abstract(6 samples)——复杂推理能力:

图5:Multi-Hop Abstract 指标柱状图
怎么读这份报告
三张图并排看,规律一目了然:
-
从左到右难度递增
,分数逐步下降——符合直觉,多跳抽象题本就是最难的。
-
Context Recall 下降最明显
(82% → 75% → 55%)——说明检索器在需要跨多个文档覆盖时力不从心,这是要优先修的环节。
-
Noise Sensitivity 上升最明显
(15% → 20% → 35%)——复杂场景下检索到的无关内容更多,系统被带偏的概率更高。
-
Context Precision 一直很高
(95%/93%/85%)——检索排序还行,排在前面的基本是相关的,问题不在排序而在覆盖。
对应的优化方向:扩大top-k(提升召回覆盖),加rerank(把多跳场景下更准确的上下文顶上去),在prompt里强调"只依据上下文回答"(降低噪声敏感度)。
· · ·
08中间产物:哪些可以复用
整条流水线最贵的是Step 2(建KG,大量NER调用)和Step 3(合成问题,大量LLM调用)。所以每一步都要持久化:
output/
├── testset.json # Step 3 产物(最重要)
└── artifacts/
├── documents.json # Step 1 产物
├── knowledge_graph.pkl # Step 2 产物
└── knowledge_graph_summary.json # Step 2 摘要
复用逻辑很简单:
| 你想做什么 | 复用哪个文件 | 省掉哪些步骤 |
|---|---|---|
| 同一份测试集,测多个RAG系统 | testset.json | Step 1-2-3全省 |
| 同一批文档,换比例重新出题 | knowledge_graph.pkl | Step 1-2省 |
| 同一批文档,什么都不复用 | 无 | 全部重跑 |
这也是为什么要分步设计——Step 3生成的测试集是"出题人"的产出,和"考生"(RAG系统)完全解耦。你可以让10个不同的RAG系统做同一份卷子,然后横向对比。
· · ·
09跑通闭环:完整命令
配套项目有两个工具,分工明确:
-
doc-indexer
:文档切片 → Embedding → 写入Qdrant(造一个RAG系统当"考生")
-
rag-evaluator
:出题 → 让考生答题 → 打分 → 出报告
# 1. 先造一个"考生"——文档入库到Qdrant
cd doc-indexer
uv run doc-indexer index -d ./my_docs -c my_collection \
--openai-base-url http://localhost:11434/v1 \
--embedding-model nomic-embed-text
# 2. 一键跑完:出题 + 答题 + 打分
cd ../rag-evaluator
uv run rag-evaluator run-all \
-d ./my_docs \
-c my_collection \
-o ./eval_output \
--openai-base-url http://localhost:11434/v1 \
--llm-model qwen2.5 \
-n 30
# 3. 生成可视化报告
uv run rag-evaluator report -r ./eval_output/evaluation_results.json
用已有测试集测另一个RAG系统:
# 直接用上次的testset.json,换一个RAG系统
uv run rag-evaluator run-all \
-d ./my_docs \
-c another_collection \
-o ./eval_output_B \
--reuse-testset ./eval_output/testset.json
· · ·
10总结:三篇文章,一条完整的线
| 核心思路一句话:让LLM既当出题人(Step 2-3的知识图谱+问题合成),又当评委(Step 5的6指标打分),用文档本身作为ground truth来源,实现无需人工标注的自动化评测闭环。 |
闭环跑通之后,"你的RAG做得怎么样"不再是一个主观感受,而是一组可量化、可对比、可追踪的数字。每次改了切片策略、换了Embedding模型、调了prompt——重跑一次闭环,看数字涨了还是跌了,就知道改对了没有。
代码在 github.com/quick-sort/hello_world/rag-eval-example,uv sync即可运行。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】

2537



被折叠的 条评论
为什么被折叠?



