首页 / 文章

为1亿份文档设计RAG:分片、混合搜索与成本控制

当RAG系统的块数增长到数十亿时,会发生哪些变化:异步导入、路由分配、分片处理、混合检索方式、重排序机制、权限控制、缓存策略以及成本管控。

发布者:REACTAPP.TOP阅读约 21 分钟4744 词

来源: Designing RAG for 100 Million Documents

运行一个RAG演示只需一个下午的时间。你将PDF文件拆分成多个片段,将其嵌入系统,把向量存储到数据库中,检索出距离最近的五个向量,将它们粘贴到提示词中并调用模型。

对于几千份文档来说,这种方法完全可行。但一旦语料库规模增长到1亿份文档,这种方法就会失效,此时需要解决的问题也发生了变化。问题不再仅仅是“该选择哪种向量数据库?”,而是:如何在不影响响应速度、检索质量、系统可用性或预算成本的前提下,从数十亿条候选信息中筛选出10到20条真正重要的内容?

这完全是另一个层面的工程难题。在如此大规模的语料库中,RAG实际上是一个分布式的数据与搜索系统,仅在末端附加了语言模型。

系统需要完成的任务

为使讨论更具针对性,先假设以下要求:

  • 超过1亿份文档,格式包括PDF、HTML和DOCX
  • 数百万用户以自然语言提出问题
  • 每个答案都附带引用信息
  • 文档会随着时间被更新或删除
  • 检索速度要快,从开始生成结果到完成大约需1到3秒
  • 系统需具备高可用性
  • 文档导入过程为异步执行

所有要求归结为一个核心挑战:**在潜在的数十亿个数据片段中找出最合适的10到50条信息。**

从数学计算入手

在大型RAG系统设计中,第一个常见错误就是将文档与向量视为同一事物。请注意:

Documents              = 100,000,000
Average chunks/document = 30
Embedding dimensions    = 768
Storage/dimension       = 4 bytes (float32)

此时的向量数量为:

100,000,000 × 30
= 3,000,000,000 vectors

30亿个向量。它们所需的原始浮点数存储空间大约为:

3B × 768 × 4 bytes
≈ 9.2 TB

而这还只是数字本身。实际部署还包括:

ANN index structures
chunk text
document metadata
ACL metadata
IDs
lexical indexes
replicas
snapshots
WALs
temporary indexes
operational headroom

由于有两组向量索引副本,仅在原始向量数据方面就已超过18 TB,这还不包括搜索结构所占的容量。如果每份文档平均包含50个数据块而非30个的话:

100M documents × 50 chunks
= 5 billion vectors

这就是为什么“我们会把所有数据块放入向量数据库并执行相似度搜索”这种描述无法体现大规模架构的实质。它只是说明了起步点而已。

两个相互独立的层面

组织系统的最清晰方式是将其划分为两个独立的层面:一个用于数据导入,另一个用于数据检索。这种分离非常重要。如果客户上传2000万份新文档,查询延迟不应从300毫秒上升到8秒。数据导入环节需注重处理速度,而数据检索环节则需控制延迟,若强制让两者通过同一流程处理,只会为日后带来运营上的麻烦。

向量数据库并非唯一真实数据源

将原始文档存储在诸如 S3、GCS 或 Azure Blob 这类廉价的对象存储服务中。向量数据库仅是索引,而索引是可以重建的。如果六个月后您从嵌入模型 E1 更换为 E2,应从存储的原始数据重新生成向量,而非试图直接从向量存储中还原内容。

对象版本控制也能起到帮助作用,因为更新内容可以被记录为新版本,而无需删除旧版本。例如,Amazon S3 可以保留一个对象的多个版本,同时通过生命周期策略将旧版本移至更便宜的存储层级或使其过期。

数据导入必须为异步方式

假设有一名客户上传了500,000份PDF文件。你肯定不希望出现这样的情况:先收到一个POST /documents请求,该请求需要解析文件、执行OCR识别、清理数据、分割文件、嵌入内容、建立索引,最后才返回200状态码。这样的处理过程可能会持续数小时。相反,该接口应当只执行极少的操作:

POST /documents
       │
       ▼
Store document
       │
       ▼
Create ingestion event
       │
       ▼
Return 202 Accepted

