针对 OpenAI 企业级 RAG 面试题的完整解答:连接器与权限同步、分块、混合检索、重排序、权限感知搜索、租户隔离、带引用的有据生成、多轮上下文、缓存与评测。

由 The Forward Deployed 编辑团队审阅

本文属于 OpenAI 系统设计题库。该题目能代表该轮面试的水平。分析与解答为本站原创。

问题描述

该产品连接每个客户的内容系统,索引其文档,并在聊天中回答员工的问题,同时附上对来源段落的引用。每家公司是一个租户。在租户内部,每位员工只能看到他们在源系统中本来就能打开的内容。为所有可能的工具构建连接器以及对基础模型进行训练不在范围内。

澄清问题

  • 有多少租户,最大租户规模多大? 练习设定:1,000 家公司;最大的有 5,000 万份文档和 10 万名员工。
  • 数据来源有哪些? Wiki、文档盘、工单系统和聊天记录,各自拥有不同的权限模型。
  • 答案需要多新鲜? 新增和编辑的内容需在几分钟内可检索。权限撤销要比这更快生效。
  • 延迟要求? 首个 token 约两秒内返回;完整答案在数秒内流式输出。
  • 是否支持多轮? 是。追问会引用之前的对话内容。
  • 答案必须包含什么? 对所使用段落的引用,并且在来源中找不到答案时明确说“我找不到这个”。
  • 查询量? 练习设定:10% 的员工每天提问 5 次。

企业级 RAG 难在哪里

教程式的 RAG 流水线只有五步:分块、向量化、存储、检索、生成。它适用于一个公开文档文件夹。企业数据会在四个方面打破它。

权限是按文档划分且不断变化的。流水线绝不能展示用户无权打开的文档中的段落,并且在权限被移除后必须在几分钟内停止展示。

租户共享基础设施。无论出现什么 bug,一家公司的数据绝不能出现在另一家公司的答案中。

内容杂乱且庞大。表格、代码、幻灯片和工单线程都不好分块,而一个 5,000 万文档的租户必须在不拖慢其他所有人的情况下完成索引。

而且没有度量就无法看到质量。一个流畅但错误、引用看起来很真实的答案比没有答案更糟,而且直到客户发现之前没人会注意到。

因此核心矛盾是召回与管控。检索希望尽可能广泛地搜索;而企业要求每个结果都被过滤、隔离、保持新鲜并经过验证。

flowchart LR U([Employee]):::user --> Q[Query service]:::svc Q --> R[Retrieve<br/>permission-filtered]:::svc --> RR[Rerank]:::svc --> G[Generate with citations]:::svc --> U C[Connectors]:::svc --> I[Ingest: chunk, embed, index]:::svc --> IDX[(Per-tenant index)]:::store R --> IDX classDef user fill:#e6efec,stroke:#315e55,color:#171717; classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
核心思想。 检索质量只是基本门槛。企业级要素——权限、隔离、新鲜度和评测——才是这一轮考察的重点。

关键概念

检索增强生成

RAG 通过检索相关段落并将其放入模型的提示中来回答问题。模型基于这些段落而非记忆来撰写答案,因此答案可以引用来源,并反映私有的、最新的数据。

稠密检索与稀疏检索

稠密检索将文本嵌入为向量,使含义相近的内容在向量空间中彼此靠近;对于关于休假的问题,它能找到“休假政策”。稀疏检索(如 BM25)根据精确的词项重叠打分;它能找到嵌入容易模糊的错误代码

E4012

和产品名称。

Hybrid retrieval

同时运行两者并合并结果。

近似最近邻搜索

将查询向量与数亿个向量逐一精确比较太慢了。HNSW 等 ANN 索引以少量召回率损失换取毫秒级的搜索速度。

重排序

交叉编码器将查询和单个段落一起读取并输出相关性分数。它比比较两个独立的嵌入准确得多,但对整个语料库来说太慢。因此先低成本检索约 100 个候选,再重排序出最好的 10 个。

有据可依

有据可依的答案只做出检索段落所支持的主张。有据性通过提示来强制执行,在生成后进行检查,并通过评测来衡量。

核心思想。 混合检索保证召回,重排序保证精确,有据可依保证可信。

