1. 从一次线上抖动说起:知识库和 Agent 网关到底在解决什么问题
做生产级知识库和 Agent 网关这件事,最初并不是因为我想造一个多复杂的系统,而是被一次线上抖动逼出来的。当时我们内部有一个问答助手,底层挂着一个不算大的文档库,大概几万份文档,日调用量在几千次左右。平时用着还行,直到某天运营同事批量导入了一批新文档,检索结果突然开始出现大量不相关的片段,回答质量肉眼可见地下滑。排查了一圈才发现,问题不在模型,而在检索层:向量召回把语义相近但业务上完全无关的内容排到了前面,而原本负责兜底的 BM25 关键词召回权重被压得太低,导致专有名词、编号、型号这类精确匹配需求直接失效。
这件事让我意识到,一个能跑 demo 的知识库和一个生产级知识库之间,差的不是模型能力,而是检索策略的工程化和请求入口的统一治理。前者决定回答准不准,后者决定系统稳不稳。而 Agent 网关,就是那个把所有模型调用、工具调用、检索调用收口到一起的入口层。它要处理的不只是转发,还有限流、降级、路由、可观测性、成本核算这些脏活累活。
所以这篇内容我想聊的,是我在优化生产级知识库和 Agent 网关过程中踩过的坑、做过的取舍,以及那些文档里不会写但实际会要命的细节。适合正在做 RAG 项目、准备把知识库从 demo 推向生产、或者正在设计 Agent 网关的同行参考。不管你是刚接触 RAG 和 BM25 的新手,还是已经在调 Agent 框架的老手,应该都能从里面找到一些能直接抄作业的东西。
2. 检索层优化:BM25 和向量召回到底该怎么配比
2.1 为什么纯向量召回在生产环境里经常翻车
很多人做 RAG 的第一反应是:上向量数据库,做 embedding,语义检索一把梭。demo 阶段确实好用,因为文档少、问题简单、语义匹配就能覆盖大部分场景。但一旦进入生产,问题就暴露了。
向量召回的本质是语义相似度,它擅长处理"意思相近但用词不同"的情况,比如用户问"怎么重置密码",文档里写的是"账户密码恢复流程",这种它能召回。但它有个致命弱点:对精确匹配不敏感。当用户问的是"ERR-4021 报错怎么处理",而文档里恰好有一篇专门讲 ERR-4021 的,向量召回很可能因为这篇文档整体语义和问题不够"像",把它排到第五第六位,甚至召回不进来。而 BM25 这种基于词频和逆文档频率的算法,恰恰能把这种精确匹配顶到最前面。
我在实际项目里做过对比测试,同一批 200 条真实用户 query,纯向量召回的 Top5 命中率大概在 72% 左右,而 BM25 单独跑只有 58%,但两者混合之后能到 89%。这个差距在生产环境里就是"能用"和"不能用"的区别。
2.2 BM25 的分数为什么不能直接和向量分数相加
这是我最开始踩的一个大坑。想当然地认为,把 BM25 分数和向量相似度分数加权相加,调个权重就完事了。结果发现两个分数的量纲完全不一样:BM25 分数是无上界的正数,可能是个位数,也可能是几十;而向量相似度通常是0 到 1 之间的余弦值。直接相加,等于让 BM25 主导了整个排序,向量召回形同虚设。
正确的做法是先做分数归一化,把两路分数映射到同一个区间,再融合。常见的融合方式有三种:
| 融合方式 | 原理 | 适用场景 | 注意事项 |
|---|---|---|---|
| 加权求和 | 归一化后按权重相加 | 两路召回质量接近 | 权重需要按业务调,建议从 0.5 起步 |
| RRF 倒数排名融合 | 按排名而非分数融合 | 两路分数量纲差异大 | 对异常分数不敏感,鲁棒性好 |
| 级联召回 | 先向量粗排再 BM25 精排 | 文档量极大 | 延迟会叠加,需控制候选集大小 |
我个人在生产环境里更倾向RRF,因为它只看排名不看绝对分数,天然规避了归一化带来的偏差。RRF 的核心公式是score = Σ 1/(k + rank),k 一般取 60。这个值不是拍脑袋来的,k 越大,排名靠后的文档权重被拉得越平,适合召回结果质量参差的情况;k 越小,头部文档优势越明显。我实测下来 k 在 40 到 80 之间对结果影响不大,60 是个稳妥的默认值。
2.3 分块策略对检索质量的影响被严重低估
聊检索策略的时候,很多人只盯着召回算法,却忽略了分块这个上游环节。分块分得不好,再好的召回算法也救不回来。
我见过最常见的错误是按固定字数硬切,比如每 500 字一块。这种做法在技术文档上会直接把一个完整的操作步骤切成两半,用户召回上半块,看到的是"第一步,打开配置面板",然后没了。正确的做法是按语义边界切,优先在段落、标题、列表项这些自然边界处断开。
具体到实操,我一般用这样的策略:先按 Markdown 标题层级切大块,再在大块内部按段落切,如果单个段落超过阈值(比如 800 字),再按句子边界二次切分。同时给每个块加上上下文前缀,比如把所属的章节标题拼到块内容前面。这个前缀看起来不起眼,但对向量召回帮助很大,因为它给块补充了语义背景。
还有一个细节是块重叠。相邻块之间保留 10% 到 20% 的重叠,能避免关键信息正好卡在切分点上被割裂。重叠太多会引入冗余,太少又起不到保护作用,15% 左右是个比较舒服的值。
2.4 元数据过滤:被忽视的召回精度放大器
生产级知识库和 demo 最大的区别之一,是元数据的丰富程度。demo 里文档就是一堆文本,生产里每份文档都带着来源、部门、版本、生效时间、密级这些属性。
这些元数据在检索时能发挥巨大作用。比如用户问的是"最新的报销标准",你可以在召回前先按"生效时间倒序"过滤,把过期文档直接排除掉。再比如用户是财务部门的,可以优先召回财务相关的文档。这种前置过滤比事后重排高效得多,因为它直接缩小了候选集。
我在网关层做了一个元数据过滤的 DSL,让业务方可以声明式地配置过滤条件,比如dept == 'finance' AND status == 'active'。这样检索层不用关心业务规则,只负责执行过滤,职责清晰。
3. Agent 网关:不只是转发,而是治理
3.1 网关要解决的核心问题:把散落的调用收口
在没有网关之前,我们的 Agent 调用是散落在各个业务代码里的。A 服务直接调模型 API,B 服务直接调检索接口,C 服务自己拼 prompt。结果就是:想加个限流,得改五个地方;想统计 token 消耗,得在每个调用点埋点;某个模型挂了想切备用,得挨个改配置。
Agent 网关的核心价值就是收口。所有对模型、检索、工具的调用都经过网关,网关统一处理鉴权、限流、路由、重试、日志、计费。业务方只需要关心"我要什么能力",不用关心"这个能力背后是谁提供的"。
这个思路和微服务里的 API 网关是一脉相承的,只不过 Agent 网关多了几个特殊维度:模型路由(同一个请求可以路由到不同模型)、工具编排(一次请求可能触发多个工具调用)、上下文管理(多轮对话的状态维护)。
3.2 模型路由的三种策略和它们的代价
模型路由是网关里最容易被低估的部分。很多人以为路由就是"配个权重轮询",实际上要考虑的维度多得多。
第一种是按成本路由。便宜的模型处理简单请求,贵的模型处理复杂请求。判断"简单"和"复杂"可以用请求长度、历史轮次、是否包含工具调用等信号。这种策略省钱效果明显,但代价是质量不稳定,因为简单和复杂的边界很难划准。
第二种是按能力路由。比如需要长上下文的路由到支持大窗口的模型,需要函数调用的路由到支持 tool use 的模型。这种策略质量有保障,但需要维护一张能力矩阵,模型一多维护成本就上来了。
第三种是按可用性路由。主模型超时或报错时自动切备用。这是保底策略,必须有,但要注意熔断,不能主模型一抖动就疯狂切备用,否则备用也会被打挂。
我在实际项目里是三种策略叠加使用:先按能力筛出候选模型,再按成本排序,最后按可用性做兜底。路由决策的日志一定要打全,否则出了问题根本不知道请求被路由到了哪里。
3.3 限流和降级:生产环境的保命机制
限流这件事,不做的时候觉得没必要,做的时候才发现处处是坑。
最朴素的限流是固定窗口计数,比如每分钟最多 100 次。但它有个临界问题:第 59 秒来 100 次,第 61 秒又来 100 次,等于两秒内放了 200 次。滑动窗口能缓解这个问题,但实现复杂度上来了。令牌桶则允许一定程度的突发,更适合 Agent 这种请求不均匀的场景。
我最后选的是令牌桶,桶容量设为平均 QPS 的 2 倍,填充速率按平均 QPS。这样既能扛住短时突发,又不会让长期速率超标。
降级比限流更微妙。当检索服务挂了,Agent 是直接报错,还是退化成纯模型回答?我的做法是分级降级:检索超时先重试一次,还失败就退化成不带检索的纯生成,同时在响应里标记"本次回答未使用知识库"。这样用户至少能拿到一个回答,而不是一个错误页。但降级一定要可观测,否则你会以为系统一切正常,实际上检索早就挂了。
3.4 可观测性:没有它,优化就是盲人摸象
Agent 网关的可观测性,我建议至少覆盖四个维度:
- 请求维度:QPS、延迟分布(P50/P95/P99)、错误率
- 模型维度:各模型的调用量、token 消耗、首 token 延迟
- 检索维度:召回数量、召回耗时、命中率
- 业务维度:按租户、按场景拆分的调用量和成本
这里有个经验:延迟一定要看 P99,不能只看平均值。平均值 200ms 看着很美,但 P99 可能是 5 秒,那 1% 的用户体验就是灾难。Agent 场景里,一次请求可能串行调用多个模型和工具,延迟是累加的,P99 超标往往意味着某个环节有长尾。
trace 的粒度也很关键。我建议给每次请求打一个 trace id,把网关、检索、模型调用、工具调用的 span 都串起来。这样排查问题时能一眼看出时间花在了哪。没有 trace 的时候,排查一次慢请求要翻好几个服务的日志,有了 trace 直接看火焰图。
4. 知识库和 Agent 的协同:RAG 在网关里怎么落地
4.1 RAG 不应该是一个独立服务,而应该是网关的一个能力
早期我把 RAG 做成了一个独立服务,Agent 需要检索时去调它。后来发现这样有个问题:检索和生成的上下文管理割裂了。检索服务不知道这次对话的历史,Agent 不知道检索的原始分数,两边信息不对称,导致重排和 prompt 拼接都很别扭。
后来我把 RAG 能力下沉到网关内部,作为网关的一个内置能力。Agent 发起请求时,网关根据配置决定是否触发检索,检索结果直接在网关内部完成重排和 prompt 拼接,再送给模型。这样上下文是连贯的,检索分数、元数据、历史轮次都能参与决策。
这个改动带来的直接好处是延迟降低。原来检索和生成之间有一次网络往返,现在都在网关进程内完成,省掉了序列化和网络开销。实测 P95 延迟降了大概 15%。
4.2 多轮对话里的检索 query 改写
多轮对话是 RAG 的一个难点。用户第一轮问"报销标准是什么",第二轮问"那出差呢",如果直接拿"那出差呢"去检索,召回质量会惨不忍睹,因为这句话本身信息量太低。
解决办法是query 改写:结合对话历史,把当前 query 补全成一个自包含的检索 query。上面那个例子,改写后应该是"出差报销标准是什么"。改写可以用小模型来做,成本低、延迟小。
但改写也有风险,就是改写跑偏。小模型可能把用户的原意改没了。我的做法是双路检索:改写后的 query 和原始 query 都去检索,结果合并。这样即使改写失败,原始 query 还能兜底。代价是检索次数翻倍,但检索本身比生成便宜得多,这个成本可以接受。
4.3 工具调用和检索的编排顺序
Agent 场景里,检索和工具调用经常要混在一起。比如用户问"帮我查一下上个月的销售数据,并对比一下去年同期",这需要先调数据库工具,再检索知识库里的分析模板,最后生成对比。
编排顺序直接影响效果。我的经验是先检索后工具,因为检索能提供背景知识,帮助模型更好地构造工具调用参数。但也有例外,如果工具调用的结果是检索的必要输入(比如先查到订单号,再检索这个订单的文档),那就得反过来。
网关层我做了声明式编排,业务方用配置描述"先做什么、再做什么、什么条件下跳过",网关按配置执行。这样不用写代码就能调整编排逻辑,迭代速度快很多。
5. 那些文档里不会写的踩坑记录
5.1 embedding 模型换版本导致的召回漂移
这个坑我踩得最惨。有次升级了 embedding 模型,从 v1 换到 v2,想着新版本效果更好。结果上线后召回质量不升反降。排查了半天才发现,新旧模型的向量空间不兼容,库里存的还是 v1 的向量,query 用的是 v2 的向量,两者根本不在一个空间里,算出来的相似度毫无意义。
教训是:换 embedding 模型必须全量重建索引,不能只换 query 侧。而且重建期间要有双写或灰度方案,否则服务会中断。后来我加了个校验,网关启动时检查 embedding 模型版本和索引版本是否一致,不一致直接拒绝启动,避免带病上线。
5.2 长文档截断导致的"答非所问"
模型有上下文窗口限制,超长文档必须截断。但截断的位置很讲究。如果从中间硬截,可能把结论截掉,只留下铺垫,模型基于残缺信息生成,就会答非所问。
我的做法是按重要性截断:优先保留文档的开头(通常是概述)和结尾(通常是结论),中间部分按段落重要性排序后填充。段落重要性可以用 BM25 对 query 打分来算,和 query 相关的段落优先保留。这样即使截断,保留的也是和问题最相关的部分。
5.3 网关超时设置和模型实际延迟的错配
网关的超时设置如果比模型的实际 P99 延迟还短,就会导致大量请求被误杀。我见过一个配置,网关超时设 10 秒,但某个模型在高峰期 P99 能到 12 秒,结果高峰期错误率飙升,还以为是模型服务挂了。
正确的做法是超时设置要参考实测的 P99 再加 buffer,而且不同模型、不同场景的超时应该分开配。简单问答可以设短一点,复杂推理要设长一点。同时网关要记录超时发生时的实际耗时,这样才能知道是超时设短了还是模型真的慢了。
5.4 缓存失效策略没做好导致的"旧答案"
给检索和生成加缓存能大幅降低成本,但缓存失效是个大坑。文档更新了,缓存里的旧答案还在,用户就会拿到过时信息。
我的策略是按文档版本做缓存 key。每份文档有个版本号,文档更新时版本号递增,缓存 key 里带上版本号,版本一变缓存自然失效。对于生成结果的缓存,还要考虑 prompt 模板的版本,模板改了缓存也得失效。这些版本号统一由网关管理,业务方不用操心。
6. 性能调优:从能用走向好用
6.1 检索层的并行化和批量化
检索层最容易优化的点是并行。向量召回和 BM25 召回本来就可以并行跑,没必要串行。我用 asyncio 把两路召回并发执行,整体检索延迟从两路之和变成了两路的最大值,效果立竿见影。
批量化也很重要。如果一次请求要检索多个 query(比如多轮对话的 query 改写场景),不要一个个串行查,而是攒成一批一次性查。向量数据库和 BM25 引擎通常都支持批量查询,批量查的吞吐比单条查高一个数量级。
6.2 网关层的连接池和预热
网关作为所有请求的入口,连接管理直接影响吞吐。到模型服务、检索服务、数据库的连接都要用连接池,避免每次请求都新建连接。连接池大小要按并发量调,太小会成为瓶颈,太大又会浪费资源。
还有一个容易被忽略的是预热。网关刚启动时,连接池是空的,第一批请求会慢。我的做法是启动时主动建立一批连接,把连接池填到最小空闲数,这样第一批请求也能快速响应。
6.3 成本优化的几个实操手段
Agent 的成本大头在 token 消耗。几个实测有效的优化手段:
- prompt 压缩:把冗余的 system prompt 精简,去掉重复的指令。我见过一个 system prompt 写了 2000 字,精简到 500 字后效果没变,成本降了 30%。
- 结果缓存:高频重复问题直接返回缓存,不调模型。内部问答场景里,重复问题占比能到 20% 以上。
- 小模型兜底:简单问题用小模型,只有复杂问题才升级到大模型。判断逻辑放在网关层,业务方无感。
- 流式输出:虽然不直接省钱,但能改善体验,让用户更早看到内容,减少因等待而重复发起的请求。
7. 我个人的一些经验和建议
做生产级知识库和 Agent 网关这两年,最大的体会是:别追求一步到位,要追求可迭代。一开始就设计一个完美架构,往往因为过度设计而迟迟上不了线。不如先做一个能跑的最小版本,把可观测性做扎实,然后在真实流量里发现问题、迭代优化。
另一个体会是检索质量比模型能力更重要。很多人一上来就想换更强的模型,但实际项目里,检索召回不准才是回答质量差的主因。把 BM25 和向量召回的融合调好,把分块策略优化好,效果提升比换模型明显得多,而且成本低。
最后一点是网关的配置要能热更新。生产环境里,限流阈值、路由权重、超时时间这些参数经常要调,如果每次调都要重启服务,那运维成本太高。我用的方案是配置中心加监听,配置一变网关自动生效,不用重启。这个能力在应急的时候特别有用,比如某个模型突然变慢,可以立刻调低它的路由权重,把流量切走。
还有个小技巧:给每次请求打上业务标签。比如标记这次请求来自哪个租户、哪个场景、哪个版本。这样出问题时能快速定位影响范围,做成本分析时也能按标签拆分。标签的维度不要太多,三五个就够,太多反而增加维护负担。
这套东西没有银弹,每个业务场景的取舍都不一样。但把检索、路由、限流、可观测性这几块做扎实,系统就能从"能跑"变成"敢跑"。剩下的就是在真实流量里慢慢磨了。