其他所有操作都在后台由事件驱动进行。Kafka 是一种可行的核心解决方案,因为其主题会被拆分为多个分区,消费者可以并行处理这些分区,且每个分区内的数据都有顺序保障,这对于需要按固定顺序处理共享同一文档或租户键的事件来说非常有用。

具体工具的重要性不如它所提供的功能:数据摄入过程需要缓冲机制。如果您的嵌入服务原本每秒能处理30,000个数据块,而突然面临每秒100,000个数据块的处理需求,系统应当能够积累待处理任务而不会崩溃。

确保每个数据摄入步骤都是可重试的

在如此规模下,任务会运行两次。消息会被重新发送。工作者在写入数据后但在确认任务完成之前就会死亡。即便远程端已完成任务,请求仍会超时。因此应从一开始就通过从稳定输入中生成确定性标识符来应对所有这些问题:

document_id
document_version
chunk_index
embedding_model_version

块标识可以是定义该块的各部分的哈希值:

hash(
    tenant_id +
    document_id +
    document_version +
    chunk_index
)

因为不会再出现两次相同的索引事件,所以不会生成重复内容。

upsert(chunk_id, ...)

可以安全地重试。实现恰好执行一次较为困难,因此通常更现实的目标是实现可重试执行。

分块属于基础设施层面的决策

在数据量较小的情况下,改变分块大小似乎不会造成影响。但当涉及数十亿个向量时,这会带来巨大的成本。以1亿份文档为例,若每份文档生成20个分块,则会产生20亿个向量;而若每份文档生成60个分块,则会产生60亿个向量。看似微不足道的改动实际上让所有数值都增加了两倍:

embedding compute
vector storage
indexing work
retrieval fan-out
reindexing time
replication traffic

因此,分块并非仅仅是自然语言处理中的一个可调参数,它还是容量规划的重要依据。

每个分块都应包含足够的元数据,以便追溯其来源:

{
  "chunk_id": "c_982...",
  "document_id": "doc_51...",
  "document_version": 8,
  "tenant_id": "tenant_17",
  "page": 43,
  "section": "Risk Factors",
  "language": "en",
  "created_at": "...",
  "acl_groups": ["finance", "executives"],
  "embedding_version": "embed_v4"
}

根据您的存储策略,文本本身可以存储在其他位置,但其溯源信息必须能够在检索时保留下来。否则,引用工作迟早会变得十分麻烦。

除非必要,否则不要搜索整个语料库

这或许是整个设计中最重要的一点。以“本季度我们的欧洲退款政策有哪些变化?”这个问题为例,简单的处理方式就是搜索全部3,000,000,000个向量。但实际上,这个问题已经为我们提供了很多线索:

organization = current tenant
region       = Europe
topic        = refund policy
time         = current quarter
permissions  = documents user may access

一种高效的检索架构会充分利用这些限制,在开始相似度搜索之前就将数十亿个数据块缩减为少量样本。具体数值取决于工作负载,因此仅可作为参考。核心原则始终不变:先路由,再检索。

分片使向量层实现分布式

当某个节点无法存储全部可检索的文档时,就必须对索引进行分片。像 Qdrant 这样的分布式向量引擎通过将数据点分散到不同分片中实现水平扩展,同时通过复制机制在多个节点上保存这些分片的副本,从而提升系统的可用性及读取能力。

不过有个问题。如果每个查询都必须分发到每一个分片,那么增加分片最终只会带来更多的协调工作,而非减少它。更好的设计应让尽可能多的查询能够被路由。像这样的属性可以帮助你判断哪些分片确实需要参与处理:

tenant_id
region
document namespace
language
time partition

在多租户系统中,这种方式的优势最为明显。与其……

Query
 ↓
128 shards

一个查询可能会被处理

tenant_id = 429
 ↓
Shard group 18
 ↓
4 shards

以 Qdrant 目前的分布式设计为例,它支持用户自定义分片,这样操作就可以针对选定的分片键进行,而无需处理整个集合。这一原则对所有供应商都适用:良好的分区策略能够减少查询范围。

复制解决的是不同的问题

人们常常将分片与复制混为一谈,但实际上它们解决的是不同的问题。分片用于处理那些对于单台机器来说过大的数据集;而复制则解决当其中某台机器出现故障时的问题。请想象这样的架构:

Shard A → Node 1
Shard B → Node 2
Shard C → Node 3

