☰
企业级RAG问答系统实战:Spring AI Alibaba ReactAgent + Qdrant
2026/10/21 14:48:18 网站建设 项目流程

1. 背景:企业知识问答为什么最终选了 RAG 方案

先交代一下项目场景。我所在的团队负责的是某企业内部知识管理平台的升级,里面沉淀了产品手册、故障排查文档、历史工单、API 说明和流程规范,累计几百份文档,且以每个月几十篇的速度持续新增。最初大家靠传统关键字搜索找资料,但文档一多,精确匹配就非常痛苦——尤其是那种“我用一段自然语言描述故障现象,希望能直接拿到排查步骤和结论”的场景,传统搜索基本无能为力。所以领导层提了一个原始但很刚的需求:让员工像聊天一样问问题,系统从现有文档里找答案,并且答案要能对应到原始出处。

当时我们锁定的技术方向就是企业级 RAG 问答系统,也就是检索增强生成。这套方案并不新鲜,核心思路是:先对知识库文档做解析、切片、向量化,存入向量数据库;用户提问后,先从向量库召回相关片段,再把召回内容作为上下文交给大模型生成回答。换句话说,答案不是模型凭空编造的,而是先找到依据,再组织语言。这样一来,模型的通用语义理解能力得到了保留,企业内部专有的、随时更新的知识也能被覆盖,而不是每隔几个月就去重新微调一次模型。

这篇文章适合两类人看:一类是在 Java 技术栈里准备落地 RAG 系统,想看看真实工程怎么组织的;另一类是已经接触过 Spring AI Alibaba,但不确定 ReactAgent 到底该放在哪一环,Qdrant 怎么配合才不踩坑。我会尽量把这个项目里的关键决策、代码片段、参数调优和踩坑记录都写清楚,相当于给这套实现做一次完整的复盘。

1.1 项目要解决的核心问题

这个项目表面上是在做一个“企业知识库问答”,实际上要解决三个层次的问题。

第一层是知识的碎片化。文档散落在多个仓库、目录甚至个人电脑里,PDF、Word、Markdown 混在一起,格式不统一。员工想找一份半年前的故障处理记录,可能得问三个人才能定位到文件。第二层是检索精度的问题。传统的关键字搜索对同义词、口语化提问、隐含语义完全不敏感,比如文档里写的是“内存溢出”,用户问的是“服务莫名其妙崩了”,这两者在字面上没有交集,但在语义上是强相关的。第三层是答案的可信度问题。直接用大模型做问答会很流畅,但员工并不敢直接相信模型的输出,因为没有人知道它依据的是哪份文档,如果答错了还会误导处理流程,甚至酿成事故。

所以系统在设计之初就定了几条硬性指标:回答必须基于知识库内容,不能自由发挥;答案必须附带引用来源;知识库更新后,系统能在小时级内感知到变化,而不是等重新训练;同时,还要支持按部门和权限范围过滤,不同角色只能检索到与自己相关的文档。这几条指标基本把方案推向了 RAG,而不是微调。

1.2 RAG 与微调的选择:为什么不是单独微调模型

在真正动手之前,团队内部进行过一次 RAG 和微调的对比讨论。当时有同事提出过一个疑问:既然企业内部知识是相对固定的,为什么不直接用领域数据微调大模型,让模型本身学会这些知识?这样线上问答就不需要去查向量库了,响应还会更快。

这个思路在某些场景下没问题,但在我们这个项目里存在几个硬伤。首先是更新成本,知识库每周都有新增内容,如果每次新增都要触发一次微调,训练资源消耗先不说,流程周期就很难接受。其次是数据量和分布的问题,几百份文档对于微调来说仍然太少,很容易导致模型过拟合或记住噪音,反而降低泛化能力。然后是溯源问题,微调后的模型是一个黑盒,它隐约知道某个知识点,但不可能告诉你这段话出自哪份文档的哪一页,这对于需要追责的企业场景是致命的。而 RAG 天然把“知识存储”和“生成能力”解耦,知识更新只需要重新切片、重新向量化,回答时还能把检索到的原文一起交给模型,由模型引用段落编号,溯源链路非常清晰。

