☰
生产级RAG知识库重构与Agent网关建设实践
2026/9/25 3:43:21 网站建设 项目流程

1. 为什么我开始折腾这条链路

先说背景。我们团队从去年开始一直在做企业内部的私有知识库,核心诉求只有一个:把散落在 Confluence、钉钉文档、本地 Markdown 和各类 PDF 里的业务知识,整合成一个能回答问题的 AI 助手。最初用了一版简单的 RAG 方案,向量库检索出来什么就答什么,线上跑了一阵子之后发现几个很致命的问题。

第一是检索质量不稳定。同一个问题,关键词换个说法,召回的内容就完全不一样,经常答非所问。第二是权限彻底失控。知识库里包含了不同部门的文档,按理说销售不该查到财务的报价策略,但当时的架构里所有员工共享同一个向量索引,权限只能在应用层做一些粗糙的过滤,稍微复杂一点的提问就能绕过限制。第三是Agent 网关这个环节基本是空白的。所谓 Agent 其实只是套了一层提示词的聊天机器人,根本没有路由、鉴权、限流、可观测这些网关该有的能力。

说白了,传统的 web 网关管的是请求转发,AI 网关管的是大模型调用、提示词注入、知识库路由和 Agent 编排。这两者是完全不同的东西。这次的优化项目,就是想把这套链路重新梳理一遍,让生产级知识库真正能扛住企业场景下的高并发、权限隔离和审计需求。

如果你正在做类似的事,比如搭 Obsidian 知识库、Dify 流水线,或者用 RAG 做企业助手,这篇文章里提到的思路和坑应该对你有参考价值。我不会讲太虚的架构理论,重点是我实际改了什么、为什么这么改、改完踩了哪些坑。

2. 生产级知识库的检索链路重构

2.1 之前在检索上犯的错

先说检索。旧方案是典型的“分块 + Embedding + Top-K 召回”,看起来没毛病,实际跑起来问题一大堆。文档按固定长度切块,比如每 500 个 token 一刀,切出来的块可能处在段落中间,语义被切断,召回质量自然上不去。更麻烦的是,当时用的是单一的向量检索,没有做任何检索前的查询改写和检索后的重排。

我举个例子。有一份内部运维手册,里面写着“如果 Redis 连接数达到上限,需要调整 maxclients 参数”。用户提问“Redis连不上怎么办”,向量检索出来的东西可能是一堆讲 Redis 集群部署的段落,因为“连接”“集群”“部署”这些词在向量空间里靠得很近,但并不是用户真正想要的故障处理办法。这就是没有语义对齐的典型表现。

后来我把检索链路改成了四段式:

  1. 查询改写:先用一个小模型对用户输入做意图识别和关键词扩展。比如“Redis连不上”会补出“maxclients”“连接数上限”“拒绝连接”等术语。这一步非常重要,能让后续检索在同一个语义空间里找东西。
  2. 混合检索:向量召回和 BM25 关键词召回并行跑。向量负责语义相似,BM25 负责精确匹配。企业知识库里大量存在“系统名称、报错代码、版本号”这种精确信息,光靠向量检索很容易丢,加一路 BM25 能兜底。
  3. 重排:两路召回合并之后,候选集大概有 100 条左右,再用重排模型精排。我当时用的是 BGE-Reranker,部署成本不高,效果提升非常明显。重排的目的不是找最相似的,而是找最“有用”的。
  4. 动态上下文压缩:把重排结果里的冗余内容去掉,只保留与问题高度相关的片段,避免把整篇文档塞进大模型的上下文窗口。这一步对成本影响也很大。

2.2 分块策略的最终形态

分块这件事,网上聊得很多,但真正有指导意义的实验很少。我最终是按照“层级文档结构优先切块”的方式处理的。

  • 如果有 Markdown 标题或 PDF 章节结构,先按标题把文档拆成二级章节;
  • 章节内部如果超过 800 token,再按段落边界切,而不是硬按 token 数切;
  • 每个块保留上下文的路径信息,比如“/运维手册/数据库分册/Redis 章节/常见故障”,这样检索之后可以给大模型提供结构化上下文;
  • 同一章节内的相邻块做 10%-15% 的文本重叠,防止检索时漏掉边界内容。

