RAG 知识库
RAG 是 Retrieval-Augmented Generation 的缩写,通常翻译为“检索增强生成”。
一句话理解:
先从资料库里查出相关内容,再把这些内容交给大模型,让模型基于资料回答。
它解决的是一个非常实际的问题:大模型本身不一定知道你的私有资料,也不一定知道最新内容。RAG 让模型在回答前先“看资料”。
为什么需要 RAG
大模型有几个天然限制:
- 不知道公司内部文档、个人笔记、私有数据库。
- 训练数据有时间截止,可能不知道最新内容。
- 可能生成看似合理但实际错误的回答。
- 上下文窗口有限,不能每次把所有文档都塞给模型。
RAG 的思路不是重新训练模型,而是把相关资料临时检索出来,作为上下文提供给模型。
一个生活化例子
假设你问:
这个项目里文件上传接口怎么使用?如果模型没有看过你的项目文档,它只能凭经验猜。但 RAG 会先做这些事:
- 从企业文档库中搜索“文件上传接口”相关内容。
- 找到最相近的几段文档。
- 把这些文档片段和你的问题一起发给模型。
- 要求模型只能根据这些资料回答。
这样回答会更接近项目真实情况。
RAG 的完整流程
RAG 通常分为“离线建库”和“在线问答”两条链路。
flowchart TD
subgraph Offline["离线建库流程"]
A["收集文档"] --> B["清洗文本"]
B --> C["切分 Chunk"]
C --> D["生成 Embedding"]
D --> E["写入向量数据库"]
end
subgraph Online["在线问答流程"]
F["用户提问"] --> G["问题向量化"]
G --> H["向量检索"]
H --> I["筛选相关片段"]
I --> J["拼接 Prompt"]
J --> K["调用大模型"]
K --> L["返回答案和引用"]
end离线流程负责把资料变成可检索的知识库,在线流程负责根据用户问题找资料并生成答案。
什么是 Chunk
Chunk 是文档切分后的片段。
例如一篇 5000 字文章不能直接当成一个整体检索,因为太长、主题太多、命中不精确。通常会切成多个小片段:
文章:Spring 事务详解
Chunk 1:事务的基本概念
Chunk 2:事务传播行为
Chunk 3:事务失效场景
Chunk 4:事务和数据库锁切分时要注意:
- 太短:上下文不够,回答容易断章取义。
- 太长:检索不精确,也浪费 Token。
- 最好保留标题、路径、段落层级等元数据。
什么是 Embedding
Embedding 是把文本转换成一串数字向量。
你不需要一开始理解数学细节,只要先记住:
语义相近的文本,向量距离通常更近。
例如:
“如何创建虚拟环境”
“Python venv 怎么用”这两句话字面不完全相同,但意思很接近,所以向量检索可以把它们匹配到一起。
什么是向量数据库
向量数据库用来存储和检索向量。它和普通数据库的区别是:
| 对比项 | 普通数据库 | 向量数据库 |
|---|---|---|
| 查询方式 | 精确匹配、条件过滤 | 相似度检索 |
| 常见查询 | where title = 'RAG' | 找和问题最相似的文本 |
| 适合数据 | 订单、用户、文章字段 | 文档片段、图片向量、音频向量 |
| 核心指标 | 条件、索引、事务 | 相似度、召回率、TopK |
RAG 里通常会同时用普通数据库和向量数据库:普通数据库保存文章、用户、权限等结构化数据,向量数据库保存文档片段的向量。
RAG Prompt 模板
RAG 的提示词必须明确要求模型基于资料回答。
你是一个技术文档问答助手。
请只根据“参考资料”回答用户问题。
如果参考资料中没有答案,请回答“资料中未提到”,不要编造。
参考资料:
{retrieved_chunks}
用户问题:
{question}
输出要求:
1. 先直接回答问题。
2. 再列出依据来自哪些资料。
3. 如果资料不足,明确说明缺少什么。这个模板的关键是“只根据参考资料回答”。如果不写这句话,模型可能会把自己的通用知识混进来。
RAG 在企业知识库里的落地方式
企业知识库非常适合做 RAG,因为制度文档、接口文档、产品手册、运维手册、FAQ、合同模板都可以作为知识来源。真正商用时,重点不是“能检索”,而是权限、版本、引用、拒答和审计。
可以按下面步骤落地:
flowchart TD
A["导入企业文档"] --> B["解析标题、正文、表格"]
B --> C["按语义和标题切分片段"]
C --> D["保存权限、版本、来源"]
D --> E["调用 Embedding 模型"]
E --> F["写入向量库"]
G["员工或客户提问"] --> H["按权限检索相关片段"]
H --> I["拼接片段、问题和引用要求"]
I --> J["调用聊天模型"]
J --> K["返回回答、引用和拒答原因"]建议保存的元数据:
| 字段 | 说明 |
|---|---|
docId | 文档唯一标识 |
title | 文档标题 |
path | 文档访问路径 |
heading | 当前片段所在标题 |
chunkIndex | 片段序号 |
content | 原始文本片段 |
updatedAt | 文档更新时间 |
permissionTags | 部门、角色、密级等权限标签 |
有了这些元数据,回答时就能告诉用户“答案来自哪份文档哪一段”,也能避免用户检索到没有权限访问的资料。
关键参数
TopK
TopK 表示检索最相似的前几个片段。
TopK 太小可能漏掉答案,TopK 太大可能塞入无关内容。入门可以从 3 到 5 开始。
更准确地说,TopK 不是“越大越保险”。因为模型会把检索结果都当作参考资料阅读,如果里面混入很多相似但无关的片段,模型可能被噪声带偏。
| TopK 设置 | 可能效果 | 适合场景 |
|---|---|---|
| 太小,例如 1 到 2 | 速度快、成本低,但容易漏掉关键上下文 | FAQ、标准答案很短 |
| 中等,例如 3 到 8 | 准确率和成本比较平衡 | 企业知识库常用起点 |
| 太大,例如 20 以上 | 召回更多,但 Prompt 更长、噪声更多 | 需要配合 rerank、摘要和引用校验 |
生产中常见做法是“两段式”:第一段召回更多候选,例如 TopK=30;第二段通过 Rerank 选出最相关的 3 到 8 个片段进入 Prompt。
相似度阈值
如果最相似的片段分数也很低,说明知识库里可能没有答案。此时不要强行回答,应该提示资料不足。
相似度阈值不是固定真理,它和 Embedding 模型、向量库距离算法、文本长度都有关系。上线前要拿一批真实问题测试,观察“正确召回”和“错误召回”的分数分布,再设置阈值。
flowchart TD
A["收集真实问题和标准答案"] --> B["执行向量检索"]
B --> C["标注召回片段是否正确"]
C --> D["观察正确片段和错误片段分数分布"]
D --> E["设置初始阈值"]
E --> F["上线灰度并记录拒答率和误答率"]
F --> G["持续调参"]如果阈值太低,系统会用不相关资料硬答;如果阈值太高,明明知识库有答案也会拒答。好的 RAG 不是永远回答,而是在没有可靠资料时敢于拒答。
Chunk 大小
常见做法是按段落或标题切分。入门阶段可以先按 Markdown 标题切,再根据长度合并或拆分。
Chunk 大小要看资料类型:
| 资料类型 | 推荐切法 | 原因 |
|---|---|---|
| 技术文档 | 按标题层级切,保留标题路径 | 标题就是语义边界 |
| 数据字典 | 一张表或一组字段为单位 | 字段含义离不开表名和系统名 |
| 合同制度 | 按条款切,保留章节号 | 用户常问某一条款含义 |
| FAQ | 一个问答对一个 Chunk | 本身就是最小知识单元 |
| 日志排查手册 | 按错误码或故障现象切 | 查询时通常带错误码或现象 |
切分时最重要的是“不要把必须一起理解的内容拆散”。例如字段说明里只保存“patient_id 是患者唯一标识”,但没有保存“所属系统是 HIS、敏感等级是高、仅信息科可见”,上线后就可能出现回答不完整或越权。
RAG 原理深入:为什么它能降低幻觉
大模型的回答来自它训练时学到的参数和当前 Prompt 中的上下文。没有 RAG 时,模型只能凭内部参数回答;如果它不知道企业私有资料,就可能用通用经验补全,形成幻觉。
RAG 的本质是把“外部资料”在推理时注入上下文,让模型从“凭记忆回答”变成“开卷回答”。
flowchart TD
A["用户问题"] --> B{"是否使用 RAG"}
B -- "不使用" --> C["模型根据参数记忆生成"]
C --> D["可能混入过时知识或编造"]
B -- "使用" --> E["检索企业资料"]
E --> F["把资料放进上下文"]
F --> G["模型基于资料组织语言"]
G --> H["答案可附引用并可验证"]但要注意:RAG 只能降低幻觉,不能天然消灭幻觉。原因有三个:
- 检索可能错:正确资料没召回,模型看到的是无关资料。
- 资料可能错:知识库本身过期、冲突或解析错误。
- 生成可能偏离资料:Prompt 约束弱,模型仍然自由发挥。
所以 RAG 的核心能力不是“把资料塞进去”,而是“检索正确资料、过滤无权资料、让模型严格基于资料回答、用引用校验答案”。
离线建库流程详解
离线建库决定了 RAG 的知识质量。不要把它理解成一次性脚本,它更像一个数据管道:资料会更新、权限会变化、Embedding 模型会升级、文档会删除。
flowchart TD
A["数据源:文档、数据库、接口、知识库"] --> B["解析文本和结构"]
B --> C["清洗:去页眉页脚、空行、噪声"]
C --> D["脱敏:手机号、身份证、患者标识"]
D --> E["按语义切分 Chunk"]
E --> F["补元数据:来源、权限、版本、标题路径"]
F --> G["生成 Embedding"]
G --> H["写入向量库"]
F --> I["写入结构化数据库"]
H --> J["记录入库批次和质量报告"]
I --> J每一步为什么重要:
| 步骤 | 作用 | 不做好会怎样 |
|---|---|---|
| 解析 | 把 PDF、Word、Excel、网页转成文本和结构 | 表格错位、字段丢失、知识源错误 |
| 清洗 | 去掉页码、目录、重复页眉、无意义符号 | 检索到大量噪声,回答含糊 |
| 脱敏 | 移除或替换敏感个人信息 | 敏感数据进入向量库和模型上下文 |
| 切分 | 控制知识片段粒度 | 太碎丢上下文,太大召回不准 |
| 元数据 | 支持权限、引用、版本、过滤 | 无法审计来源,无法防越权 |
| Embedding | 把文本变成可相似度检索的向量 | 模型选错会导致语义召回差 |
| 质量报告 | 记录失败文档、空 Chunk、异常长度 | 上线后才发现大量资料没入库 |
一个商业系统至少要记录入库批次:
CREATE TABLE rag_ingest_batch (
id BIGINT PRIMARY KEY,
source_type VARCHAR(50) NOT NULL,
source_name VARCHAR(200) NOT NULL,
status VARCHAR(30) NOT NULL,
total_docs INT NOT NULL,
success_docs INT NOT NULL,
failed_docs INT NOT NULL,
total_chunks INT NOT NULL,
embedding_model VARCHAR(100) NOT NULL,
started_at TIMESTAMP NOT NULL,
finished_at TIMESTAMP NULL
);没有批次记录,文档入库失败时你很难回答:“哪些文档没有进知识库?当前答案用的是哪一版资料?换 Embedding 模型后有没有全量重建?”
在线问答流程详解
在线链路决定了用户每次提问如何被处理。一个生产级 RAG 不应该直接“问题向量化 -> 搜索 -> 拼 Prompt”,还要考虑多轮对话、权限、拒答、引用和日志。
flowchart TD
A["用户提问"] --> B["身份认证和租户识别"]
B --> C["问题清洗和长度校验"]
C --> D["多轮问题改写"]
D --> E["抽取实体:系统、表、字段、错误码"]
E --> F["生成权限过滤条件"]
F --> G["混合检索:向量 + 关键词"]
G --> H["候选去重和 Rerank"]
H --> I{"是否达到相似度阈值"}
I -- "否" --> J["拒答或要求补充问题"]
I -- "是" --> K["组装上下文和引用"]
K --> L["调用模型生成答案"]
L --> M["校验答案是否被引用支持"]
M --> N["返回答案、引用、耗时"]
N --> O["记录检索、模型、Token、反馈日志"]多轮问题为什么要改写
用户可能这样问:
第一轮:LIS 系统有哪些核心资产?
第二轮:这些资产里哪些包含患者标识?第二轮的“这些资产”依赖第一轮上下文。如果直接拿“这些资产里哪些包含患者标识?”去检索,向量库不知道“这些”指什么。问题改写要把它变成:
LIS 系统核心数据资产中,哪些资产包含患者标识字段?改写不是让模型自由发挥,而是把省略的上下文补全,方便检索。
权限为什么要在检索前做
错误做法:
全库检索 -> 召回敏感 Chunk -> 放进 Prompt -> 告诉模型不要泄露这已经晚了,因为敏感内容已经进入模型上下文。正确做法是把权限变成检索过滤条件:
用户身份 -> 部门/角色/租户/密级 -> 向量库 metadata filter -> 只召回可见 Chunk权限过滤最好能下推到向量库或检索服务里,而不是在应用层拿到全量结果后再过滤。应用层后过滤会浪费资源,也容易因为日志、调试输出、异常堆栈造成泄露。
混合检索和 Rerank
只用向量检索容易漏掉精确关键词,只用关键词检索又不能理解语义。生产 RAG 常用混合检索。
| 检索方式 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 向量检索 | 理解语义,相似表达也能召回 | 字段名、错误码、编号不一定准 | 概念、制度、说明文档 |
| 关键词/BM25 | 精确匹配字段、接口、错误码 | 同义表达召回差 | 数据字典、接口文档、日志手册 |
| 元数据过滤 | 支持权限、系统、版本、类型筛选 | 依赖元数据质量 | 企业多部门、多租户知识库 |
| Rerank | 对候选结果二次排序,提高前几条质量 | 增加延迟和成本 | 候选多、概念相似、答案要求高 |
混合检索的核心不是把结果简单相加,而是合并、去重、按业务规则加权:
def merge_candidates(vector_hits: list[dict], keyword_hits: list[dict]) -> list[dict]:
merged: dict[str, dict] = {}
for hit in vector_hits:
item = merged.setdefault(hit["chunk_id"], hit.copy())
item["vector_score"] = hit["score"]
for hit in keyword_hits:
item = merged.setdefault(hit["chunk_id"], hit.copy())
item["keyword_score"] = hit["score"]
for item in merged.values():
vector_score = item.get("vector_score", 0)
keyword_score = item.get("keyword_score", 0)
title_boost = 0.1 if item.get("matched_title") else 0
item["final_score"] = vector_score * 0.6 + keyword_score * 0.3 + title_boost
return sorted(merged.values(), key=lambda x: x["final_score"], reverse=True)真实项目里还可以把“文档版本更新、标题命中、字段名完全匹配、用户所在部门文档”等因素加入排序。
引用和答案校验
RAG 回答必须带引用,不是为了好看,而是为了验证。
一个靠谱的 RAG 答案至少包含:
- 直接答案。
- 依据来自哪些文档和片段。
- 如果资料不足,明确拒答。
- 如果资料冲突,指出冲突来源。
错误回答:
LIS 系统包含患者基本信息、检验结果和费用信息。问题:用户不知道依据是什么,也不知道是不是模型编的。
更好的回答:
根据《LIS 数据资产目录》第 2.1 节,LIS 系统核心资产包括检验申请、检验结果、样本信息和报告信息。其中检验结果资产包含 patient_id 字段,因此属于敏感资产。[引用:doc-lis-asset-2.1]如果引用片段里根本没有“patient_id 属于敏感资产”,后处理就应该拦截或标记低置信度。
生产日志和排查
RAG 线上出问题时,不能只看最终答案。必须把检索链路记录下来。
| 日志字段 | 为什么要记录 |
|---|---|
requestId | 串起一次请求 |
userId、tenantId、roles | 排查权限问题 |
originalQuestion | 用户原始问题 |
rewrittenQuestion | 多轮改写后用于检索的问题 |
permissionFilter | 本次检索实际使用的过滤条件 |
vectorTopK、keywordTopK | 判断召回参数 |
retrievedChunkIds | 看是否召回正确资料 |
rerankScores | 判断排序是否合理 |
promptVersion | 排查 Prompt 改动影响 |
model、embeddingModel | 排查模型版本影响 |
inputTokens、outputTokens | 成本和截断排查 |
latencyMs | 性能排查 |
answer、references | 判断答案是否被资料支持 |
排查路径:
flowchart TD
A["用户反馈 RAG 答错"] --> B["根据 requestId 查日志"]
B --> C{"正确文档是否存在"}
C -- "不存在" --> D["补充或同步知识库"]
C -- "存在" --> E{"是否召回正确 Chunk"}
E -- "否" --> F["检查切分、Embedding、关键词、TopK"]
E -- "是" --> G{"Rerank 是否排到前面"}
G -- "否" --> H["调整重排和业务加权"]
G -- "是" --> I{"Prompt 是否严格基于资料"}
I -- "否" --> J["收紧 Prompt 和拒答规则"]
I -- "是" --> K["检查资料冲突、引用校验和模型输出"]商业 Demo:医疗数据资产 RAG
下面是一个不依赖真实向量库的简化 Demo,用于理解“权限过滤、混合召回、引用输出”的结构。真实项目中可以把 vector_search 换成 Milvus、Elasticsearch、pgvector、Chroma 等检索服务。
from dataclasses import dataclass
@dataclass
class User:
user_id: str
tenant_id: str
department: str
roles: set[str]
@dataclass
class Chunk:
chunk_id: str
title: str
content: str
tenant_id: str
permission_tags: set[str]
vector_score: float
keyword_score: float
def build_permission_tags(user: User) -> set[str]:
return {f"dept:{user.department}", *(f"role:{role}" for role in user.roles)}
def can_read(user: User, chunk: Chunk) -> bool:
if user.tenant_id != chunk.tenant_id:
return False
return bool(build_permission_tags(user) & chunk.permission_tags)
def retrieve(question: str, user: User, chunks: list[Chunk]) -> list[Chunk]:
allowed = [chunk for chunk in chunks if can_read(user, chunk)]
for chunk in allowed:
exact_hit = 1.0 if any(word in chunk.content for word in question.split()) else 0.0
chunk.keyword_score = max(chunk.keyword_score, exact_hit)
return sorted(
allowed,
key=lambda item: item.vector_score * 0.6 + item.keyword_score * 0.4,
reverse=True,
)[:3]
def build_rag_prompt(question: str, chunks: list[Chunk]) -> str:
if not chunks:
return "当前资料不足或你无权访问相关资料,请拒答。"
refs = "\n\n".join(
f"[{index}] {chunk.title}\n{chunk.content}"
for index, chunk in enumerate(chunks, start=1)
)
return f"""
你是医疗数据资产平台助手。
只能基于参考资料回答,不能编造。
如果资料不足,请回答“当前资料不足,无法确认”。
回答最后必须列出引用编号。
用户问题:{question}
参考资料:
{refs}
""".strip()
chunks = [
Chunk(
chunk_id="c1",
title="LIS 检验结果资产",
content="LIS 检验结果资产包含 patient_id、report_id、result_value,其中 patient_id 属于高敏字段。",
tenant_id="hospital-a",
permission_tags={"dept:信息科", "role:data_admin"},
vector_score=0.91,
keyword_score=0.0,
),
Chunk(
chunk_id="c2",
title="公开数据资产说明",
content="公开数据资产不包含患者身份标识,可用于普通业务统计。",
tenant_id="hospital-a",
permission_tags={"role:employee"},
vector_score=0.72,
keyword_score=0.0,
),
]
user = User("u1001", "hospital-a", "业务科", {"employee"})
hits = retrieve("patient_id 是什么字段", user, chunks)
prompt = build_rag_prompt("patient_id 是什么字段", hits)
print(prompt)这个 Demo 里,普通业务科员工不会拿到 dept:信息科 和 role:data_admin 才能访问的敏感片段。也就是说,安全不是靠“模型不要说”,而是靠检索阶段就不给它看。
面试标准回答
RAG 是什么?
RAG 是检索增强生成。它先根据用户问题从企业知识库检索相关片段,再把片段和问题一起交给大模型,让模型基于资料回答。它适合解决模型不知道私有知识、知识过时、回答需要引用来源的问题。
RAG 为什么能降低幻觉?
因为它把外部可信资料放进模型上下文,让模型从“凭参数记忆回答”变成“基于资料回答”。但 RAG 不能天然消灭幻觉,检索错、资料错、Prompt 约束弱都会导致错误,所以还要做引用、拒答、评估和日志排查。
RAG 离线和在线流程是什么?
离线流程是文档解析、清洗、脱敏、切分 Chunk、补元数据、生成 Embedding、写入向量库;在线流程是用户提问、问题改写、权限过滤、混合检索、Rerank、组装 Prompt、模型生成、引用校验和日志记录。
为什么 RAG 要做权限过滤?
因为向量检索可能召回用户无权访问的敏感资料。权限过滤必须发生在检索前或检索过程中,最好下推到向量库 metadata filter。不能先全库检索再要求模型不要泄露,因为敏感内容已经进入上下文。
为什么只用向量检索不够?
向量检索擅长语义相似,但表名、字段名、接口名、错误码等精确实体可能召回不稳定。商业 RAG 常用向量检索 + 关键词/BM25 + 元数据过滤 + Rerank,提高召回和排序质量。
常见问题
检索不到答案
可能原因:
- 文档没有入库。
- Chunk 切得太碎,语义不完整。
- 用户问题和文档表达差异太大。
- Embedding 模型效果不适合当前语言或领域。
- TopK 太小。
检索到了无关内容
可能原因:
- Chunk 太长,包含多个主题。
- 文档标题、路径等元数据没有参与排序。
- 只做向量检索,没有关键词过滤。
- 没有做重排 rerank。
模型仍然胡说
可能原因:
- Prompt 没限制只能根据资料回答。
- 检索片段本身不相关。
- 资料里有互相矛盾的内容。
- 没要求模型引用来源。
代码 Demo:最小 RAG 问答
下面示例用关键词检索模拟 RAG 流程,真实项目可以把 retrieve 换成向量数据库检索。
DOCS = [
{
"title": "文件上传接口",
"path": "/file/server/fileManage/upload",
"content": "文件上传接口使用 multipart/form-data,请求字段名为 file。",
},
{
"title": "Python 虚拟环境",
"path": "/python/environment",
"content": "Python 可以使用 python -m venv .venv 创建虚拟环境。",
},
]
def retrieve(question: str) -> list[dict]:
return [doc for doc in DOCS if any(word in doc["content"] for word in question.split())]
def build_prompt(question: str, chunks: list[dict]) -> str:
references = "\n".join(
f"- 标题:{doc['title']}\n 路径:{doc['path']}\n 内容:{doc['content']}"
for doc in chunks
)
return f"""你是技术文档问答助手。
请只根据参考资料回答,资料不足就说“资料中未提到”。
参考资料:
{references}
用户问题:
{question}
"""
def rag_answer(question: str) -> str:
chunks = retrieve(question)
prompt = build_prompt(question, chunks)
return call_llm(prompt)这个 Demo 体现了 RAG 的骨架:检索资料、组装 Prompt、调用模型。后续再替换成 Embedding、向量库和重排模型。
学习练习
- 选一篇 Markdown 文章,手动切成 3 个 Chunk。
- 给每个 Chunk 写上
title、path、heading元数据。 - 写一个 RAG Prompt,要求模型只能根据这 3 个 Chunk 回答。
- 故意问一个资料里没有的问题,观察模型是否会编造。
- 调整 Prompt,让模型在资料不足时明确拒答。
本章小结
RAG 的核心不是“让模型变聪明”,而是“让模型在回答前看到正确资料”。零基础学习时先抓住三件事:文档切分、向量检索、基于资料生成。只要这三步稳定,知识库问答就有了基础。
