RAG 不只是问答接口:一次知识库项目里的切分、检索与评测实践
为了解决上面的问题,我在项目里加了证据门禁。当前检索结果是否足够支持这个问题?我这里做的是一个学习版的证据门禁,主要通过问题中的关键字段和检索结果中的字段是否匹配,来判断证据是否足够。它不是最完美的方案,但能先解决“主题相关却没有答案”的误答问题。IP云服务器生产环境薪资内部公司那检索结果里也应该真的出现这些词。如果问题问的是生产环境 IP,但检索结果里完全没有 IP 或生产环境,就应该拒答。这一
RAG 不只是问答接口:一次知识库项目里的切分、检索与评测实践
一开始我以为 RAG 就是“上传文档,然后让大模型回答问题”。
真正做 X-RAG Agent 项目后我才发现,影响答案质量的往往不只是模型本身,而是文档怎么切、检索怎么做、证据够不够,以及有没有评测能证明效果真的变好了。
这篇文章不讲复杂理论,只记录我在项目里踩到的几个具体问题。它更像一份面向 RAG 初学者的项目复盘:从文档上传、chunk 切分、向量检索,到证据门禁和评测。
1. RAG 到底在解决什么问题
普通大模型有一个问题:它不一定知道我项目里的私有文档。
比如我问:
X-RAG Agent 支持哪些 RAG 评测指标?
这个答案在我的项目文档里,但大模型本身不一定知道。
RAG 的做法是:
先从知识库里找资料
再把找到的资料交给大模型
让大模型基于资料回答
所以 RAG 的完整流程不是直接问模型,而是:
用户提问
-> 检索知识库
-> 找到相关文本片段
-> 组织成 Prompt
-> 生成回答
-> 返回引用来源
这里有一个很重要的原则:如果知识库里没有依据,就不要硬答。
2. 文档为什么要切成 chunk
知识库文档通常比较长,不能每次都把整篇文档塞给模型。
所以需要先把文档切成很多小片段,这些小片段就叫 chunk。
例如一篇 Markdown 文档可以被切成:
chunk_0001:项目介绍
chunk_0002:系统架构
chunk_0003:接口说明
chunk_0004:评测指标
用户提问时,系统只需要找出最相关的几个 chunk。
这样做有几个好处:
- 检索更快。
- Prompt 不会太长。
- 回答可以追溯到具体来源。
- 不相关内容不会全部塞给模型。
我一开始用的是固定长度切分:
每 500 个字符切一段
相邻两段重叠 80 个字符
这个方法简单,但也有问题。
比如一个词:
keyword_hit_rate
可能被切成:
keywor
剩下的部分跑到下一个 chunk 里。
这会导致检索找到了正确文档,但回答里缺少完整关键词。
3. 相邻 chunk 补全解决了什么
固定长度切分会把内容切断。
比如一个表格刚好被切在两个 chunk 中间,检索只命中了前半段,答案就可能不完整。
所以我加了相邻 chunk 补全:
检索命中 chunk N
回答时额外带上 chunk N+1
这样如果答案被切断,后半部分也能进入上下文。
但这个方法不能无限使用。
如果补太多 chunk,会带来几个问题:
Prompt 变长
无关内容变多
模型更容易被干扰
调用成本增加
所以现在只补后一个 chunk,作为一个简单折中。
4. 为什么 Markdown 需要智能切分
Markdown 文档不是普通文本,它有结构。
常见结构包括:
标题
段落
列表
表格
代码块
如果完全按 500 个字符硬切,可能会切断这些结构。
所以我把 Markdown 切分改成了结构感知切分。
大致步骤是:
先识别标题、段落、表格、代码块
把它们拆成 block
再把 block 合并成 chunk
如果单个 block 太长,再兜底切分
这样做的好处是:chunk 更像一个完整的信息单元。
例如标题不会被随便塞到上一个 chunk 的末尾,代码块也尽量不被切开。
5. 表格太长时怎么办
Markdown 表格是我今天重点处理的问题,因为项目文档里有很多接口表、指标表和能力对比表。
短表格比较好处理,可以整体放进一个 chunk。
但如果表格很长,超过了 chunk_size,就必须拆开。
问题是:表格不能像普通段落一样随便按字符切。
例如:
| 指标 | 说明 |
| --- | --- |
| source_hit_rate | 检索结果是否命中预期文档 |
| keyword_hit_rate | 回答是否包含关键字 |
如果直接按字符硬切,可能会出现两类问题。
第一类是切断某一行:
| keyword_hit_rate | 回答是否包含
这一行已经不完整了,模型很难理解。
第二类是丢失表头。
比如某个 chunk 只剩:
| keyword_hit_rate | 回答是否包含关键字 |
模型可能不知道第一列是“指标”,第二列是“说明”。
所以长表格更适合按“行”来切,而不是按字符硬切。
我现在的做法是:
保留表头
保留分隔行
数据行按 chunk_size 分组
每个表格 chunk 都带表头
也就是说,一张长表格可以被拆成这样:
chunk 1:
| 指标 | 说明 |
| --- | --- |
| source_hit_rate | 检索结果是否命中预期文档 |
| keyword_hit_rate | 回答是否包含关键字 |
chunk 2:
| 指标 | 说明 |
| --- | --- |
| avg_latency_ms | 平均响应时间 |
| no_answer_accuracy | 无依据问题是否正确拒答 |
这样做有两个好处:
- 每一行数据都是完整的。
- 每个 chunk 都有表头,模型能知道每一列是什么意思。
这是一个很小的功能,但对结构化文档问答很重要。
6. 关键词检索和向量检索有什么区别
项目最开始使用的是关键词检索。
关键词检索很好理解:问题里的词是否出现在 chunk 里。
例如用户问:
RAG 评测建议统计哪些指标?
如果某个 chunk 里出现了:
source_hit_rate
keyword_hit_rate
avg_latency_ms
no_answer_accuracy
那它就应该排在前面。
关键词检索的问题是:如果用户表达方式和文档不一样,可能搜不到。
后来我接入了向量检索。
向量检索会先把问题和 chunk 都转换成 Embedding。
Embedding 可以简单理解成:把一段文字变成一组数字,这组数字表示这段文字的语义。
然后系统比较:
用户问题的向量
和
每个 chunk 的向量
哪个更接近,就说明哪个 chunk 更相关。
项目里用 Chroma 保存这些向量。
接入向量检索后,来源命中率明显提升。也就是说,系统更容易找到正确文档。
7. 为什么向量检索也会出错
向量检索不是万能的。
它会返回“最相似”的内容,但最相似不一定代表能回答。
项目里有一个无依据问题:
X-RAG Agent 当前生产环境部署在哪台云服务器 IP 上?
知识库里其实没有这个 IP。
但向量检索可能返回这些内容:
X-RAG Agent 是一个独立 AI 应用服务
X-RAG Agent 可以独立运行
项目二需要有自己的 README
这些内容和“X-RAG Agent”主题相关,但没有回答“云服务器 IP”。
如果系统直接把这些 chunk 当成依据,就会误答。
所以我学到一个很重要的点:
检索结果相关,不等于证据充分。
8. 什么是证据门禁
为了解决上面的问题,我在项目里加了证据门禁。
证据门禁就是在回答前先判断:
当前检索结果是否足够支持这个问题?
我这里做的是一个学习版的证据门禁,主要通过问题中的关键字段和检索结果中的字段是否匹配,来判断证据是否足够。它不是最完美的方案,但能先解决“主题相关却没有答案”的误答问题。
比如问题里包含这些精确信息:
IP
云服务器
生产环境
薪资
内部
公司
那检索结果里也应该真的出现这些词。
如果问题问的是生产环境 IP,但检索结果里完全没有 IP 或生产环境,就应该拒答。
这一步的作用是减少“看起来相关,但其实没有依据”的回答。
项目里加了证据门禁后,无依据拒答准确率提升到了 1.0。
9. sources 为什么不能让模型自己写
RAG 回答通常需要带引用来源。
比如:
来源:ai-internship-requirements.md,第 18 个 chunk
这里有一个坑:来源不能让模型自己生成。
原因很简单,模型可能编造来源。
所以项目里的做法是:
模型只负责回答内容
后端负责返回真实 sources
后端会根据检索结果返回:
chunk_id
doc_id
source
chunk_index
score
这样前端展示的来源才可信。
10. 为什么要做 RAG 评测
如果只手动问几个问题,很难判断系统到底有没有变好。
所以我做了一个简单评测集。
每条评测数据包含:
{
"question": "RAG 评测建议统计哪些指标?",
"should_answer": true,
"expected_keywords": ["source_hit_rate", "keyword_hit_rate"],
"expected_sources": ["ai-internship-requirements.md"]
}
评测脚本会做几件事:
读取问题
调用 /rag/ask
检查是否应该回答
检查答案是否包含关键词
检查来源是否命中文档
统计整体指标
生成 Markdown 报告
我目前统计的指标有:
case_pass_rate:整体通过率
source_hit_rate:来源命中率
keyword_hit_rate:关键词命中率
no_answer_accuracy:无依据拒答准确率
avg_latency_ms:平均耗时
这些指标可以帮助我判断每次优化有没有效果。
比如从关键词检索升级到向量检索后,来源命中率明显提升。
加证据门禁后,无依据拒答准确率提升。
优化 chunk 切分后,关键词命中率提升。
这比“感觉效果更好了”要可靠很多。
11. 重新解析后为什么要同步更新向量索引
这里需要先说清楚 SQLite 和 Chroma 的分工。
在我的项目里,SQLite 保存的是文档和 chunk 的业务数据。比如文档列表、解析状态、chunk 内容、chunk 顺序、文档删除记录等,都适合放在 SQLite 里管理。
Chroma 保存的是用于向量检索的索引。它会保存 chunk 对应的向量,也会带上一些 metadata,比如 doc_id、chunk_index、source。用户提问时,系统主要靠 Chroma 找到语义上最相关的 chunk。
所以它们虽然都和 chunk 有关,但职责不同:
SQLite:保存资料本身,方便管理知识库
Chroma:保存搜索索引,方便根据问题检索资料
可以把 SQLite 理解成“资料账本”,把 Chroma 理解成“搜索目录”。
问题出现在重新解析文档的时候。
如果我修改了 chunk 切分策略,SQLite 里会生成一批新 chunk。但如果 Chroma 里还保留着旧 chunk 的向量,RAG 检索时就可能搜到旧内容。
这时就会出现一种很隐蔽的问题:
后台管理看到的是新 chunk
向量检索命中的却是旧 chunk
所以重新解析文档后,不能只更新 SQLite,还要同步更新 Chroma。
完整流程应该是:
重新解析文档
删除 Chroma 里的旧向量
把新 chunks 重新做 Embedding
写入新的向量和 metadata
这一步通常就叫“重新构建 Chroma 索引”。它不是重新训练模型,而是把最新的 chunk 重新转换成向量,并更新到向量数据库里。
这样才能保证:
SQLite 管理的是最新资料
Chroma 检索的也是最新资料
12. 我现在对 RAG 的理解
做完今天这些功能后,我对 RAG 的理解变成了:
RAG 不是一个接口,而是一条链路。
这条链路包括:
文档上传
文档解析
chunk 切分
Embedding
向量索引
向量检索
证据判断
Prompt 构造
回答生成
来源引用
无依据拒答
效果评测
其中任何一环出问题,最后答案都可能出问题。
比如:
chunk 切坏了,检索可能找不完整
向量检索太宽松,可能误答无依据问题
sources 交给模型生成,可能出现假引用
没有评测,就不知道优化有没有效果
所以一个比较靠谱的 RAG 项目,不只是“能问答”,还要能解释:
资料从哪里来
为什么检索到这些 chunk
为什么可以回答
为什么有些问题要拒答
优化后指标有没有变化
13. 小结
这次项目让我对 RAG 有了一个更具体的认识。
RAG 的难点不只是“怎么调用模型”,而是怎么把资料处理好、怎么检索到合适内容、怎么判断有没有足够依据、怎么证明系统真的变好了。
如果只看最终回答,很容易忽略中间的问题。
比如:
检索结果可能主题相关,但没有真正答案
chunk 可能把表格或关键词切断
模型可能生成看起来合理但没有依据的回答
来源如果不由后端控制,就可能出现假引用
所以做 RAG 时,我现在会更关注这几个问题:
资料是怎么切的?
检索结果为什么是这些?
回答依据是否足够?
没有依据时能不能拒答?
优化前后有没有评测数据对比?
当我开始关注这些问题时,RAG 在我眼里就不再只是一个“知识库问答接口”了。它更像是一条需要持续调试的工程链路:资料处理、检索、证据判断、生成回答和效果评测,每一步都会影响最终答案。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)