需求

继续阅读之前。 先列出需求。然后指出绝不能被破坏的属性,以及驱动设计的约束。

1.1 功能需求

  • 连接每个租户的内容系统,同步文档和权限。
  • 在聊天中以流式方式回答问题,并附上对段落的引用。
  • 支持依赖前文对话轮次的追问。
  • 当来源中不包含答案时如实说明。
  • 让管理员查看使用情况、禁用来源并删除内容。

1.2 非功能需求

  • 访问正确性。 绝不向无权限用户展示任何段落。删除操作在几分钟内生效。
  • 租户隔离。 数据绝不跨租户流动,即使某一层出现 bug 也不行。
  • 新鲜度。 编辑在几分钟内即可被搜索到。
  • 延迟。 p95 下首 token 在约两秒内返回。
  • 回答质量。 必须实测而非假设,并作为发布门槛。

1.3 约束与属性

访问正确性是属性。 一份薪资文档泄露就会终结客户关系。索引规模是约束。 每个大型租户数以亿计的分块决定了索引设计、摄取管道和成本。

粗略估算

2.1 最大租户的索引大小

5000 万份文档 × 每份约 10 个分块 = 5 亿个分块。一个 1024 维的嵌入向量以 4 字节浮点数存储为 4 KB,因此原始向量占用 5 亿 × 4 KB ≈ 2 TB。量化为每维 1 字节可将其降至约 512 GB,一个分片索引可以在几台机器的内存中容纳。假设所有租户的总量是最大租户的数倍。

2.2 摄取

最大租户的初始回填需要嵌入 5 亿个分块。如果嵌入服务每个 GPU 每秒处理 5,000 个分块,一块 GPU 需要 100,000 秒,约 28 小时。二十块 GPU 可在 1.5 小时内完成。回填之后,编辑只是涓涓细流:如果每天有 1% 的文档变更,那就是 500,000 份文档,约每秒 6 份。

2.3 查询负载

100,000 名员工 × 10% × 5 个问题 = 最大租户每天 50,000 个问题,所有租户合计可能每天 500 万个:平均约每秒 58 个,峰值几百个。每个问题会触发一两次检索调用、对约 100 个段落的一次重排,以及一次生成。

2.4 每个答案的 token 数

十个各 400 token 的段落,加上指令和历史记录,每个问题约 5,000 个输入 token。按每天 500 万个问题计算,即每天 250 亿个输入 token。输入 token 主导成本,这就是为什么重排到 10 个段落而非 30 个如此重要。

关键要点。 每个大型租户数以亿计的向量、需要 GPU 并行的回填、适中的查询速率,以及主导成本的输入 token。

API 设计

继续阅读之前。 客户端应该在每个问题中附带聊天历史,还是由服务器保存?

由服务器保存。服务器需要历史记录来改写后续问题,必须对历史中提到的任何文档应用相同的权限检查,而且管理员可能需要审计或删除对话。客户端只需发送会话 ID。

3.1 提问

POST /v1/conversations/:id/messages
  {text}
  -> text/event-stream
       answer_delta   {text}
       citation       {n, doc_id, title, url, snippet}
       done           {message_id, answered: true|false}

POST /v1/conversations          -> {conversation_id}
GET  /v1/conversations/:id      -> messages with citations
POST /v1/messages/:id/feedback  {rating: up|down, reason?}

3.2 管理与连接器

POST   /v1/sources            {type: wiki|drive|tickets, credentials_ref, scope}
GET    /v1/sources/:id        -> {status, docs_indexed, last_sync, errors}
DELETE /v1/sources/:id        -> removes all content from the index
POST   /v1/documents/:id/purge

数据模型

document      (tenant_id, doc_id, source_id, title, url, mime, updated_at,
               acl_groups[], content_hash, deleted_at)
chunk         (tenant_id, chunk_id, doc_id, section_path, text, token_count,
               acl_groups[], updated_at)
vector_index  per tenant or namespace:
               chunk_id -> embedding, filterable fields: acl_groups, source, updated_at
