1. 生产级知识库和 Agent 网关到底在解决什么问题
先把场景摆出来。你手头有一套 RAG 知识库,可能是 Dify 流水线拉的,也可能是 Ollama + LangChain + Chroma 自己拼的,文档进了向量库,检索也能跑通。然后你接了一个 Agent,让它去查知识库回答问题。Demo 阶段一切正常,一旦上生产,问题就来了:检索回来的片段答非所问、Agent 反复调用同一个工具、多个业务线共用一个网关导致互相挤占、某次文档更新后整个索引质量断崖式下跌。
这些问题的根源,往往不在模型本身,而在知识库的检索质量和 Agent 网关的调度策略这两层。我最近做的优化,核心就是把这两层拆开看:知识库负责"能不能找到对的料",网关负责"找到料之后怎么稳定地喂给模型并管住它的行为"。
这篇文章适合两类人看。一类是已经把 RAG 跑起来、但效果不稳定想深挖原因的人;另一类是正在设计 Agent 网关、纠结工具路由和限流怎么做的人。我会把踩过的坑、参数怎么定、为什么这么定,都摊开讲。基础概念不绕弯子,但重点放在生产环境才会暴露的那些细节上。
先给一个整体判断:知识库和网关是两个独立的失效域,必须分开监控、分开调优。把它们混在一起调,你会永远搞不清到底是检索错了还是调度错了。这是我优化过程中第一个、也是最值钱的结论。
2. 知识库检索质量:从"能搜到"到"搜得准"
2.1 分块策略决定了检索的天花板
很多人搭知识库第一步就是调 embedding 模型,觉得换个更强的模型效果就好了。实测下来,分块策略对最终效果的影响远大于 embedding 模型的选择。原因很简单:embedding 只能在你切好的块上做语义匹配,块切得不对,再强的模型也救不回来。
我试过三种典型切法,对比很明显:
| 分块方式 | 块大小 | 适用场景 | 实测问题 |
|---|---|---|---|
| 固定长度 | 512 token | 结构松散的散文 | 句子被拦腰截断,语义残缺 |
| 按标题层级 | 动态 | 有清晰层级的文档 | 层级过深时块过大 |
| 语义分块 | 动态 | 技术文档、规范 | 计算开销高,边界不稳定 |
我最后采用的是标题层级 + 长度兜底的混合策略:优先按 Markdown 标题切,遇到单个块超过 800 token 就按段落二次切分,同时给每个块加上它所属的标题路径作为前缀。这个前缀很关键,它让一个孤立的段落重新获得了上下文。比如一个讲"限流阈值"的段落,前缀带上"Agent 网关 > 稳定性保障",检索时语义就完整了。
提示:分块大小没有万能值。中文技术文档我实测 400 到 800 token 之间比较稳,英文可以到 1000。别照搬别人的数字,拿你自己的文档跑一轮召回率测试再定。
2.2 混合检索不是可选项,是必选项
纯向量检索有个致命弱点:对精确匹配的关键词不敏感。用户问"maxkb 知识库怎么配置",向量检索可能返回一堆讲"知识库配置"的通用内容,却漏掉真正提到 maxkb 的那篇。反过来,纯关键词检索又抓不住语义相近但用词不同的情况。
所以生产级知识库我强烈建议上混合检索:向量召回 + BM25 关键词召回,然后用 RRF(Reciprocal Rank Fusion)融合。RRF 的好处是不需要调权重,直接按排名倒数相加,鲁棒性很好。公式很简单:
score = Σ 1 / (k + rank_i)k 一般取 60。这个值我试过 40 到 80,差异不大,60 是个稳妥的默认值。融合之后再过一个 rerank 模型精排,取 top 3 到 5 个块喂给模型。注意,rerank 之后的块数量不要贪多,喂太多反而会稀释关键信息,还会挤占上下文窗口。
2.3 检索质量的可观测性怎么建
优化最怕的是"感觉变好了"。我给自己搭了一套检索质量监控,核心是三个指标:
- 召回率:构造一批标准问答对,看正确答案所在的块有没有被召回进 top K。
- 命中位置:正确答案排在第几位,位置越靠前越好。
- 空召回率:检索结果为空或全部低于阈值的比例,这个指标飙升通常意味着索引出问题了。
具体做法是维护一个几十条的评测集,每次改分块或换模型就跑一遍。别小看这几十条,它能帮你挡住 80% 的"优化反而变差"的情况。我踩过一次坑:换了个新 embedding 模型,主观感觉回答更流畅了,结果评测集一跑,召回率掉了 12 个百分点,原因是新模型对中文长文本的截断处理不一样。
3. Agent 网关:管住工具调用和流量这两件事
3.1 网关的核心职责边界
Agent 网关听起来玄乎,拆开看其实就干两件事:工具路由和流量治理。工具路由是决定 Agent 的某次请求该调用哪个工具、传什么参数;流量治理是保证多个 Agent、多个业务线共用后端时不互相拖垮。
我见过不少项目把网关做成了一个巨大的 if-else 分发器,工具一多就维护不动。正确的做法是把工具描述结构化,让路由基于描述而不是硬编码。每个工具注册时带上:名称、功能描述、参数 schema、适用场景关键词。路由时先用关键词粗筛,再用一个小模型或向量匹配做精排。这样加工具只需要注册,不用改路由代码。
3.2 工具调用的幂等和超时设计
Agent 最容易出的事故是重复调用。模型有时候会连续两次调用同一个工具,参数还一样。如果这个工具是写操作(比如往知识库写数据),就会产生脏数据。解决办法是给每次工具调用生成一个基于"工具名 + 参数哈希"的幂等键,网关层做去重,短时间内相同键的调用直接返回缓存结果。
超时设计同样重要。我给的默认值是:检索类工具 3 秒,生成类工具 30 秒,写操作 10 秒。超过就中断并返回明确的错误信息给 Agent,让它决定是重试还是换策略。这里有个细节:超时后不要直接抛异常,要返回结构化的失败结果,否则 Agent 拿到一个异常字符串会不知所措,容易陷入死循环。
# 工具调用的幂等 + 超时封装示意 import hashlib, time from functools import wraps def idempotent_tool(timeout=3): cache = {} def decorator(func): @wraps(func) def wrapper(tool_name, params): key = hashlib.md5( f"{tool_name}{sorted(params.items())}".encode() ).hexdigest() if key in cache and time.time() - cache[key]['ts'] < 60: return cache[key]['result'] start = time.time() result = func(tool_name, params) if time.time() - start > timeout: return {"status": "timeout", "retryable": True} cache[key] = {"result": result, "ts": time.time()} return result return wrapper return decorator3.3 多租户下的限流与隔离
当知识库和网关要服务多个业务线时,必须做租户隔离。我采用的是令牌桶限流,每个租户一个桶,桶容量按业务重要性分配。关键点是:限流要分维度,不能只按请求数限,还要按 token 消耗量限。因为一次长文档检索消耗的资源可能是短查询的几十倍。
隔离还有一层是故障隔离。某个租户的慢查询不能拖垮整个网关。做法是给每个租户的请求走独立的线程池或协程池,池满就快速失败,返回 429 让上游退避重试。这个"快速失败"比"排队等待"在生产里更安全,因为排队会让延迟雪崩。
4. 知识库和网关联调时最容易踩的坑
4.1 检索结果格式和 Agent 预期的错位
这是联调阶段最高频的问题。知识库返回的是一堆带 score 的文本块,Agent 期望的是结构化的答案或明确的引用来源。中间如果没有一层格式化,Agent 就会把 score 当成内容的一部分,或者把多个不相关的块硬拼在一起。
我的做法是在网关里加一个结果规整层:把检索块按 score 排序,去掉低于阈值的,合并相邻的块,最后输出成"答案片段 + 来源列表"的结构。来源列表很重要,它让 Agent 能给出引用,也方便你排查问题。
4.2 上下文窗口被检索结果撑爆
Agent 的上下文窗口是有限的。如果检索返回 10 个块,每个 800 token,光检索结果就 8000 token,再加上对话历史和系统提示,很容易超限。超限之后模型要么报错,要么悄悄截断,导致关键信息丢失。
解决办法是动态控制检索块数量。根据当前对话已占用的 token 数,反推还能塞多少检索内容。我设的规则是:检索内容最多占上下文窗口的 40%,剩下的留给对话历史和生成。这个比例可以根据任务类型调,纯问答可以高一些,多轮对话要低一些。
4.3 索引更新和线上查询的冲突
知识库文档更新时,如果直接重建索引,线上查询会短暂不可用。我踩过一次坑:白天更新文档,重建索引花了 20 分钟,这期间所有查询都返回旧数据或报错。
正确做法是双索引切换:新文档写入新索引,构建完成后原子切换指针,旧索引延迟删除。这样线上查询始终可用。如果用的是支持增量更新的向量库,那就更简单,但要小心删除操作——很多向量库的删除是软删除,需要定期做 compaction,否则索引会越来越臃肿,检索变慢。
5. 我实际用下来的一些参数和心得
5.1 一套可复用的默认参数
经过几轮调优,我沉淀了一套默认参数,新项目直接拿来用,再按实际情况微调:
| 环节 | 参数 | 默认值 | 调整方向 |
|---|---|---|---|
| 分块 | 块大小 | 600 token | 文档密集调小 |
| 分块 | 重叠 | 80 token | 一般不超 15% |
| 检索 | 向量 top K | 20 | 召回不足调大 |
| 检索 | BM25 top K | 20 | 同上 |
| 融合 | RRF k | 60 | 基本不用动 |
| 精排 | rerank top N | 5 | 上下文紧张调小 |
| 网关 | 检索超时 | 3s | 慢库调大 |
| 网关 | 幂等窗口 | 60s | 按业务调 |
5.2 评测集比调参更重要
我最大的心得是:别在没有评测集的情况下调参。你会陷入"改一个参数,感觉好了,再改一个,又感觉差了"的循环。花半天时间构造 50 条标准问答对,标注好正确答案所在的文档块,之后每次改动都跑一遍,用数据说话。这半天能省下你后面几十个小时的瞎调。
评测集的构造也有讲究:要覆盖高频问题、边界问题(比如问一个知识库里没有的东西)、以及容易混淆的问题(两个相似概念)。我一般按 6:2:2 的比例分配。
5.3 日志要记到能复现问题的粒度
生产环境出问题,最怕的是日志不够。我的日志里必记这几样:原始 query、改写后的 query、召回的块 ID 和 score、rerank 后的顺序、最终喂给模型的完整 prompt、工具调用链路、每步耗时。有了这些,任何一个 bad case 都能完整复现。
注意:日志里可能包含敏感的业务数据,落盘前要做脱敏,尤其是知识库里的原文内容。我一般只记块 ID 和摘要,不记全文。
5.4 关于模型选择的取舍
embedding 模型和 rerank 模型的选择,我的原则是优先选中文表现好、且能本地部署的。原因有两个:一是数据不出域,二是延迟可控。云端 API 虽然省事,但网络抖动会直接影响检索延迟,生产环境里这种不确定性很致命。本地部署的话,用 Ollama 拉一个中文 embedding 模型,配合一个轻量 rerank 模型,在普通 GPU 上就能跑,性价比很高。
至于生成模型,反而可以灵活一些,因为生成不是瓶颈,检索才是。生成模型可以按成本和效果权衡,甚至不同租户用不同的模型。
6. 后续可以继续深挖的方向
这套东西跑稳之后,我还在琢磨几个方向。一个是查询改写,把用户口语化的提问改写成更适合检索的形式,实测能提升不少召回率,但改写本身也会引入误差,需要评测集盯着。另一个是反馈闭环,把用户对回答的点赞点踩收集起来,反哺到 rerank 模型的微调上,让检索越来越懂业务。
还有一个是知识库的自动巡检,定期跑一批探针问题,发现召回率下降就告警。这个对文档频繁更新的场景特别有用,能第一时间发现索引质量问题,而不是等用户投诉。
这些方向我还在试,有结论了再单独写。眼下这套知识库加网关的组合,已经在几个业务线上稳定跑了几个月,最大的感受就是:把检索和调度分开治理,问题定位的效率会高一个数量级。以前一个 bad case 要查半天,现在看日志就能定位到是分块问题、检索问题还是网关调度问题,改起来有的放矢。