目录

Rag 实现问题

今天开始,我们来讨论 RAG 技术实现。目前为止,我对 RAG 理解仅局限于 RagFlow。在当下 AI 编程能力如此强的背景下,我个人觉得,如果我们能把一个技术所要解决的问题描述清楚,AI 完全能给我们提供一个生产可用的解决方案。

我个人认为提问题是 AI 时代下,最快的学习路径。最快能结合自己知识扩展知识的途径。对于新的框架、技术。你只要通过 AI 不断提问,直至形成你自己的知识闭环,那大概率你就已经掌握了这方面的知识。所以今天这篇文章,主要就是来梳理 RAG 技术实现中可能要解决的问题。

1. RAG 需要解决的技术问题

从用户的视角,Rag 首先需要对文档做分块、向量化,然后是读,最后是写。

用户关心的是文档的召回率和准确率。但是从技术角度,分块、读、写都有不同的问题需要解决。

1.1 分块

分块有很多方法,问题的核心是在速度和语义完整性之间的平衡。但是无论如何分块,分块最大的问题是破坏了文档的完整性和语义。后面我们结合写来看这个问题。

1.2 读

读取时

  1. 我们需要为 Rag 设计对应的工具,工具设计包括:
    • Tool Description
    • Input Schema
    • System Prompt
  2. 其次,需要对做好权限管控

1.3 写

更新时,我们也需要设计对应的工具,但是更新远比读要复杂的多的多:

  1. 首先我们要提供什么样的更新接口。文档有增加、删除、替换、全局替换等等操作,每个操作有可能是批量的
  2. 如何做版本控制,以支持回退
  3. 如何避免并发更新带来的冲突

由于文档是分块保存和更新的,在一个动态更新的 Rag 系统中,我们很难获取一份文档的统一视图。

显然分块的行为本身不符合人类查看和更新文档的习惯。

基本上我把我能想到的问题都列出来了。接下来我让 AI 帮我做了一个补充。下面就是基于 AI 做的总结。

2. AI 总结的问题

分类 必须维持的不变量
事实完整性 任意文档版本都有唯一、完整的事实表示
证据充分性 回答建立在相关且足够的文档证据上
变更正确性 每次写入都产生确定、原子、可回退的新版本
投影一致性 一次查询只读取同一个已就绪版本的派生数据
访问隔离性 未授权数据不会进入召回、上下文和输出
结果可验证性 每次回答和变更都可以解释、复现和评估

AI 总结了以上问题,并给出了核心原则: 文档是事实,Chunk 和向量只是由事实生成的检索投影。

1
2
3
4
事实源:原始文档 / 结构化文档
检索投影:Chunk / Embedding / 摘要 / 索引

版本控制和并发冲突的解决办法是: 更新发生在事实源,读取发生在检索投影。并给出了读写分离执行路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
                         ┌──────────────────────────┐
                         │       RAG Gateway        │
                         │ Auth / Rate Limit / Tool │
                         └────────────┬─────────────┘
              ┌───────────────────────┴───────────────────────┐
              │                                               │
       ┌──────▼──────┐                                 ┌──────▼──────┐
       │ Write Path  │                                 │  Read Path  │
       │ Patch / Tx  │                                 │ Query Router│
       └──────┬──────┘                                 └──────┬──────┘
              │                                               │
    ┌─────────▼──────────┐                     ┌───────────────▼──────────────┐
    │   Document Store   │                     │ Hybrid Retrieval            │
    │ Object + PostgreSQL│                     │ BM25 + Dense + RRF          │
    │ Version / AST / ACL│                     │ Filter + Rerank + Expansion │
    └─────────┬──────────┘                     └───────────────┬──────────────┘
              │ Outbox                                        │
       ┌──────▼───────┐                                ┌──────▼───────┐
       │ Event Queue  │                                │ Evidence Pack│
       └──────┬───────┘                                │ Citation     │
              │                                        └──────┬───────┘
    ┌─────────▼──────────┐                                    │
    │ Ingestion Workers  │                             ┌──────▼───────┐
    │ Parse / Chunk      │                             │     LLM      │
    │ Context / Embed    │                             │ Grounded Gen │
    └─────────┬──────────┘                             └──────────────┘
    ┌─────────▼──────────┐
    │ Search Projection  │
    │ Text + Vector Index│
    └────────────────────┘

核心组件保持明确分工:

组件 职责
Document Store 保存原始文件、结构树、版本、ACL 和审计记录
Write Service 生成 Patch、检查并发、提交文档事务
Outbox/Event Queue 可靠地把已提交版本交给索引管线
Ingestion Workers 解析、父子分块、上下文化和 Embedding
Search Projection 保存 BM25、Dense Vector 和检索元数据
Query Router 在 Fast、Planned、Graph 三种路径间选择
Retrieval Service 权限过滤、混合召回、融合、重排和上下文扩展
Evidence Service 去重、控制 Token、生成引用并判断证据是否充分

对于怎么提高召回和准确率的问题上,就是:

  1. 写放大: 保存 chunk 的父子关系和临近关系
  2. 读放大: 查询 chunk 时,连带读取 父、子、临近 chunk;甚至是 RAG‑Fusion 将一个查询变多条相似查询

虽然读写分离理论上可以解决前面提到的种种问题。但是这样一个多组件需要保持一致性的系统也带来了相当大的实现难度。通过连续的 AI 对话,我也没得到一个完美的解决方案。