阶段三走到最后一篇。前面十七篇把 RAG 的每个环节都拆过了,但那些都是"单独能跑"的状态。真正上线时面对的是另一类问题:怎么让它在已有的 Java 服务里稳定运行、成本可控、还不出安全事故。 这一篇讲的就是这些收尾工作,其中有一处安全问题特别容易被忽略。
一、最后一公里是工程问题,不是算法问题
先说清这一篇的定位。前面讲的切分、向量化、检索、重排、评测,都属于"让效果达标"。而这一篇要解决的是另一批问题:
- 每次查询都要跑一遍完整链路,成本怎么压;
- 资料天天在变,新内容怎么快速生效、旧内容怎么清理;
- 系统是多租户的,怎么保证 A 部门查不到 B 部门的资料;
- 链路从一次模型调用变成四五个环节串起来,延迟和失败面都变大了。
这些问题跟算法无关,但它们决定这套东西能不能真的上线。而且其中任何一条没处理好,后果都比"效果差一点"严重——尤其是租户隔离,那是事故级别的。
二、缓存:分三层做,但有个前提
RAG 的链路长,每一段都值得缓存,但性质不一样。
第一层:查询改写缓存。 同一个问题反复出现时,改写结果可以直接复用,省掉一次模型调用。
第二层:检索结果缓存。 同样的问题,检索到的那批块通常不会变(除非资料刚更新过)。这一层的收益最大——检索本身包含一次 embedding 调用加一次向量搜索。
第三层:最终答案缓存。 命中了就直接返回,连生成都省了。这层最省,但风险也最大:答案会过期。
第三层要用语义缓存效果才好——不是精确匹配问题原文,而是把问题向量化,相似度超过阈值就复用。因为同一个意思,用户的问法千变万化。
但这里有个必须先解决的前提:缓存不能只按"问题"做 key。
这就是容易被忽略的那处安全问题。设想:
- 员工 A 问"本部门的预算流程是什么",系统检索到了 A 有权限看到的文档,答案被缓存;
- 员工 B 问了措辞相近的问题,缓存命中,直接拿到 A 的答案——而这份答案来自 B 无权查看的文档。
整个过程没有报错,检索、生成都没执行,因此权限过滤根本没被触发。这就是纯粹由缓存引入的越权。
所以缓存的 key 里必须带上权限维度,而不是只带问题。实践上有两种做法:
- 把租户 ID(或权限标签的哈希)拼进 key,不同租户各自一套缓存;
- 按权限等级分命名空间,同等级共用。
第一种更安全、更简单,代价是缓存命中率下降(不同租户之间不能共享)。在没有把握之前,宁可牺牲命中率。 越权这类事故的代价,远高于多跑几次检索。
另外提醒一点:语义缓存本身会放大这个风险。 精确缓存只在问题完全相同时命中,而语义缓存是"意思相近就命中"——两个不同租户的人本来可能问出措辞相近的问题,所以它撞上的概率更高。如果要用语义缓存,权限维度更不能不进 key。
三、增量更新:最容易漏的是"删旧块"
资料更新是常态。一份文档改了,你得让新内容能被检索到,同时让旧内容消失。
完整流程是这样:
- 检测到文档变更;
- 重新解析、切分;
- 对新的块做向量化;
- 写入向量库;
- 删掉这份文档对应的旧块。
第五步是最容易漏的,第十五篇专门强调过。漏了它的后果是:新旧块同时存在,检索时可能同时命中,模型收到自相矛盾的两份资料,然后给出一个基于过时内容的答案。而且这种错误不会报错,只会表现为"AI 给了旧答案"。
要能删干净,前提是块必须能追到它的来源文档——这就是为什么元数据里要有 source_id(第十三篇的元数据四件套)。没有它,你根本不知道哪些块属于这份文档。
增量还是全量? 这里有明确的边界:
- 可以增量:文档内容变了,但切分策略和 embedding 模型都没变。这时只需处理变更的文档。
- 必须全量重建:切分策略改了、或者换了 embedding 模型。这两种情况下,库里所有向量和所有块的边界都失效了(第十四篇讲过:换模型意味着坐标系变了),只能全部重来。
所以"切分策略不能轻易改"这句话的实际含义,就是"改一次要全量重建一次"。 这个代价要在动手前想清楚,而不是上线之后才发现。
工程项目化上还有几点要注意:
- 变更检测要有可靠依据,比如内容哈希或更新时间戳,别靠文件名——文件改了名字没变,增量就漏了。
- 重建是后台任务,不是同步操作。 资料量大时它要跑很久,必须异步执行,并且能被观测:处理了多少、剩多少、失败在哪。
- 中断要能续传。 任务跑到一半挂了,重启后应该从断点继续,而不是从头再来。
- 重建期间线上查询怎么办。 如果直接删旧库再写新库,中间会有一段"什么都查不到"的空窗。稳妥做法是双写加切换:新库建好再切流量,旧库留一段时间再删。
四、多租户隔离:权限过滤必须在检索层
这一节是整篇里最需要执行到位的一条。
检索必须在权限过滤之后进行,不能先检索再在应用层过滤。 原因有两个:
第一,应用层过滤会漏。 检索返回了 20 个块,你在代码里筛掉无权访问的——看起来没问题,但更危险的是:只要有任何一条代码路径忘了筛,就是一次越权。 而过滤放在检索层,是无权访问的块根本不返回,从源头就断了。
第二,过滤放在检索层对召回率有影响。 第十五篇讲过:过滤条件很激进时,近似搜索可能在前几十个候选里一个都不满足。所以要配合分区或分库策略——权限边界清晰的场景,不同租户的数据物理分开(不同 collection 或不同库),比在同一堆数据里加过滤条件更可靠。
几条硬性要求:
- 权限标签随块入库(第十三篇提过,第一版就要加),检索时作为必传条件;
- 缓存按租户隔离(上一节的结论);
- 日志和埋点里不要记录原文内容,尤其是无权访问的块——日志泄露是常见的越权渠道;
- 回答里带上出处(第十二篇讲过),但要确认出处本身也是用户有权看到的。
五、性能:延迟被摊薄到四个环节
这一节讲一个容易被低估的事实:RAG 的延迟天然比单次模型调用高好几倍。
拆开看,一次 RAG 查询串了这些环节:
- (可选)查询改写 —— 一次模型调用;
- 查询向量化 —— 一次 embedding 调用;
- 向量检索 —— 一次库查询;
- 重排 —— 一般也是一次模型调用,而且要对多个候选算;
- 生成 —— 一次模型调用(这一步通常最慢)。
任意一环慢,整体就慢。 而单次模型调用只有一个环节,所以那种"模型调用很快"的直觉,在 RAG 里不成立。
几个实际做法:
- 先把耗时分解测出来。 别猜哪一环慢——在每段打点,拿到真实分布再决定优化哪一块。很多时候你以为慢在检索,实际慢在重排。
- 每段都要能独立超时和降级。 重排超时就跳过重排,用原始召回顺序;改写超时就用原始问题检索。这些降级几乎不影响功能可用性,但能避免一个次要环节把整个请求拖死。
- 检索挂了要有明确的行为。 如果向量库不可用,是直接失败,还是降级成"不带资料直接问模型"?后者可以接受,但必须在答案里明确标注"本次未查询资料"——否则用户会把模型的自由发挥当成有依据的回答,那比报错危险得多。
流式能不能用? 可以,但有前提:检索必须全部完成,生成阶段才能开始流式输出。 也就是说,用户等待的首字延迟里包含了检索和重排的时间,流式只能优化"生成"这一段。所以 RAG 的首字延迟明显高于纯对话场景——这一点要在产品层面接受,或者靠"先给个进度提示"来缓解。
六、集成形态:库还是服务
两种常见做法。
做成 SDK 库:业务方引入依赖直接调用。优点是调用简单、没有网络开销;缺点是版本升级要各家跟着升,而且向量库的连接、模型调用都在业务进程里,资源不好统一管理。
做成独立服务:业务方通过 HTTP 调用。优点是资源集中、升级只改一处、限流和缓存可以全局生效;缺点是多一跳网络,且它本身会成为新的故障点(这时候第四篇和第十一篇的超时与降级就用上了)。
多数团队在中后期会演进到独立服务,因为模型调用和向量库连接这两样东西,放在业务进程里管理成本很高。但如果只是内部少量调用,先用库跑起来也完全合理——别为了架构漂亮提前拆服务。
另外,资料入库/重建这类重活应该走任务队列,不要放在请求链路里。它们是分钟级甚至小时级的操作,和在线查询的资源需求完全不同,混在一起会互相挤占。
七、可观测性:该记什么
RAG 出问题时,光看日志里一句"接口返回 200"是没法排查的。至少要记下:
- 原始问题与改写后的查询。这一对数据能立刻看出改写是否把问题带偏了。
- 命中的块 ID 与来源。用来核对检索对不对,也是发现"召回了不该召回的内容"的依据。
- 各环节耗时分解。定位慢在哪一段。
- 是否走了降级路径(跳过重排、未查资料等)。降级路径上的质量下降,如果不记录,你会把它当成正常表现。
- 最终答案与引用来源。用于事后抽检。
这些埋点还有个附带价值:它们就是评测迭代和后续优化的数据来源。 上一篇讲的在线指标、人工抽检,全都依赖这些记录。
八、小结与检查清单
- 缓存分三层(改写 / 检索 / 答案),语义缓存收益大但必须把租户与权限维度写进 key,否则会绕过权限过滤造成越权
- 增量更新:以内容哈希检测变更、重新切分向量化、必须删旧块、重建走异步任务且可观测可续传、双写切换避免空窗
- 全量重建的边界:切分策略变或换 embedding 模型就是全量,改动前先算代价
- 权限过滤在检索层做,不放应用层;权限边界清晰时物理分区比过滤更可靠;缓存按租户隔离;日志不记无权内容
- 性能:先把耗时分解测出来再优化;每个环节独立超时与降级;检索不可用时降级要在答案中明确标注
- 流式:检索完成后才能开始生成,首字延迟天然高于纯对话
- 集成:先用库、后期拆服务;入库重建走任务队列,不占请求链路
- 可观测性:记原始/改写后查询、命中块与来源、耗时分解、是否降级、答案与引用
一道判断标准:如果有人问你"现在这次查询命中的块,是哪些文档、用户有没有权限看",你能不能立刻答出来? 答不出来,说明这套系统的可观测性或权限设计还有缺口——而这两个缺口中的任何一个,都足以把一次技术尝鲜变成一次事故。
下一篇
阶段三到这里收尾:从"该不该做 RAG",到切分、向量化、入库、检索优化、评测,再到今天的工程落地,这条链路走完了。
接下来进入阶段四,主题是让模型会做事——也就是工具调用和 Agent。第一个问题是 Function Calling:模型怎么知道有哪些工具可用、参数怎么传、执行结果怎么回灌给它。下一篇从一个最小可运行的例子开始。
本文是「用 Java 接入大模型」系列第十八篇,阶段三收尾。上一篇讲了 RAG 效果怎么量化评测,建议按顺序看。文中缓存比例、重建策略与集成分层为通用实践,具体取值请按你的资料规模、租户模型与延迟预算确定。