rag/retrieval
📌 基本名词
检索增强生成(Retrieval Augmented Generation): 检索增强生成,将传统信息检索系统(如数据库)与生成式大语言模型进行结合,实现智能信息检索和生成。
切片(Chunking / Slicing): 将大规模数据集或复杂结构按特定规则分割为更小、更易处理的部分。
具体作用:
- 克服长度限制
- 提高检索精度,但需要度控制粒度: 切片太小可能丢失上下文,太大又可能引入噪音。
- 提供高密度、低噪音的有效信息,优化答案生成。
元数据(Metadata): “关于数据的数据”,用于描述数据的属性、结构、来源、用途等信息。它本身不包含数据的具体内容,而是提供数据的管理和检索依据。
向量化(Embedding): 把文字变成计算机能理解的一串数字向量。
召回(Recall): 从海量候选集中筛选出与目标相关的子集,作为后续排序或生成的输入。
透传(Transparent Transmission): 数据在传输过程中不进行任何处理或修改,保持原始格式和内容,如同“透明管道”。
📌 数据流
【离线阶段】
原始文档 → 切片 → 附加元数据 → Embedding → 向量数据库
↓
(记录位置/权限等)
【在线阶段】
用户问题 → Embedding → 向量检索 + 元数据过滤 (召回) → 透传原文 → LLM → 答案
离线建库阶段(把知识变成可检索的索引)
目标: 把原始文档加工成“向量 + 元数据”的结构化索引,存进数据库。
第1层: 切片 (Slicing)
- 输入: 原始文档(PDF、Word、Markdown、数据库记录等)
- 处理: 按固定长度(如512 token)或按语义边界(段落、句子)分割成块,相邻块保留重叠(如64 token)。同时记录每个切片在原文中的位置(页码、章节、偏移量)。
- 输出: 一个切片列表,每个切片包含文本内容 + 位置信息。
第2层: 元数据 (Metadata) 附加
- 输入: 上一步输出的切片,以及文档本身的属性(作者、创建时间、文档类型、部门、权限等级等)
- 处理: 为每个切片打上标签。例如:
- 文档级元数据:
doc_id=123,department=财务,security_level=机密 - 切片级元数据:
page=5,chunk_index=12,heading="合同违约责任" - 输出: 带有丰富元数据的切片对象。
注意: 元数据 不参与向量化,但在检索时可以用于 过滤(比如只查“财务部”的文档)。
第3层: Embedding(向量化)
- 输入: 每个切片的 文本内容(纯文字,不包括元数据标签)
- 处理: 调用 Embedding 模型(如
text-embedding-ada-002),把文本映射成一个固定维度的浮点数数组,比如[0.12, -0.45, 0.73, ...](常见维度: 768、1024、1536)。 - 输出: 每个切片的 向量。
第4层: 存储(向量数据库)
- 输入: 每个切片的向量、原始文本、元数据
- 处理: 存入向量数据库(如 Milvus、Pinecone、Elasticsearch)。数据库会为向量建立索引(如 HNSW),方便后续快速近似最近邻检索。
- 输出: 持久化的索引。
在线检索阶段(用户提问 → 召回相关信息)
第5层: 用户问题 Embedding
- 输入: 用户输入的问句(如“去年财务部的合同违约条款是怎么写的?”)
- 处理: 调用 同一个 Embedding 模型,把问句也变成一个向量(
query_vector)。 - 输出: 问句向量。
第6层: 召回 (Recall) —— 核心检索步骤
- 输入: 问句向量,以及可选的元数据过滤条件(如
department=财务+security_level<=机密) - 处理:
- 向量数据库收到
query_vector,通过向量相似度(余弦距离、点积)找到最相似的 Top-K 个切片向量(例如 K=20)。 - 同时应用 元数据过滤: 只考虑满足部门、时间、权限等条件的切片。这一步通常发生在向量检索之前或之后(取决于数据库支持,最好是前置过滤)。
- 输出: Top-K 个切片的 原始文本 + 对应的元数据。
召回的目标是 快且全,不要求完全精确,但要保证相关的内容都被捞上来。
第7层: 透传 (Transparent Transmission)
- 输入: 召回的 Top-K 切片文本(以及附带的元数据)
- 处理: 不做任何修改、总结、重写或格式化。只是把这些原始切片文本按顺序拼接成一个上下文块,可能加上分隔符,但内容一字不改。
- 输出: 干净的、可被大模型直接使用的上下文字符串。
透传的价值在于 保证事实无损。如果在这个环节对文本做了摘要或改写,可能会引入错误或丢失细节。
第8层: 大模型生成答案
- 输入: 用户原始问题 + 透传过来的上下文
- 处理: 组装成 prompt(例如:
基于以下资料回答问题: \n{上下文}\n问题: {用户问题}),调用 LLM。 - 输出: 最终的自然语言答案。
案例
一份 2024年财务合同.pdf,第8页有一句“违约方需支付合同金额20%的违约金”。
离线处理:
- 切片: 把这句单独或与邻近句子切在一起(chunk_123)。
- 元数据:
doc_id=789,department=财务,page=8,chunk_text="违约方需支付..."。 - Embedding: 计算这个文本的向量
[0.2, -0.5, ...]。 - 存储
在线问答:
- 用户问: “财务合同的违约金比例是多少?”
- 问题 Embedding → 向量检索。
- 召回: 找到
chunk_123(相似度最高),同时过滤department=财务。 - 透传: 把原文
"违约方需支付合同金额20%的违约金"原样传给 LLM。 - LLM 回答: “根据资料,违约金比例为合同金额的20%。”
常见问题/痛点
切分粒度怎么定?
没有绝对的标准,但有一个决策流程和经验范围。
1.主要影响因子
- 业务场景: 问答型(FAQ/错误码)用小粒度;摘要/综述型用大粒度。
- 文档类型: 结构化的手册、合同条款 → 按段落/章节切;非结构化的对话、邮件 → 按固定长度+重叠切。
- Embedding 模型的长度限制: 主流模型输入上限 256~8192 token,一般建议不超过限制的 75%。
- LLM 上下文窗口: 窗口越大,可以容忍更大粒度,但注意引入噪音。
2.经验起点(基于 token 数,用 cl100k 或近似)
| 场景 | 切片大小(token) | 重叠(token) | 说明 |
|---|---|---|---|
| 精确 Q&A(如错误码、定义) | 128~256 | 16~32 | 保证每个切片只含一个知识点 |
| 一般企业文档(合同、手册) | 512 | 64 | 最通用的平衡点 |
| 长篇幅叙述(报告、论文) | 768~1024 | 128 | 保留上下文,依赖 LLM 长上下文能力 |
实际粒度按字符数控制更直观: 512 token ≈ 300~400 中文字符;1024 token ≈ 600~800 中文字符。
3.确定粒度的具体步骤(实验驱动)
- 取样: 从真实文档中取 50~100 个不同片段的样本。
- 人工标注: 标注出理想的检索单元(一个完整答案/事实应该覆盖多长的文本)。
- 尝试 3~5 种粒度: 例如 256、512、1024,分别建索引。
- 用典型问题测试召回效果: 看是否能把“正确答案所在的片段”排在前 3~5 位。
- 选择召回率最高且不显著增加噪音的粒度。
召回内容不匹配怎么办?
“不匹配”分两类: 查不到(漏召) 和 查不准(低相关/噪音)。对策不同。
1.漏召(应该有的切片没被召回)
原因: 切片太小导致关键信息被拆散;Embedding 模型语义理解偏差;查询词与文档词汇不匹配(同义词、缩写)。
解决方案:
- 增大切片粒度或增大重叠,避免信息被截断。
- 混合检索: 结合 关键词检索(BM25) 和向量检索,然后融合(RRF 重排)。
- 查询改写: 用 LLM 把用户问题扩写成多个不同表述,分别检索后合并。
- 调整 Embedding 模型: 换用更适合企业领域(如法律、医疗)的微调模型。
2.低相关(召回了大量无关内容)
原因: 切片粒度太大,一个切片包含多主题;元数据过滤不足;相似度阈值太低。
解决方案:
- 缩小粒度,使每个切片语义更聚焦。
- 加强元数据过滤: 检索时先限定部门、时间、文档类型。
- 提高相似度阈值: 只保留余弦相似度 > 0.7 的结果(阈值需实测调优)。
- 重排序(Rerank): 召回 Top-K(如 50)后,用一个更强的 cross-encoder 模型重新打分,只保留 Top-N(如 5)。
语义丢失严重怎么办?
“语义丢失”指切片后,原本需要跨段落理解的信息被破坏了(比如“然而”“综上所述”这种转折关系被切断)。
根本原因
- 硬切分(按固定字符数)打断了连贯的语义单元。
- 重叠不足,导致指代词(“它”“这些”)的先行词被切到上一块。
解决方案
| 策略 | 具体做法 |
|---|---|
| 基于语义边界切分 | 按句子边界、段落边界、Markdown/HTML 标题切,而不是死按固定长度。使用 langchain 的 RecursiveCharacterTextSplitter,以段落、句号为优先分隔符。 |
| 增加重叠 | 重叠大小设为切片大小的 10%~25%,例如 512 token 切片重叠 64~128 token。保证上下文连接。 |
| 滑动窗口召回 | 检索时不仅返回匹配的切片,还返回其前后相邻的切片(例如各 1 个),还原上下文。 |
| 父子切片结构 | 把文档切成大块(父切片),再细切成小块(子切片)。检索时用子切片(精确),传给 LLM 时连带父切片(完整上下文)。 |
| 使用长上下文模型 | 如果 LLM 支持 128k token,可以放弃细粒度切片,直接把整段章节甚至整篇文档放入 context。但要注意成本与噪音。 |
📌 怎么测试
端到端和分模块测试
1.准备数据
- 从真实业务中收集 100~300 个问答对(用户问题 + 标准答案 + 答案所在的源文档及具体位置)。
- 目标至少覆盖: 简单事实查询、多跳推理、否定/边界条件、误导性问题。
2.模块化测试项
| 测试层级 | 测试内容 | 测试方法 | 通过标准 |
|---|---|---|---|
| 索引正确性 | 切片是否覆盖所有文档?元数据是否正确附着? | 抽样检查: 原始文档某句话,在库里是否能通过文档ID+位置准确找到对应切片。 | 抽样准确率 ≥ 99% |
| 召回测试 | 对于测试集的问题,Top-5 召回是否包含正确答案所在的切片? | 批量跑召回,计算 Recall@5(前5条中包含正确答案的比例)。 | ≥ 85% |
| 重排序测试(如果有 rerank) | 正确切片是否被提升到 Top-1 或 Top-3? | 计算 MRR(平均倒数排名)和 Hit@1。 | 根据业务要求,一般 Hit@1 ≥ 60% |
| 端到端答案正确性 | LLM 最终生成的答案是否与标准答案语义一致、无幻觉? | 人工评分或使用 LLM-as-Judge(如 GPT-4 对比打分)。打分维度: 相关、正确、完整、无害。 | 综合准确率 ≥ 80% |
| 延迟与稳定性 | 检索+生成的总耗时,以及高并发下的错误率。 | 压测工具,模拟 10~50 QPS。 | P99 < 5秒,错误率 < 1% |
| 安全与权限 | 用户 A 是否能看到用户 B 的私有文档? | 构造不同权限的测试账号,尝试通过提问越权访问。 | 无越权 |
3.测试人员实用脚本思路(伪代码)
# 批量测试召回
for question, expected_chunk_id in test_set:
results = retriever.retrieve(question, top_k=5)
if expected_chunk_id in [r.id for r in results]:
recall_hit += 1
recall_at_5 = recall_hit / len(test_set)
4.典型问题及测试 Checklist
- 同义词测试: “营收” vs “收入” → 应召回相同文档。
- 缩写测试: “LLM” vs “大语言模型” → 应能匹配。
- 多跳问题: “A 公司的 CEO 在 B 项目中的角色?” → 需要两个切片联合。
- 时间敏感: “去年的销售数据” → 元数据过滤年份。
- 拒答测试: 问不相关的问题(如天气),系统应回答“资料中无相关信息”,而不是瞎编。