如果节点2失效,对应的一部分语料库也会随之消失。而采用复制机制后:

Shard A → Node 1 + Node 4
Shard B → Node 2 + Node 5
Shard C → Node 3 + Node 6

即使单个节点出现故障,系统仍能持续响应查询。冗余设计虽然会增加存储成本、写入带宽以及运维工作量,但没有冗余的生产环境搜索集群会出现一些令人困扰的故障模式。

仅靠向量搜索不够

想象有人询问:“HR-48291号政策内容是什么?”语义搜索可能会返回有关人力资源政策的一般性内容,而词法检索则擅长精准定位HR-48291这一具体条目。反过来问:“员工领养孩子后可以申请额外休假吗?”这时语义搜索就派上用场了。这两种方法存在重叠,但覆盖的范围不同,正因如此,通常会采用同时运行这两种方法的混合检索方式。

Elastic目前的混合搜索指南采取了类似的方法,结合词汇检索与向量检索,并推荐使用互反排名融合作为整合排序结果的一种方式。RRF的一个优点在于,无需将BM25得分与余弦相似度视为可比较的数值,而是直接结合它们的排名位置。简化表达如下:

RRF(d) = Σ 1 / (k + rank_i(d))

rank_i(d)表示文档d在检索系统i中的排名。

谨慎使用顺序预过滤

有一种诱人的捷径:

3B documents
   ↓
BM25 returns 10,000
   ↓
vector search only those

它对某些工作负载有效,但也会破坏语义检索能力。想象一下,有用户输入“采用孩子的工作人员适用哪些规则?”,而相关文档的内容是:“通过非生育方式领养孩子的员工可享受16周的家庭休假。”该文档与用户查询语句的关键词重叠度很低。如果BM25在语义检索之前就将其排除,那么任何嵌入模型都无法挽救它。

因此在存在严格路由限制的情况下,更好的做法是同时让多个检索器生成候选结果,然后再对合并后的列表进行重新排序。这些严格限制本身属于另一类问题:

tenant
permissions
document status

通常应在开始检索之前就执行这些措施。

在LLM看到任何内容之前必须进行授权

想象一个包含以下内容的企业语料库:

Engineering documents
HR documents
Board documents
Legal documents
Executive compensation
Customer contracts

语义相关性并不等同于许可。首席财务官的文件可能在数学上与员工的提问最为匹配,但这并不意味着员工就有权阅读它。错误的设计表现如下:

Vector Search
     ↓
Retrieve confidential chunk
     ↓
Send chunk to LLM
     ↓
Check permission

在权限检查执行时,机密文本早已被传递到下游组件中。正确的处理流程应首先确定用户身份:

User
  ↓
Identity
  ↓
Groups / roles / ACL
  ↓
Authorized search space
  ↓
Retrieval

这意味着访问控制元数据必须能够高效地参与检索过程。在规模极大的情况下,那种需要在检索后扫描数百万条记录的ACL方案既会带来安全问题,也会造成性能瓶颈。

检索产生的是候选项,而非上下文

假设混合检索得到了400个候选项,不要将这400个内容全部放入提示词中。应先对它们进行重新排序:

Vector candidates: 200
Lexical candidates: 200
          │
          ▼
        Fusion
          │
          ▼
    ~250 unique chunks
          │
          ▼
       Reranker
          │
          ▼
       top 20–40

第一阶段搜索旨在快速找出可能相关的内容。而重排器则承担不同的任务:在数量少得多的数据集中,更细致地评估相关性。这种两阶段机制使得原本成本高昂的相关性模型能够处理数百个项目,而非数十亿个。

接着去除冗余内容

即便是一个优秀的重排器,也可能给出类似这样的结果:

Chunk 1 → page 14
Chunk 2 → page 14
Chunk 3 → page 15
Chunk 4 → page 14
Chunk 5 → page 15

这五种方式表达的实质可能相同,全部使用都会浪费上下文令牌。在构建最终提示语之前,请先执行以下步骤的组合:

deduplication
document diversity
section diversity
near-duplicate detection
adjacent-chunk merging
token budgeting

有时,一个答案也需要其周围的上下文。如果第47块数据包含答案,但只是从解释的中间部分开始,上下文构建器就可以获取相关内容。

chunk 46
chunk 47
chunk 48

不必指望向量搜索能够自动检索出所有连续的片段。

