
Spring AI Milvus 简单 RAG 知识库案例笔记案例模块springAi-Demo/demo1-databases-ragSpring AI 1.1.3 / Spring Boot 3.x / JDK 17核心内容文档上传 → 解析 → 切分 → 向量化 → 写入 Milvus → 检索增强生成RAG一、整体流程上传文件(MultipartFile) ↓ TikaDocumentReader 解析 PDF/Word/TXT/MD 等 ListDocument ↓ TokenTextSplitter 按 token 切块 ListDocument切块后 ↓ EmbeddingModel 文本 → 向量本例 384 维本地模型 ↓ MilvusVectorStore.add 写入 Milvus 集合 用户提问 ↓ 向量化 → similaritySearch(topK 阈值 过滤) 召回片段 ↓ QuestionAnswerAdvisor 拼进 Prompt检索增强 ChatClient → ChatModel 输出二、依赖pom.xml版本由父工程的spring-ai-bom统一管理无需手写 version!-- 本地 Embedding 模型ONNX不需要联网、不需要 API Key--dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-transformers/artifactId/dependency!-- Milvus 向量库starter 只是把自动配置客户端 SDK 带进来仍可手动装配--dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-vector-store-milvus/artifactId/dependency!-- 文档解析Tika--dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-tika-document-reader/artifactId/dependency!-- QuestionAnswerAdvisor 所在包 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-advisors-vector-store/artifactId/dependency!-- RAG 模块RetrievalAugmentationAdvisor 等--dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-rag/artifactId/dependency!-- ChatMemoryMessageChatMemoryAdvisor 需要 ChatMemory bean--dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-autoconfigure-model-chat-memory/artifactId/dependencySpring AI BOM放父 pom 的dependencyManagementdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion1.1.3/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagement三、手动配置 MilvusVectorStore不使用自动配置自动配置application.yaml里写spring.ai.vectorstore.milvus.*够用但当你需要自定义客户端鉴权、多数据源、URI 动态拼装、Testcontainers时就手动声明两个 BeanMilvusServiceClientVectorStore。packagecom.ai.config;importio.milvus.client.MilvusServiceClient;importio.milvus.param.ConnectParam;importio.milvus.param.IndexType;importio.milvus.param.MetricType;importorg.springframework.ai.embedding.EmbeddingModel;importorg.springframework.ai.embedding.BatchingStrategy;importorg.springframework.ai.model.embedding.TokenCountBatchingStrategy;importorg.springframework.ai.transformer.splitter.TokenTextSplitter;importorg.springframework.ai.transformers.TransformersEmbeddingModel;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.ai.vectorstore.milvus.MilvusVectorStore;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Primary;ConfigurationpublicclassRagConfig{/** 切分器默认按 800 token 一块 */BeanpublicTokenTextSplittertokenTextSplitter(){returnnewTokenTextSplitter();}/** * Embedding 模型本地 ONNX默认 sentence-transformers/all-MiniLM-L6-v2输出 384 维。 * 加 Primary 覆盖 OpenAI 的自动配置避免两个 EmbeddingModel 冲突。 */Bean(localTransformersEmbeddingModel)PrimarypublicEmbeddingModelembeddingModel(){returnnewTransformersEmbeddingModel();}/** Milvus 客户端手写连接参数不走自动配置 */BeanpublicMilvusServiceClientmilvusClient(Value(${milvus.host:101.42.40.19})Stringhost,Value(${milvus.port:19530})intport,Value(${milvus.username:root})Stringusername,Value(${milvus.password:Milvus})Stringpassword){returnnewMilvusServiceClient(ConnectParam.newBuilder().withHost(host).withPort(port).withAuthorization(username,password)// 也可用 withUri(http://host:19530)测试用容器时withUri(milvusContainer.getEndpoint()).build());}/** 向量库手动指定集合/索引/度量覆盖自动配置 */BeanpublicVectorStorevectorStore(MilvusServiceClientmilvusClient,EmbeddingModelembeddingModel){returnMilvusVectorStore.builder(milvusClient,embeddingModel).collectionName(vector_store_test)// 集合名.databaseName(default).embeddingDimension(384)// ★ 必须和 EmbeddingModel 输出维度一致MiniLM384.indexType(IndexType.IVF_FLAT).metricType(MetricType.COSINE)// 余弦相似度score 越大越相似0~1.batchingStrategy(newTokenCountBatchingStrategy()).initializeSchema(true)// 集合不存在时自动建含索引与元数据字段.build();}}自动配置写法二选一不要同时用spring:ai:vectorstore:milvus:client:host:101.42.40.19port:19530username:rootpassword:MilvusdatabaseName:defaultcollectionName:vector_store_testembeddingDimension:384indexType:IVF_FLATmetricType:COSINEinitialize-schema:true一旦自己声明了VectorStorebean上面这段 yaml 就不会生效了连接信息改由ConnectParam提供。四、灌库解析 → 切分 → 入库PostMapping(value/upload,headerscontent-typemultipart/form-data)publicObjectuploadFile(RequestParam(namefile)ListMultipartFilefiles){if(files.isEmpty()){returnMap.of(code,500,msg,file is empty);}files.forEach(file-{// 1. 解析PDF/Word/Excel/TXT... 由 Tika 自动识别ListDocumentdocumentsnewTikaDocumentReader(file.getResource()).read();// 2. 切分避免超长文本被截断 / 语义过于分散ListDocumentsplitDocumentstokenTextSplitter.apply(documents);// 3. 可写入业务元数据后续按知识库隔离检索// splitDocuments.forEach(d - d.getMetadata().put(dabaseID, dabaseID));// 4. 向量化 入库内部按 BatchingStrategy 分批vectorStore.add(splitDocuments);});returnMap.of(code,200,msg,success);}要点TikaDocumentReader读的是纯文本扫描版 PDF图片读出来是空的 → 入库 0 条检索必然为空。元数据字段如dabaseID必须在入库前放进document.getMetadata()否则后续过滤表达式匹配不到。五、分片Chunking策略分片是 RAG 里最影响效果的一步切得太碎 → 语义残缺、召回不准切得太大 → 噪声多、挤占上下文。1八种常见策略速览策略一句话本质优点缺点适用场景固定长度分块最原始的机械切分像尺子一样精准但无情实现最简单、块大小恒定、成本极低会从句子/语义中间一刀切断日志、低结构化长文本、快速原型基于句子分块先切碎再拼凑保证每一句话的完整呼吸不破坏句子完整性可读性好句子长短不一 → 块大小抖动FAQ、问答语料、新闻递归字符分块由粗到细的漏斗筛选段落 → 句子 → 字符寻找最佳切分点兼顾块大小与语义边界通用性最好分隔符层级需按语料调优通用文档无脑默认首选结构化分块尊重文档骨架按标题层级H1/H2…打包内容块自带层级语境、内聚性高强依赖文档结构纯文本无效Markdown、技术手册、规章制度对话式分块滑动窗口机制保留问与答的上下文保留对话语义不会把问答拆散需识别说话人/轮次与窗口大小客服会话、聊天记录语义分块倾听数据的心跳在话题突变处切分语义最完整检索准确率通常最高需额外 embedding 计算慢且贵高质量知识库、长篇综述父文档检索索引的是碎片召回的是全貌小块做索引、大块喂模型小块好匹配、大块好作答需两级存储、实现复杂度高合同条款、说明书等精准定位但需完整上下文LLM 智能分块利用大模型的理解力像人类编辑一样精准断句效果最好、规则最灵活最慢最贵、结果有不确定性高价值小体量语料、离线预处理2Spring AI 1.1.3 的现状实测实际只提供org.springframework.ai.transformer.splitter.TokenTextSplitter 基类 TextSplitter implements DocumentTransformer没有SentenceSplitter / 递归 / 语义 / 父文档 / LLM 分片器spring-ai-rag里的VectorStoreDocumentRetriever是检索器不是分片器别搞混。其余策略要么自己实现DocumentTransformerListDocument apply(ListDocument)要么引入 LangChain4j 的DocumentSplitters等外部实现。TokenTextSplitter本质是按 token 定长 标点回退先按 token 切再尝试回退到最近的标点处断句所以它是固定长度 句子/递归的折中体不是纯机械切分。3TokenTextSplitter 参数与默认值javap 实测参数默认值说明chunkSize800目标块大小token 数minChunkSizeChars350小于该字符数的块尝试与相邻块合并minChunkLengthToEmbed5低于该长度直接丢弃maxNumChunks10000单文档最大块数防止爆量keepSeparatortrue是否保留分隔符punctuationMarks. ? ! \n断句标点集合BeanpublicTokenTextSplittertokenTextSplitter(){returnTokenTextSplitter.builder().withChunkSize(500)// 块更小 → 定位更准、上下文更少.withMinChunkSizeChars(200).withMinChunkLengthToEmbed(5).withMaxNumChunks(10000).withKeepSeparator(true).build();}注意TokenTextSplitter没有 overlap重叠窗口参数。需要重叠得自己实现相邻块之间多带 N 个 token 的上文否则跨块的语义会被切断。4结论最佳分片策略 由你自己按场景选择不存在放之四海皆准的最佳分片分片效果取决于你的语料结构 × 查询方式 × 成本预算不知道选什么 → 先用TokenTextSplitter / 递归字符分块跑通基线文档有明显标题层级 → 升级结构化分块长文档且要求答案完整 → 用父文档检索基线仍不达标又有预算 → 上语义分块 / LLM 智能分块会话数据 →对话式分块。最终采用哪一种、chunkSize 取多少都应由使用者自己按真实语料选择并验证不要迷信任何最优解。唯一的验收标准拿真实问题跑similaritySearch看 topK 召回的内容是否足以支撑答案不够就调分片再测。六、检索与对话1纯检索调试用GetMapping(/testVectorSearch)publicListDocumenttestVectorSearch(RequestParam(namemessage)Stringmessage){SearchRequestsrSearchRequest.builder().query(message).topK(5).similarityThreshold(0.2)// COSINE 下 score 越大越相似阈值别设太高.build();ListDocumentdocsvectorStore.similaritySearch(sr);docs.forEach(doc-System.out.println(doc doc.getText()));returndocs;}2带知识库过滤 会话记忆 检索增强的对话GetMapping(value/chat,producestext/event-stream;charsetUTF-8)publicFluxStringgenerate(RequestParam(valuemessage,defaultValue)Stringmessage,RequestParam(namedabaseID,requiredfalse)StringdabaseID){StringuserID1;// 实际取登录用户 id用作会话隔离SearchRequest.BuildersrBuilderSearchRequest.builder().similarityThreshold(0.2).topK(5);// 只有指定知识库时才加过滤条件Milvus 的过滤字段必须先存在于元数据中if(dabaseID!null!dabaseID.isBlank()){Filter.ExpressionfilternewFilterExpressionBuilder().eq(dabaseID,dabaseID).build();srBuilder.filterExpression(filter);}returnchatClient.prompt().user(message).advisors(a-a.param(curren_data,LocalDateTime.now().toString())).advisors(a-a.param(ChatMemory.CONVERSATION_ID,userID)).advisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(srBuilder.build()).build()).stream().content();}ChatClient 装配系统提示词 日志 会话记忆publicRagController(ChatClient.BuilderchatClientBuilder,ChatMemorychatMemory){this.chatClientchatClientBuilder.defaultAdvisors(newSimpleLoggerAdvisor(),MessageChatMemoryAdvisor.builder(chatMemory).build()).defaultSystem(你是个人知识库ai助手,今天的日期是: {curren_data}).build();}对话模型OpenAI 协议兼容示例用 DeepSeekspring:ai:openai:base-url:${AI_BASE_URL:https://api.deepseek.com}api-key:${AI_API_KEY:sk-xxxx}chat:options:model:${AI_MODEL:deepseek-chat}temperature:0.3七、自测顺序# 1. 上传必须带 multipart/form-datacurl-XPOST http://127.0.0.1:8004/rag/upload-FfileD:/test.pdf# 2. 检索是否召回先确认不为 []curlhttp://127.0.0.1:8004/rag/testVectorSearch?message你的问题# 3. 对话检索增强 记忆curlhttp://127.0.0.1:8004/rag/chat?message你的问题dabaseID0c93fc70-...