keyword_index per tenant: inverted index on chunk text, same filter fields
group_member  (tenant_id, group_id, user_id)          -- synced from identity provider
conversation  (tenant_id, conversation_id, user_id, created_at)
message       (conversation_id, message_id, role, text, citations[], created_at)
eval_case     (tenant_type, question, relevant_doc_ids[], reference_answer)

分块会复制文档的 ACL 组,这样索引无需连接操作即可按其过滤。

高层设计

5.1 教程管道

flowchart LR D[Documents]:::store --> CH[Chunk + embed]:::svc --> V[(Vector DB)]:::store Q([Question]):::user --> E[Embed] --> V --> P[Top 5 chunks]:::svc --> L[LLM]:::svc --> A([Answer]):::user classDef user fill:#e6efec,stroke:#315e55,color:#171717; classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;

它回答关于公开文档的问题。它向任何用户展示任何文档,混淆租户,遗漏精确术语,忘记上一轮对话,而且无法知道自己是否正确。

5.2 修复 1:同步权限的连接器

每个连接器从源系统拉取文档及其访问列表,并从身份提供商拉取组成员关系。每个分块都携带其文档的 ACL 组。

flowchart LR SRC[源系统]:::store --> CN[连接器]:::new IDP[身份提供商]:::store --> CN CN --> DOC[(文档 + ACL)]:::store CN --> GM[(组成员)]:::store classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717; classDef new fill:#ffffff,stroke:#c4492d,stroke-width:2px,stroke-dasharray:5 3,color:#171717;

5.3 修复 2:在检索时按租户过滤

查询服务展开用户的组,并将其作为过滤器传入租户自己的索引中的向量搜索和关键词搜索。被禁止的段落永远不会离开索引。

5.4 修复 3:混合检索与重排序

并行运行稠密检索和关键词检索,使用倒数排名融合进行合并,然后用交叉编码器对前 100 个结果重排序,选出最好的 10 个。

flowchart LR Q[Standalone query]:::svc --> D[Dense search<br/>top 100]:::new Q --> K[Keyword search<br/>top 100]:::new D --> F[Rank fusion]:::new K --> F F --> X[Cross-encoder rerank<br/>to top 10]:::new classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef new fill:#ffffff,stroke:#c4492d,stroke-width:2px,stroke-dasharray:5 3,color:#171717;

5.5 修复 4:感知对话的查询与有据可依的回答

一个小模型利用最近的对话轮次将每个追问改写为独立查询。生成过程使用带编号的段落,并且必须引用它们。在回答完成之前,一个检查器会验证引用。

5.6 修复 5:将评测作为发布门槛

针对每种租户类型的黄金测试集和权限测试集,在每次更改分块、检索、提示词或模型时运行。出现回归则阻止发布。

5.7 组合后的设计

sequenceDiagram
  autonumber
  actor U as Employee
  participant A as API
  participant Q as Query service
  participant ACL as Group cache
  participant IX as Tenant indexes
  participant RR as Reranker
  participant LLM as Model
  U->>A: follow-up question
  A->>Q: question + conversation id
  Q->>Q: rewrite into standalone query using history
  Q->>ACL: user's groups (cached, short TTL)
  par dense and keyword
    Q->>IX: vector search, filter tenant + groups
    Q->>IX: keyword search, filter tenant + groups
  end
  Q->>Q: rank fusion
  Q->>RR: rerank top 100
  RR-->>Q: top 10
  Q->>Q: re-check access for top 10 docs (source API)
  Q->>LLM: numbered passages + question + instructions
  LLM-->>A: streamed answer with [n] citations
  A->>A: validate citations
  A-->>U: answer + citations
核心思想。 将权限与内容同步,在租户索引内部过滤,混合检索,重排序,改写追问,引用并验证,并以评测作为发布门槛。

深度剖析

6.1 权限感知检索

继续阅读之前。 为什么在检索之后过滤结果是一个 bug,而不只是一种较慢的等效做法?

假设用户询问薪酬区间。按相似度排名前 100 的段落大多来自用户无权打开的 HR 文档。后置过滤会移除其中 95 个,留下 5 个弱匹配。用户得到一个糟糕的回答,而排名在 101 到 300 之间更好、且有权访问的段落从未被考虑过。后置过滤恰恰在权限最关键的问题上损害了召回率。

