1. 从一个真实痛点说起:为什么我需要RAG
先交代一下背景。前段时间我接手了一个内部知识库项目,需求很朴素:把散落在几十篇技术文档、操作手册和问题记录里的信息统一管理起来,让同事能用自然语言提问,系统直接给出带出处的答案。
一开始我天真地以为,直接把文档喂给大模型做微调就行。结果真做了才发现,这条路压根走不通。微调的成本先不谈,光是“知识更新”这一个问题就能让人崩溃——文档每周都在变,你总不能每周都重新训练一次模型。而且微调是“把知识揉进参数里”,模型回答的时候你根本不知道它哪句话是可靠的,出了问题也没法追责。
后来我换了个思路,就是标题里写的这套方案:Spring Boot 3 + LangChain4j 的 RAG(Retrieval-Augmented Generation,检索增强生成)。核心思路其实一句话就能讲明白:不把知识硬塞给模型,而是先把文档切碎、向量化、存入向量数据库,用户提问时先从库里检索出最相关的片段,再把“问题+相关片段”一起交给大模型,让它基于这些片段作答。这样既保证了答案有据可查,又不用频繁微调模型,换一批文档只要重新灌库就行。
这篇文章我会把这套系统从零到一完整拆开讲:包括技术选型为什么是这两样、环境怎么搭、文档切分和向量化怎么做、检索链路怎么调优、实测效果如何、踩过哪些坑。适合的人群大概是:想在企业内部做知识库问答、正在调研RAG落地路径、或者已经看过概念但不知道具体怎么写的开发者。下面的内容全部基于我自己的项目实践,代码会贴关键部分,但不是那种“拷贝就能跑”的缝合教程,每个步骤我都会解释背后的取舍逻辑。
2. 技术栈选型:为什么是Spring Boot 3和LangChain4j
2.1 选型之前的三个约束条件
在动笔写代码之前,我先给自己定了三条硬性约束:
- 技术栈必须贴合现有团队习惯。我们团队主语言是Java,后端服务全是Spring Boot体系,如果为了RAG强行引入Python生态,维护成本会直接翻倍。
- 部署环境受限。生产服务器在内网,不能直接访问国外模型服务,所以模型要么走内网部署的推理服务,要么选国内可访问的API。这意味着框架必须支持灵活替换模型来源。
- 知识库需要频繁更新。文档内容变更频繁,系统要支持低成本重建索引,而不是每次更新都要重新走一遍重型训练流程。
在这三条约束下,Python系的LangChain虽然生态最成熟,但和Java团队磨合成本太高。最终我选了LangChain4j——它的定位就是Java版的LangChain,封装的抽象和链式调用风格对Spring Boot开发者相当友好,而且支持很多流行的向量数据库和模型供应商。
2.2 LangChain4j核心概念速览
很多第一次接触LangChain4j的人会被一堆名词绕晕,其实核心就四个:ChatLanguageModel(对话模型)、EmbeddingModel(嵌入模型)、EmbeddingStore(向量存储)、DocumentSplitter(文档切分器)。它们组合起来就构成了 RAG 的最小闭环。
- ChatLanguageModel:负责最终回答问题的LLM,可以是OpenAI兼容接口、本地部署的推理服务,或者其他任何实现了接口的模型。
- EmbeddingModel:负责把文本变成向量。注意,做检索用的嵌入模型和回答问题的对话模型往往是两个独立的模型,不需要是同一个。
- EmbeddingStore:存放向量的数据库,常见有Chroma、Qdrant、Milvus、Pgvector等。生产环境我会优先考虑自带持久化和过滤能力的方案。
- DocumentSplitter:把长文档切成块(chunk)。切得好不好,直接决定检索准不准,这一步我后面会单独讲。
搞懂这四个组件,LangChain4j的入门门槛基本就跨过去一大半了。它不像一些重型框架那样要求你必须按模板写代码,而是允许你像拼积木一样自由组合这些组件,这对做技术验证和个人项目非常友好。
2.3 Spring Boot 3带来的整合优势
Spring Boot 3这个版本不是随便选的。它基于Spring Framework 6,默认使用Jakarta EE规范,和LangChain4j的自动装配模块能无缝配合。LangChain4j官方提供了spring-boot-starter,引入之后你只需要在application.yml里配好模型和向量库的连接参数,框架会自动帮你创建相关的Bean,不需要手写一堆工厂类。
另外,Spring Boot 3的Actuator、配置管理等成熟能力可以直接复用,我后来在系统里加了启动时自动灌库、定时重建索引的定时任务,都是靠Spring自身的机制实现的。这一点对于要把RAG集成进老项目的人尤其重要——你不需要为RAG单独造一套配置和生命周期管理轮子。
这里再说一个细节:LangChain4j和Spring Boot的版本兼容有时候没有官方文档写那么清楚。我踩过一次版本冲突的坑,后面在常见问题章节会给出我当时用的具体版本组合。
3. 环境准备与项目初始化
3.1 依赖项清单与版本组合
我最终使用的核心依赖版本组合如下,长期稳定,仅供参考:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <!-- LangChain4j 核心和 Spring Boot 整合模块 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.33.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>0.33.0</version> </dependency> <!-- 使用 OpenAPI 兼容接口,方便切换本地或云端模型 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.33.0</version> </dependency> <!-- 向量数据库,这里用 PGVector 作为示例 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-pgvector</artifactId> <version>0.33.0</version> </dependency> <!-- 解析常用办公文档 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-document-parser</artifactId> <version>0.33.0</version> </dependency> </dependencies>这里解释一下几个选择:
- 我用的是OpenAPI兼容模块,而不是绑定某个厂商的专用模块。这样无论是换成云端厂商的API,还是换成内网部署的兼容服务,都只需要改配置,不用改代码。
- 向量库选了PGVector,因为团队已有PostgreSQL基础设施,少维护一套中间件。向量检索量级不大时,PGVector完全够用;如果你要支撑千万级向量,再考虑Milvus或Qdrant。
3.2 配置文件的完整示例
在application.yml里,我这样配置:
spring: datasource: url: jdbc:postgresql://localhost:5432/ragdb username: postgres password: postgres langchain4j: open-ai: chat-model: base-url: http://localhost:8081 # 本地兼容OpenAI接口的推理服务 api-key: dummy-key model-name: qwen2.5-7b-instruct temperature: 0.2 embedding-model: base-url: http://localhost:8081 api-key: dummy-key model-name: bge-large-zh-v1.5 embedding-store: pgvector: host: localhost port: 5432 database: ragdb user: postgres password: postgres table: document_embedding dimension: 1024 drop-table-if-exists: false几个配置项说明一下:
temperature我调到0.2。RAG场景下答案应该更多依赖于检索到的文档,而不是模型自由发挥,温度太高会让模型“脑补”出不存在的细节。dimension必须和嵌入模型输出的向量维度一致。我之前用的bge-large-zh-v1.5输出1024维,如果你换成其他模型,维度变了就要同步修改。drop-table-if-exists在生产环境千万不要设成true,否则应用启动时会把表删了重建,数据直接清空。
顺便提醒一句:这里我用的是本地推理服务,所以api-key是假的。如果你的实际环境使用云端服务,配置格式完全一样,只是base-url和api-key不同。
3.3 依赖注入方式
完成配置后,Spring Boot会自动装配好以下对象,你可以直接注入使用:
@Configuration public class RagConfiguration { @Bean public ChatLanguageModel chatLanguageModel() { return ChatLanguageModel.builder().build(); } }其实如果你已经引入了langchain4j-spring-boot-starter,上面这段bean定义都可以省,直接在Service里@Autowired接口就行。为了演示结构,我还是显式定义一下,方便看清楚各个组件的入口。
4. 文档处理:切分策略是检索质量的基石
4.1 为什么不能把整篇文档直接灌进库
直观想法是:把每篇文档当成一条记录存向量库,提问时做相似度搜索,把整篇文档交给模型。但这个做法在工程上有多处硬伤:
- 整篇文档向量化后,语义会被平均化,一个上千字的文档中,真正和用户问题相关的可能只有一小段,但它的向量会被大量无关内容稀释。
- 大语言模型对上下文长度有上限,整篇文档塞进去容易超长,还浪费token。
- 检索目标是“精确命中相关片段”,而不是“返回整本书”。把整篇文档作为检索单元,召回粒度太粗,会混入大量噪声。
所以文档切分是RAG工程质量的重中之重。
4.2 切分参数的经验值
LangChain4j内置了DocumentSplitter,常见的有按固定长度切分,也有按段落/句子切分。我最终用的是DocumentByParagraphSplitter配合自定义重叠逻辑,窗口大小设为500字符,重叠50字符。
这里先解释一下“重叠”的意义。如果两句话在语义上紧密相连,但恰好被切分边界隔开,那么检索时如果用户问的是跨边界的内容,就会漏掉关键信息。重叠部分可以让相邻切块共享一部分边界文本,减少这种信息丢失。
实际配置如下:
DocumentSplitter splitter = DocumentByParagraphSplitter.builder() .chunkSize(500) .chunkOverlap(50) .build(); List<TextSegment> segments = splitter.split(document);这个500和50不是拍脑袋定的。我测试过多种组合,简单说明规律:
- chunk太小(100以下):检索精度高,但每个分块上下文不足,模型经常“答非所问”或者引用了不完整的上下文。
- chunk太大(1000以上):语义稀释明显,而且单分块长度接近模型输入上限,多个相关分块拼接时容易超长。
- 500左右对中文技术文档来说是个不错的中位数,兼顾语义完整性和检索精度。
4.3 切分时保留结构化信息
还有一个容易被忽略的细节:切分后要把标题、页码等元信息跟着存进向量库。为什么要这样做?因为RAG不仅要做检索,还要给答案提供可追溯的引用。如果模型给出的回答无法定位到原文标题和出处,那这个系统在企业内部基本没人敢用。
我自己的做法是在每个TextSegment的metadata里存了source(来源文件名)和heading(所属章节标题)。这样后面做引用展示时,能直接拼出一句“根据《xx文档》第x章”,用户一看就知道答案是否可信。
TextSegment segment = TextSegment.from(content, Map.of( "source", documentName, "heading", currentHeading, "page", String.valueOf(pageNumber) ));这里的metadata一定要在灌库之前就准备好,因为向量库里只存向量和metadata,原始文本虽然也存一份,但没有结构化字段,后补的代价很高。
5. 向量化与索引构建:把知识灌进系统
5.1 灌库的整体链路
灌库的链路并不复杂,核心步骤如下:
- 读取文件。
- 解析文档内容,提取正文和元信息。
- 按策略切分为TextSegment。
- 用EmbeddingModel将每个TextSegment向量化。
- 将向量和metadata一起写入EmbeddingStore。
我用一个简单的Service封装了这条链路:
@Service public class IndexingService { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public IndexingService(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } public void indexDocument(Path path, String documentName) { Document document = loadDocument(path); List<TextSegment> segments = splitDocument(document); for (TextSegment segment : segments) { // 给每个片段生成唯一的id,方便后续删除或更新 String segmentId = UUID.randomUUID().toString(); segment.metadata().put("segmentId", segmentId); } // LangChain4j 提供了批量方法,会内部调用 embeddingModel List<String> ids = embeddingStore.addAll(embeddingModel.embedAll(segments).content(), segments); } }这里再强调一下embeddingModel.embedAll()和embeddingStore.addAll(),这两个批处理是LangChain4j比较香的地方。以前用Python生态做的时候,经常要自己写循环分批embed,Java这边封装好了一次搞定,内部会自己控制批量大小。
5.2 嵌入模型的选择细节
嵌入模型和对话模型不同,它的任务是“给文本生成语义向量”。我实际测试过几个模型,最终在我这个知识库场景里留下了bge-large-zh-v1.5。选择它的原因:
- 中文效果比同尺寸的通用英文模型好一截。
- 输出1024维,在PGVector里检索速度尚可。
- 社区使用量大,排错资料相对多。
这里提醒一个问题:embedding模型的选择影响的是“检索召回”的上限,而不是答案生成的智力水平。如果你的检索经常recall不到相关内容,换更强的embedding模型通常比换更强的对话模型更有效。
5.3 增量索引与全量重建
很多项目刚开始做RAG时只考虑一次性灌库,上线后才发现知识更新根本没法办。文档新增、删除、改动,对应的向量都要同步变更。我在这里提供两种策略:
- 全量重建:删掉旧表或者按批次ID清理,重新灌库。优点实现简单;缺点如果知识库大,耗时较长。
- 增量更新:根据文档的变更时间或唯一标识,只删除变更文档对应的旧向量,重新插入新向量。
我目前实现的是“启动时检测,变化则增量更新”的模式:为每条入库记录打上批次号,更新时按批次号清理,再重新入库。代码大致如下:
public void updateDocument(Path path, String documentName, String batchId) { // 删除同一source下旧的向量 List<String> oldSegmentIds = findSegmentIdsBySource(documentName); embeddingStore.deleteAll(oldSegmentIds); // 重新索引 indexDocument(path, documentName, batchId); }findSegmentIdsBySource在PGVector里执行的是一个按metadata字段过滤的查询,这要求建表时metadata字段可以被索引。生产环境请记得提前设计好表结构,不要等数据多了再回填。
6. 检索增强生成:从向量库到完整回答
6.1 构建检索链路
检索增强生成的核心,是把“用户问题”也向量化,然后去库里做相似度搜索,把命中的文本片段取出来,拼进Prompt,交给对话模型生成答案。
LangChain4j里有个RetrievalAugmentor概念,帮你把这条链路封装起来了。我这里为了讲清楚原理,先手动写一遍,不做黑盒封装:
public String ask(String question) { // 1. 用户问题向量化 Vector embedding = embeddingModel.embed(question).content(); // 2. 相似度检索,取top 5 List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(embedding, 5); // 3. 拼接上下文 StringBuilder context = new StringBuilder(); for (EmbeddingMatch<TextSegment> match : matches) { context.append(match.segment().text()).append("\n\n"); } // 4. 构造Prompt String prompt = """ 请基于以下文档内容回答用户问题。 如果文档中没有相关信息,请直接说明“文档中未找到相关内容”,不要编造。 文档内容: %s --- 用户问题: %s """.formatted(context.toString(), question); // 5. 调用对话模型 return chatLanguageModel.generate(prompt); }这段代码虽然只用了generate接口,但已经把RAG的五个关键环节都走了一遍。复杂一点的项目还可以在这中间加入重排(rerank)、元数据过滤、多路召回等优化,后续会讲。
6.2 使用LangChain4j的高级封装
手动版能让你理解原理,实际开发时我更推荐用框架自带的RetrievalAugmentor,因为它自动帮你处理了各种边界情况,比如多轮对话的查询改写等。配置方式如下:
RetrievalAugmentor augmentor = RetrievalAugmentor.builder() .queryTransformer(CompressingQueryTransformer.builder() .chatLanguageModel(chatLanguageModel) .build()) .contentRetriever(EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .maxResults(5) .minScore(0.7) .build()) .contentAggregator(ReRankingContentAggregator.builder() .reranker(embeddingModel) // 可用专门的Rerank模型 .build()) .build();这中间我想单独说一下minScore(0.7)。这个值是相似度门禁,低于它的匹配会被过滤掉。设太低会带入噪声,设太高会漏掉正确答案。这个阈值和你的embedding向量分布强相关,无法直接套用别人的值,必须自己采样一批问题去试。
我测试后发现,bge-large-zh-v1.5在中文问答场景里,正确相关内容的相似度通常在0.75~0.85之间,不相关的内容经常低于0.6,所以我设0.7作为门禁比较安全。
6.3 Prompt设计:如何让模型“克制”地生成答案
RAG的Prompt有一个很重要的原则:告诉模型“不知道就说不知道”,而不是让它强行编一个答案。下面是我在项目里实际使用的Prompt模板:
你是企业知识库助手。你只能依据被提供的中文文档内容来回答用户问题。 回答要求: 1. 如果信息不足,请回答:根据现有知识库,未找到该问题的明确答案。 2. 不要根据常识揣测或补充文档之外的信息。 3. 回答时标注信息来自哪份文档的哪个章节,格式:信息来源:xxx。 4. 回答简明扼要,控制在200字以内。这段Prompt看起来简单,但“控制模型不乱说”是企业落地RAG最难的一环。大模型有很强的“幻觉惯性”,你必须在Prompt里反复强调“只能依据文档”“不要编造”,并且在实际测试时要专门准备一些知识库中没有答案的“越界问题”,看模型能不能老实承认不知道。
7. 测试与调优:别急着上线,先用真实问题轰炸一遍
7.1 构建一套测试问题集
我在项目进入稳定期后,专门维护了一套回归测试问题集,按难度分三层:
- 基础层:答案直接来自某篇文档原文,考察检索和引用是否正常。
- 综合层:答案分散在多个文档中,需要系统检索多个片段后综合回答。
- 越界层:问题在知识库中完全没有答案,考察模型是否会拒绝回答而不是编造。
这三层测试不是写一次就完,每当知识库内容更新,我都会把这三组问题重新跑一遍,再人工核对回答质量。
我举个越界层的例子:我们知识库里根本没有财务制度的内容,但测试问题里有一条“报销流程是什么”。系统早期版本会一本正经地给你编一套报销流程出来,每次看到这种回答,我都觉得后背发凉。后来我把Prompt和检索门禁调好之后,模型终于学会了回答“未在知识库中找到报销流程相关内容”。
7.2 影响检索质量的三个核心因素
从实测经验看,RAG回答质量受三个因素影响最大,按优先级排序:
- 切分质量。如果相关信息被切碎或错切,后面再怎么调模型都白搭。
- 检索召回。有没有把正确答案召回到top N里,决定模型有没有“原材料”可用。
- Prompt与模型克制力。原材料拿到了,模型会不会忠实于材料去总结,又是另一个考验。
这三个因素其实是一个串联链路,前一环漏了,后一环节再强也补不回来。所以调优时永远从链路最前端开始排查,而不是一上来就换大模型。
8. 常见问题与排查技巧实录
8.1 版本冲突导致Bean注入失败
我第一次搭建项目时,用了一个较新的LangChain4j版本,结果启动Spring容器时报错,提示找不到EmbeddingStore类型的Bean。排查了大半天,最终发现是和Spring Boot 3.2.5的自动配置存在兼容问题。
后来我锁定了0.33.0这个版本组合,又把启动类加上@SpringBootApplication(exclude = ...)排掉自动配置干扰,才恢复正常。我的经验是:LangChain4j版本迭代很快,API在几个小版本之间就可能变,不要盲目追新;锁定一套稳定组合后不要频繁升级。
8.2 向量维度不匹配
这个问题几乎必踩。嵌入模型换了,比如从768维的模型换到1024维的模型,但数据库表结构还是老的768维字段,写入时报错“dimension mismatch”。
解决办法:换模型前先检查application.yml里的dimension配置,同时手动改数据库表结构,或者直接在配置里开drop-table-if-exists: true做一次全量重建。注意生产环境千万别随手drop表,先备份。
8.3 检索到的内容与问题完全无关
如果发现topN结果全是无关内容,先别急着调阈值,按以下顺序排查:
- 检查是否只有一部分文档被灌入向量库,是不是漏掉了某些文件。
- 检查切分后是否产生了大量空片段或乱码片段。
- 检查metadata里的source字段是否混乱,导致不同文档污染了检索结果。
- 用一条和问题语义相近但不完全相同的文本,单独测试embedding模型,看相似度分数分布是否正常。
我遇到过最诡异的一次,是某个PDF解析后出现了大量重复空行,导致切分出来的片段全是空字符串,向量化之后成了一堆“零向量”,在检索时疯狂命中。后来我在解析阶段加了空内容过滤才解决。
8.4 答案正确但引用出处不对
这个问题容易被人忽视,但其实很要命。可能原因是切分时metadata没有正确绑定到文本片段,或者是文档解析时标题层级识别错误,导致大量片段都被归到了同一个错误章节下。
排查方法很简单:随便抽几个片段,打印它的metadata,人工看一下source和heading字段是否和实际内容吻合。我通常会在灌库之后写个抽查脚本,统计每个source下有多少片段,做一次“数量级”上的校验,防止文档被整体漏掉或者重复灌库。
9. 更好的RAG:几个进阶优化方向
9.1 混合检索与重排
纯向量检索有个天然短板:它是“语义相似”,不是“关键词精确匹配”,在专业术语、编号、型号这类场景里,向量检索经常不如一个简单的全文检索准确。
我的改进方案是“向量检索+全文检索”混合,把两部分结果合并后,再做一次重排。重排模型通常用交叉编码器,把“问题-候选文档”一起编码打分,效果比单纯看向量相似度准很多。
LangChain4j里做混合检索稍微绕了一点,因为它内置的ContentRetriever是纯粹向量检索。我的做法是自定义一个ContentRetriever,在内部同时查询向量库和全文索引,再把结果合并返回。实现不算复杂,关键点是定义好两条路各自的召回数量和合并策略。
9.2 多轮对话的查询改写
用户如果连续追问,比如先问“报销流程是什么”,再问“要多久能到账”,第二问里没有明确提到报销,但语义上是承接上文。如果直接把第二问拿去检索,往往会召回无关内容。
解决方案是加一个query改写环节:把上文对话历史和新问题一起交给大模型,让它把“要多久能到账”改写为“报销到账时间需要多久”,再用改写后的句子去检索。LangChain4j的CompressingQueryTransformer就是干这个的。
9.3 知识库权限管理
企业内部知识库往往涉及权限:有的文档只能特定部门看,如果不加过滤就超范围检索,很容易出现越权信息泄露。这个优化在大多数RAG教程里没人讲,但真实落地时几乎逃不掉。
我的做法是在metadata里增加department、accessLevel等字段,检索时把用户权限作为过滤条件传入向量库查询。PGVector支持按metadata过滤,可以直接在SQL查询时带上条件。这里我不展开代码,只提醒一句:权限过滤一定不能只做在展示层,必须在检索阶段就过滤掉,否则向量库本身就把不该给用户的内容召回了,后面再怎么藏都藏不住。
10. 项目上线后的真实效果与维护心得
最终系统上线后,我总结了三个维度的实测数据,供参考:
- 知识库规模:已索引文档约120篇,总文本量约80万字。
- 平均响应时长:单次问答约2~5秒,主要耗时在向量检索和模型生成,还没出现明显瓶颈。
- 基础层问题准确率:在测试集上约92%;越界层问题“拒答率”从最初的不足10%提升到稳定85%以上。
这个结果并不能说明这套配置对所有场景都适用,但它至少证明了一点:只要切分和检索做扎实,RAG在企业内部知识库场景中是完全可以落地的。
关于维护,我再分享几条真实教训:
- 知识库更新不能只加不删。时间一长,库里的旧文档和新文档会互相矛盾,模型会随机选取其中一边。我后来在灌库流程里强制加入“按source清理旧数据”的步骤,才把这个问题控制住。
- 模型调参是持久战。不同业务场景下,合适的temperature、召回数量都可能不同。不要指望跑一次实验就定死一套参数,要持续用真实问题集去回归。
- 日志要记录检索明细。生产环境一旦发现回答异常,只有把当时检索命中的片段和一些相关信息记录下来,才能复现问题。我后来给查询接口加了完整日志,每次回答都能追溯到召回了哪些片段、相似度多少、最终返回了什么,排障效率提升非常明显。
这套系统现在还在持续迭代。如果后续有机会,我会进一步做基于用户反馈的检索质量自动评估,以及把重排环节做成独立服务。不过那就是另一个故事了。如果你也正在做类似的项目,希望这篇文章能帮你少走几段弯路。
最后再分享一个小技巧:别急着写复杂代码,先用几十条真实文档和十个真实问题,手工走一遍“切分-向量化-检索-生成”全流程。只要这个最小闭环跑通、观察到检索命中率稳定,再逐步完善工程细节也不迟。RAG这个方向,第一步的“感觉”对了,后面就顺了。