上下文构建需要独立的处理层

一种相当常见的实现方式是将上下文视为仅仅如此:

context = "\n\n".join(retrieved_chunks)

拥有专用的上下文构建工具,生产系统性能会更佳。它的任务是依据排序后的证据生成高效的提示信息,同时保留引用信息。从概念上讲:

Retrieved Evidence
        ↓
Deduplicate
        ↓
Merge related chunks
        ↓
Enforce token budget
        ↓
Preserve source IDs
        ↓
Order evidence
        ↓
Generate context

模型随后可能会接收到结构类似如下的内容:

SOURCE S1
Document: Employee Handbook
Page: 42
Version: 18
Text: ...
SOURCE S2
Document: European Leave Addendum
Page: 7
Version: 4
Text: ...
QUESTION
...

并且会被要求引用 S1、S2 等。这比让模型凭记忆重新生成网址或文档标识符要安全得多。

查询理解应当成本低廉

某些查询在检索前进行规范化处理会更有益。如果用户输入“去年欧洲的营收增长了多少?”,结合当前日期以及该公司自身的术语体系,从该句子中提取结构化字段后再进行检索,效果可能会更好:

metric = revenue
region = Europe
year = 2025
intent = financial comparison

在对话中,像“亚洲的情况如何?”这样的追问需要结合之前的上下文重新表述:

How much did revenue grow in Asia in 2025?

设计要点在于,这些步骤很少需要最昂贵的推理模型,只需使用能够可靠完成任务的最低成本组件即可。成熟的 AI 技术栈通常如下所示:

small model
    ↓
classification
small model
    ↓
query rewriting
embedding model
    ↓
retrieval
reranker
    ↓
relevance
large model
    ↓
final reasoning

而不是这样:

largest model everywhere

多跳问题的分解

有些问题无法仅通过单篇文本就给出可靠答案。像“哪种欧洲产品的营收下降最多,管理层是如何解释的?”这样的问题需要拆分为两次查询:

Question 1:
Which European product had the largest decline?
Question 2:
What explanation did management provide for that product?

随后,检索系统可以分别为每个子问题收集证据,所有结果在生成之前会被合并在一起。

Complex Query
                         │
                         ▼
                 Query Decomposer
                    /         \
                   /           \
                  ▼             ▼
              Subquery A    Subquery B
                  │             │
                  ▼             ▼
              Retrieval     Retrieval
                   \           /
                    \         /
                     ▼       ▼
                  Evidence Join
                       │
                       ▼
                      LLM

此时,大规模RAG更像是查询规划系统,而非简单的最近邻查找。

新鲜度改变了索引方式

假设语料库包含1亿份文档,其中每天有200万份会发生变化。每晚重新构建索引可能并非理想选择,因此需要增量索引:

Document updated
       │
       ▼
new document version
       │
       ▼
parse changed document
       │
       ▼
generate new chunks
       │
       ▼
embed
       │
       ▼
write new index entries
       │
       ▼
mark previous version inactive

逻辑版本控制优于立即进行破坏性替换。以某份文档为例:

document_id = D17
version 41 → status=inactive
version 42 → status=active

查询筛选条件

status = active

旧的条目可以在后台被垃圾回收。从应用程序的角度来看,更新是以原子方式提交的,无需所有存储系统同时进行更改。

也要为模型版本控制

迟早你会更换嵌入模型,因此绝不能在不知道向量生成来源的情况下存储它们。至少要确保以下信息可追溯:

embedding_model
embedding_version
embedding_dimensions
chunking_version
parser_version

否则,六个月后您可能会发现,有15%的索引是使用旧流程构建的,而且很难确定具体是哪15%。迁移操作可以在并行索引上同时进行:

Documents
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
   embedding model V3   embedding model V4
          │                   │
          ▼                   ▼
      Index V3            Index V4
                              │
                              ▼
                         shadow traffic
                              │
                              ▼
                           evaluate
                              │
                              ▼
                         switch alias

不要移动数十亿个向量,只希望新的检索效果更好。在切换之前先进行测试。

多层缓存

并非每个查询都值得使用完整的处理流程。如果有成千上万的员工询问“我们有多少天假期?”,那就没必要

embedding
→ distributed retrieval
→ reranking
→ large LLM

每次这样做都是浪费。采用分层缓存结构会更合理:

User Query
    │
    ▼