预过滤会将用户的组传入 ANN 搜索,因此索引只返回有权限的分块。大多数向量数据库都支持过滤搜索;当过滤器非常严格时,有些数据库会切换为对匹配子集进行暴力扫描,这没有问题,因为该子集很小。

组展开是下一个细节。用户属于组,而组会嵌套。在查询时从缓存展开完整的组集合,该缓存每隔几分钟从身份提供方刷新一次,并在提供方报告变更时使用户的条目失效。

同步延迟会留下一个窗口期:访问权限在 10:00 被移除,连接器在 10:05 才同步。对于答案中实际引用的少数文档,在生成答案时通过源系统的 API 重新检查访问权限。每个被引用的文档只需一次调用,就能在关键之处堵住这个窗口期。

区分答案优劣的关键:权限

弱在提示词中过滤或在检索后过滤

要求模型不要泄露受限内容,或在检索后进行过滤,这会导致泄露或损失召回率。

良好在搜索内部过滤

在分块上存储 ACL 组,并在向量查询和关键词查询中进行过滤。

强预过滤、展开组并重新检查

在两个索引中都进行预过滤,从最新的缓存展开嵌套组,在生成答案时重新检查被引用的文档,并用一个权限测试集进行泄露测试,门槛设为零。

6.2 租户隔离

在继续阅读之前。 查询服务出现一个 bug,丢失了租户过滤器。是什么阻止一家公司的数据出现在另一家公司的答案中?

纵深防御。大型租户拥有自己的物理索引,因此没有其他数据可被找到。小型租户共享一个索引,但检索服务会自行从经过身份验证的请求中添加租户过滤器,绝不使用客户端输入,并且索引会拒绝没有租户过滤器的查询。每个租户的文档都用该租户自己的密钥加密,租户 A 的摄取路径无法获取租户 B 的密钥。缓存首先以租户 ID 为键。日志和评测数据也按租户分区。

要明确说出:提示词不是隔离边界。无论告诉模型什么,都无法阻止它复述其上下文中的内容。

6.3 对真实企业内容进行分块

在继续阅读之前。 一个 wiki 页面上有一张很长的定价层级表。你如何对它进行分块,使得关于某一层级的问题能检索到正确的行及其表头?

首先按结构拆分:标题、章节、列表项、表格行、代码块。对于表格,让每组行都保留表头,这样包含第 40 到 50 行的分块仍然能说明各列的含义。对于工单线程,按每条消息分块,并将工单标题和状态保留为元数据。对于幻灯片,每张幻灯片一个分块,并附上演示文稿标题。

然后限制分块大小,几百个 token,并带少量重叠。存储每个分块所属的章节路径,例如 "Pricing > Enterprise > Limits",并在嵌入前把它加到分块文本前面。这段简短的上下文能显著提升那些没有点明自身主题的分块的检索效果。

保留指向父级章节的链接。如果检索到的分块内容太单薄、不足以回答问题,生成器可以拉取相邻的分块。

6.4 有依据的生成与引用检查

提示词给出带标题的编号段落,要求模型仅根据这些段落作答,在每条论断后标注段落编号,当段落中不包含答案时,回答“我在你的资料来源中找不到这个”。

生成之后,机械地检查引用:每个被引用的编号必须存在于检索到的段落集合中。如果答案中的事实性论断没有引用任何段落,或引用了未提供的编号,则重新生成或附带警告返回。对于高风险租户,抽样答案并运行支持性检查:对每个句子及其引用的段落,该段落是否支持这一论断?一个小模型可以在大规模上运行这种检查,并用人工标注进行校准。

6.5 多轮对话

“那承包商呢?”无法单独检索。一个小而快的模型会结合最近几轮对话,把它改写为“承包商的育儿假政策是什么?”。检索基于改写后的问题进行。生成时仍然能看到完整对话,因此答案读起来自然流畅。

限制上下文范围。最近几轮对话逐字保留,更早的对话做摘要。不要把旧的检索段落带入后续轮次;每一轮都重新检索,这样权限检查和时效性检查每次都能生效。

6.6 缓存而不泄露