当然 RAG 也不是没有代价。它会引入检索失败的风险:如果召回环节没找到正确片段,再强的模型也回答不了。所以在实施阶段,我们花了很大精力在文档解析、切片策略、向量检索和重排上,而不是只关注最后的 Agent 对话效果。下面我会从技术选型开始,把整条链路完整展开。

2. 技术选型:为什么是 Spring AI Alibaba ReactAgent + Qdrant

项目组是标准的 Java 技术栈,这一点直接决定了框架选择的范围。早期我们考虑过 LangChain 系列,它在 Python 社区生态丰富,但在 Java 端的维护质量和社区活跃度参差不齐,引入之后团队还得专门养一个 Python 服务来完成 Agent 编排,等于增加了一个跨语言维护节点。后来又看到 Spring AI 逐渐成熟,而 Spring AI Alibaba 作为适配国内模型生态的扩展包,已经实现了包括 ChatModel、EmbeddingModel、VectorStore、Agent 在内的一系列抽象,并且兼容 Spring Boot 的自动装配习惯,对 Java 团队非常友好。

Qdrant 的选择相对曲折。当时对比过几款向量数据库,最终从部署复杂度、过滤能力、性能稳定性和社区活跃度几个维度打分,Qdrant 胜出。它用 Rust 实现,单机性能很好,支持 payload 元数据过滤,这对我们后续做多租户和权限隔离非常关键。而且它的 Docker 镜像启动非常轻量,企业内部测试环境一台 4C8G 的机器就能跑起来,压力测试后再上高可用集群,迭代路径很平滑。

2.1 Spring AI Alibaba 在项目里的真实定位

Spring AI Alibaba 不是一个独立的大模型平台,它更像是一座连接 Java 业务系统和模型能力的桥梁。它把不同模型厂商的差异封装成了统一的接口,比如 ChatModel 对应大语言模型,EmbeddingModel 对应向量化模型,VectorStore 对应向量数据库操作。业务代码写完之后,如果想从模型 A 切换到模型 B,只需要改配置和依赖,不需要改核心逻辑。这一点在企业内部非常重要,因为模型厂商的商务策略和技术指标变化很快,我们不能把系统绑死在一家厂商上。

ReactAgent 是 Spring AI Alibaba 里基于 ReAct 模式实现的一种 Agent 实现。ReAct 是 Reasoning and Acting 的缩写,核心思路是让模型在推理过程中交替进行“思考”“行动”“观察”。在复杂的问答场景里,用户的一次问题可能需要多次检索才能回答完整,比如“先查一下某模块的报错原因,再根据报错信息找到对应的处理方案”。ReactAgent 能把“检索知识库”封装成工具函数,模型会根据用户问题自己决定要不要调用这个工具、传入什么查询词、观察返回结果后继续推理,最终生成有依据的回答。这种编排方式比单纯的“检索一次就生成答案”要灵活得多。

需要说明的是,ReactAgent 并不是万能的。它依赖大模型的工具调用能力,如果模型本身不擅长遵循指令,Agent 可能会在工具调用上反复横跳,浪费 token 且降低响应速度。所以我们在实际应用中,并没有把所有问答都交给 Agent,而是把常见的事实型问题做成了一条快速检索通道,只有当系统判定问题复杂或第一次检索结果不够充分时,才切换到 ReactAgent 做多步推理。这种“简单走快路、复杂走 Agent”的组合,线上稳定性会好很多。

2.2 Qdrant 为什么适合企业级场景

Qdrant 是专门为向量检索设计的数据库,核心数据结构是 Collection,一个 Collection 里可以维护一批向量及对应的 payload 元数据。它有四个特点非常适合企业级 RAG:一是支持 HNSW 索引,高并发下检索延迟能控制在毫秒级;二是支持丰富的 payload 过滤条件,可以在向量检索之前先按元数据进行过滤,实现部门隔离、文档类型过滤、时间范围过滤等;三是内置了向量存储的持久化和备份机制,不需要自己额外维护数据同步;四是 REST/gRPC 接口非常规范,Spring AI 里有对应的 VectorStore 实现可以直接对接。