Exact Response Cache
    │ miss
    ▼
Semantic Cache
    │ miss
    ▼
Retrieval Cache
    │ miss
    ▼
Search

嵌入缓存是显而易见的配套工具:

hash(normalized_query)
      ↓
query embedding

当内容发生变化时,情况就会变得复杂。针对该内容缓存的答案

Policy version 17

可能有一次会出错。

Policy version 18

系统开始运行。因此,缓存键或失效机制必须了解语料库与版本的边界。在没有新鲜度规则的情况下对模型输出进行缓存是危险的。

不仅要为正常流程做规划,还要为故障情况做准备

大型系统会逐步出现故障。可能出现的情况包括:

embedding provider unavailable
one vector shard unavailable
search cluster degraded
reranker timeout
LLM rate limited
LLM provider outage
queue backlog growing
parser crashing on malformed PDF
OCR worker running out of memory

每种故障情况都需要明确的处理策略。例如:

Vector search fails
        ↓
Can lexical search answer?
        ↓
yes
        ↓
return degraded retrieval mode

或:

Primary LLM unavailable
        ↓
Fallback model
        ↓
Lower quality but service continues

或:

Document parser fails 5 times
        ↓
Dead-letter queue
        ↓
record failure reason
        ↓
operator / automated remediation

无限重试并非韧性体现。有时它只不过是一个会带来额外云服务费用的无限循环而已。

背压机制

想象一下,数据摄取过程通常是正常进行的

20M chunks/hour

其嵌入容量能够处理

25M chunks/hour

一切正常。这时有客户执行了迁移操作,结果出现了问题。

200M chunks/hour

在没有背压的情况下,故障会这样发生:

embedding service overloaded
        ↓
timeouts
        ↓
retries
        ↓
more load
        ↓
more failures
        ↓
retry storm

在工人前方设置缓冲区:

incoming workload
      ↓
durable queue
      ↓
workers consume at safe rate
      ↓
backlog temporarily grows

现在,容量扩展可以与数据到达率独立进行。这也是为什么数据摄取层需要独立架构的另一个原因。

可观测性必须涵盖数据检索质量

常规监控会报告以下内容:

HTTP 200 rate
CPU
memory
disk
p95 latency
error rate
requests/second

这些都很有用,但RAG服务可能在800毫秒内返回HTTP 200状态码,却给出完全错误的答案。因此也需要针对AI的专用遥测功能。一份有用的每次请求追踪信息可能如下所示:

request_id: rq_912
query rewrite:
"parental policy?" → "parental leave policy"
vector retrieval:
200 candidates
145 ms
lexical retrieval:
200 candidates
91 ms
fusion:
278 unique candidates
reranking:
278 → 20
212 ms
context:
13 chunks
8,420 tokens
LLM:
input 9,104 tokens
output 681 tokens
1.8 sec
citations:
3 documents
total:
2.4 sec

离线评估需要一些指标来回答诸如:是否检索到了正确的证据、其排名是否足够高、答案是否得到了该证据的支持、重新排序是否足以提升查询质量以弥补其带来的延迟,以及哪些查询类型最常出错等问题。这些指标比仅用一个总体百分比来衡量幻觉现象更具实际指导意义。

将检索与生成分开评估

当最终答案出错时,至少存在两种不同的故障类型。在检索失败的情况下,所需的证据根本未能进入上下文:

Correct document exists
       ↓
retriever misses it
       ↓
LLM never sees it
       ↓
wrong answer

再怎么编辑提示词也无法解决这个问题。在生成失败的情况下,正确的证据其实已经出现了:

correct evidence
       ↓
context
       ↓
LLM
       ↓
incorrect interpretation

此时,提示词设计、模型选择、上下文组织方式或推理策略都可能起到关键作用。如果仅对最终答案进行评分,两者看起来并无差异,因此严谨的评估流程必须检查中间环节。

为延迟设定预算

“系统需要快速响应”并非一项硬性要求。应为每个阶段设定相应的时间预算。举例来说,假设目标是在大约2.5秒内给出首个答案,各阶段的分配可以如下:

API + auth                    50 ms
query understanding          100 ms
retrieval                    300 ms
fusion                        20 ms
reranking                    250 ms
context construction          30 ms
LLM time-to-first-token     1,200 ms
network / safety margin      550 ms
-----------------------------------
target                     ~2,500 ms

