1. 生产级知识库与 Agent 网关的整体设计思路
1.1 为什么要把知识库和 Agent 网关放在一起做
单独做一个 RAG 知识库,或者单独做一个 Agent 网关,这两件事在 Demo 阶段都不难。难的是把它们放到生产环境里,让它们协同工作,还要保证延迟、成本、可观测性都在可控范围内。我最近这段时间主要就是在干这件事,踩了不少坑,也积累了一些还算靠谱的经验。
先说清楚这两个东西各自是什么。知识库负责把非结构化的文档、网页、PDF、Markdown 等资料,经过切块、向量化、索引之后,变成可以被检索的知识源。Agent 网关则是所有大模型调用的统一入口,它要处理路由、鉴权、限流、重试、缓存、日志这些事情。把两者放在一起,是因为 Agent 在回答用户问题时,往往需要先检索知识库,再基于检索结果生成回答,这个链路如果各自为政,排查问题会非常痛苦。
我见过太多团队的做法是:知识库用一套服务,Agent 调用用另一套服务,两边日志格式不一样,trace id 对不上,出了问题只能靠猜。所以我的核心设计思路是——把知识库检索也当成网关的一个上游能力来对待,统一入口、统一日志、统一超时控制。
1.2 核心链路的拆解
一条完整的请求链路,从用户提问到最终回答,大致经过这几个环节:
- 用户请求进入 Agent 网关,网关做鉴权和限流
- 网关判断这次请求是否需要检索知识库
- 如果需要,调用知识库的检索接口,拿到候选文档片段
- 对候选片段做重排序(rerank),筛掉不相关的
- 把筛选后的片段拼进 prompt,调用大模型
- 大模型返回结果,网关做后处理和日志记录
- 返回给用户
这个链路里,第 3 步和第 4 步是最容易出问题的地方。检索召回不准,后面再好的模型也救不回来;重排序太慢,整体延迟就上去了。所以我在设计时,把这两步单独拎出来做了性能预算。
1.3 方案选型背后的考量
关于检索方案,我最终选择了BM25 + 向量检索的混合方案,而不是纯向量检索。原因很直接:纯向量检索在语义相似度上表现好,但对精确匹配的关键词、专有名词、编号这类内容经常翻车。比如用户问"XX-2024-001 这个工单的处理流程",纯向量检索很可能召回一堆语义相近但编号不对的文档。BM25 在这种场景下就是补位选手,它对词频和逆文档频率敏感,精确匹配能力强。
混合检索的常见做法是两路召回后做融合,融合算法我用的是 RRF(Reciprocal Rank Fusion),因为它不需要两路分数的量纲对齐,实现简单,效果稳定。这个后面会详细讲。
Agent 网关这边,我选择自己写一层薄薄的网关,而不是直接用现成的 API 网关。原因是现成网关对 LLM 场景的特殊需求支持不够,比如 token 计费、流式响应的中断处理、多模型路由这些,自己写反而更灵活。
2. 知识库构建的核心细节与实操要点
2.1 文档切块策略:切块大小不是拍脑袋定的
切块(chunking)是知识库构建里最容易被忽视、但影响最大的环节。我见过有人直接把整篇文档塞进去,也见过有人按固定 512 字符硬切。这两种做法在生产环境都会出问题。
我的切块策略是这样的:
- 按语义边界切:优先按段落、标题、列表项切,而不是按字符数硬切
- 控制块大小在 300-800 token 之间:太小会丢失上下文,太大会稀释语义
- 保留重叠:相邻块之间保留 10%-15% 的重叠,避免边界处的信息被切断
- 给每个块加上元数据:来源文档、章节标题、页码、更新时间
为什么是 300-800 token?这是实测下来的经验值。低于 300 token 的块,往往只包含半句话,检索出来也没法用;高于 800 token 的块,向量化之后语义被平均掉了,检索精度反而下降。当然这个值跟你的文档类型有关,技术文档可以偏小,叙述性文档可以偏大。
元数据这块我要特别强调。没有元数据的知识库,在生产环境基本没法用。因为用户问的问题往往带有时间、来源、版本这些约束,没有元数据你就没法做过滤。比如用户问"最新的部署流程是什么",你得能按更新时间过滤。
2.2 向量化模型的选择
向量化模型的选择,我主要看三个维度:中文效果、维度大小、推理成本。
维度大小直接影响存储和检索速度。768 维和 1536 维,在百万级文档下,存储和检索延迟差距是肉眼可见的。我的建议是,如果中文场景为主,优先选针对中文优化过的模型,不要盲目追求维度高。
推理成本这块,如果文档量大,建议做批量向量化,并且把向量化任务做成异步的。我一开始是同步做的,结果一次全量重建索引把服务拖垮了,后来改成队列 + 批量处理才稳定下来。
注意:向量化模型一旦选定,后续换模型的成本极高,因为所有文档都要重新向量化。所以选型时一定要留足评估时间,别急着上线。
2.3 BM25 索引的构建与参数调优
BM25 的实现我用的是常见的开源库,核心参数是两个:k1 和 b。
- k1控制词频饱和度,默认 1.2-2.0。值越大,词频的影响越强
- b控制文档长度归一化,默认 0.75。值越大,长文档的惩罚越重
我的调参方法是:先固定 b=0.75,在验证集上网格搜索 k1;然后固定最优 k1,再搜 b。验证集用真实的用户查询和标注的相关文档构建,规模不用大,几百条就够看出趋势。
中文场景下,BM25 还需要处理分词问题。我用的分词方案是结合词典的,因为通用分词器对专有名词的切分经常不准。词典里我会把业务相关的术语、产品名、编号规则都加进去,这样 BM25 的召回质量会明显提升。
2.4 混合检索的融合策略
两路召回之后,怎么融合是个关键问题。我试过几种方案:
| 融合方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 加权求和 | 实现简单 | 需要分数归一化,量纲难对齐 | 两路分数分布接近时 |
| RRF | 无需归一化,稳定 | 丢失分数绝对值信息 | 通用场景,推荐 |
| 学习排序 | 效果最好 | 需要标注数据,成本高 | 有充足标注时 |
我最终用的是 RRF,公式很简单:每个文档的得分是1/(k + rank)的累加,k 一般取 60。这个方案的好处是,不管两路召回的分数是什么量纲,只看排名,融合结果都很稳定。
RRF 之后,我会再取 Top-N 做重排序。重排序模型比向量检索慢,但精度高,所以只对少量候选做。这个"先粗排后精排"的两阶段思路,是生产环境的标配。
3. Agent 网关的实现与关键环节
3.1 网关的核心职责划分
Agent 网关不是一个简单的反向代理,它要承担这些职责:
- 统一鉴权:所有模型调用都从这里走,API Key 统一管理
- 模型路由:根据请求特征路由到不同模型,比如简单问题走小模型,复杂问题走大模型
- 限流与配额:按用户、按租户做限流,防止单个用户打爆服务
- 重试与降级:上游模型超时或报错时,自动重试或降级到备用模型
- 缓存:相同或相似的请求直接返回缓存结果
- 日志与追踪:记录完整的请求链路,方便排查
这六件事里,模型路由和缓存是省钱的两大杀器。我实测下来,加上缓存之后,重复请求的模型调用成本能降 30% 以上。
3.2 模型路由的实现细节
模型路由的规则我分了三层:
- 按请求类型路由:检索类请求走便宜模型,生成类请求走强模型
- 按复杂度路由:先用一个轻量分类器判断问题复杂度,再决定用哪个模型
- 按降级策略路由:主模型不可用时,自动切到备用模型
复杂度分类器我用的是一个小模型,输入是用户问题,输出是"简单/中等/复杂"三档。这个分类器不需要很准,只要能把明显简单的问题分流出去就行。实测下来,简单问题占比通常在 40% 左右,这部分走小模型能省不少钱。
提示:路由规则一定要可配置,不要硬编码在代码里。因为模型的价格和能力变化很快,硬编码会导致每次调整都要发版。
3.3 流式响应的处理
LLM 的流式响应(streaming)在生产环境处理起来比想象中麻烦。主要问题有三个:
- 中断处理:用户关闭页面时,要能及时中断上游请求,避免浪费
- 超时控制:流式响应没有明确的结束时间,需要设置首字节超时和总超时
- 错误恢复:流到一半上游报错,要能优雅地告诉前端
我的做法是,网关层维护一个请求上下文,记录每个流式请求的状态。用户断开连接时,通过上下文取消上游请求。首字节超时设 10 秒,总超时按模型的最大输出长度估算。
3.4 缓存策略的设计
缓存这块,我分了三级:
- 精确缓存:请求参数完全一致时命中,用哈希做 key
- 语义缓存:请求语义相似时命中,用向量相似度判断
- 片段缓存:知识库检索结果缓存,因为检索比生成便宜但也不免费
精确缓存最简单,命中率也最高,但只对完全重复的请求有效。语义缓存命中率更高,但有误判风险,需要设置较高的相似度阈值。片段缓存是我觉得性价比最高的,因为很多问题的检索结果是重叠的。
缓存的失效策略也要考虑。知识库更新后,相关的缓存要失效。我的做法是给缓存加上知识库版本号,版本变了就整体失效。
4. 常见问题与排查技巧实录
4.1 检索召回不准的排查思路
检索召回不准是最常见的问题,排查时我一般按这个顺序来:
- 先看原始文档有没有被正确切块:切块错了,后面全错
- 再看向量化有没有问题:拿一个已知相关的查询,看它的向量和文档向量的相似度
- 然后看 BM25 分词:专有名词有没有被切碎
- 最后看融合和重排序:是不是融合把好的结果排下去了
我遇到过一个典型案例:用户问某个产品型号的问题,检索总是召回不相关的内容。排查后发现是分词器把这个型号切成了几段,BM25 完全匹配不上。把型号加进自定义词典后,问题就解决了。
4.2 延迟过高的优化路径
延迟高的时候,先做链路拆解,看时间花在哪一段。我一般会打点记录这几个时间:
- 网关接收请求到发出检索请求
- 检索耗时(向量检索 + BM25 + 融合 + 重排序)
- 模型调用耗时(首字节 + 总时长)
- 后处理耗时
实测下来,延迟大头通常在模型调用和重排序。模型调用能优化的空间有限,主要是选更快的模型或者做流式。重排序可以优化,比如减少候选数量、用更轻量的重排序模型、或者对简单查询跳过重排序。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 检索结果不相关 | 切块不合理/分词错误 | 检查切块边界和分词结果 | 调整切块策略,补充词典 |
| 响应延迟高 | 重排序慢/模型慢 | 分段打点 | 减少候选,换轻量模型 |
| 缓存命中率低 | 请求参数不稳定 | 看缓存 key 分布 | 归一化请求参数 |
| 流式响应中断 | 超时设置不当 | 看超时日志 | 调整首字节和总超时 |
| 成本超预算 | 路由规则太粗 | 看模型调用分布 | 细化路由,加缓存 |
4.4 几个踩过的坑
坑一:向量维度和索引不匹配。换向量化模型时,忘了重建索引,导致检索结果全是乱的。这个坑很隐蔽,因为服务不报错,只是结果不对。
坑二:BM25 索引没更新。知识库更新了文档,但 BM25 索引没重建,导致新文档检索不到。后来我把索引更新做成了文档更新的触发动作。
坑三:网关重试导致重复计费。上游超时后网关自动重试,但上游其实已经处理了,结果计费了两次。后来加了幂等 key 才解决。
坑四:缓存穿透。大量不存在的查询打到后端,缓存完全没起作用。加了空结果缓存和布隆过滤器之后缓解了。
5. 生产环境的可观测性建设
5.1 日志与追踪的设计
生产环境没有可观测性,等于闭着眼睛开车。我的日志设计遵循几个原则:
- 每个请求一个 trace id,贯穿网关、检索、模型调用全链路
- 结构化日志,用 JSON 格式,方便后续分析
- 关键节点打点,记录耗时、token 数、命中缓存等
trace id 的传递我用的是标准的 header 透传,这样跨服务也能串起来。日志里我会记录请求的摘要(不记录完整内容,避免隐私问题)、模型名称、token 消耗、耗时这些。
5.2 关键指标的监控
我重点监控这几个指标:
- QPS 和错误率:基础健康指标
- P50/P95/P99 延迟:看长尾,P99 往往暴露问题
- 缓存命中率:直接影响成本和延迟
- token 消耗:按模型、按租户统计,控制成本
- 检索召回率:需要标注数据,定期评估
这些指标我会做成看板,设置告警阈值。比如 P99 延迟超过 5 秒就告警,缓存命中率低于 20% 也告警。
5.3 成本控制的实操经验
成本控制这块,我总结了几个有效的手段:
- 缓存优先:能缓存的都缓存,尤其是检索结果
- 路由分流:简单问题走小模型,实测能省 30%-40%
- prompt 精简:检索片段不要全塞进去,重排序后只留最相关的
- 输出长度限制:设置 max_tokens,避免模型啰嗦
- 批量处理:非实时任务批量调用,有些模型批量有折扣
这几个手段叠加下来,我的整体成本比最初降了大概一半。当然前提是效果不能降,所以每次优化都要做效果回归。
6. 后续可以继续深挖的方向
这套东西跑起来之后,我还在继续优化几个方向。一个是Agentic RAG,就是让 Agent 自己决定要不要检索、检索几次、怎么改写查询,而不是固定的一次检索。这个方向效果提升空间大,但控制复杂度也高,需要仔细设计。
另一个是知识库的自动更新和版本管理。现在文档更新还是半自动的,理想状态是文档一变,索引自动重建,缓存自动失效,全程不用人工干预。
还有就是多租户隔离。现在租户之间的数据和配额隔离做得还不够细,后续要加上更严格的隔离机制。
这些方向我还在摸索,有新的进展再分享。这套架构不是一蹴而就的,是一点点迭代出来的,每个环节都踩过坑。如果你也在做类似的事情,希望这些经验能帮你少走点弯路。