这套方案跑下来,召回准确率提升了大概 30% 左右。当然,不同知识库的文档结构差异很大,你不能指望一套分块策略通吃所有场景。如果你们的知识库大量是短文本,比如工单记录、聊天记录,那切块策略又得换。

2.3 知识库权限隔离的落地

这是这次优化的核心需求之一,也是最容易被忽视的。老板一开始说“知识库权限很简单,文档系统有权限,你们照着做就行”,但是文档系统的权限是“谁能看这篇文档”,而 RAG 的权限问题远比这个复杂——你没法在检索阶段精准控制“谁能看到哪个文本片段”。

我最终采用的方案是知识域隔离 + 文档级标签过滤:

  • 把知识库按业务域(销售、技术、财务、人事)物理拆成多个索引空间;
  • 每个索引空间内的文档再打标签,比如“机密”“内部”“公开”;
  • 用户的请求进来时,先根据用户角色确定可访问的知识域集合;
  • 检索时在查询条件里硬性拼接域 ID 和标签白名单,而不是检索完再过滤。

这个顺序很关键。先过滤再检索,能保证被过滤掉的片段根本不会进入候选集,从机制上杜绝了权限绕过的问题。旧方案是检索全量索引再在后端做过滤,实测发现有些高权限知识片段会通过向量相近的路径被召回,只要过滤逻辑漏了一条,机密信息就出去了。重构之后这个漏洞算是堵死了。

3. Agent 网关到底在解决什么问题

3.1 从“套壳聊天机器人”到真正的 Agent 网关

前面说的是知识库部分,接下来说 Agent 网关。我之前写过一篇文章,把 AI 网关和传统网关做过对比,核心观点是:传统网关站在用户和微服务之间,AI 网关站在应用和大模型/知识库/工具之间。

当时我们搭的第一版 Agent 网关非常简陋——FastAPI 写了一个路由层,每个 Agent 是一个固定 prompt 的接口,用户请求进来后按关键词路由到对应接口,然后调大模型。这个架构在生产环境里跑了两周就出问题了:销售部门的 Agent 和运维部门的 Agent 用的是同一个大模型 API Key,没有分别限流,某个部门一搞活动,流量全打过来,其他部门的 Agent 全部超时。

所以这次优化,我给 Agent 网关定了四个核心能力,基本是按照企业级 API 网关的标准来要求的:

  1. 路由:不只是简单的关键字匹配,而是根据用户意图、知识域、工具能力做多级路由。
  2. 鉴权与租户隔离:每个业务方一套 API Key,可以在网关卡住未授权的调用。
  3. 限流与配额:针对每个 Agent、每个租户、每个模型分别做限流,防止互相干扰。
  4. 可观测:记录每一次调用的模型、Token 消耗、耗时、召回片段、最终答案,方便追溯。

3.2 网关的核心编排逻辑

网关不只是一个转发层,它还要负责任务的拆分与编排。比如一个典型的请求是“帮我写一封给客户的周报邮件,总结这周服务器的变更”。这背后涉及:查知识库(变更记录)、生成邮件草稿、调用企业内部通讯录(获取客户联系人)。在网关层,我定义了一个编排器,把这个请求拆成三个子任务,并按依赖关系串起来:先查变更记录,再查通讯录,最后生成邮件。

用代码大致描述一下编排逻辑:

def handle_request(user_id, prompt): intent = router.classify(prompt) if intent == "weekly_report": change_records = knowledge_base.search( query=prompt, domain="ops", user_id=user_id ) contacts = address_book.lookup(user_id) result = llm.generate( prompt_template="weekly_report", context={"changes": change_records, "contacts": contacts} ) return result

