1615 字
约 5 分钟
18
RAG检索增强生成
2026-10-13
无标签
RAG 检索增强生成
系列:ToyAgent 智能体知识库 07/26
一句话核心:模型不知道你的私有知识——先检索资料、再把资料塞进提示词、最后让生成带出处,三步动作把"瞎编"变成"有据可查"。
为什么需要
一个只靠参数记忆的模型有三块天然盲区:
- 知识过期:训练截止日之后发生的任何事,模型一无所知。你问它"上个月的发布会有什么内容",它要么沉默要么现编。
- 私有知识:公司内部文档、本项目代码细节、你个人整理的笔记——这些从未进入过任何模型的训练集。
- 幻觉:最危险的是前两种盲区叠加下的表现。模型不会说"我不知道",它会用极其流利自信的语气编一个答案出来。
RAG(Retrieval-Augmented Generation)的思路不是去"修"模型,而是在模型外面加一圈流程:Retrieve 检索 → Augment 增强 → Generate 生成。强制模型"戴着资料说话"。
核心机制
三步骨架用最小代码就能表达清楚:
// 1. 检索:给每条知识片段与问题的相关性打分,取 Top-N
List<Hit> hits = KNOWLEDGE.stream()
.map(text -> new Hit(text, score(text, input)))
.sorted(Comparator.comparingDouble(Hit::score).reversed())
.limit(2)
.toList();
// 2. 增强:把命中片段注入提示词,每条带来源编号
StringBuilder ctx = new StringBuilder("【参考资料】\n");
for (int i = 0; i < hits.size(); i++) {
ctx.append("[").append(i + 1).append("] ")
.append(hits.get(i).text()).append("\n");
}
// 3. 生成:用系统提示词约束模型——优先依据资料,不足则拒答
String answer = model.chat(List.of(
Message.system("优先依据【参考资料】回答并标注来源 [1];"
+ "资料不足时如实说不知道,禁止编造。"),
Message.user(ctx.toString()),
Message.user(input)));
三段里各自藏着一个容易做错的设计决策:
- 检索的核心是
score()。教学版可以用字符 bigram 重合率这种土办法,生产上换成 Embedding 余弦相似度,再往上还有重排序(rerank)、HyDE、GraphRAG。 - 增强要给片段编号来源,这是后面"回答带出处"的前提。
- 生成的约束提示词必须显式授权拒答。不写这条,模型在资料不足时依然会编。
三步骨架的完整流转:
graph LR
Q["用户问题"] --> R["检索 Retrieve"]
K["知识库/向量库"] --> R
R -->|Top-N 命中片段| A["增强 Augment"]
A -->|带来源编号的参考资料| G["生成 Generate"]
M["模型"] --> G
G -->|资料充足| ANS["带出处的回答"]
G -->|资料不足| REF["拒答:如实说不知道"]
图示:检索从知识库取 Top-N 片段,增强环节编号注入提示词,生成环节在资料不足时走拒答分支而非编造。
举例
模拟对话轨迹——知识库存三条手工条目:
KB[0]: RAG 的三步是检索、增强、生成。
KB[1]: 拒答是允许模型说"不知道"的能力。
KB[2]: 向量库常用 Milvus、ES、pgvector。
用户输入:向量库有哪些常见选择?
[检索] score(KB[0])=0.05 score(KB[1])=0.02 score(KB[2])=0.71
→ Top2 = [KB[2], KB[0]]
[增强] 系统提示词追加:
【参考资料】
[1] 向量库常用 Milvus、ES、pgvector。
[2] RAG 的三步是检索、增强、生成。
[生成] 模型输出:
常见的向量库有 Milvus、Elasticsearch 和 pgvector [1]。
其中 pgvector 适合已有 PostgreSQL 的团队……
反过来,用户输入:量子力学的泡利不相容原理是什么?
[检索] 三条 KB 全部低分,Top2 也不相关
[生成] 模型输出:
当前参考资料中没有覆盖量子力学的内容,我无法给出可靠回答。
第二次对话就是拒答。允许模型说"不知道",是 RAG 工程里最容易被忽略、却最重要的设计——宁可信度下降一点,也不要一个编造出来的答案。
LangChain4j 视角的同构:生产里把 score() 换成 EmbeddingStore 的 search(EmbeddingSearchRequest),把内存 List 换成向量库,其余两步原样保留:
List<EmbeddingMatch<TextSegment>> hits = store.search(
EmbeddingSearchRequest.builder()
.queryEmbedding(embeddingModel.embed(input).content())
.maxResults(2).build())
.matches();
教学版 vs 生产版
| 维度 | 教学版 | 生产实现 |
|---|---|---|
| 相似度 | 字符 bigram 重合率 | Embedding 余弦相似度 |
| 存储 | 内存 List | 向量库(Milvus / ES / pgvector) |
| 预处理 | 手工知识条目 | 文档切分 → 向量化 → 入库 |
| 进阶 | — | 重排序、HyDE、GraphRAG、Agentic RAG |
骨架不变论:把 score() 换成 Embedding、把 List 换成向量库,教学版立刻升级成生产级 RAG——三步骨架(检索→增强→生成)永不过时。变的只是每一步的实现质量。
面试考点
- RAG 三步分别解决什么? 检索解决"找到相关资料",增强解决"资料进入模型视野",生成约束解决"模型不得脱离资料编造"。少了第三步的拒答授权,前两步白做。
- 为什么检索结果要带来源编号? 让输出可溯源,用户能核对;同时给模型的引用行为一个明确锚点,降低"资料与记忆混写"的概率。
- RAG 一定比微调好吗? 各管一头:RAG 管知识新鲜度与私有事实、可随时换库;微调管风格与固定行为模式。事实性知识用 RAG,行为塑形用微调,二者不互斥。
生产化路径
- 切分策略:按标题/段落结构切而非定长硬切,保留标题路径作为 chunk 元数据(知识库类文章尤其重要,否则检索到的片段丢失上下文层级)。
- 混合检索:向量召回 + 关键词召回(BM25)双路并行,再 rerank 合并,能显著缓解专有名词检索失效。
- 拒答工程:除提示词约束外,加"检索分数阈值"硬门——Top1 分数低于阈值直接走拒答分支,不进模型。
- 引用校验:生产上可对生成结果做一次"来源核对"(模型自检或规则匹配 [n] 是否存在),防止模型挂了个不存在的引用编号。
- 缓存与增量:高频问题的检索结果与 embedding 做缓存;知识库更新走增量入库,避免全量重建。
评论
0 条
还没有评论,先写一条吧。