工程主线RAG 不是“文档 → Embedding → 向量库”,而是 Source Contract → Faithful Parse → Retrieval Unit → Authorized Index → Change Propagation → Evidence。

RAG 从数据源契约开始

RAG 的原始研究把参数化模型与可检索的非参数知识结合,以改善知识密集型任务和来源更新问题,但 论文中的受控知识库不等于企业数据天然可用。接入前确认内容 Owner、权威范围、访问协议、更新频率、许可、敏感级别、删除要求和服务等级。一个 Wiki 页面可能只是讨论稿,一份 PDF 可能只对特定产品版本有效;若不在源契约中表达,检索分数会把它们伪装成同等事实。

每个源对象需要稳定 sourceId、version 或内容哈希、lastModified、ACL 与分类标签。URL 不是稳定主键,标题也不是版本。内容移动、重命名和重发必须能识别“同一对象的新版本”与“新对象”。

Why Not 全量抓取抓得到不代表有权索引、向模型发送或用于评测。先完成用途和访问契约,避免后期无法区分哪些 Chunk 必须删除。

采集与解析要保留文档结构

解析目标不是提取最多字符,而是保留能决定语义和引用的位置结构:标题层级、段落、列表、表格行列、代码文件/行号、页码、图片说明与脚注。解析器输出同时携带质量信号;无法可靠解析的文档进入隔离队列,不应静默生成空或乱码 Chunk。

不同格式需要不同解析策略

HTML、Markdown 与 Office

HTML 先识别正文与模板区域,删除导航、页眉和重复 Cookie 文案;Markdown 保留标题路径和代码围栏;Office 文档需处理批注、页眉和嵌套表格。直接转纯文本会丢失哪些单元格属于哪个列头。

PDF 与扫描件

PDF 是排版容器,不保证阅读顺序。区分可提取文本、布局解析与 OCR;保存页码、区域、旋转、OCR 置信和跨页表格关系。双栏文字若按坐标错误交错,后续 Embedding 再强也只会检索乱码。

表格与代码

表格至少把列头与行绑定,必要时同时保留结构化 JSON 和可读文本;代码按符号、文件和调用关系切分,并保留仓库版本。按固定字符切开函数签名和实现,会让查询命中却无法回答。

解析质量门禁示例
信号检测处置
空白/乱码可打印字符比例、语言异常重试替代解析器或隔离
顺序错误标题层级、阅读顺序抽样切换布局模型并保留页区域
重复模板跨页/跨文档高频块去噪但保留规则与版本
表格损坏列数漂移、表头缺失结构化专用路径,不入普通段落索引

Chunking 由查询和答案粒度决定

Chunk 是检索与引用单位,不是存储切片。先分析真实问题需要返回一句定义、一段步骤、整张表还是跨章节比较,再选择边界。固定长度容易实现但可能切断语义;递归字符切分保护段落;结构/语义切分更贴合内容,却依赖解析质量和模型。

Overlap 能缓解边界断裂,也会制造重复召回、Token 浪费和相同证据被多次计数。不要沿用“10% 或 20%”经验值作为事实,应在目标问题集上比较 Recall、重复率和答案质量。Microsoft 的 Azure AI Search 文档也把固定、句子/结构化切分列为不同策略,而非唯一最佳参数。

每个 Chunk 保存父文档、标题路径、前后邻接和字符/页码范围。Parent/Child 检索可以用小 Chunk 召回,再展开更完整父区域;摘要索引用于发现,最终回答仍引用原文范围。

索引前补充可过滤元数据

来源、作者、产品、版本、地域、语言、内容类型、有效期、租户、ACL 和数据分类应成为一等字段。自动抽取的实体和关键词标记提取器版本与置信,不能覆盖源系统字段。

type Chunk = {
  chunkId: string; sourceId: string; sourceVersion: string
  text: string; headingPath: string[]; locator: { page?: number; start: number; end: number }
  tenantId: string; aclPrincipals: string[]; classification: string
  parserVersion: string; chunkerVersion: string; embeddingVersion: string
  contentHash: string; indexedAt: string; validUntil?: string
}