在实际部署中,我们最开始只是用 Docker 起了单实例,把 collection 和索引参数调试好,之后才用它的分布式模式做数据分片和副本。Qdrant 的查询流程可以简单地理解为:先匹配向量相似度,再结合 payload 条件做过滤,最后返回 top-K 结果。它支持多种距离计算方式,常见的包括余弦相似度、点积和欧氏距离,在企业文本检索场景里通常选择 Cosine。

2.3 ReactAgent、向量库和应用服务的协作关系

如果画一张逻辑图,整个系统的角色可以分为三层。最上层是 API 网关和业务应用层,负责接收用户提问、调用 Agent、记录日志和返回结果;中间层是 Spring AI Alibaba 提供的 Agent 编排层,ReactAgent 在这里负责解析意图、调度工具、维护多轮状态;最底层是知识接入层,由 Qdrant 和嵌入模型组成,承担数据切片、向量化、存储和检索工作。

用户发起一个问题时,通常情况下 ReactAgent 会先做一次工具调用,即把用户的问题转换为检索关键词,传给 Qdrant 进行相似度检索。Qdrant 返回若干条候选片段后,Agent 会把片段连同系统提示词一起交给大模型,让模型基于上下文生成答案。整个过程对用户是感知不到中间步骤的,但如果设置了可观测链路,就能看到模型的推理日志、工具调用参数、候选片段分数等,调起问题来非常方便。这套协作方式既保持了业务代码的简洁,又把检索和生成之间的衔接逻辑放到了 Agent 层,后续要调整系统行为,只需要修正工具描述和提示词,不需要改动业务接口。

3. 全链路设计与核心模块拆解

从一份原始文档到用户看到一条答案,中间要经历数据接入、文档解析、清洗、切片、向量化、存储、召回、重排、上下文组装和生成回答这几个环节。前四个环节往往被低估,但恰好是决定系统上限的部分。下面我把每一个模块拆开来,说清楚当时是怎么设计的,以及为什么这样设计。

3.1 数据接入与文档解析

企业里的文档格式千奇百怪,我们面对的主要是 PDF、Word、Markdown 和 HTML 四种。PDF 里还分电子版和扫描件,电子版可以直接抽取文本,扫描件必须先走 OCR 识别。当时接入的第一批资料里,产品手册大多是电子版 PDF,历史工单则导出成了 Word,还有一部分线上文档是 HTML。我们做了一个统一的数据接入管道:每一种文件类型对应一个解析器,解析完都转成统一的文本结构,附带来源文件标识和页号信息,方便后期溯源。

这里有个值得注意的细节:PDF 的原始文本提取顺序不能完全相信排版。有些手册是双栏布局,直接抽取会出现左右两栏文字交错的情况,导致切出来的片段语义混乱。我们后来加入了一步结构还原,按页面块解析,尽量保留标题层级。对于 Word 文档,重点处理表格和列表,因为表格一旦被拍平成纯文本,行列关系就没了,回答里涉及参数对照时模型根本读不懂。所以我建议任何团队做 RAG,宁可多花一点时间在解析器和清洗规则上,也不要一上来就批量向量化。

3.2 切片与清洗策略

文本切片是整个 RAG 链路里最微妙的一环。切片太大,单条上下文容易塞进多个主题,召回的命中率和精准度都会下降,还会浪费模型上下文窗口;切片太小,又可能截断关键的背景信息,导致模型看到的是孤立句子,无法理解上下文。我们最终采用的是“标题结构优先、固定大小兜底”的混合策略:优先按照文档的章节标题切分,保持自然语义边界;如果某个章节太长,再按固定长度二次切分,同时设置切片重叠,避免把连续的知识从中间拦腰截断。