这些并非通用数值。这项测试很重要,因为它能显示时间真正消耗在何处。如果数据检索耗时1.8秒,更换大语言模型提供商并不会解决核心瓶颈问题。如果大语言模型本身导致了90%的延迟,那么花费一个月时间只为将向量搜索的耗时减少40毫秒,很可能并非最有效的努力方向。

成本控制也需要同样的严谨态度

一个大型RAG系统包含许多独立的成本构成部分:

object storage
document parsing
OCR
embedding generation
vector storage
vector replicas
lexical search
metadata database
network transfer
reranking
LLM input tokens
LLM output tokens
observability
backups

因此,需计算每个有效单位的成本,例如:

cost / 1,000 indexed documents
cost / million chunks
cost / query
cost / successful answer
cost / tenant

在SaaS领域,最后一点最为重要。你可能会发现:

Tenant A
5% of revenue
42% of LLM spend

如果没有针对每个租户的计量方式,你只会看到一笔高得惊人的月度基础设施费用。

将昂贵的存储资源用于真正需要它的场景

一个有助于理解的思路是:从成本高且速度快的存储到成本低但容量大的存储,存在一个渐变关系:

expensive / fast
                      ▲
                      │
               vector indexes
               search indexes
               Redis caches
                      │
               metadata DB
                      │
               object storage
                      │
                      ▼
                 cheap / large

完整的 PDF 文件没有理由存储在向量索引中。应将昂贵的存储空间用于真正的检索需求,让对象存储来保存原始文档,而搜索基础设施则负责存储可检索的表示形式。一旦数据量达到数百 TB,这种区分就变得极为重要。

热数据与冷数据

并非所有 1 亿份文档都会被同等频繁地查询。也许情况是:

Last 30 days        → 70% of queries
Last 12 months      → 25%
Older archive       → 5%

对所有事物一视同仁会浪费基础设施。一种解决方案是采用如下划分方式:

Query
  │
  ├────→ Hot index
  │
  └────→ Archive index when necessary

基于时间的路由机制还能避免将本周的事件与十年的历史记录混为一谈。大型系统的高效运行源于避免不必要的工作,而不仅仅是更快地完成这些工作。

多租户模式改变一切

假设你的平台为

10,000 organizations

你几乎肯定不希望这样。

10,000 completely independent vector clusters

然而,将所有租户都放入同一个命名空间会导致“噪声邻居”问题以及授权故障。一种可行的方法是按规模对租户进行分组:

Small tenants
        ↓
shared shard groups
Medium tenants
        ↓
partitioned shared infrastructure
Very large tenants
        ↓
dedicated shard groups

这可以防止某个客户导入5亿个数据块而挤占数千个较小的数据块,同时为有需求的企业客户提供隔离方案。没有一种单一的租户分区策略能适合所有人。

大语言模型接近最终阶段

看看在到达模型之前我们讨论了多少工程细节。这是有意为之。一个成熟的RAG请求处理流程如下:

User
 ↓
API Gateway
 ↓
Authentication
 ↓
Authorization
 ↓
Rate Limiter
 ↓
Query Understanding
 ↓
Shard Routing
 ↓
Hybrid Candidate Retrieval
 ↓
Rank Fusion
 ↓
Reranker
 ↓
Deduplication
 ↓
Context Builder
 ↓
LLM
 ↓
Citation Validation
 ↓
Response

大语言模型固然重要,但在1亿份文档的规模下,它只是众多组件之一。

完整的生产流程

综合来看,首先是查询路径,接着是数据接入路径,最后是支撑这两者的系统:

CLIENTS
                                 │
                                 ▼
                         ┌──────────────┐
                         │ API Gateway  │
                         └──────┬───────┘
                                │
                         ┌──────▼───────┐
                         │ Auth / ACL   │
                         │ Rate Limits  │
                         └──────┬───────┘
                                │
                                ▼
                     ┌────────────────────┐
                     │ Query Orchestrator │
                     └─────────┬──────────┘
                               │
                 ┌─────────────┼──────────────┐
                 │             │              │
                 ▼             ▼              ▼
              Cache      Query Rewrite    Metadata
                                           Filters
                 │             │              │
                 └─────────────┼──────────────┘
                               ▼
                        Shard Router
                               │
                  ┌────────────┴────────────┐
                  ▼                         ▼
           Lexical Search              Vector Search
                  │                         │
                  └───────────┬─────────────┘
                              ▼
                         Rank Fusion
                              │
                              ▼
                           Reranker
                              │
                              ▼
                         Deduplicate
                              │
                              ▼
                       Context Builder
                              │
                              ▼
                             LLM
                              │
                              ▼
                     Citation Validation
                              │
                              ▼
                          RESPONSE