查询嵌入可以安全缓存:它们不包含文档内容。检索结果和答案不能在用户之间共享,因为提出相同问题的两个用户可能拥有不同的权限。如果要缓存它们,缓存键应基于租户 ID 加上用户完整组集合的哈希值,并将存活时间设置得较短,以便权限变更能及时生效。在这个产品中,跨用户的语义缓存是一个陷阱。

6.7 评测

在继续阅读之前。 团队想更换嵌入模型。你如何决策?

用黄金集的数字来决策。为每类租户各构建一个黄金集:真实问题、能回答这些问题的文档,以及参考答案,由熟悉内容的人标注。衡量以下指标:

层级指标能发现的问题
检索Recall@10、Recall@100正确的段落从未到达模型
重排序nDCG@10正确的段落被埋没了
生成对所引用段落的忠实度来源不支持的论断
生成答案与参考答案的正确性引用了真实段落但答案错误
访问权限测试集上的泄露必须严格为零
线上差评率、无答案率、来源点击率现实世界中的漂移

LLM 评判器可以大规模地为正确性和忠实度打分,前提是你先验证它与人工标注的一致性。每次变更都要运行整套评测,并阻止出现退化的版本发布。只有当召回率提升且其他指标没有下降时,才允许切换嵌入模型上线。

拉开答案差距的关键:评测

弱:在演示中看起来没问题

没有黄金数据集,没有指标,也没有发布门禁。

良好:每一层都有离线指标

在标注数据集上衡量检索召回率和答案正确性。

强:把评测当作基础设施

区分检索、排序、生成和访问指标,校准 LLM 评判器,为发布设置门禁,并监控线上信号以发现漂移。

6.8 扩展数据摄取

将摄取流水线与查询服务分离。连接器将变更的文档按租户写入队列。解析器和分块工作进程根据队列深度进行扩展。嵌入计算在 GPU 上分批运行。索引写入器批量应用更新。大型租户的回填以较低优先级和独立配额运行,因此不会延误小型租户的增量更新。

基于内容哈希去重:许多企业会在多个位置存储同一个文件。删除操作以墓碑记录的形式通过同一条流水线传播,并从两个索引中移除相应的分块。

6.9 新鲜度与删除

在继续阅读之前。 一名员工在 10:00 删除了一份包含客户个人数据的文档。10:02,有人提出了一个本可由该文档回答的问题。他们会看到什么?

什么也看不到——如果流水线是为删除而构建的。连接器通过两种方式获知变更:当源系统提供 webhook 时使用 webhook,否则采用周期性增量轮询。删除以墓碑事件的形式到达,并插队到普通编辑之前。索引写入器从向量索引和关键词索引中同时移除该文档的分块,文档存储则将其标记为已删除。

删除与索引更新之间的时间窗口就是风险所在。用与权限相同的方式来消除它:在展示引用之前,先检查被引用文档的

deleted_at

与访问状态,而文档存储是最先更新的。即使其分块在索引中还残留几秒钟,已删除的文档也绝不会出现在答案中。

编辑以正常优先级走同样的路径。只对发生变更的文档重新分块和重新嵌入。如果内容哈希未变,例如仅元数据的编辑,则只更新元数据并跳过嵌入成本。

设定目标并加以衡量:在实践中,99% 的删除应在一分钟内反映到索引中,编辑在五分钟内。按连接器跟踪延迟,因为一个停滞的连接器会悄悄地提供过时的答案。

6.10 延迟预算

将两秒内输出首个 token 的目标拆分为多个部分,并为每部分分配预算:

步骤预算 (p95)方式
鉴权、组查询20 ms缓存的组展开
查询改写250 ms小型快速模型,简短提示词
稠密检索 + 关键词检索80 毫秒并行运行;内存中的过滤式 ANN
对前 100 条结果重排序120 毫秒GPU 上批量运行的交叉编码器
对前 10 条结果重新校验访问权限100 毫秒并行调用源 API,短暂缓存
模型生成首个 token 的时间1,000 毫秒5,000 token 的提示词;对指令启用提示词缓存
Slack430 毫秒网络、排队

如果改写步骤超出预算,首轮提问(本身已是独立问题)可跳过该步骤。如果某些源的访问权限复核较慢,可并行开始生成,待校验通过后再放出引用。