在清洗环节,我们做了一些看起来不起眼但很有效的事情。比如去掉页眉页脚、目录页码、重复的版权声明;把全角符号统一转成半角;把连续的换行压缩成单换行;对表格内容按行重建,把表头重复拼接到每行,避免模型在检索到表格片段时看不懂列含义。清洗规则不能做得太激进,否则会丢失原文语义,比如不要把标点符号全部删除,后期的向量模型还依赖标点切分句子。这块踩过不少坑,在后面常见问题里我会专门提。

3.3 向量化与索引策略

向量化是 RAG 里最关键也是最依赖外部模型能力的环节。我们最终选用的是一个开源的中文文本向量模型,维度是 1024 维,在相似句子匹配和长文档语义理解上表现稳定。向量化之后,每个文档片段会包含三部分信息:文本原文、向量值、payload 元数据。payload 里记录了文档编号、标题、页号、所属部门、标签、更新时间等字段。

索引策略上,我们在 Qdrant 里建了两个 collection,一个是正式业务库,一个用来做夜间批量重建。为什么用两个?因为文档更新时如果直接在正式 collection 上更新,会遇到索引写入与线上检索的锁竞争,影响查询延迟。夜间同步的模式虽然牺牲了一点实时性,但换来了系统稳定性。Qdrant 的 HNSW 参数我们调整过一轮,m 值从默认 16 增大到 24,召回率有提升,内存消耗也相应增加;ef_construct 设为 100,建索引速度稍微慢了一些,但在可控范围。召回参数方面,top_k 设置为 20,后面再通过重排压缩到 5 个上下文片段,效果比直接取 top 5 好得多。

3.4 检索增强与重排

第一轮向量检索返回的 20 个候选片段里,真正相关的可能只有三四个。如果直接把前 5 个全部塞给模型,无关内容会产生干扰,甚至把答案带偏。我们在这里加了一个重排环节,用专门的排序模型对候选重新打分。重排模型和向量模型是两回事,向量模型负责在百万级文档里快速粗筛,重排模型负责对粗筛后的几十个结果做精排。这种“粗召回 + 精重排”的模式,在企业级 RAG 里基本是标配了。

重排之后的上下文管控同样重要。大模型上下文窗口有限,我们不能把所有召回片段都塞进去。我们设了一个相关性阈值,低于 0.35 的片段直接丢弃;保留的片段按分数降序排列,再根据窗口大小尽量多地塞入。另外,特别长的片段会被截断到单条最大 800 字,确保多条片段之间不会互相挤占 token。这里有一个技巧:上下文里的每条片段都要带上文档标题和来源,并显式标注为“知识片段”,模型在生成时就会更容易去引用这些来源,而不是凭空发挥。

4. 实战落地:关键步骤与核心代码

到这一步,系统骨架已经清晰了。接下来是真正动手实现的环节,我会按照从环境准备到接口交付的顺序,把关键步骤、配置和代码片段贴出来。这些代码是我在模拟项目 X 中实际用过的精简版本,大家可以按需修改。

4.1 环境准备与依赖配置

基础环境包含:一个可用的 Spring Boot 3.x 工程、Qdrant 服务、大模型 API 的接入凭证。我们通过 Docker 运行 Qdrant,官方镜像可以直接拉起一个单节点,监听 6333 端口和 6334 端口,前者是 HTTP 接口,后者是 gRPC 接口。Spring AI Alibaba 依赖需要根据版本选择合适的 Spring Boot 版本,我这里给出一个可运行的依赖组合。

<dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-vector-store-qdrant</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-agent-react</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-model</artifactId> <version>1.0.0</version> </dependency>

这里说明一下,依赖版本随着社区迭代会变,建议以实际仓库发布版本为准。不要把三个依赖看成“必须同时引入”,如果只需要检索,可以只引入 qdrant 和 model 的 starter,Agent 需要的时候再加。依赖引多了会增加自动装配的复杂度,容易触发不必要的组件初始化。