================================================================
INGESTION SYSTEM
Documents
                            │
                            ▼
                       Object Store
                            │
                            ▼
                       Event Queue
                            │
               ┌────────────┼────────────┐
               ▼            ▼            ▼
            Parser        Parser       Parser
               │            │            │
               └────────────┼────────────┘
                            ▼
                      Normalization
                            │
                            ▼
                         Chunker
                            │
             ┌──────────────┴──────────────┐
             ▼                             ▼
        Embedding Queue               Text Index Queue
             │                             │
       ┌─────┼─────┐                       ▼
       ▼     ▼     ▼                 Lexical Index
      GPU   GPU   GPU
       │     │     │
       └─────┼─────┘
             ▼
       Vector Index
================================================================
SUPPORTING SYSTEMS
Metadata DB         → document state, versions, ownership
Object Storage      → original source documents
Vector Cluster      → semantic retrieval
Search Cluster      → lexical retrieval
Redis               → caches and rate limiting
Message Queue       → asynchronous ingestion
Observability       → traces, metrics, evaluation
Secrets / IAM       → service authorization
Evaluation System   → retrieval + answer-quality tests

应避免的反模式

在审视此类架构时,以下问题会立即引发关注:

One giant vector collection with no routing strategy
Synchronous PDF ingestion
Only vector retrieval, no lexical search
ACL filtering after retrieval
No document versioning
No embedding model version
No reranking
Sending top-100 chunks directly to the LLM
Using the LLM for every trivial classification task
No dead-letter queue
No retrieval-level evaluation
No tenant-level cost attribution
Treating the vector DB as permanent document storage
Re-embedding the entire corpus for every small content change
Benchmarking only average latency instead of tail latency

其中任何一个都可以传入一个原型。但当它们以数十亿向量的规模共同出现时,就会变得极其棘手。

核心原则

思考大型RAG时,最有用的方式并非如此:

question
→ vector search
→ LLM

就是这个:

billions of possible evidence units
              ↓
        routing constraints
              ↓
      relevant partitions
              ↓
      cheap candidate search
              ↓
        hundreds of items
              ↓
      expensive reranking
              ↓
        tens of passages
              ↓
      context optimization
              ↓
             LLM
              ↓
      grounded answer

该系统类似漏斗结构。在每个阶段,我们都会在更少的候选项上投入更多的计算资源:

Cheap operation      → huge search space
Moderate operation   → smaller candidate set
Expensive operation  → tiny candidate set
Most expensive model → final context only

正是这一理念使得该架构在经济上具有可行性。

要点总结

基于几千份文档的RAG系统主要属于应用工程范畴。而当处理量达到1亿份文档时,它就演变成了搜索基础设施、分布式系统、数据工程、安全保障以及成本控制等需要同时关注的领域。向量数据库固然重要,嵌入模型和大型语言模型也同样关键,但围绕这些技术的架构重要性却往往被大多数教程忽视。

可扩展的思维模型:以低成本存储数据,异步处理,为所有内容建立版本控制,刻意进行分区,尽早实施授权机制,通过多种信号检索信息,严格重新排序,仅向模型提供高质量证据,并单独评估每个阶段的表现。一旦数据量达到数十亿个片段,最佳的优化方式不再是加快搜索速度,而是调整系统结构,让系统的大部分组件无需直接处理海量数据。

相关阅读

  • 生产环境RAG的数据清洗:编码、OCR处理、去重与个人信息保护 —— 如何将解析后的企业文档转换为可用于检索的文本:编码修复、模板内容与重复项删除、高风险OCR标记识别、语言处理以及个人信息掩码技术。
  • 千万级文档规模的RAG检索优化:过滤机制、路由系统、智能代理与评估方法 —— 基于SQL过滤器、混合搜索与重排序技术的200毫秒以内检索流程,还包括查询路由、智能代理协调、评估指标以及Qdrant管道脚本。