这个逻辑看起来简单,但生产环境里有一个很隐蔽的坑:编排器的每一步都可能失败。知识库检索超时怎么办?通讯录接口没返回怎么办?之前我只做了整体请求的超时控制,结果经常出现“整个请求超时了但子任务还在跑”的情况,资源被白白占用。后来我改成每步独立的超时控制和错误重试,编排器本身只负责串流程,不负责兜底。兜底逻辑放到每个子任务的执行器里。

3.3 网关与 Agent 框架的关系

很多人分不清 Agent 网关和 Agent 框架的区别。我拿一个简单类比解释:Agent 框架相当于给你一套积木和图纸,告诉你可以搭房子;Agent 网关则是房子门口的门禁和交通指挥,决定谁可以进来、走哪条路、能不能同时进来一百个人。

具体到技术选型,现在市面上的 Agent 框架很多,比如 LangChain、Dify 的 Agent 节点、字节的 Coze、开源的 MetaGPT 等。但我们做的是生产级系统,框架只是底层执行单元,网关才是统一入口。你可以在网关后面接不同的 Agent 框架,甚至可以同时挂 LangChain 的 Agent 和 Dify 的流水线,通过网关做统一路由和鉴权。

我在实际部署中发现,网关层不能对 Agent 内部的工具链做太多假设,否则耦合会很严重。比如 Dify 的 Agent 内部自己会调用知识库检索,但如果你在网关层又给它传了一份检索结果,就会产生冲突。后来我明确了一个约定:网关负责上下文准备和权限校验,Agent 框架负责推理与工具调用;如果 Agent 框架有内置检索能力,需要在网关层显式禁用,避免重复检索。

3.4 网关部署形态:从单体到独立服务

第一版网关是跟 Flask 应用放在一起的进程内模块,后来单独拆成了一个无状态服务。为什么拆?因为网关需要水平扩展,而 Flask 应用是有状态的(比如会话管理)。拆成独立服务后,我用 Nginx 做负载均衡,网关服务本身没有本地状态,所有的路由配置、限流配额都放在 Redis 里,方便动态调整。

网关的配置管理我也踩了不少坑。一开始限流策略写死在代码里,每次调整都要发版;后来改成配置中心管理,把限流阈值、路由规则、模型 Key 全都抽出来,运营同学也能通过管理后台调整,不用再找开发改代码。这一步对于生产环境的运维体验提升非常明显。

4. 生产环境里的性能优化与成本控制

4.1 缓存策略:不只是 LLM 缓存

聊到性能,第一反应都是“给大模型加缓存”。确实,对高频的重复问题做语义缓存能省不少 Token 费用。我实现的方案是用嵌入向量做缓存键:先把用户的问题向量化,然后在缓存里找“向量距离小于阈值”的条目,命中直接返回缓存回答,不再调用大模型。这个方案对“XX 系统怎么配置”“XX 报错是什么意思”这种重复率高的知识库问题效果很好。

但有个细节需要注意:语义缓存一定要做权限校验。A 用户问了一个问题,如果 B 用户问同样的问题,而两个用户的知识域权限不同,那 B 用户的答案可能完全不同。缓存键里不能只有问题的向量,还要加上用户角色和知识域 ID。我第一次实现的时候没意识到这一点,导致出现一个特别诡异的 bug:某天销售部门问“这份报价单的内容是什么”,系统返回的是财务部门缓存过的答案,因为两个问题的向量太接近,而缓存没有区分用户的权限域。后来我改了缓存键结构,把 user_id 对应的权限域也塞了进去,才彻底解决。

4.2 向量数据库选型与索引优化

向量数据库我们调研了很久,最后还是选了开源的方案,没有上云厂商的托管向量服务。原因倒不是云厂商产品不好,而是我们数据量只有几百万条 Embedding,开源自部署完全够用,还能省一笔成本。最终选的是 Qdrant,因为它的过滤条件(Filter)支持得很好,对权限隔离这种场景特别友好。

索引参数上也踩了坑。刚开始用 HNSW,M 值设成 64,ef_construct 设为 200,召回率确实高,但内存占用爆炸。后来参考官方文档的建议,把 M 降到了 16,ef_construct 设为 100,召回率掉了不到 2%,内存却省了接近一半。如果你也在自部署向量库,强烈建议做一轮参数调优,不要无脑用默认值。

