做 UniRAG 之前我其实先写了三遍单租户的 RAG Demo每次都是跑通就丢。真正让我决定认真做一个多租户 RAG 平台的是一次内部排期三条业务线几乎同时要做知识库问答如果各搭各的就会有三个向量库、三套 Embedding 服务、三份检索逻辑全都得长期养着而底层又都是同一批模型和同样的切分规则。与其重复造轮子不如把多租户当成 RAG 平台的一等公民从根上重新设计。UniRAG 就是这么来的。这篇文章不聊 PPT 架构只聊我在设计 UniRAG 时真正纠结过的取舍以及踩完坑之后沉淀下来的经验。内容大致覆盖这几块多租户到底隔离什么、索引层怎么选型、向量知识库和结构知识库怎么共存、检索参数怎么定、在 Mac 上怎么从零跑通一个最小多租户 RAG以及上线后最常见的故障排查。适合正在做 RAG 平台化、知识库产品化或者纯粹想把多租户概念落到代码里的朋友。1. 多租户 RAG 的问题清单不是把数据库加个字段就行1.1 单租户到多租户真正变化的是“边界”先说一个最常见的误解很多人觉得多租户 RAG 就是在文档表里加一个tenant_id查询的时候带上这个字段就完事了。如果是给公司内部两三个人用的工具这么干确实够了。但 UniRAG 要服务的都是独立业务单元每个业务单元可能有自己的术语、自己的权限范围、自己的文档更新节奏甚至自己的模型偏好。单租户系统只需要考虑“怎么把答案答对”多租户系统要考虑的是三个边界数据边界租户 A 的文档、切片、检索结果绝对不能出现在租户 B 的上下文里。资源边界某个租户如果批量导入文档或频繁调用检索不能把共享的向量库和模型服务打满导致其他租户集体超时。配置边界每个租户的切分参数、提示词模板、知识库路由规则都是独立的改一个租户的配置不能影响另一个。这三个边界不是同一个维度的东西。数据边界靠检索链路保证资源边界靠配额和限流保证配置边界靠配置管理保证。如果一开始脑子里没有领域模型写到后面一定是所有判断都散在代码里等租户多了会很难收场。另外看开源社区的动向很有意思像 Dify 社区版这样的项目也开始在往多租户上使劲说明这确实是个共性问题。不过开源平台给的往往是通用能力真正接进自己的业务体系后还是要回答“你的租户到底是什么、怎么隔离、怎么计费、怎么审计”这些问题这些才是 UniRAG 这类自建平台的核心工作量。1.2 隔离级别怎么选从轻隔离到重隔离再具体说隔离。我梳理了一遍RAG 平台常见的隔离方案大概分三档第一档是共享向量索引通过元数据里携带租户 ID 做过滤。这是最省资源的方式索引只有一份存储和内存开销最小但前提是检索链路的每一环都要强制带租户上下文只要漏一处就会出现文档串味儿。第二档是按租户分 Collection 或分 Partition。索引还是跑在同一个集群里但物理上分区了隔离性比第一档好查询时也不用把租户过滤条件写进每一个 filter性能通常更可控。缺点是租户数量大了以后collection 数量膨胀运维模型会变复杂需要配套自动创建和回收机制。第三档是每个租户一套独立集群。隔离最彻底出问题不容易互相波及但成本非常高无论是机器资源还是运维人力。除非是数据合规要求极高的场景否则没有必要一上来就做。UniRAG 最终的选择是“共享索引 租户过滤为主敏感租户独立 Collection 为辅”。这个组合看起来不那么纯粹但胜在灵活普通租户默认进共享区有合规要求或数据量特别大的租户通过配置把它提升到独立 Collection平台代码不用改只改租户的部署策略。选择这套方案的核心逻辑是“按需隔离而不是按想象隔离”。如果一开始就全做独立索引100 个租户就是 100 个索引Embedding 的内存占用和后台任务数量都会线性上涨前期根本撑不住。反过来如果全做共享索引遇到一个每天导入几万份文档的大租户检索质量和你能不能兜住这个并发都是问题。所以我把隔离级别做成了租户配置项也不建议把某个隔离方式写死在代码里。1.3 UnI RAG 设计前我定义的四条铁律动工之前我给自己定了四条约束后面所有细节的取舍都以这四条为准任何入库和检索动作都必须有租户上下文。没有租户 ID 的调用直接拒绝而不是默认丢进某个公共空间。入库链路和检索链路必须走同一条租户映射。也就是说文档切片写进索引的租户标签和查询时用来过滤的租户标签必须来自同一个配置源。任何一次问答输出都必须能回溯到来源片段和租户信息。不然出了问题连是哪个租户的哪份文档污染了结果都查不出来。默认不允许租户自建模型或自选 Embedding除非通过平台申请。因为这会让成本模型彻底失控。这几条看着像废话但实际项目里很容易被打破。比如有人图省事直接在某个库文件里硬编码了一个tenant_idpublic后面所有没显式传租户的调用都跑到了公共区这就是典型的“默认值污染”。我后来要求所有接口都必须显式传递租户信息连默认租户都不给宁可多写几行代码也不留后门。2. UniRAG 的整体架构控制面和数据面分离2.1 架构分层的核心思路UniRAG 的架构没有太多新鲜东西就是经典的“控制面 数据面”分离。控制面负责租户管理、知识源注册、模型路由、参数配置数据面负责文档采集、文本切分、向量化、索引写入、检索问答。为什么要分这么清楚因为这两部分的变更频率完全不一样。控制面的配置可能每天都要变新增一个租户、调整某个知识库的召回参数、换一个提示词模板数据面底层则相对稳定索引结构、Embedding 模型、检索服务不会频繁改。把它们混在一个模块里会导致“只是为了改一个配置就得重新发一版检索服务”非常影响迭代效率。在技术选型上UniRAG 采用了一套相对务实的组合服务框架用 FastAPI异步接口在检索场景下天然合适。向量存储用 Qdrant支持 payload 过滤对于“共享索引 租户过滤”的模式很友好。关系型元数据用 PostgreSQL保存租户、知识源、词库、配置这类结构化信息。本地开发环境里的 Embedding 和 LLM 统一走 Ollama方便 Mac 上直接跑生产环境再切换到独立模型服务。这套组合没有追求极致的性能但胜在每层都能独立替换。比如把 Qdrant 换成 es 或其他向量库只需要改存储适配层上层检索逻辑完全不用动。2.2 为什么选“共享索引 租户过滤”而不是“每租户一套”1.2 节里我已经说过整体思路这里把决策过程展开对比一下方便你按照自己的场景判断。对比项共享索引 租户过滤每租户独立 Collection/索引资源占用低一份索引所有人共享高每个租户都有独立索引内存和磁盘上涨明显检索性能受 filter 性能影响需要正确建 payload 索引相对稳定查询天然限定分区租户数量上限可以支持很多但需要配额机制租户几百个之后管理成本和故障面都会变大数据泄漏风险高必须全链路强制过滤低物理隔离天然防串味运维复杂度低索引统一管理高需要自动创建、备份、迁移机制UniRAG 选择共享为主主要是想把资源效率拉满。但在实现上做了一个关键设计在 Qdrant 里给租户字段建了专门的索引并且检索时强制带租户过滤条件不是为了省事而是为了保证性能不会随着租户数增长而明显劣化。如果你是从零开始我建议先做共享方案跑通全流程然后在代码里把“存储适配层”抽象出来。也就是说检索服务不直接依赖 Qdrant API而是依赖一个VectorStore接口。这样以后某个大租户真的飙到需要独立索引时只需要给这个租户绑定一个新的 store 实例不用改业务代码。2.3 租户上下文中间件设计多租户系统的第一道防线就是租户上下文的注入和传递。UniRAG 的做法是用 FastAPI 中间件统一解析请求头里的X-Tenant-ID把租户对象塞进一个 Context 对象后续所有业务函数从 Context 里取而不是从参数里传来传去。# tenant_context.py import contextvars from dataclasses import dataclass dataclass class TenantContext: tenant_id: str tenant_config: dict _tenant_context_var contextvars.ContextVar(tenant_context, defaultNone) def get_current_tenant() - TenantContext: ctx _tenant_context_var.get() if ctx is None: raise RuntimeError(tenant context is missing) return ctx def bind_tenant(tenant_id: str, tenant_config: dict): _tenant_context_var.set(TenantContext(tenant_idtenant_id, tenant_configtenant_config))# middleware.py from starlette.middleware.base import BaseHTTPMiddleware from tenant_context import bind_tenant from config_service import get_tenant_config class TenantMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): tenant_id request.headers.get(X-Tenant-ID) if not tenant_id: return JSONResponse({error: missing tenant}, status_code400) config get_tenant_config(tenant_id) bind_tenant(tenant_id, config) response await call_next(request) return response这个设计的好处是业务代码里几乎看不到“租户”两个字但每一步都受租户约束。检索服务不用关心租户 ID 是从哪来的反正入口已经保证了它的存在。这其实也是多租户最容易踩雷的地方如果让每个业务接口手动接收 tenant_id非常容易在某个内部调用里忘掉一旦忘记就会落到公共索引。3. 知识库的三种形态RAG 不是只有“向量 PDF”3.1 向量知识库解决的是“语义检索”一提到 RAG很多人默认就是把一堆 PDF 和 Word 文档切碎然后做向量检索。这确实是最常见的场景但向量知识库的能力边界也很明显它擅长“语义相似”不擅长“精确计算”和“复杂关系查询”。比如租户 A 的知识库里有一份产品手册用户问“设备过热怎么办”向量检索能把相关章节捞出来效果好是因为这样的问题在文档里往往有对应表述。但如果用户问“2024 年第一季度销量是多少”而数据只存在于一张 Excel 表格里向量检索就有点勉强了。文本格式的表格被切块后模型需要自己拼接上下文“精确性”会打折扣。所以在 UniRAG 里我们把向量知识库定位为“非结构化文本的语义召回入口”而不是唯一的知识形态。文档类知识源走切片 Embedding这一点没有任何悬念。3.2 结构知识库和知识图谱什么时候需要结构知识库解决的是“事实查询”。它面向的往往是数据库表、Excel 清单、配置列表这类有明确字段的数据查询时需要精确匹配不能靠“语义相近”。典型场景包括查询某个订单状态、某个商品的库存、某个员工所属部门。这种知识不太适合塞进向量库。你可以把一张订单表切成多个片段再向量化但查询“订单 1024 状态是什么”时向量检索很难保证返回的就是那一行。更合理的方式是先把问题映射成结构化查询再去数据表里精确查找。知识图谱KG则更进一步它解决的是“实体关系和多跳推理”。比如“A 产品的供应商与 B 产品的供应商是哪家公司”这类问题横跨多份文档和多个实体纯向量检索往往顾此失彼。知识图谱会把实体之间的关系显式建模查询时可以沿着边去走。还有一个常被忽略的价值图谱里的关系本身可以为 LLM 提供约束让答案不会跳出既定的领域框架。三类知识形态各有各的适用场景我把它们的边界和典型应用整理成了下面这张表知识形态解决什么问题典型数据源最适合的查询方式向量知识库语义匹配、模糊召回PDF、Word、网页、Markdown自然语言相似度检索结构知识库精确查询、固定字段数据库表、Excel、API结构化查询 SQL/参数知识图谱关系推理、多跳查询、概念对齐业务实体数据、本体定义图遍历 规则推理3.3 Ontology 在多租户场景中的作用在 UniRAG 里除了上面的三类知识我们还引入了一层“本体定义Ontology”。本体不直接存储文档而是存储“这个领域里有哪些概念、概念之间有什么关系、每个概念对应哪些检索入口”。比如租户 A 是电子产品售后文档里大量出现“耗材”租户 B 是办公设备服务文档里叫“配件”。表面上这是两个不同的词但映射到领域本体后它们都能指向同一个抽象概念。如果没有这层映射租户 A 的用户问“配件坏了怎么办”系统可能在租户 A 的知识库里完全找不到对应内容。所以 UniRAG 的检索入口不是“只搜向量”而是先做一次轻量级概念识别把用户问题中的实体词映射到当前租户的本体节点上再根据节点类型决定走向量库、结构库还是图谱。这种实现也对应了现在常说的 Ontology RAG 思路核心就是让检索不再完全依赖表面词汇而是依赖概念结构。实现时要注意租户的本体定义本身也属于租户配置的一部分必须纳入控制面管理不能全局共用。否则 A 租户精心调过的本体映射很可能把 B 租户的检索方向带偏。3.4 UniRAG 的数据源抽象设计为了能让向量库、结构库和图谱在一个问答接口里共存我设计了一个数据源抽象层。每个知识源都是一个KnowledgeSource它需要暴露统一的能力召回候选片段、返还给上层统一的上下文格式。class KnowledgeSource: def retrieve(self, query: str, tenant_id: str, top_k: int) - list[ContextChunk]: raise NotImplementedError class VectorKnowledgeSource(KnowledgeSource): def retrieve(self, query, tenant_id, top_k): # 生成向量在 qdrant 中按租户过滤召回 ... class StructuredKnowledgeSource(KnowledgeSource): def retrieve(self, query, tenant_id, top_k): # 用 NL2SQL 或规则映射从库里精确查询 ... class GraphKnowledgeSource(KnowledgeSource): def retrieve(self, query, tenant_id, top_k): # 识别实体和关系在图谱中遍历返回路径 ...路由层做的事情就是先拿租户的本体配置把用户问题分类然后决定调哪几个 source。如果问题明显是“某件事是什么”这种语义问题直接走向量源如果带“哪个、多少、什么状态”这类精确查询词优先走结构源如果问题里出现了多个实体而且看起来需要比较实体间的关系则引入图谱源。多个 source 召回的结果会在重排阶段融合不是只挑一个。4. 入库和检索的关键参数可以照抄的配置4.1 分块策略不是所有文本都切 512切分参数直接影响召回率而且没有一个万能值。UniRAG 里我按文档类型做了不同预设你拿到后可以先照抄再根据实际效果调整。文档类型分块大小重叠窗口说明产品手册/说明文档512 字符64 字符保留较完整上下文适合说明性文本工单/FAQ256 字符32 字符问题答案通常短而集中小块召回更精准合同/法律文本768 字符96 字符需要保持条款完整性避免把一个条款切开表格文本按行/块切少量重叠尽量不跨行列切否则行列关系会断代码或配置文件256 字符32 字符代码上下文敏感切太大容易混入无关逻辑分块大小背后有一个计算逻辑假设模型上下文窗口是 8K token一次问答要放指令、历史对话、检索片段和回答空间。512 个中文字符经过模型 Tokenizer 大约会变成 200~300 token。如果检索 top_k 取 5就有 1000~1500 token 的上下文被文档占掉留给回答和历史的余量并不宽裕。所以别再盲目加大块块越大召回越多越容易把模型的注意力带偏。重叠窗口的作用是避免“切点正好断在一个句子的关键部位”。64 字符大概是两行正文的长度足够让前后块共享一部分上下文。如果你发现一些本该能命中的文档总是召不回来第一件事就是检查切分是否把关键句子从中间切断了。4.2 检索链路参数怎么定UniRAG 的检索链路不是一次查询就完事而是多级过滤。初始向量召回 top_k 我会取 30目的很简单先把候选范围拉大宁可多召回一些不相关的内容也不漏掉真正相关的片段。召回 30 条之后再用关键词和结构匹配做一次融合选出 20 条。最后用重排模型Cross Encoder在这 20 条里逐条打分只保留最相关的 5 条作为上下文。这个“30→20→5”的漏斗很保守但对多租户共享索引尤其重要。因为共享索引场景下租户过滤条件已经从潜在层面收窄了范围但文档本身的噪声仍然存在。如果 top_k 只取 5可能在第一轮就错过真正有用的片段而重排救不回来没召回的文档。不如一开始多取一些把准确率的重任交给重排阶段。相似度阈值方面我会建议一个相对宽的值比如 0.65~0.7。阈值设太高容易误伤因为 Embedding 对短问题的表达能力有限很多表述不同的句子向量余弦相似度天然不会太高。你真正要卡的是重排阶段的分数而不是向量检索阶段的初筛分数。4.3 嵌入模型和重排模型选型Embedding 模型决定了检索的上限。UniRAG 在中文场景下默认用的是 bge-m3原因是它在中文长文本和变体表述上的表现比较稳而且支持 8192 长度的输入遇到长文档切片时不用频繁担心截断。生产环境如果对中文效果要求更高也可以换成基于对比学习训练的领域定制模型但要维护一套训练流程一般不建议只有几十个租户的平台过早投入。重排模型建议和 Embedding 模型解耦。Embedding 负责粗召回重排负责精挑选两者使用同一个模型其实并不合适。UniRAG 里用的重排模型是 bge-reranker-v2输入是一对 query 和 document直接输出相关度分数比向量相似度更接近“人类判断”。需要注意重排模型不能跨租户共享 prompt 或答案内容但模型权重本身是全局共享的。也就是说重排服务根据租户过滤好的候选片段逐个打分而不是让模型看到某个租户的原始数据后把结果存下来再给另一个租户用。这个边界一定要守住。5. 在 Mac 上从零跑通一个最小多租户 RAG5.1 本地环境的准备很多朋友问怎么在 Mac 上搭建 RAG 知识库尤其是本地跑一套还带多租户能力的其实并不复杂。我先说下环境要求macOS 13 及以上安装 Docker DesktopPython 3.11再加一个 Ollama 用来跑本地模型。如果你不想用 Docker也可以用 Homebrew 直接装 PostgreSQL 和 Qdrant但 Docker 会让整个环境干净很多卸载也方便。Embedding 模型和 LLM 我建议在 Ollama 里跑。Embedding 模型可以用bge-m3的量化版本LLM 可以用qwen2.5:7b。这样做的最大好处是隐私可控文档内容不会在调试阶段就发到外部 API而且不依赖网络在咖啡厅也能继续开发。brew install docker brew install python3.11 brew install ollama # 启动 ollama 服务后拉取模型 ollama pull bge-m3 ollama pull qwen2.5:7b如果你的 Mac 内存只有 16G建议 LLM 换qwen2.5:3b或者llama3.2:3bEmbedding 模型也选择内存占用更小的版本。多租户开发和验证不依赖大模型多聪明关键在于数据隔离链路通不通。5.2 启动基础服务Docker Compose 一键拉起基础组件我用了三个PostgreSQL 保存租户和知识源配置Qdrant 保存向量和租户过滤字段Redis 做缓存和限流。下面这个 Compose 文件可以直接用来起本地环境。version: 3.8 services: postgres: image: postgres:16 environment: POSTGRES_USER: uniraq POSTGRES_PASSWORD: uniraq_dev POSTGRES_DB: uniraq ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage redis: image: redis:7 ports: - 6379:6379 volumes: pg_data: qdrant_data:启动命令很简单docker compose up -d curl http://localhost:6333 # 看 Qdrant 是否起来在 Mac 本地跑这套组合非常轻量内存占用大约是 Postgres 200MB、Qdrant 300MB、Redis 100MB再加上 Ollama 的模型整体 2GB 以内能搞定。5.3 最小代码实现入库和检索都强制带租户下面这段代码是一个最小可跑的多租户 RAG 核心逻辑。我删掉了大量细节保留了最关键的两步写入时把tenant_id放进 payload检索时用tenant_id做强制过滤。from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance client QdrantClient(hostlocalhost, port6333) COLLECTION uniraq_docs def ensure_collection(): # 创建共享 collection所有租户的向量都在这里 client.recreate_collection( collection_nameCOLLECTION, vectors_configVectorParams(size1024, distanceDistance.COSINE), ) def add_document_chunks(tenant_id: str, chunks: list[dict], embeddings: list[list[float]]): points [] for idx, (chunk, emb) in enumerate(zip(chunks, embeddings)): point_id f{tenant_id}_{chunk[doc_id]}_{idx} points.append( PointStruct( idabs(hash(point_id)), vectoremb, payload{ tenant_id: tenant_id, doc_id: chunk[doc_id], text: chunk[text], source: chunk[source], }, ) ) client.upsert(collection_nameCOLLECTION, pointspoints) def search(tenant_id: str, query_vector: list[float], top_k: int 30): # 最关键的一行tenant_id 过滤 results client.search( collection_nameCOLLECTION, query_vectorquery_vector, limittop_k, query_filter{ must: [ {key: tenant_id, match: {value: tenant_id}} ] }, ) return results这里有两个细节很关键。第一写入时用tenant_id doc_id seq生成 point 的 id避免不同租户的相同文档 ID 在共享集合里产生主键冲突。第二检索时 query_filter 只用了租户字段没有用其他业务条件这样既能保证隔离又不至于把过滤条件扩大成“性能杀手”。实际接生产时tenant_id不是由业务代码传进来的而是由前面提到的中间件从请求头解析后绑定到 Context再在 controller 层取出并传给 search。也就是说业务方根本不可能“忘记传租户”因为入口已经强制了。5.4 验证隔离效果写一个检测脚本本地跑通后强烈建议写一个“串味检测”脚本目的是自动化观察租户隔离是否失效。这个脚本不需要复杂逻辑很简单给租户 A 插入一条“红色条款”给租户 B 插入一条“蓝色条款”然后用两个租户的身份分别搜索对方的内容断言搜不到即可。python check_isolation.py # 期望输出 # PASS: tenant_a 搜索 tenant_b 内容被拦截 # PASS: tenant_b 搜索 tenant_a 内容被拦截这个脚本要放到持续集成里每次改检索逻辑后都跑一遍。多租户系统最怕的不是第一次隔离做错而是某次重构时不小心把 filter 漏了回归测试又没覆盖到。6. 上线之后最常踩的坑问题排查与优化方向6.1 多租户 RAG 问题速查表我把自己遇到过的典型故障整理成一张速查表你可以直接作为排障清单用。现象可能原因排查方法租户 A 能看到租户 B 的文档片段检索时没带租户过滤或者中间件解析租户失败检查请求头传递、上下文绑定、检索代码的 filter明明文档入库了但检索结果为空切分太小、向量相似度阈值太高、查询改写后跑偏先放开阈值再逐个检查切片和 Embedding检索结果相关但内容太碎分块太小上下文被切断把分块调大增加重叠窗口结构知识库问题总是答错误走了向量检索NL2SQL 没有路由到结构库检查本体路由规则确认问题里的实体被正确识别租户配置改了但线上还走旧配置控制面和数据面的配置缓存未失效检查缓存 key 是否带租户版本号一个大租户批量导入文档时其他人检索变慢共享索引写入占用资源读写互相影响在存储层做读写分离或限流必要时给大租户独立索引6.2 重新认识的“RAG 瓶颈”网上讨论 RAG 瓶颈时很多结论都指向“幻觉”和“上下文不够”。 UniRAG 跑到稍大规模后我的体感完全不同真正的瓶颈几乎都集中在召回链路。最常见的问题是索引里的文档密度很高但切分策略太粗糙导致真正关联的内容被切到了多块里每块都只覆盖一部分语义。模型拿到这些不完整的片段自然会脑补出错误信息。另一个常见问题是用户提问的表述和文档原文差距太大向量相似度不够导致相关片段没有被召回。针对这两个瓶颈我建议的排查路径是先看租户的召回日志统计前 30 条候选中是否出现了最终应该作为答案的片段如果出现了说明问题出在重排阶段如果根本没出现说明问题出在切分或 Embedding 阶段。然后对症下药不要一上来就换大模型。同时RAG 不是只做一轮检索就万事大吉。UniRAG 在复杂问题上会尝试先做查询改写比如用户问“这两个方案有什么区别”系统会先拆解出两个实体分别去检索各自的定义再统一交给重排。这个过程也被纳入租户配置不同租户可以选择不同改写策略。6.3 运维层面的两个方向工程上跑通还只是第一步。多租户系统上线后我建议尽早补齐两件事租户级评测集和自动化回归。给每个租户准备一份 Golden Set里面包含 30~50 个典型问题和期望答案片段。每次检索模型或参数调整都拿这套集子跑一遍对比召回率和答案命中率。这样既能防止为了优化一个租户而搞坏另一个租户也能在新租户接入时快速估值。另一个方向是租户级别的监控指标。除了 QPS 和延迟更要多关注每个租户的召回率、无结果率、来源片段离散度。无结果率突然飙升往往不是模型问题而是文档更新或本体配置被误动。来源片段离散度则能反映出一份答案是否总是依赖同一段文本如果离散度过低模型很可能在背答案而不理解上下文。写在最后我搭 UniRAG 的一些体会如果只说一条最核心的经验那就是多租户 RAG 的难点不在算法而在基础设施抽象。向量检索、Embedding、重排这些技术都已经很成熟真正决定项目能不能长期跑下去的是你有没有把“租户”这个概念真正设计进每一层。我自己的流程是先花两周把单租户 RAG 跑通再花两周把租户上下文和隔离策略抽出来最后用一周时间处理了十几个细节坑。这个节奏比直接写“通用平台”要顺得多。因为你只有亲自经历过单租户的检索链才知道哪些地方容易漏隔离、哪些配置需要按租户区分。如果你正准备做类似的系统别急着把功能堆全。先保证一个租户在共享索引和过滤策略下能跑通再逐步放开大租户独立索引、本体路由、混合检索这些能力。多租户的复杂度是慢慢长出来的一开始摊太开反而很难收场。
阅读完成 · 觉得有帮助?