1. 生产级知识库与 Agent 网关的整体设计思路
1.1 为什么要把知识库和 Agent 网关放在一起做
单独做一个 RAG 知识库,或者单独做一个 Agent 调度服务,其实都不算太难。难的是把这两样东西塞进同一个生产环境里,还要保证它们在高并发、多租户、长链路调用下不互相拖垮。我最近这段时间主要就在干这件事:一边优化知识库的检索质量,一边把 Agent 网关的稳定性和可观测性补起来。
先说清楚这两个东西各自是什么。知识库在这里指的是一个面向大模型应用的检索增强生成系统,核心链路是文档入库、切分、向量化、索引、检索、重排、拼装上下文。Agent 网关则是所有 Agent 请求的统一入口,负责路由、鉴权、限流、会话管理、工具调用编排、模型切换和结果回传。把两者放在一起,是因为实际业务里 Agent 几乎必然要查知识库,而知识库的检索结果又直接影响 Agent 的回答质量。如果网关和知识库各自为政,就会出现超时叠加、上下文重复、权限穿透、计费混乱这些典型问题。
我见过不少团队的做法是:知识库用一套服务,Agent 用另一套服务,中间靠 HTTP 硬调。前期 demo 跑得挺欢,一上生产就原形毕露。原因很简单,检索和推理是两种完全不同的负载特征。检索是短平快、高 QPS、对延迟极度敏感;推理是长耗时、占显存、对并发敏感。你把它们混在一条同步链路上,任何一个环节抖动都会放大成整体雪崩。
所以我的整体设计思路是:网关做编排和治理,知识库做检索和召回,两者通过明确的内部协议通信,而不是互相直接依赖对方的实现细节。网关不关心你用的是向量检索还是 BM25,知识库也不关心请求来自哪个 Agent,只认标准化的检索请求和返回结构。这样后面换嵌入模型、换重排策略、加新的检索通道,都不会影响网关侧的逻辑。
1.2 核心链路的拆解与职责边界
把整条链路拆开看,大概是这么几段:
- 请求进入网关,完成鉴权、租户识别、限流、会话绑定。
- 网关根据 Agent 配置决定是否需要检索,需要的话构造检索请求。
- 知识库执行混合检索,返回候选片段和分数。
- 网关做重排、去重、上下文裁剪,拼装成最终 prompt。
- 调用大模型,处理流式返回,记录 token 消耗和链路耗时。
- 结果回传,同时异步写入日志和评估样本。
这里面有几个边界必须划清楚。重排放在网关还是知识库,我纠结过很久。放知识库,好处是检索侧可以统一调优,坏处是网关拿不到原始候选,做不了跨知识库的融合。放网关,灵活但会增加网关的计算负担。最后我选择折中:知识库返回带原始分数的候选集,网关做轻量重排和裁剪,重模型重排作为可选步骤放在知识库侧异步执行。这样既保留了灵活性,又不至于让网关变成计算密集型服务。
另一个边界是会话上下文和检索上下文的关系。很多实现会把历史对话直接拼进检索 query,这在小规模下没问题,但生产环境里会导致检索漂移。我的做法是网关维护一个独立的 query 改写层,用最近几轮对话生成检索用的独立 query,而不是把原始对话一股脑塞进去。这个改写层可以很轻,甚至用规则加小模型就够了。
1.3 方案选型背后的取舍逻辑
选型这块我踩过不少坑,说几个关键决策。
向量库选型。早期用过内存版的 FAISS,单机跑得飞快,但一到多副本、持久化、增量更新就露怯。后来换成支持持久化和分布式的主流向量库,代价是运维复杂度上来了。我的建议是:如果数据量在百万级以下、更新不频繁,单机加定期快照完全够用;一旦涉及多租户隔离和实时增量,就必须上支持命名空间和增量写入的方案。
检索策略。纯向量检索在语义匹配上强,但对专有名词、编号、代码符号这类精确匹配很弱。纯 BM25 反过来,精确匹配强但语义泛化差。所以生产级知识库基本都要做混合检索,把两路结果融合。融合方式我用过两种:一种是加权分数融合,简单但需要调权重;另一种是 RRF(Reciprocal Rank Fusion),对分数尺度不敏感,工程上更稳。实测下来 RRF 在跨领域数据上表现更一致,推荐优先考虑。
Agent 网关的编排方式。有人喜欢把编排逻辑写死在代码里,有人喜欢用配置驱动。我倾向于配置驱动加插件化工具注册,因为 Agent 的工具集经常变,写死代码意味着每次加工具都要发版。配置驱动的好处是运营侧可以自己调整,坏处是需要一套校验机制防止配置错误导致线上事故。
2. 知识库检索质量的核心细节与实操要点
2.1 文档切分:最容易被低估的一步
很多人把精力全花在换嵌入模型上,却忽略了切分策略。我可以很负责任地说,切分没做好,换再好的模型也救不回来。切分的核心目标是让每个 chunk 语义自洽,同时保留足够的上下文线索。
我常用的策略是结构化切分加语义切分结合。对于 Markdown、HTML 这类有明确结构的文档,优先按标题层级切,保证每个 chunk 不跨章节。对于纯文本或结构混乱的文档,用固定长度加重叠窗口,重叠比例一般取 10% 到 20%。重叠太少会丢上下文,太多会导致检索结果重复。
这里有个细节:chunk 大小不是越小越好。小 chunk 检索精度高,但拼装上下文时容易碎片化,模型拿到的信息不完整。大 chunk 上下文完整,但检索时噪声大。我的经验值是中文场景下 300 到 500 字比较均衡,英文场景 200 到 400 token。当然这要看你的文档类型,技术文档可以小一点,叙述性文档可以大一点。
还有一个容易被忽略的点是元数据保留。每个 chunk 必须带上来源文档、章节路径、更新时间、权限标签。这些元数据在检索过滤和结果展示时至关重要。我见过有人只存文本不存元数据,后面想做权限过滤或者按时间排序时只能重建索引,代价极大。
2.2 混合检索的分数融合与参数调优
混合检索的关键在于怎么把向量分数和 BM25 分数合到一起。这两路分数的量纲完全不同,向量相似度通常在 0 到 1 之间,BM25 分数则可能是任意正数。直接相加是没有意义的。
我推荐用 RRF,公式很简单:每个文档的最终分数等于它在各路结果中排名的倒数之和,再乘一个平滑常数。这个方法的妙处在于它只看排名不看原始分数,天然规避了量纲问题。参数上主要调那个平滑常数,一般取 60 左右,太小会让头部结果过于集中,太大则区分度不够。
如果非要用加权融合,那必须先做分数归一化。我一般用 min-max 归一化,但要注意归一化要在同一批候选内做,不能跨批次,否则分数不可比。权重方面,语义检索为主、关键词检索为辅的场景,向量权重给 0.7、BM25 给 0.3 是个不错的起点。但具体数值一定要用你自己的评估集去调,别照搬。
提示:混合检索的召回数量要留足余量。比如你最终要 5 个片段,那每路至少召回 20 个候选,融合后再重排取前 5。召回太少会导致融合空间不足,好结果可能在单路里排名靠后就被丢了。
2.3 重排模型的选择与部署考量
重排是提升检索精度的最后一道关卡。它的原理是把 query 和候选片段一起送进一个交叉编码器,直接输出相关性分数。相比向量检索的双塔结构,交叉编码器精度更高但速度慢得多,所以只能用在候选集上,不能用于全量检索。
选型上,中文场景我一般用专门的中文重排模型,英文场景用通用的多语言重排模型。部署时要注意几点:批处理能显著提升吞吐,把多个候选拼成一个 batch 送进去;量化能降低显存占用,但会损失一点精度,需要评估;超时控制必须做,重排服务一旦卡住会拖垮整条链路。
我的做法是给重排设一个硬超时,比如 200 毫秒,超时就降级用融合分数排序。这样即使重排服务抖动,整体链路也不会挂。这个降级逻辑一定要在网关侧实现,不能指望重排服务自己保证。
2.4 检索评估:没有评估就没有优化
优化检索质量最怕的就是凭感觉。你觉得这次改得好,可能只是碰巧。所以必须建立评估集。我的做法是从真实日志里采样 query,人工标注每个 query 的相关片段,形成黄金集。然后每次改动都跑一遍,看召回率、精确率、MRR 这些指标的变化。
评估集不用很大,几百条就能看出趋势。但一定要覆盖不同类型的 query:事实型、对比型、多跳型、模糊型。只测单一类型会误导优化方向。我吃过这个亏,早期评估集全是事实型 query,优化后指标很好看,上线后发现多跳问题依然一塌糊涂。
3. Agent 网关的实操实现与关键环节
3.1 网关的核心模块划分
Agent 网关我拆成了几个核心模块,每个模块职责单一,方便独立扩展和测试。
接入层负责协议解析和连接管理,支持流式和一次性返回两种模式。鉴权与租户模块负责识别请求归属,加载对应的配额和权限策略。路由模块根据 Agent 配置决定走哪条链路,是否需要检索、调用哪些工具、用哪个模型。编排模块负责多步调用的状态管理,包括工具调用的串行和并行。可观测模块负责埋点、日志、链路追踪和指标上报。
这几个模块之间通过内部事件总线通信,而不是直接函数调用。这样做的好处是每个模块可以独立扩缩容,也方便做灰度。比如我想换一个新的路由策略,只需要让新策略订阅请求事件,逐步切流量即可。
3.2 限流与配额的具体实现
生产环境里限流是保命的东西。我用的方案是令牌桶加滑动窗口组合。令牌桶控制瞬时突发,滑动窗口控制长期速率。两者结合既能应对突发流量,又能防止长期超用。
配额维度上,我按租户、按 Agent、按模型三个维度分别设限。租户维度防止单个客户拖垮整体,Agent 维度防止某个 Agent 配置错误导致疯狂调用,模型维度防止昂贵模型被滥用。每个维度的配额都可以动态调整,不需要重启服务。
这里有个实操细节:限流要在最外层做,越早拒绝越好。如果请求已经进了编排模块才被限流,那前面消耗的资源就白费了。所以我在接入层就做第一道粗粒度限流,在路由前做第二道细粒度限流。
注意:限流的拒绝响应要带明确的错误码和重试建议,不要只返回一个 429。客户端拿到明确信息才能做正确的退避重试,否则会疯狂重试加剧拥堵。
3.3 工具调用的编排与错误处理
Agent 调用工具是生产环境里最容易出问题的环节。工具可能超时、可能返回格式错误、可能部分成功。我的处理原则是每个工具调用都必须有超时、重试和降级。
超时设置要区分工具类型。查询类工具可以给短超时,比如 3 秒;写入类工具要给长超时,因为可能涉及事务。重试要区分幂等性,查询类可以重试,写入类不能盲目重试。降级策略要提前定义,比如检索失败时是返回空上下文还是返回缓存结果。
并行工具调用能显著降低总延迟,但要注意依赖关系。没有依赖的工具可以并行,有依赖的必须串行。我用一个有向无环图来描述工具依赖,编排模块按拓扑顺序执行。这样既保证了正确性,又最大化了并行度。
3.4 流式返回与背压处理
流式返回是提升用户体验的关键,但也是背压问题的重灾区。模型生成速度快于客户端消费速度时,如果不做背压,内存会迅速膨胀。
我的做法是在网关和客户端之间加一个带缓冲的通道,缓冲区满了就暂停从模型侧读取。同时给每个连接设一个最大缓冲上限,超过就断开并记录。这样单个慢客户端不会影响其他连接。
流式场景下还有一个坑是错误处理。流已经开始返回后如果中途出错,不能简单地返回错误码,因为 HTTP 状态码已经发出去了。我的做法是在流内发送一个结构化的错误事件,客户端解析到这个事件就知道后续内容不可信。这个约定必须在客户端和服务端之间提前对齐。
4. 常见问题与排查技巧实录
4.1 检索质量突然下降的排查路径
检索质量下降是最常见也最头疼的问题。我整理了一套排查顺序,基本能覆盖大部分情况。
| 排查项 | 检查方法 | 常见原因 |
|---|---|---|
| 索引是否完整 | 对比文档总数和索引条目数 | 增量更新失败导致部分文档缺失 |
| 嵌入模型是否变更 | 检查模型版本和配置 | 模型升级后旧向量未重建 |
| 切分策略是否调整 | 对比 chunk 数量和平均长度 | 切分参数改动导致语义断裂 |
| 检索参数是否被改 | 检查权重和召回数量 | 运营侧误改配置 |
| 数据分布是否漂移 | 抽样看新入库文档类型 | 新业务文档风格差异大 |
排查时一定要先看指标再看日志。指标能告诉你问题出在哪个环节,日志能告诉你具体是什么。我见过有人一上来就翻日志,翻半天没头绪,其实指标上早就显示是召回率掉了。
4.2 网关超时与雪崩的预防
网关超时往往不是单一原因,而是多个环节延迟叠加。我的预防策略是全链路超时预算。给整条链路设一个总超时,然后按环节分配预算,每个环节的超时必须小于剩余预算。这样任何环节超时都不会导致整体超时。
雪崩预防的核心是隔离和熔断。不同租户、不同 Agent 之间要资源隔离,一个出问题不影响其他。熔断器要按依赖维度设置,某个下游服务错误率超过阈值就快速失败,给它恢复时间。
还有一个容易被忽略的点是连接池管理。下游服务的连接池如果配置不当,高并发时会大量等待。我一般把连接池大小设为预期并发的 1.5 倍,并设置合理的获取超时。
4.3 上下文超长的处理技巧
上下文超长是 RAG 场景的经典问题。检索回来的片段加上历史对话,很容易超过模型窗口。我的处理顺序是:先裁剪历史对话,保留最近几轮和摘要;再对检索片段做去重和压缩;最后如果还超,就按相关性截断。
去重这块有个技巧:用语义去重而不是字符串去重。两个片段可能表述不同但信息重复,字符串比对发现不了。我一般用嵌入相似度做粗筛,相似度超过阈值的只保留分数高的那个。
压缩方面,可以用小模型对片段做摘要,但要注意摘要可能丢关键信息。我的做法是只对低相关性的片段做压缩,高相关性的保留原文。
4.4 多租户数据隔离的实现要点
多租户隔离做不好会出大事故。我的原则是物理隔离优先,逻辑隔离兜底。能物理隔离的(独立索引、独立存储)就物理隔离,成本高但安全。不能物理隔离的,必须在每个查询里强制带上租户过滤条件,而且这个条件不能由业务代码传入,必须由网关从鉴权信息里提取。
这里有个坑:向量检索的过滤是在检索后做的。如果先检索全量再过滤租户,不仅性能差,还可能因为候选集被其他租户占满导致本租户结果缺失。正确做法是把租户过滤下推到检索层,让向量库在检索时就只搜本租户的数据。
5. 性能优化与成本控制的实战经验
5.1 缓存策略的分层设计
缓存是降本增效的利器,但要用对地方。我做了三层缓存。
第一层是检索结果缓存,key 是 query 加租户加过滤条件,适合高频重复查询。第二层是嵌入缓存,key 是文本内容哈希,避免相同文本重复计算嵌入。第三层是模型响应缓存,只对确定性高的场景启用,比如固定模板的问答。
缓存失效策略要区分。检索结果缓存可以设短 TTL,比如 5 分钟,因为知识库更新不频繁。嵌入缓存可以长期有效,因为文本不变嵌入就不变。模型响应缓存要谨慎,涉及实时数据的场景不能缓存。
5.2 批处理与异步化的收益
很多操作可以批处理来提升吞吐。嵌入计算可以攒一批一起算,比逐条算快好几倍。日志写入可以批量提交,减少 IO 次数。指标上报可以聚合后定时发送。
异步化方面,非关键路径的操作全部异步。比如日志、评估样本采集、计费统计,这些都不应该阻塞主链路。我用一个内部队列把这些操作解耦,主链路只管返回结果。
但异步化要注意可靠性。队列可能丢消息,所以关键操作要有补偿机制。我的做法是异步操作先写本地日志,再由后台任务消费,消费失败可以重放。
5.3 模型调用的成本优化
模型调用是最大的成本项。优化手段有几个:用小模型做路由和改写,只把真正需要大模型的请求交给大模型;控制输出长度,通过 prompt 约束让模型简洁回答;复用 KV 缓存,相同前缀的请求可以复用缓存降低计算量。
还有一个技巧是分级响应。简单问题用便宜模型,复杂问题才升级到贵模型。判断复杂度可以用规则,也可以用一个小分类器。实测下来能省不少成本,而且用户体验没有明显下降。
6. 可观测性建设与线上问题定位
6.1 关键指标的定义与采集
可观测性不是日志越多越好,而是要采集对的指标。我关注的核心指标分几类。
延迟类:端到端延迟、检索延迟、模型首 token 延迟、模型总生成延迟。质量类:检索召回率、重排命中率、回答采纳率。成本类:token 消耗、模型调用次数、缓存命中率。稳定性类:错误率、超时率、熔断次数。
这些指标要按租户、按 Agent、按模型维度分别聚合,否则出了问题定位不到具体范围。采集频率上,延迟和错误率要秒级,质量和成本可以分钟级。
6.2 链路追踪的落地方式
链路追踪能让你看到单个请求在各个环节的耗时。我用的是标准的 trace 加 span 模型,每个环节一个 span,记录开始时间、结束时间、状态和关键属性。
落地时要注意采样策略。全量采集成本太高,我一般对正常请求采样 1%,对错误请求全量采集。这样既能控制成本,又不会漏掉问题请求。
span 的属性要包含足够信息用于定位,比如租户 ID、Agent ID、检索到的片段数、模型名称、token 数。但要注意不要记录敏感内容,用户 query 和文档内容要做脱敏或哈希。
6.3 告警规则的设计原则
告警设计不好会导致告警疲劳,真正的问题反而被淹没。我的原则是告警必须可行动。每条告警都要有明确的处理动作,没有处理动作的告警不如不设。
告警阈值要基于历史数据动态调整,而不是拍脑袋定死值。比如延迟告警可以用 P99 的历史分位数作为基线,超过基线一定倍数才告警。这样能适应流量的自然波动。
告警分级也很重要。P0 告警要能叫醒人,P1 告警工作时间处理,P2 告警只记录不通知。分级标准要提前和团队对齐,避免所有告警都当 P0 处理。
7. 我在这套系统上踩过的坑与个人体会
说几个印象深刻的坑。第一个是嵌入模型升级没有重建索引。当时觉得新模型更好,直接切了,结果检索质量暴跌。原因是新旧模型的向量空间不兼容,query 用新模型编码,文档还是旧模型的向量,根本对不上。后来老老实实全量重建,停机了几个小时。教训是模型升级必须配套索引重建,而且要提前规划好重建期间的降级方案。
第二个是网关的会话状态存在了本地内存。单机测试没问题,一扩容就出问题,同一个会话的请求被路由到不同实例,状态对不上。后来改成外部存储,虽然多了一次 IO,但换来了水平扩展能力。这个坑很典型,任何有状态的东西都不能存在本地内存里。
第三个是限流阈值设得太死。上线初期按预估流量设了阈值,结果业务增长后频繁触发限流,用户体验很差。后来改成动态阈值,根据系统负载自动调整,才解决了问题。限流的目的是保护系统,不是限制业务,这个平衡要把握好。
我个人在实际操作中的体会是,生产级系统的难点从来不在单个技术点,而在各个组件之间的协作和边界。知识库和网关各自做好不难,难的是它们之间的协议设计、错误传播、超时传递、权限穿透。这些边界问题在 demo 阶段暴露不出来,只有上了生产、有了真实流量才会显现。所以我的建议是,在架构设计阶段就要把这些边界想清楚,定义好协议和降级策略,而不是等出了问题再补。
最后再分享一个小技巧:给每个关键链路都准备一个开关。检索可以关,重排可以关,工具调用可以关。出问题时能快速降级到最简链路,先保住可用性,再慢慢排查。这个开关机制在几次线上故障里救过我,比任何复杂的容错逻辑都管用。