4.3 Token 成本核算与模型路由

生产级系统的模型调用成本也不能不看。我实现了一个简单的模型路由逻辑:先判断问题复杂度,简单问题走便宜的小模型(比如 7B-14B 的开源模型),复杂问题才走 GPT-4 级别的大模型。判断方式有两种:

  • 规则法:问题长度、是否包含代码块、是否涉及复杂逻辑推理;
  • 分类模型法:用一个便宜的小分类模型判断问题类型,输出路由到不同模型。

实测下来,规则法简单直接,能过滤掉一大半简单问题,节省的 Token 费用可以达到 30%-40%。分类模型法准确率高一些,但需要维护训练样本,投入产出比一般。

另一个省钱技巧是用本地小模型对知识库召回内容做压缩。大模型的上下文窗口是有限的,之前习惯把 Top-5 的召回片段全塞进去,有些片段跟问题根本不相关,白白消耗 Token。用本地小模型对候选片段做一次相关性过滤,只保留 2-3 段最相关的,一次调用能省下 30% 左右的 Token。这个跟前面说的动态上下文压缩是一件事,但强调一下:压缩这一步最好放到网关层做,因为不同的 Agent 可能都需要这层能力,重复实现反而浪费。

5. 踩坑实录:链路联调期的怪问题

5.1 Agent 报错 execution terminated 的排查过程

联调阶段遇到一个高频报错:Agent execution terminated due to error。一开始我以为是模型服务超时,查了半天日志发现根本不是。这个报错其实是 Agent 内部的某一步工具调用抛出了异常,但框架把这个异常包装成了一个通用错误,真正的堆栈信息被吞掉了。

当时我们的排查链路是这样的:

  1. 先在网关层打印出完整的请求链路 ID 和子任务执行序号;
  2. 再到 Agent 框架的日志里找对应链路 ID 的执行痕迹;
  3. 最终发现是一个工具调用的参数格式不正确——工具期望接收 JSON 格式的入参,但 Agent 框架传入的是一个 Python dict,序列化之后工具解析失败。

修复方案是给 Agent 框架加了一层参数校验和标准化转换,在调用工具之前统一把入参序列化成 JSON,并做类型检查。这个问题排查花了将近两天,根源就是框架吞掉了底层异常,导致问题定位非常困难。所以如果你也在用现成的 Agent 框架,建议从第一天就给所有工具调用加 try-except,把异常详情记录到日志里,别依赖框架的默认报错信息。

5.2 知识库权限隔离导致的“空索引”问题

另一个问题发生在权限隔离上线后。某个部门反馈很多问题答不上来,连之前能答的也答不出来了。排查后发现问题出在权限过滤上:这个部门的用户角色对应的知识域 ID 配置错了,导致检索条件里拼接了一个不存在的域 ID,查出来的结果为空。

这类问题很难通过常规功能测试发现,因为测试账号的权限域基本都是完整配置的。后来我加了一个监控项:当检索请求的过滤条件未命中任何文档时,在日志里打一个 warning,并附上过滤条件的具体内容、用户角色和域 ID。这样一来,后续空索引问题都能在第一时间在监控面板里看到,而不是等用户反馈。

5.3 网关限流的“洪峰”考验

上线第一周就遇到了一个不小的考验。公司某个产品发布新版本,运维同事在内部群分享了知识库助手,一上午涌进来大量请求。网关的限流策略是按用户维度限的——每个用户每秒最多 5 个请求,看起来够用,但忽略了整体并发。网关前面没有做全局并发数控制,导致后端模型服务的连接池被打满,大量请求直接排队超时。

这次之后我调整了限流策略,增加两层:

  • 用户维度:单用户每秒不超过 3 个请求,防止单个用户刷爆;
  • 全局维度:整个网关的并发请求数上限设为 200,超过的部分直接返回 429 限流状态码。