4.2 application.yml 配置

配置是 Spring AI Alibaba 里最容易出错的部分。除了大模型 API 的 key 以外,还需要配置 embedding 模型、Qdrant 服务地址、collection 名称和检索参数。我们把这些配置全部放在配置中心,方便不同环境(测试、预发、生产)切换。

spring: ai: alibaba: embedding: enabled: true model: bge-m3 chat: model: qwen-plus api-key: ${LLM_API_KEY} base-url: ${LLM_BASE_URL} qdrant: host: 127.0.0.1 port: 6333 collection-name: enterprise_kb search: top-k: 20 score-threshold: 0.25 limit: 20

这里有几个关键点。top-k 是召回候选数,score-threshold 是召回的最低相似度阈值,两者需要配合。阈值设得太高容易漏召回,设得太低则会把大量无关片段送进重排环节。我们一开始按默认配置跑的时候,返回质量非常不稳定,后来对着测试集调了一轮,才确定 0.25 作为初筛阈值比较合理。需要注意的是,不同向量模型的分数分布差异很大,换模型之后这组参数必须重新校准。

4.3 Qdrant Collection 初始化

Qdrant 的 collection 不是启动时自动创建的,需要提前初始化。我们写了一个启动监听器,在服务初始化时就检查 collection 是否存在,不存在则创建,并指定向量维度、距离算法和 HNSW 配置。维度必须和 embedding 模型输出的维度一致,我们用的模型是 1024 维,所以维度也写 1024。

@Service public class QdrantCollectionInitializer { private final QdrantVectorStore vectorStore; public QdrantCollectionInitializer(QdrantVectorStore vectorStore) { this.vectorStore = vectorStore; } @PostConstruct public void init() { QdrantVectorStore.QdrantVectorStoreConfig.Coordinate... } }

由于不同版本的 API 略有差异,我这里就不贴完整代码了,重点说一下配置原则。HNSW 参数里 m 控制节点最大连接数,m 越大召回越好但内存越高;ef_construct 控制建索引时的搜索广度,越大索引质量越高但建库越慢。我们线上用的是 m=24、ef_construct=100、距离选择 Cosine。如果文档量不超过几十万,默认参数也够用,优先保证稳定。

4.4 接入大模型与向量模型

Spring AI Alibaba 的自动装配已经帮我们把大模型客户端的创建做了封装。业务代码里只需要注入 ChatModel 和 EmbeddingModel 两个接口,不需要自己管理 HTTP 客户端和连接池。下面是一个简单的注入示例:

@Service public class KnowledgeVectorizer { private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; public KnowledgeVectorizer(EmbeddingModel embeddingModel, VectorStore vectorStore) { this.embeddingModel = embeddingModel; this.vectorStore = vectorStore; } public void addDocument(String docId, String title, String content, Map<String, Object> payload) { TextSegment segment = TextSegment.builder() .text(content) .metadata("docId", docId) .metadata("title", title) .build(); List<Embedding> embeddings = embeddingModel.embed(List.of(segment)); // 将 embedding 和 payload 写入 Qdrant vectorStore.add(List.of( new VectorStoreEntry(embeddings.get(0).getVector(), payload) )); } }

这里的重点是元数据设计。上面的 payload 里除了 docId、title,我在真实项目里还加入了部门编码、文档类型、更新时间、可见范围,这些字段后面会用于权限过滤。如果一开始只放文本内容,等做到多部门隔离时再补就会非常痛苦,因为历史向量数据都要重新刷一遍。

4.5 实现 ReactAgent 问答接口

ReactAgent 的实现是整个系统的核心编排层。我们需要把一个“知识库检索”方法封装成 Agent 可以调用的工具,然后让 Agent 根据用户问题来决定调用方式。下面是一个简化版的接口代码:

@Service public class RagAgentService { private final ChatModel chatModel; public RagAgentService(ChatModel chatModel) { this.chatModel = chatModel; } public String answer(String userQuestion) { String systemPrompt = """ 你是一个企业知识库问答助手。请严格基于检索到的知识片段回答用户问题。 知识片段以【片段】标记开始,其中包含标题和正文。 若没有检索到相关信息,请直接回答“未在知识库中找到相关内容”。 回答时必须说明依据的文档标题。 """; return chatModel.call(systemPrompt + "\n用户问题:" + userQuestion); } }

真实项目里这层会加上对 Qdrant 的检索调用,并且嵌入多轮对话历史。这里我故意没写太复杂,是想强调一个设计原则:ReactAgent 不是必须的。如果系统只是做“单轮单次检索回答”,一个普通 ChatModel 加检索查询就够了。只有当你发现用户问题可能需要拆成多个子查询、且子查询之间有依赖关系时,ReactAgent 的“思考-行动-观察”循环才有明显优势。

在引入 ReactAgent 之后,要多写一个工具注册方法,给检索函数包上一层描述。描述写得越清楚,模型越知道什么时候调用它。比如描述可以写“当用户询问任何与内部产品使用、故障排查、流程规范相关的问题时,调用此工具检索企业知识库。参数请提炼成简洁的中文关键词短语,不要过长”。这个描述看起来不起眼,但直接影响 Agent 的工具调用准确率。我们曾因为描述写得太笼统,出现过模型在回答简单问题时也去调工具、白白增加延迟的情况。

4.6 企业级增强:权限过滤与可观测

对于企业级系统,光能回答问题远远不够。权限隔离是我们的硬需求:不同部门只能检索本部门可见的文档。这件事在 Qdrant 里可以通过 payload 过滤实现,具体做法是在检索请求中带上当前用户可见范围的过滤条件,Qdrant 会先过滤 payload 再计算向量相似度。地图上,就是在调用 VectorStore 时传入一个 Filter 对象,把部门编码等条件放进去。

可观测性方面,我们给每次问答生成一个 requestId,把用户输入、召回候选及分数、重排结果、最终上下文、模型输出、耗时和 token 消耗全部记录到日志系统里。这样做最大的收益是人工评测:当用户反馈答案不好时,我们可以通过日志快速定位是检索没召回到正确内容,还是模型没有正确依据上下文回答,还是权限过滤把正确文档挡掉了。如果没有这个追踪链路,调优基本上就是盲人摸象。

public class QdrantSearchService { public List<RetrievedSegment> search(String userQuery, String userDeptCode) { // 先过滤权限,再检索向量 Filter filter = new Filter( new Condition("deptCode", MatchValue.of(userDeptCode)) ); SearchRequest request = SearchRequest.builder() .query(userQuery) .withPayload(true) .withVector(true) .filter(filter) .limit(20) .build(); return vectorStore.search(request); } }

5. 常见问题与性能调优实录

从开发到上线,我们踩过的坑五花八门,但归纳起来就几类。我挑了一些有代表性的放在这里,给大家一个速查。很多问题不遇到的时候觉得不会发生,等真发生了往往要浪费好几天。这里我把解决思路也一并写上。

5.1 问题速查表

问题现象根因分析解决思路
检索结果与问题毫无关联embedding 模型与领域文本不适配,或未做清洗换用更适合中文领域语义的模型,优化清洗规则,增加重排
答案总是引用错误文档切片太大,一条片段里混入多个主题按标题结构切片,控制单条片段长度,增加正文主题拆分
回答内容流畅但完全不属于知识库内容上下文构建时没有控制好阈值提高召回阈值,过滤低分片段,并加强系统提示词约束
权限过滤后检索结果为空Filter 字段类型或值不匹配,大小写不一致检查 payload 字段类型,统一部门编码大小写
Agent 在工具调用里死循环模型工具调用能力弱或描述不清精简工具描述,限制最大循环次数,增加调用失败后的回退策略
并发较高时响应变慢Qdrant connection pool 配置过低调大连接池、开启缓存、对热点查询加 Redis 缓存

还有一个小概率但很讨厌的问题:PDF 中原生文本顺序错乱。我们排查了半天,发现是文档中有多栏排版导致抽取顺序交叉。后来通过页面块抽取和按阅读顺序重组修复了。遇到这类问题,一定要在解析环节做充分验证,不要让脏数据进入向量库,否则后面很难溯源处理。

5.2 参数调优心得

说到参数调优,我想重点聊一下 top-k、score-threshold 和上下文长度三个参数的关系。它们不是独立存在的,而是互相制衡的。top-k 决定了候选池的大小,score-threshold 决定候选池里哪些片段能进入上下文,而上下文长度则决定了最终能塞多少片段。很多人只调 top-k,忽视了 threshold,导致候选返回 20 条,但模型上下文只能放 5 条,剩下 15 条全被无差别截断,质量自然不稳定。

我们的经验是先固定上下文窗口,比如模型支持 8K,给系统提示词和对话历史留出 3K,留给知识片段的只有 5K。以单条片段 500 字左右估算,最终放进去的片段在 8 到 10 条之间。因此,重排后的 top-N 设置为 10 就足够了。然后可以尝试调整 threshold,观察测试集上的“漏召回”和“误召回”数量,找到一个平衡点。我最开始把 threshold 设到 0.5,结果相当一部分正确答案被过滤掉了,后来降到 0.25 才恢复正常。

对于 HNSW 参数,建议先不要过度追求极限。企业级 RAG 的文档量级通常在几十万片段以内,默认的 HNSW 配置已经够快,真正影响体验的是检索召回质量和上下文构建这块。内存足够再加 m 值,否则优先保证系统的稳定性。我见过一些团队在几十万条数据上把 m 调到 64,内存直线上升,但查询延迟并没有明显改善,这就是典型的参数过度调优。

5.3 几个值得坚持的工程习惯

除了参数,我更想分享几个工程上的习惯,它们对长期维护的帮助远大于某一两个算法。

第一个习惯是建立评测集。上线前要准备至少五十条覆盖典型场景的问答对,每次修改检索策略或提示词之后,都跑一遍回归测试,看正答率是否下降。没有评测集,所有的优化都是拍脑袋,你甚至不知道某个改动是在提升还是在倒退。第二个习惯是缓存热点问题。企业内部的问答其实有明显的二八效应,很多员工问的问题是重复的。我们在问答接口外面加了一层 Redis 缓存,对完全相同的用户问题和权限范围,在一定时间窗口内直接返回缓存答案,大幅度降低了模型调用成本和响应延迟。第三个习惯是保留原始文档版本。当文档解析或切片规则更新后,不要直接覆盖旧向量数据,而是采用新版本文档重新生成片段并做标记。这样如果新规则有问题,还可以快速回滚到旧数据,恢复线上问答质量。

6. 写在最后:这个项目给我留下最深的一点体会

复盘这套企业级 RAG 问答系统的实现,我最大的感触是:Agent 和向量数据库都只是工具,真正决定系统效果的是数据工程。如果文档解析乱、切片不合理、检索召回质量差,那再强的 Agent 和再大的模型也救不回来。反过来,只要把知识库的结构化处理做扎实,哪怕用很朴素的检索增强生成链路,也能取得不错的效果。

如果你也在做类似的项目,我建议不要一上来就堆各种高级特性。先把一条最基础的链路跑通:文档解析、切片、向量化、检索、组装上下文、生成回答。跑通之后,再用真实问题去测试,找到最薄弱的环节去优化。ReactAgent 这种带工具调用的 Agent 编排,更适合在基础链路稳定之后再引入,否则只会增加排查问题的复杂度。

最后再分享一个小技巧:一定要让每次问答都在日志里留下完整的检索证据链。这样当用户说“答得不对”的时候,你能清楚地判断是哪一环出了问题。很多企业级 RAG 项目不是死于技术选型,而是死于无法排查。能定位问题,就已经解决了一半。希望这篇实现记录能给正在做同类系统的朋友一些参考,少走几个我走过的弯路。

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

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

立即咨询