// 查询前过滤是授权控制的一部分
const visible = (chunk: Chunk, actor: { tenantId: string; principals: Set<string> }) =>
  chunk.tenantId === actor.tenantId &&
  chunk.aclPrincipals.some(principal => actor.principals.has(principal))

索引服务不同召回路径

原始 Artifact 与搜索索引分离:对象存储保存可验证原文,索引保存检索投影和定位符。这样可以重建、迁移 Embedding,也能在引用时读取原文校验。

向量索引

Embedding 擅长语义近邻,但模型、维度、归一化和距离函数必须匹配并版本化。近似最近邻索引用速度换 Recall;例如 pgvector 官方文档明确说明 HNSW/IVFFlat 的候选参数和后过滤可能导致结果数下降。生产系统需用业务数据测速度—召回曲线。

倒排/BM25

BM25 对错误码、专有名词、版本、ID 和精确短语常有不可替代的价值。Elastic 文档把 BM25 作为默认全文相似度,并暴露词频饱和与文档长度归一化参数。它不是“旧技术兜底”,而是 Hybrid 的独立证据通道。

分区不是只为性能。语言、租户、数据域、地域或密级分区可以减少候选空间并强化隔离,但分区过细会让跨域问题漏召回。按安全硬边界先分区,按业务域做可路由索引,热度只作为性能层。

更新与删除必须端到端传播

采集任务使用源版本、内容哈希和幂等写入键实现可重放;它们只能识别重复输入,仍需数据库约束确保不会产生重复可见版本。更新时先产生新版本 Chunk,再原子切换可见版本并失效旧缓存;直接原地覆盖会在部分失败时留下新旧混合。Embedding 升级使用双索引回填、影子查询、指标对照和切换回滚,而不是停机全量重建。

删除与撤权优先级高于普通新鲜度。传播到 Chunk、Embedding、倒排/向量索引、摘要、缓存和 Eval 副本,并使用 Tombstone 阻止滞后的抓取任务复活内容。完成标志不是“消息已发出”,而是各下游返回可核验状态且查询不再命中。

增量索引的安全切换路径
检测变更源版本、ACL 或内容哈希变化。
新版本构建解析、Chunk、Embedding 与质量门禁。
影子验证覆盖率、权限、检索回归和引用定位。
切换/回收原子切换可见版本,旧版本延迟回收可回滚。

数据链路也需要 SLO

监控采集延迟、解析成功率、隔离率、索引新鲜度、Chunk 数量分布、重复率、ACL 缺失率和删除传播时间。从线上检索结果必须能回溯 source → parser → chunker → embedding → index 版本,才能把“回答变差”定位到数据还是查询策略。

数字指标不能替代抽样:定期检查 PDF 阅读顺序、表格、代码、标题路径、引用定位和不同语言。建立小型 Golden Query 集,在任何解析器、Chunk、Embedding 或索引变更时运行数据面回归。

主动提取:为什么 Chunk 越小不一定检索越准?
小 Chunk 可能提高局部匹配,却丢失限定条件、主语和表头;还会增加重复候选与组装成本。粒度必须由问题和答案证据范围共同验证。
故障定位:答案引用了旧政策,首先应该看哪些证据?
源系统版本与更新时间、采集水位、旧 Chunk 失效状态、索引可见版本、缓存键和最终引用的 sourceVersion;不要先改 Prompt。
架构证据:如何证明一次 Embedding 升级可安全迁移?
双索引与版本字段、同一 Golden Query 的影子对照、关键权限/长尾切片门槛、原子流量切换、旧索引保留窗口和明确回滚条件。

资料与事实边界

以下以官方文档、标准、论文和原始工程资料为主。厂商 API、价格、限额与模型能力会变化,落地时仍需按目标版本做能力探测和回归。

  1. Lewis et al.:Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks — RAG 结合参数化模型与显式非参数知识的原始工作
  2. Microsoft:Chunk large documents for RAG and vector search — 固定、重叠、结构化切分的当前工程实现与限制
  3. Microsoft:Chunk and vectorize by document layout — 布局保留、父子字段和结构化 Chunk 的工程参考
  4. Elasticsearch:Similarity settings — BM25 的当前实现参数与默认行为
  5. pgvector:Official repository — HNSW/IVFFlat、过滤、索引构建和速度—召回边界