ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI Alibaba RAG实战:从文档解析到知识库问答

Spring AI Alibaba RAG实战:从文档解析到知识库问答 Spring AI Alibaba 系列写到第四篇我把主角让给了 RAG。前三篇我们聊过 ChatClient 的基本调用、Prompt 模板和结构化输出这些都是把大模型“用起来”的地基但从这一篇开始你会遇到一个绕不开的问题模型只记得训练时的知识你公司内部的文档、产品手册、这周刚更新的排期它一概不知道。RAG 就是解决这个问题的标准姿势而 Spring AI Alibaba 恰好把这条链路做了很大程度的封装。这篇文章我打算用一套完整可跑的代码带你从文档解析一路做到问答联调读完你可以直接把知识库问答搬进自己的 Spring Boot 项目。适合已经跟着前三篇写过代码的读者也适合没看过系列、但想搞清楚 Java 里 RAG 到底怎么落地的朋友。1. 为什么第四篇要把主角让给 RAG1.1 前三篇解决了什么又留下了什么前几篇的路径很清晰先创建项目、把spring-ai-alibaba-starter引进来然后通过ChatClient发一个简单的对话请求接着用 Prompt Template 把用户输入拼进更复杂的指令里让模型按照既定格式输出再往后是用结构化输出把返回内容映射成 Java 对象避免自己写一堆字符串解析代码。这几步做完你的应用已经能“问模型”了。但仔细想想模型回答的内容全部来自它自己权重里存着的“世界知识”。你问它 Java 8 和 Java 17 的区别它能答你问它自己公司的《差旅报销管理办法》里住宿标准是多少它大概率开始一本正经地编。这不是模型变笨了而是训练数据里根本没有这段信息。此时你需要做的是让模型在作答之前“看”到这些业务文档。1.2 RAG 本质上是一场开卷考试RAG全称 Retrieval-Augmented Generation中文叫检索增强生成。一句话解释先把你的业务资料切成一段段文本转成向量存进向量库用户提问时从向量库里找出最相关的几段原文连同问题一起交给大模型让它基于这些原文作答。这个思路像极了开卷考试。闭卷作答模型依赖记忆遇到没背过的题就容易胡编开卷作答模型先翻目录定位到相关章节把原文摘出来再组织语言答错的概率立刻下降。更重要的是资料更新不需要重新训练模型改文档、重灌向量库就能生效这正是企业内部知识库场景最需要的灵活性。对比维度RAG微调知识更新成本低重新跑一遍文档即可高每次调整都要重新训练硬件成本低普通应用服务器即可高需要 GPU 资源可控性与可解释性高能追溯引用的原文片段低行为像一个黑盒适合场景私域知识、持续变化的内容固定风格、稳定逻辑的领域适配1.3 为什么用 Spring AI Alibaba 来做这件事如果你的目标是快速做出一套知识库问答用 Spring AI Alibaba 的原因很直接它对 DashScope 百炼的模型做了自动装配接入成本低中文场景效果好。向量化用的是text-embedding-v3一个专门为中文内容训练的 Embedding 模型检索出来的结果比很多通用模型更贴题意。再加上 Spring AI 本身抽象出的标准接口数据管线里那些 Document、Splitter、VectorStore 都能复用将来哪怕要换底层向量库改动范围也能被限制在配置层。提醒一句Spring AI Alibaba 的版本迭代非常快不同 M 版本之间的 API 会有调整。本文以当前 1.0 系列的写法为准核心思路不变具体类名和包名以你实际引入版本为准。2. 环境准备把最小可跑骨架立起来2.1 依赖要引哪些先看 Maven 依赖。我这里以 Spring AI Alibaba 1.0 系列为例实际使用时到官方 Maven 仓库确认最新版本号。dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-vector-store/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-advisors-vector-store/artifactId version1.0.0/version /dependency第一个依赖负责把 DashScope 的 Chat 模型、Embedding 模型自动配好后两个提供向量存储和检索增强 Advisor。如果你的 Spring AI 版本里这些类已经合并进核心模块就不需要重复引入具体看 IDE 里能否 import 到对应类。2.2 配置项与密钥管理在application.yml里加配置。API Key 一定别写死在文件里用环境变量注入。spring: application: name: rag-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus embedding: options: model: text-embedding-v3 vectorstore: simple: persist: path: ./data/vector-store.jsonqwen-plus是通义千问的中等规模模型日常问答性价比不错text-embedding-v3做中文向量化最大输入长度和维度都能满足入门项目需求。向量库持久化路径建议单独建目录避免和打包产物混在一起。2.3 验证自动配置是否生效写一个最简单的检查类启动后看是否正常注入了ChatModel和EmbeddingModel。如果 Spring AI Alibaba 的自动配置生效容器里会直接出现这两个 Bean什么都不用手动 new。Component public class StartupCheck { private static final Logger log LoggerFactory.getLogger(StartupCheck.class); public StartupCheck(EmbeddingModel embeddingModel, ChatModel chatModel) { log.info(EmbeddingModel: {}, embeddingModel.getClass().getSimpleName()); log.info(ChatModel: {}, chatModel.getClass().getSimpleName()); } }启动日志里能看到对应的实现类名就说明链路已经连通。这一步过了后面所有操作才有意义。3. RAG 数据管线从一份 TXT 到可召回的知识库3.1 准备样本文档我习惯在src/main/resources/docs下放一份测试文档这里用一份虚构的员工手册片段做示例。差旅报销标准国内出差住宿费一线城市每晚不超过 600 元其他城市不超过 450 元。单次报销金额在 5000 元以下的直接在 OA 系统提交发票和行程单5000 元以上需额外附部门负责人审批意见。餐饮补助按实际出差天数计算每天 100 元无需提供发票。你的真实场景可能是产品 FAQ、售后话术、合同模板内容换成自己的即可。3.2 读取文档并按 Token 切分RAG 的第一个关键操作是切分。文档不可能整篇塞进 Prompt一方面超长文本会稀释相关性另一方面 Embedding 模型对输入长度也有上限。标准做法是把文档切成一个个语义相对完整的段落每个段落独立向量化、独立检索。Spring AI 提供了TextReader和TokenTextSplitter可以直接这么写Service public class KnowledgeBaseBuilder { private final VectorStore vectorStore; public KnowledgeBaseBuilder(VectorStore vectorStore) { this.vectorStore vectorStore; } public void build(Resource resource) throws IOException { ListDocument documents new TextReader(resource).read(); TokenTextSplitter splitter new TokenTextSplitter(); ListDocument chunks new ArrayList(); for (Document document : documents) { ListDocument split splitter.split(document); for (Document chunk : split) { chunk.getMetadata().put(source, resource.getFilename()); } chunks.addAll(split); } vectorStore.write(chunks); } }这段代码的逻辑很直白读入文档按 Token 数量切成多段给每段加上来源元数据最后统一写入向量库。不同版本的 Spring AI 在TextReader.read()的返回类型上有改动有的返回单个Document有的返回ListDocument如果你拿到手的是旧版本用Collections.singletonList(document)包一下就行。3.3 切分参数怎么定切分粒度直接影响检索质量。我踩过几次坑之后目前比较稳定的经验值如下。参数建议值说明chunk size300 到 800 Token太小语义不完整太长噪声太多overlap80 到 160 Token防止关键信息被切在两段中间切分单位Token 而非字符中文一句往往对应多个 Token字符切分会把句子拆碎在实际项目里如果文档结构很强建议先按标题拆成章节再对每个章节做二次切分。比如产品手册通常有明确的“第一章、第二章”直接固定长度切会把章节的上下文切断检索时容易召回一个缺头少尾的段落。Spring AI 的DocumentTransformer接口可以串多条切分规则先按正则分节再按 Token 拆段效果会比单一切分好不少。3.4 向量化与落库切好的每一段文本都会被 Embedding 模型变成向量。这里不需要自己写调用逻辑vectorStore.write(chunks)内部会自动逐个调 Embedding 模型。Configuration public class RagConfiguration { Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) throws IOException { SimpleVectorStore store SimpleVectorStore.builder(embeddingModel).build(); File storeFile new File(./data/vector-store.json); if (storeFile.exists()) { store.load(storeFile); } return store; } }SimpleVectorStore是入门最合适的向量库零额外依赖数据落在一个 JSON 文件里调试时可以直接打开看每一条向量对应哪段原文。生产环境可以换成 Redis、PGVector 或 Elasticsearch 向量索引因为上层代码用的是统一的VectorStore接口切换成本很低。启动项目后调用一次build()观察日志里是否出现向量写入记录。然后打开vector-store.json你会发现每条记录除了向量本身还带着source等元数据。这正是后面过滤检索范围的基础。4. 问答联调一个能回答业务问题的最小系统4.1 用 Advisor 把检索结果自动注入问答Spring AI 里提供了一条捷径QuestionAnswerAdvisor。这个 Advisor 会在每次提问时自动完成“向量检索 → 拼接上下文 → 调用模型”三个步骤你只需要把它注册进ChatClient。Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); }当用户发起提问QuestionAnswerAdvisor会把问题转成向量在库里做相似度搜索取回最相关的几段原文然后拼到 Prompt 里告诉模型“请根据以下资料回答”。模型被约束在给定资料内作答而不是天马行空自己发挥。4.2 提供一个 REST 接口接收问题RestController RequestMapping(/api/rag) public class RagController { private final ChatClient chatClient; public RagController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/ask) public String ask(RequestBody String question) { return chatClient.prompt() .advisors(advisor - advisor.param(topK, 4)) .user(question) .call() .content(); } }这里最值得关注的是topK参数。它决定召回多少段原文值太小可能漏掉关键信息值太大可能把无关段落也带进来。我建议入门阶段先固定为 4后面再根据反馈微调。4.3 问答效果验证启动服务用 curl 模拟一次真实提问curl -X POST http://localhost:8080/api/rag/ask \ -H Content-Type: text/plain \ -d 差旅住宿费标准是多少如果链路正常模型会根据刚才灌进去的文档回答类似这样的话根据公司制度国内出差住宿费标准为一线城市每晚不超过 600 元其他城市不超过 450 元。接着你可以做一个反向测试问一个文档里完全没有的信息curl -X POST http://localhost:8080/api/rag/ask \ -H Content-Type: text/plain \ -d 公司组织架构中研发中心下设几个部门理想情况下模型应该表示“根据提供的资料无法回答”。如果它还是强行编了一个答案说明 Advisor 的限定指令没有生效或者你的 Prompt 里没有强调“资料外信息不要答”。这时可以手动在ChatClient的system里补一句约束如果资料中没有相关内容直接说明没有找到不要推测。4.4 拆开看 Advisor 的内部行为如果你好奇检索到底召回了什么可以绕过 Advisor直接调向量库看查询结果ListDocument hits vectorStore.similaritySearch( SearchRequest.builder() .query(差旅住宿费标准) .topK(4) .build() ); for (Document hit : hits) { System.out.println(hit.getText()); }这一步能帮你定位问题如果召回结果本身就答非所问那问题出在切分或向量化阶段如果召回结果没问题但模型回答不对那问题出在 Prompt 或参数设置上。先定位再调优效率比瞎调高得多。5. 调参与避坑清单5.1 召回结果差先查切片再查模型很多刚上手的朋友遇到回答不准第一反应是换更大更强的模型。但我实测下来绝大多数 RAG 效果差根源都在召回阶段。打开上一节写的相似度检索输出看看召回的前几段和问题是否相关。一个常见问题是 chunk size 设得太大。比如一份 2000 Token 的制度文档被切成两段每段 1000 Token检索时即使命中了也会把大量无关内容带进上下文模型容易被噪声带偏。把 chunk size 调到 400 左右overlap 设 80召回精度通常会明显提升。代价是知识库里的片段数量变多但向量检索的速度足够快体量在几十万条以内都不需要担心性能。5.2 topK 不是越大越好topK 过大时第 3、4 段很可能已经和问题关系不大。这些低相关文本混进 Prompt不仅浪费 Token还会增加模型“被带偏”的概率。我习惯把 topK 控制在 3 到 5 之间。如果你的文档质量很高、切分很干净可以压到 3如果文档杂、冗余多建议配合相似度阈值一起用只保留相关度超过阈值的片段。5.3 中文乱码是新手第一坑Windows 环境下读取 TXT 文件时TextReader默认按系统编码读取如果文档是 UTF-8 编码而系统默认 GBK读进来的内容就会变成乱码向量化出来的结果自然毫无意义。解决方法是建文档时统一保存为 UTF-8并在代码里显式指定字符集Resource resource new ClassPathResource(docs/company-rules.txt); String content new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);不要相信“我打开文档看着没问题”因为编辑器和终端很可能已经悄悄转换了编码。最稳妥的判断方法是写入向量库后把某一条 chunk 的文本打印出来看一眼。5.4 多轮对话时不要让历史问题污染召回做知识库问答很容易顺手把历史聊天记录也传给 Advisor 做召回。后果很常见用户上一轮问过考勤制度这一轮问“那请假呢”如果召回时只拿“那请假呢”这句话去检索结果可能什么都召不回但如果把整段历史拼进去检索向量会被上一轮的“考勤制度”主导结果永远召回到上一轮相关的内容。我的做法是RAG 检索只用当前这一轮的问题去向量化历史消息只作为对话背景放进ChatClient的 message 历史里不参与召回查询。这样既保证了多轮对话的连贯性也避免了旧话题的干扰。5.5 别忘了 RAG 不是万能钥匙最后说一句可能得罪人但很重要的经验如果业务数据本身是结构化表格比如数据库里的订单表、账单明细RAG 是绕远路更合适的是 NL2SQL让模型把自然语言问题转成 SQL 再查库。RAG 的舒适区是非结构化的文本资料比如制度、手册、问答对。认清边界选型就不会翻车。我个人做这个项目时最大的体会是RAG 的成功与否七分在前期的切分和召回三分在模型和 Prompt。先把文档切好、召回结果打出来验证再谈什么高级用法。骨架搭好之后后续可以往里面加文档实时更新、多知识库路由、Source 引用标注等功能。下一篇我准备聊聊 Spring AI Alibaba 里 Agent 方向的玩法让模型不只是被动回答还能主动调工具。专栏写到这希望你手里的知识库已经不是 Demo而是能真正回答业务问题的小系统。
返回列表