Spring Boot + LangChain4j + Milvus构建企业级RAG知识库问答系统

Spring Boot + LangChain4j + Milvus构建企业级RAG知识库问答系统 做企业级RAG问答系统时Java后端团队最常被问到的一个问题就是别人都用Python那套FastAPI LangChain你们Spring Boot行不行这篇文章我想用一次完整落地记录来回答这个问题。项目技术栈是Spring Boot 3.5.4 LangChain4j Milvus向量数据库模型侧接入阿里百炼APIDashScope兼容OpenAI协议最终实现的是一个开箱即用的企业知识库问答系统支持文档导入、文本切分、向量化存储、语义检索、大模型流式生成以及答案来源引用。这套方案适合已经在Java技术栈上沉淀了大量业务代码、不想为了一个AI功能单独维护一套Python服务的团队参考也适合刚接触RAG、想用Java跑通全链路的新手照着实操。1. 整体设计思路为什么选这套技术栈1.1 需求拆解企业知识库问答到底在解决什么问题很多团队一上来就聊大模型但企业知识库RAG的核心根本不是模型而是知识管理和检索效率。企业内部的文档散落在Wiki、飞书、语雀、内部网盘、数据库字段里员工问一个制度问题往往要翻好几个系统。大模型本身不会知道你公司报销流程长什么样它只会一本正经地胡说八道。RAG的方案是把企业文档先切块、向量化、存入向量数据库用户提问时先把问题也向量化在库里检索出最相关的几个片段再把这些片段拼进Prompt交给大模型让模型基于这些真实素材作答。这个链路拆开来看就是几个必须想清楚的问题数据从哪来、文档怎么切、向量怎么算、检索怎么召回、模型怎么生成。把这五件事想明白了任何技术栈都只是实现方式差异。而为什么选Spring Boot LangChain4j这个问题的答案恰恰就是我的核心结论如果一个团队已经用Java写了多年业务系统那么RAG服务不应该成为一座孤岛让它直接生长在现有微服务体系里权限、审计、配置中心、监控全部复用这比多维护一个Python服务划算得多。1.2 技术路线比较为什么不用Python栈而选LangChain4j选型阶段我做了个简单的对比表直接摆在评审会上讲避免Java不行这种纯感受式争论。维度Python FastAPI LangChainSpring Boot LangChain4j团队上手成本需要Python技能栈独立部署链路Java团队零额外门槛与现有系统集成Feign/RestTemplate调Python接口多一跳直接注入Bean共享配置中心与监控框架成熟度LangChain生态最丰富LangChain4j较年轻但Java核心场景覆盖完整向量库存量集成官方SDK齐全LangChain4j内置Milvus/PGVector/RedisVector等适配性能与并发控制GIL、多进程部署需要额外设计虚拟线程、Spring的线程池模型天然优势LangChain4j虽然在生态丰富度上确实比不上Python版LangChain但我们要做的企业问答场景本质上就是加载文档、切分、向量化、检索、调大模型这几件事LangChain4j对这些场景的封装已经完全够用。更关键的是LangChain4j的各种EmbeddingStore适配器是直接可用的我们这次选Milvus在依赖里引入一个langchain4j-milvus就能获得完整的向量读写能力不需要自己封装Milvus SDK。1.3 向量数据库选型Milvus为什么合适市面上的向量数据库不少Milvus、pgvector、Redis Vector、Elasticsearch的向量插件、Qdrant、Weaviate每个都能跑RAG。我最终选Milvus首先是因为它本身就是专为向量检索设计的分布式数据库对亿级向量的检索延迟控制得比pgvector这类传统数据库加装向量功能的方案好。企业知识库刚开始可能只有几十万条向量但一旦要接多个业务线的文档量级会快速上涨预留扩展能力比临时换库成本低得多。其次Milvus对LangChain4j有官方维护的MilvusEmbeddingStore实现这意味CRUD逻辑框架已经帮你处理好了。而且Milvus支持标量字段过滤我们可以在存储向量的同时存文档ID、来源URL、部门标签等元数据检索时先做条件过滤再算相似度这对企业场景非常友好。比如可以只检索市场部范围内的文档而不是全库检索一遍。2. 环境准备与基础设施搭建2.1 Milvus单机版部署Docker Compose一步步来企业知识库体量在初期用单机版Milvus完全够等数据量上来了再切分布式集群也不晚。我在本地和测试环境都用了Docker Compose方式部署Milvus standalone模式整体包含三个组件etcd元数据存储、MinIO数据持久化、milvus standalone核心服务。下面是完整可运行的docker-compose.yml我在多个环境验证过。version: 3.5 services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.18 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 - ETCD_SNAPSHOT_COUNT50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd healthcheck: test: [CMD, etcdctl, endpoint, health] interval: 30s timeout: 20s retries: 3 minio: container_name: milvus-minio image: minio/minio:RELEASE.2024-12-21T18-27-00Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address :9001 healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 standalone: container_name: milvus-standalone image: milvusdb/milvus:v2.5.4 command: [milvus, run, standalone] security_opt: - seccomp:unconfined environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus healthcheck: test: [CMD, curl, -f, http://localhost:9091/healthz] interval: 30s start_period: 90s timeout: 20s retries: 3 ports: - 19530:19530 - 9091:9091 depends_on: etcd: condition: service_healthy minio: condition: service_healthy启动命令只有一个docker compose up -d然后在docker compose logs -f standalone里看到Milvus Proxy ... started之类的日志就说明起来了。注意19530是gRPC服务端口Java SDK连的就是这个9091是健康检查端口。如果只是Spring Boot应用连Milvus只需要暴露19530就行仪表盘端口8090在新版本里已经挪到独立组件了。我第一次部署时按老教程找8090端口找半天结果什么都没看到原因就是新版Milvus把监控和UI组件拆分出去了。2.2 Spring Boot 3.5.4项目初始化与依赖引入项目直接用start.spring.io生成Java版本选21Spring Boot选3.5.4。我习惯在生成时勾选Spring Web、Spring Validation、Actuator这几个基础依赖至于LangChain4j和Milvus相关依赖我喜欢手工加到pom里因为版本号需要和Spring Boot 3.5.x兼容放build.gradle或pom里更好控制。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.4/version relativePath/ /parent properties java.version21/java.version langchain4j.version1.0.0-beta2/langchain4j.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency !-- LangChain4j 核心 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency !-- OpenAI 兼容协议适配阿里百炼 / DashScope -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- Milvus 向量数据库适配 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-milvus/artifactId version${langchain4j.version}/version /dependency /dependencies这里我重点说一下langchain4j-open-ai这个依赖。阿里百炼API的接口通过OpenAI兼容模式暴露所以我们可以直接复用LangChain4j的OpenAI适配器只需要把baseUrl指到百炼的/compatible-mode/v1就行。这是省事的关键一步不需要去引入一个单独的DashScope SDK也不需要在代码里单独维护一套鉴权逻辑。2.3 阿里百炼API接入模型配置与环境变量去阿里云百炼控制台开通DashScope服务之后拿到API Key然后创建模型。我这边实际用的是qwen-plus当对话模型、text-embedding-v4当Embedding模型。把Key配置到环境变量DASHSCOPE_API_KEY不要写死在application.yml里否则代码一提交Key就泄露了。application.yml里的LangChain4j配置大致长这样langchain4j: open-ai: chat-model: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} model-name: qwen-plus temperature: 0.2 embedding-model: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} model-name: text-embedding-v4 dimensions: 1024这里有个容易踩的坑dimensions这个参数不是所有模型都支持text-embedding-v4可以指定向量维度默认1024。在LangChain4j的OpenAI适配器里如果模型不支持自定义维度而你又传了dimensions可能会被模型侧忽略甚至报错。所以我在配置里显式写了1024后面创建Milvus集合时也用这个维度参数两边保持一致避免查询时报向量维度不匹配的错误。如果不想依赖Spring Boot自动配置也可以直接构造Bean显式一点Configuration public class LLMConfig { Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(qwen-plus) .temperature(0.2) .build(); } Bean public EmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(text-embedding-v4) .dimensions(1024) .build(); } Bean public MilvusEmbeddingStore embeddingStore() { return MilvusEmbeddingStore.builder() .host(localhost) .port(19530) .collectionName(enterprise_rag) .dimension(1024) .build(); } }手动构造Bean的好处是减少了配置文件与Java字段名的匹配问题而且MilvusEmbeddingStore的构建参数一目了然。我刚开始用Spring Boot自动配置时经常分不清langchain4j.milvus.collection-name还是langchain4j.embedding-store.milvus.collection-name这种命名索性改成手动Bean省掉一半烦恼。3. 核心链路实现数据接入、向量化与存储3.1 文档加载与文本切分策略把文档喂给RAG系统之前第一步是加载和切分。LangChain4j里Document就是一个带metadata的文本对象TextSegment则是切分后的最小检索单元。我这边写了一个文件接收接口支持上传Markdown、TXT和PDF上传后统一转成纯文本再切分。Service public class DocumentIngestionService { private final EmbeddingModel embeddingModel; private final EmbeddingStoreTextSegment embeddingStore; public void ingest(String content, MapString, String tags) { Document document Document.from(content); DocumentSplitter splitter DocumentSplitters.recursive(600, 100); ListTextSegment segments splitter.split(document); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments); } }切分参数需要解释一下recursive(600, 100)表示每个segment最大600个字符重叠部分100个字符。这里用recursive切分器是因为它是按段落、句子、子句的层级依次尝试分隔的处理的文档类型范围最广。字符数到底设多少取决于你的Embedding模型和业务场景。如果你是中文文档600个字符大概能覆盖一个制度条目的完整上下文如果是代码文档可以适当调低到300到400因为代码片段本身就结构化。重叠区的设计是我最初忽略掉、后来才明白的关键细节。两个相邻文本块如果完全没有重叠切分边界正好落在一个完整语义中间时检索时就会漏掉关键信息。重叠100个字符相当于给每个片段留了“跨上下文”的冗余实测检索命中率比不重叠高出一截。重叠也不是越多越好重叠多了等于同一个内容存两份向量数量膨胀存储成本和检索干扰都上升。3.2 Embedding模型调用与向量维度匹配Embedding模型负责把文本变成一串浮点数数组也就是向量。text-embedding-v4输出的1024维向量不同文本的语义越接近向量余弦相似度越高。Java端调用非常简单Embedding embedding embeddingModel.embed(报销流程是什么).content(); System.out.println(embedding.dimension()); // 1024这里有一个需要全链路对齐的约束文档切片向量维度、问题向量维度、Milvus集合声明的向量维度三者必须一致。我之前犯过一个很低级的错误测试环境用text-embedding-v3生成向量维数是1536后来把模型换成text-embedding-v4忘了同步改Milvus的collection定义结果就是往集合里写向量时报维度校验失败。Milvus的collection schema一旦创建dimension字段是改不了的只能drop掉重建。所以要么一开始就用动态schema要么提前把模型、维度方案固化清楚。还有一个并发写入的问题。embeddingModel.embedAll批量处理时阿里百炼API有单次调用条数上限印象中默认是批量不超过10条或16条具体以控制台配额为准。企业文档一多千万不能把几千个切片一股脑embedAll一定要分批。我在Service里加了一个简单的分批逻辑每批50条循环调用private static final int BATCH_SIZE 50; for (int i 0; i segments.size(); i BATCH_SIZE) { ListTextSegment batch segments.subList(i, Math.min(i BATCH_SIZE, segments.size())); ListEmbedding batchEmbeddings embeddingModel.embedAll(batch).content(); embeddingStore.addAll(batchEmbeddings, batch); }实测下来50条一批并发压力比较可控也不会触发百炼的限流。如果文档量到几十万条建议上消息队列异步处理HTTP接口只负责接收文档写入向量可以放到消费者慢慢跑。3.3 Milvus集合设计与数据写入MilvusEmbeddingStore在第一次使用时如果发现collection不存在会自动创建。但自动创建的schema是通用的不会自动给你加业务字段。所以我更建议把集合设计显式做出来比如定义好主键、向量字段以及几个常用标量字段。public void initCollection() { try (MilvusServiceClient client new MilvusServiceClient( ConnectParam.newBuilder().withHost(localhost).withPort(19530).build())) { if (client.hasCollection(hasCollectionParam(enterprise_rag)).getData()) { return; } CreateCollectionParam createParam CreateCollectionParam.newBuilder() .withCollectionName(enterprise_rag) .withDimension(1024) .withPrimaryField(id) .withVectorField(vector) .withAutoId(true) .build(); client.createCollection(createParam); } }不过这里要提醒一下如果你直接用上面这段Milvus原生SDK代码而LangChain4j的MilvusEmbeddingStore内部也有自己对collection schema的定义两边字段名对不上就麻烦了。我实际生产环境里绝大部分情况根本不需要手动调用Milvus原生SDK直接让MilvusEmbeddingStore管理collection就行它会自动创建id、vector、text、metadata等核心字段。需要加业务标量字段时可以通过配置或自定义schema的方式扩展。图省事的话先把元数据塞进TextSegment的metadata里LangChain4j查找时会自动把metadata存进Milvus的metadata字段检索结果里一样可以拿到来源。我上线的版本没有用原生SDK初始化而是直接靠MilvusEmbeddingStore的自动建表只在配置里指定collectionName和dimension。原因很简单一行代码的事为什么非要再维护一套原生SDK的建表逻辑等到需要定制索引参数比如切换HNSW的M和efConstruction参数再单独写原生SDK建表也不迟。4. 问答链路实现语义检索与大模型生成4.1 语义检索相似度匹配与阈值调优检索环节是整个RAG系统里最影响答案质量的地方。检索不到相关文档后面Prompt写得再好也白搭。LangChain4j封装了EmbeddingStoreContentRetriever使用非常直接ContentRetriever contentRetriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.6) .build();这段代码里两个参数我解释一下。maxResults(5)表示最多召回5个文本片段这个数量不能拍脑袋定取决于你的模型上下文窗口和文档切分大小。如果每段600字符5段大概3000字符qwen-plus的上下文窗口完全能容纳。minScore(0.6)是相似度阈值Embedding模型输出的是余弦相似度百炼这个模型在标准中文数据集上相关片段相似度普遍在0.7到0.85之间不相关的基本在0.5以下。所以0.6作为阈值能挡住大多数低质量召回。但不同领域、不同Embedding模型对相似度分数的分布影响很大上线前一定要抽样检查实际分数再定阈值。我也尝试过更细的元数据过滤方式当用户所属部门被识别出来后只检索该部门文档Filter filter new Filter( new Condition(department, Operator.EQUAL, 市场部)); EmbeddingMatchTextSegment matches embeddingStore.findRelevant( queryEmbedding, 5, 0.6, filter);这就是前面选Milvus的一个重要原因标量字段过滤能直接集成进向量检索流程把检索范围缩小避免相似度不相关的文本因为全局分数高而误召回。4.2 Prompt构造与RAG问答流程LangChain4j里组织RAG问答最常用的方式是定义AiServices接口。我会事先定义一个Assistant接口方法上标注SystemMessage和UserMessage来指定Prompt模板public interface Assistant { SystemMessage( 你是一个企业知识库助手。只能根据提供的资料回答问题。 如果资料中没有答案直接说明“资料库中没有相关内容”不要编造。 回答时尽量使用简体中文并保持简洁。 ) String answer(UserMessage String question); }然后组装服务和检索器Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .contentRetriever(contentRetriever) .build(); String answer assistant.answer(公司的报销流程是什么);你可能会问为什么这么简单的接口就能实现RAG因为LangChain4j在AiServices内部做了大量装配用户提问后自动调用ContentRetriever去向量库检索把检索到的文本片段拼接到Prompt里然后调用ChatLanguageModel最后把模型回答返回。整个过程开发者只需要定义好接口和配置检索器。这种封装方式非常符合Spring Boot的编程习惯面向接口编程框架负责把细节串起来。这里要提醒一个我最常看到的错误系统提示词里写“只能根据提供的资料回答问题”但并没有真正把资料传给模型。在AiServices模式下不会出现这个问题因为ContentRetriever的检索结果会自动注入到Prompt的information或documents上下文里。但如果大家自己用ChatLanguageModel裸调API记得手动把检索结果拼进Prompt否则模型根本没有上下文可供参考RAG就名存实亡了。4.3 流式输出与前端交互体验企业用户对问答系统的等待耐心很短如果问一句要转圈五秒才出结果体验就是灾难。所以接口一定做成流式输出。LangChain4j的StreamingChatLanguageModel接口可以逐字返回配合Spring WebFlux的Flux或者Servlet异步响应都可以。我这边用的是Spring Web MVC的SseEmitter来实现因为Spring Boot 3.5.x里SseEmitter用起来最直观跟前端对接SSE也简单。RestController RequestMapping(/api/rag) public class RagController { private final Assistant assistant; private final StreamingChatLanguageModel streamingChatModel; GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(RequestParam String question) { SseEmitter emitter new SseEmitter(0L); streamingChatModel.chat(question) .onPartialResponse(token - { try { emitter.send(SseEmitter.event().data(token)); } catch (IOException e) { emitter.completeWithError(e); } }) .onCompleteResponse(response - emitter.complete()) .onError(emitter::completeWithError); return emitter; } }“流式返回”看起来只是体验优化实际还解决了大模型接口超时问题。非流式情况下qwen-plus生成一段长回答可能要十几秒网关层经常会因为读超时断开连接。流式模式下服务端持续有数据返回网关层不会判定超时前端也能实时看到生成过程。我强烈建议RAG问答接口全部走流式哪怕你现在的前端只是内部工具也值得为这个体验买单。5. 生产化要点与性能优化5.1 检索参数调优topK、阈值与重排序参数调优是最依赖数据的一条环节我直接给一套多次验证过的初始参数大家在此基础上基于自己的文档调整。参数初始推荐说明切分大小600字符中文场景下兼顾语义完整与检索粒度切分重叠100字符缓解边界信息丢失topKmaxResults5上下文长度充裕时可调到8minScore0.6依Embedding模型分数分布微调流式输出开启优化体验避免网关超时温度0.2知识问答场景追求确定性温度要低我自己调参时踩过一次重组装的坑以为topK越高答案越准调到20之后发现大模型反而被无关片段干扰回答开始东拉西扯。因为Milvus召回的是“相似但未必都相关”的片段topK太高会把边缘内容也塞进上下文。更合理的做法是topK保持在5到8之间如果业务上需要更高精度可以引入重排rerank模型先粗召回20条再用rerank模型精排取前5条这在企业级知识库场景里是性价比很高的优化。不过百炼API的重排模型这边我们还没有接入暂时不做展开。5.2 缓存策略与连接池RAG问答系统的性能瓶颈常常不在大模型而在向量检索和网络往返。我给系统加了一层简单缓存对用户问题做MD5把“问题到答案”的映射缓存到Redis里。回答相同问题时直接命中缓存省去向量检索和大模型调用。但缓存粒度要控制好如果企业内部政策每周更新缓存过期时间建议不超过1小时否则用户会一直看到旧答案。Milvus客户端连接也要讲究不能在Service里每次new MilvusServiceClient那是灾难。用LangChain4j的MilvusEmbeddingStore时连接管理由框架处理内部复用连接池。但如果你们像我一样在某些环节直接操作Milvus原生SDK务必把MilvusServiceClient做成单例并且合理设置连接超时。我自己遇到过一次性并发写入5000个向量时默认客户端参数下出现grpc连接数过多的情况后来通过调整线程池和批量大小解决了。另外关于阿里百炼API的连接池OpenAI兼容SDK底层是okhttp或类似HTTP客户端Spring Boot默认的连接池配置不一定适用于高并发。我们给大模型调用的QPS不高所以没有专门调优但如果你们要承接更大流量建议在API网关层做限流和重试避免百炼侧429限流直接打到用户界面上。6. 常见问题与避坑实录6.1 高频报错与解决办法我把这几个月被问得最多的几个问题整理成了一张速查表遇到问题直接对照处理。现象根因解决方法Milvus连接拒绝Docker容器未启动或端口未映射docker compose ps检查19530是否正常监听向量维度不匹配Embedding模型维度与Milvus集合维度不一致确认text-embedding-v4输出维度重建集合百炼API返回401API Key错误或未配置环境变量检查DASHSCOPE_API_KEY是否已设置百炼控制台核对Key流式接口返回乱码SSE格式不规范或响应编码问题指定produces text/event-stream统一UTF-8编码模型答案不着边际检索结果相关性差或topK过大调低topK提高minScore检查切分粒度批量写入慢/超时一次写入切片过多按50条一批循环写入必要时上异步消息队列Spring Boot启动失败但无端口显示端口被占用或应用未完全启动查看完整日志检查server.port配置确认没有其他进程占用6.2 实操心得与避坑清单最后分享几条我认为最值得写进团队文档里的经验。第一先把数据准备工作做好再谈模型调优。我见过太多团队上来先折腾Prompt结果检索库里导入的文档都是乱切的、元数据残缺的。数据质量决定RAG上限这句话绝对不是空话。文本切分后的片段一定要人工抽样检查看边界是不是断在了奇怪的地方标题和正文有没有混在一起。第二LangChain4j版本升级要谨慎。这个框架目前迭代速度很快API变动确实存在。我们从0.36.x升级到1.0.0-beta系列时部分构造方法签名发生了变化。生产环境不建议每月追新版本锁定一个稳定版本重点验证检索和问答链路回归没问题再升。第三日志里不要记录完整用户问题和答案。企业知识库经常涉及内部敏感信息一旦日志脱敏没做好流经大模型的数据会暴露给运维甚至三方平台。我们在网关层做了脱敏把所有包含用户问题、模型答案的日志都截断或打码只保留长度、耗时、检索命中数量等无敏感信息的指标。第四成本控制要从第一天开始考虑。阿里百炼API按Token计费RAG系统最烧钱的就是每次提问都要把几段召回文本拼进Prompt里重复计算Token。如果你发现一个月账单很高优先考虑加Redis缓存层把高频问题直接缓存答案能省下一大笔费用。这个项目后续如果要继续演进我下一步会重点做两件事一是接入重排模型把粗召回和精排彻底分开提升长尾问题的回答质量二是做文档增量更新机制避免每次全量重建向量库用Milvus的按主键删除能力实现局部刷新。如果你也在用Java技术栈搭企业知识库希望这篇文章能帮你少踩一些我踩过的坑直接走通一条相对成熟的路。