同时把模型服务的连接池大小跟网关的并发上限做了联动配置,避免二者不一致导致请求堆积。

6. 可观测性与审计:生产级系统的底气

6.1 全链路追踪怎么做

企业级系统跟个人项目最大的区别就是,出了问题你要能说清楚“发生了什么”。我基于 OpenTelemetry 做了全链路追踪,从用户请求进入网关开始,生成一个 trace_id,然后所有下游调用——知识库检索、模型调用、工具执行——都带上这个 trace_id。这样排查问题不需要靠猜,直接在日志平台搜 trace_id 就能看到整个链条的耗时分布。

链路追踪的数据结构比较直接:

{ "trace_id": "abc123", "spans": [ {"name": "gateway.route", "duration_ms": 12, "status": "ok"}, {"name": "knowledge_base.search", "duration_ms": 340, "status": "ok"}, {"name": "llm.generate", "duration_ms": 1800, "status": "failed", "error": "timeout"} ] }

实际使用中,这种追踪最大的价值在于找出链条里最慢的环节。比如有一次用户反馈“回答问题很慢”,一查发现 80% 的时间花在知识库检索上,而模型生成只占了很小一部分。如果没有链路追踪,我们可能会浪费大量时间在优化模型调用上,方向完全错了。

6.2 审计日志的粒度

做企业级知识库,审计日志不是“要不要”的问题,而是“记多细”的问题。我记录的审计数据包括:

  • 用户 ID、角色、所属部门
  • 完整的问题输入
  • 检索命中的文档 ID 列表和片段
  • 最终返回给用户的答案摘要
  • 调用的模型名称、Token 消耗、耗时
  • 权限过滤条件的命中结果

为什么连“中间结果”也要记?因为如果只有最终答案,出了问题你根本不知道是检索错了还是模型答错了。有一次用户投诉“回答错误”,我们把当时的完整链路调出来,发现知识库命中了正确的文档,但模型在生成时由于上下文截断没有看到关键片段,导致回答偏离。这种问题没有审计日志基本无解。

6.3 监控告警的阈值设置

监控告警的阈值设置也是一门学问。太敏感会被告警疲劳淹没,太宽松则失去了监控的意义。我们最终设置的几个核心告警项:

  • 网关 P95 延迟超过 5 秒:说明系统整体性能下降,需要关注;
  • 单 Agent 错误率超过 5%:说明可能出现了工具调用或模型服务异常;
  • 知识库检索零命中比例超过 10%:可能存在文档未同步、权限配置或检索质量问题;
  • 模型调用 Token 消耗环比增长超过 50%:防止有人恶意刷量或配置错误导致成本异常。

这些阈值不是一次性定的,都是经过一两周的运行数据反复调整出来的。刚开始设得太严格,天天被打扰,后来放松一些才进入稳定状态。

7. 这套方案的适用边界与可复制性

做完这轮优化,我最大的感受是:生产级知识库的复杂度不在“模型”,而在“工程”。大模型的能力大家都差不多,真正拉开差距的是你把它放在什么样的基础设施里。你愿不愿意为检索质量做混合召回和重排、为权限隔离做物理索引拆分、为系统稳定性做网关限流和链路追踪、为审计做到全链路日志——这些才是决定系统能不能上线跑一年的关键。

这套方案的可复制性取决于你们团队的规模和技术栈。如果你只是个人用 Obsidian 搭本地知识库,那上面说的网关、限流、审计这些确实用不上,你更需要的可能是好用的分块策略和向量数据库选型。但如果你跟我一样,做的是企业内部多部门共享的知识库系统,那么权限隔离、高频缓存、全链路追踪这三件事,建议优先做。

个人经验分享一个小技巧:先在测试环境把完整的链路跑起来,再慢慢加生产级的能力。不要一上来就追求全功能,否则排查问题的时候你根本不知道是哪一层出的问题。我当时是先跑通了最简单的 RAG,然后再逐步加权限过滤、加网关、加可观测,每一层都验证通过之后才上生产。这个流程看起来慢,实际是最快的路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询