区分答案优劣的关键:新鲜度与延迟

弱:没有新鲜度方案

把索引当作批处理任务,且没有延迟拆解。

良好:增量更新与预算

增量处理编辑和删除,并将延迟拆解为多个步骤。

强:删除安全且有预算

优先处理墓碑记录,对被引用文档与文档库进行复核,为每个连接器设定并衡量新鲜度目标,并为每个步骤分配延迟预算及备用方案。

变体

7.1 智能体式检索

对于复杂问题,让模型发起多次搜索、阅读结果并再次搜索。这能提升困难的多部分问题,但会增加延迟和 token 消耗。每次工具调用都要以相同方式进行权限过滤,并限制步骤数量。

7.2 结构化数据

像“上周有多少个 P1 工单”这类问题需要数据库查询,而不是文本段落。将它们路由到文本转 SQL 工具,在启用行级安全的只读副本上执行,并引用该查询。

7.3 本地部署

一些客户要求索引和模型部署在其网络内部。相同的组件可部署到他们的环境中;供应商只运行控制平面,任何内容都不会外泄。

7.4 租户规模扩大十倍时

当有 10,000 个租户时,大多数租户规模很小。将小租户按地区放入共享索引,并强制使用租户过滤器,只为最大的几百个租户提供独立索引。连接器调度成为公平性问题:为每个租户建立一个队列并分配一定份额的连接器工作进程,这样某个租户的重新同步就不会延误所有人的编辑。评测也要随之扩展:按行业而非按租户维护黄金数据集,并允许大租户添加自己的数据集。

可迁移的模式

企业级 RAG 就是带授权层和校验器的搜索。检索找出候选内容,授权在任何其他环节看到它们之前进行修剪,生成基于剩余内容进行写作,校验则将输出与输入进行比对。同样的结构适用于任何将私有数据与模型结合的系统:基于私有代码库的代码助手、基于工单历史的支持机器人,以及拥有工具访问权限的智能体。

回顾:30 秒答案

  • 权限与内容同步。 每个分块都带 ACL 组。
  • 在搜索内部按租户过滤。 绝不在检索之后,也绝不在提示词中。
  • 混合检索,然后重排。 密集检索加关键词检索,融合后用交叉编码器重排出前 10 条。
  • 改写追问;引用并验证。 编号段落、机械化的引用检查、抽样支持性检查。
  • 评估每一层并把关发布。 包括一个权限测试集,必须做到零泄露。

测验

+为什么说按权限做后过滤是一个召回缺陷?

如果排名靠前的段落大多是被禁止的,事后过滤它们会留下很少且较弱的结果,而排名靠后的允许段落从未被检索到。预过滤只检索允许的集合,因此最好的允许段落会被返回。

+为什么在向量检索旁边使用关键词检索?

向量检索能捕捉语义,但会模糊精确字符串,如错误代码、产品名称和工单号。关键词检索能精确匹配它们。融合两者比单独使用任何一种都能获得更好的召回。

+为什么只对前 100 个候选重排?

交叉编码器将查询和段落一起读取,准确但缓慢。对数百万个块运行是不可能的;对 100 个廉价的候选运行只需几十毫秒,并能显著改善前 10 条结果。

+为什么共享语义答案缓存在这里很危险?

两个用户可能用不同的权限问同一个问题。基于某个用户可访问文档构建的缓存答案,可能向无权限的用户泄露内容。

+权限测试集检查什么,目标是什么?

它以无权访问特定文档的用户身份提问,并检查这些文档是否出现在检索结果或引用中。目标是精确的零泄露,并且它把关每一次发布。

+系统如何避免引用几秒前刚被删除的文档?

删除以墓碑记录的形式先于编辑传播,在引用任何文档之前,答案路径会检查文档存储,而删除会先被记录在那里。被标记为已删除的文档绝不会被引用,即使其块短暂地留在索引中。

+为什么要为每个步骤分配延迟预算?

单一的端到端目标无法显示时间花在哪里。分步预算能显示当总时间超限时,应该优化或跳过哪一步。

资料来源与延伸阅读

下一章:向量与搜索设计