ARTICLE DETAIL

资讯详情

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

基于LangChain4j与SpringBoot构建企业级RAG智能对话系统实战

基于LangChain4j与SpringBoot构建企业级RAG智能对话系统实战 简介检索增强生成RAG技术通过结合信息检索与大型语言模型生成能力有效解决了大模型在处理长文本、实时信息及私有知识时的局限性。其核心原理是将外部知识库向量化存储在用户提问时进行语义检索并将相关上下文注入提示词从而生成更准确、可追溯的答案。这一技术架构在提升回答质量、保障信息时效性与可控性方面具有重要价值广泛应用于智能客服、企业知识库问答、文档分析等场景。本文以LangChain4j和SpringBoot为核心详细阐述了构建一个支持多模态、工具调用和流式输出的综合性智能对话系统的完整工程实践涵盖了从文档解析、向量化存储到RAG链路优化等关键环节并深入探讨了性能调优与部署监控等生产级考量。1. 项目缘起为什么选择LangChain4j与SpringBoot构建智能对话系统最近在做一个内部知识库问答的POC项目团队最初的想法很简单找个现成的开源框架把文档喂进去能回答就行。但真动起手来发现坑一个接一个。比如直接用某个大模型的API做问答上下文长度有限稍微长一点的文档就处理不了想支持多格式文件PDF、Word、图片解析和预处理代码写得乱七八糟更别提用户想要“像ChatGPT那样”一边生成一边显示结果的流式体验了。折腾了两周原型是出来了但代码耦合度高扩展性差加个新功能就得伤筋动骨。痛定思痛我决定用更工程化的思路重构这个项目。核心选型就两个LangChain4j和SpringBoot。选LangChain4j是因为它作为Java生态的LangChain实现提供了一套声明式的、模块化的API把RAG检索增强生成、工具调用、多模态这些复杂概念封装成了相对易用的组件不用再自己从零造轮子处理提示词工程、上下文管理和函数调用。选SpringBoot则是看中了其成熟的依赖注入、自动配置和强大的Web开发能力能快速搭建起一个稳定、可维护的后端服务方便集成数据库、消息队列、监控等企业级组件。这个实战项目就是这次重构的完整记录。它不仅仅是一个“Hello World”式的Demo而是一个包含了RAG检索增强生成、MCP模型上下文协议处理、向量化存储与搜索、多模态图像合成、SSE流式输出、工具调用等核心功能的综合性系统。我会带你从零开始拆解每个模块的设计思路、具体实现和那些文档里不会写的“坑”。2. 项目核心架构与模块拆解在开始写代码之前我们先得把系统的骨架搭好。一个健壮的智能对话系统不能把所有逻辑都堆在一个Controller里。基于LangChain4j的设计哲学我采用了分层架构将不同职责解耦。2.1 整体架构设计整个系统可以划分为五个核心层次应用接口层Web Layer基于SpringBoot的REST Controller处理HTTP请求如/chat负责参数校验、身份认证简易版和将请求路由到服务层。对于流式输出这里使用SseEmitter来实现Server-Sent Events。核心服务层Service Layer这是业务逻辑的核心。它不直接依赖LangChain4j的具体类而是定义诸如ChatService、DocumentIngestService、ToolService等接口。实现类里会组装LangChain4j的各种组件如ConversationalRetrievalChain。AI能力层AI Layer完全由LangChain4j主导。这一层创建和管理各种“模型”Model、“嵌入模型”EmbeddingModel、“工具”Tool和“记忆”Memory的实例。例如通过OpenAiChatModel连接GPT通过AllMiniLmL6V2EmbeddingModel进行本地文本向量化。数据存储层Data Layer负责知识的持久化。包含两部分向量数据库Vector Store用于存储文档切片后的向量嵌入Embeddings。项目选择了Chroma通过langchain4j-store-embedding-chroma集成因为它轻量、易部署且和LangChain4j兼容性好。你也可以换成Pinecone、Weaviate等。关系型数据库使用Spring Data JPA操作MySQL用于存储原始的文档元数据如文件名、上传时间、状态和对话历史Session的非向量部分。工具与集成层Tool Integration Layer实现Tool接口的各类工具。例如一个查询天气的WeatherTool或者一个调用内部API查询订单状态的OrderQueryTool。这些工具通过LangChain4j被大模型智能调用。用户请求 | v [SpringBoot Controller] (处理HTTP/SSE) | v [Service Layer] (组装业务流程) | v [AI Layer (LangChain4j)] --- [Tool Layer] | | v v [Vector Store (Chroma)] [External APIs] | v [Relational DB (MySQL)]2.2 技术栈选型与依赖管理确定了架构接下来就是定技术栈和配pom.xml。这里有几个关键选择需要解释1. LangChain4j 版本与模块化引入LangChain4j采用了模块化设计你需要什么就引入什么避免依赖膨胀。核心依赖如下dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.31.0/version !-- 请使用最新稳定版 -- /dependency !-- 集成OpenAI -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.31.0/version /dependency !-- 本地嵌入模型可选避免调用OpenAI收费 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-all-minilm-l6-v2/artifactId version0.31.0/version /dependency !-- 向量数据库Chroma集成 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-store-embedding-chroma/artifactId version0.31.0/version /dependency !-- 如果需要解析PDF等文档 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-document-parser-apache-pdfbox/artifactId version0.31.0/version /dependency注意版本号务必保持一致不同模块间版本不兼容会导致诡异的ClassNotFoundException。建议使用langchain4j-bom来统一管理版本。2. 为什么选择Chroma作为向量数据库在POC阶段我们评估过PgVector需要PostgreSQL插件、Milvus功能强大但较重。Chroma的优势在于它是一个独立的、专注于向量存储的服务安装简单一个Docker命令并且提供了友好的HTTP API和Python/Java客户端。对于Java项目LangChain4j对Chroma的集成封装得很好几行代码就能完成连接和操作非常适合快速验证和中小规模应用。3. SpringBoot 与 WebFlux 的选择由于我们需要支持流式输出SSE这是一个I/O密集型操作。传统的SpringMVCServlet虽然也能通过SseEmitter实现但在高并发流式响应时可能会遇到线程阻塞的问题。Spring WebFlux基于Reactor项目采用非阻塞、异步响应式编程模型在处理大量并发连接和流数据时更具优势。因此本项目选择了spring-boot-starter-webflux。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency3. 知识库构建从文档到向量的完整流水线RAG系统的效果一半取决于检索的质量。而检索的质量又很大程度上取决于知识库的构建过程。这一步没做好后面大模型再强也白搭。3.1 文档解析与预处理用户上传的可能是PDF、Word、TXT、Markdown甚至PPT。我们需要一个统一的入口来处理它们。我创建了一个DocumentProcessor服务类。Service Slf4j public class DocumentProcessor { Autowired private ApachePdfBoxDocumentParser pdfParser; // LangChain4j提供的解析器 Autowired private TextDocumentParser textParser; public ListDocument parse(MultipartFile file) throws IOException { String fileName file.getOriginalFilename(); String contentType file.getContentType(); ListDocument documents new ArrayList(); if (fileName.endsWith(.pdf) || application/pdf.equals(contentType)) { // 注意Apache PDFBox 可能解析某些复杂PDF时内存溢出 // 生产环境应考虑使用付费云服务或更健壮的解析库 documents pdfParser.parse(file.getInputStream()); } else if (fileName.endsWith(.txt) || fileName.endsWith(.md)) { String content new String(file.getBytes(), StandardCharsets.UTF_8); documents Collections.singletonList(Document.from(content)); } else if (fileName.endsWith(.docx)) { // 需要引入Apache POI等库进行解析 // documents docxParser.parse(...); } else { throw new UnsupportedOperationException(Unsupported file type: contentType); } // 为每个Document添加元数据便于后续检索和溯源 for (Document doc : documents) { doc.metadata().put(source, fileName); doc.metadata().put(upload_time, Instant.now().toString()); } return documents; } }踩坑记录PDF解析的内存陷阱初期直接用PDFBox解析一个300页的技术手册直接导致Pod内存OOMOut Of Memory崩溃。原因是PDFBox在解析某些嵌入大量字体或图片的PDF时会将所有资源加载到内存。解决方案是设置解析时的内存参数pdfParser.setMaxMemoryBytes(50 * 1024 * 1024); // 限制50MB。对于超大PDF采用分页解析的策略一次只处理几页。更稳妥的方案是使用像Amazon Textract或Google Document AI这样的云服务它们能提供更准确、更稳定的解析并直接输出结构化文本。3.2 文本分割Chunking策略RAG效果的关键解析出来的长文档不能直接扔给向量模型需要切成有意义的“块”Chunk。分割策略直接影响检索的准确性。按固定长度分割最简单但可能把一个完整的句子或概念拦腰截断。按分隔符分割比如按段落\n\n、句号、标题等。更符合语义但块的大小可能不均匀。重叠分割在块与块之间保留一部分重叠文本例如后一个块的前100字是前一个块的后100字这能防止关键信息恰好落在块边界而丢失是提升召回率的有效技巧。LangChain4j提供了DocumentSplitter工具。我采用的是递归字符分割器它尝试按一组优先级递减的分隔符如\n\n,\n,。,,,,, 来分割并保证每个块在指定大小范围内。Component public class DocumentSplitterConfig { Value(${rag.chunk.size:1000}) private int chunkSize; Value(${rag.chunk.overlap:200}) private int chunkOverlap; Bean public RecursiveDocumentSplitter recursiveDocumentSplitter() { // 目标块大小1000字符重叠200字符 return new RecursiveDocumentSplitter(chunkSize, chunkOverlap); } }参数调优经验chunkSize需要和嵌入模型的上下文窗口匹配。例如text-embedding-ada-002建议不超过8191个token。对于中文一个汉字约1.5-2个token所以1000-1500汉字是安全范围。太小会丢失上下文太大会引入噪声。chunkOverlap通常设置为chunkSize的10%-20%。我实测下来对于技术文档15%的重叠能有效改善连续概念的检索。3.3 向量化嵌入与存储文本块准备好后需要将其转换为向量Embedding。这里面临一个选择使用本地模型还是云端API本地模型如AllMiniLmL6V2EmbeddingModel优点是完全离线、零成本、数据隐私安全。缺点是嵌入质量尤其是对中文和领域专业术语可能不如OpenAI的text-embedding-3-small等大型模型且会消耗本地计算资源。云端API如OpenAI Embeddings优点是嵌入质量高、稳定、省心。缺点是有成本按token计费且有网络延迟和数据出境风险。对于内部知识库我选择了折中方案敏感、非公开文档使用本地模型公开、通用的技术文档使用云端API。通过一个策略模式来动态选择。Service public class EmbeddingService { Autowired private EmbeddingModel localEmbeddingModel; // AllMiniLmL6V2 Autowired(required false) private EmbeddingModel openAiEmbeddingModel; // OpenAI public EmbeddingModel getModel(boolean useLocal) { return useLocal ? localEmbeddingModel : openAiEmbeddingModel; } public void ingestDocuments(ListDocument chunks, String collectionName, boolean useLocal) { EmbeddingModel model getModel(useLocal); EmbeddingStoreTextSegment embeddingStore ChromaEmbeddingStore.builder() .baseUrl(http://localhost:8000) .collectionName(collectionName) .build(); // LangChain4j 提供了便捷的嵌入存储方法 embeddingStore.addAll(model.embedAll(chunks).content(), chunks); } }存储到ChromaChromaEmbeddingStore是LangChain4j提供的客户端。你需要先在本机或服务器上通过Docker运行Chroma服务docker run -p 8000:8000 chromadb/chroma。collectionName相当于数据库的表可以用来隔离不同项目或不同版本的知识库。4. 智能对话核心RAG链路的构建与优化知识库建好了接下来就是核心的问答环节。LangChain4j的核心抽象是Chain链我们将把检索器Retriever、大语言模型LLM、记忆Memory等组件组装成一个ConversationalRetrievalChain。4.1 组装ConversationalRetrievalChainConfiguration public class ChatChainConfig { Value(${openai.api.key}) private String openAiApiKey; Value(${openai.model:gpt-4o-mini}) private String openAiModel; Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(openAiApiKey) .modelName(openAiModel) .temperature(0.2) // 降低随机性让回答更确定 .maxTokens(2000) .build(); } Bean public EmbeddingStoreTextSegment embeddingStore() { return ChromaEmbeddingStore.builder() .baseUrl(http://localhost:8000) .collectionName(company_knowledge_base) .build(); } Bean public EmbeddingModel embeddingModel() { // 这里使用OpenAI的嵌入模型保证检索质量 return OpenAiEmbeddingModel.builder() .apiKey(openAiApiKey) .modelName(text-embedding-3-small) .build(); } Bean public ConversationalRetrievalChain conversationalChain( ChatLanguageModel chatModel, EmbeddingStoreTextSegment embeddingStore, EmbeddingModel embeddingModel) { // 1. 创建检索器从向量库中找出与问题最相关的文本块 EmbeddingStoreRetriever retriever EmbeddingStoreRetriever.from(embeddingStore, embeddingModel, 5); // 返回最相关的5个块 retriever.setMinScore(0.7); // 设置相关性最低分数阈值过滤掉低质量结果 // 2. 创建内容注入器将检索到的内容整合到给LLM的提示词中 DocumentContentInjector contentInjector DefaultDocumentContentInjector.builder() .metadataFieldsToInclude(Arrays.asList(source, page)) // 在提示词中告诉LLM来源 .build(); // 3. 创建对话记忆让LLM记住上下文 ChatMemory chatMemory MessageWindowChatMemory.withMaxMessages(10); // 4. 组装链 return ConversationalRetrievalChain.builder() .chatLanguageModel(chatModel) .retriever(retriever) .documentContentInjector(contentInjector) .chatMemory(chatMemory) .promptTemplate( 你是一个专业的助手请根据以下上下文信息回答问题。 如果上下文信息不足以回答问题请直接说“根据现有资料无法回答”不要编造信息。 上下文信息 {{documents}} 历史对话 {{chat_history}} 用户问题{{question}} 请用中文回答 ) // 自定义提示词模板 .build(); } }关键点解析retriever.setMinScore(0.7)这是一个非常重要的优化。向量检索返回的是相似度分数余弦相似度范围-1到1越接近1越相似。设置一个阈值如0.7可以过滤掉那些似是而非、相关性不高的文档块防止它们“污染”LLM的上下文导致回答偏离或胡言乱语。这个值需要根据你的数据和嵌入模型进行调优。DocumentContentInjector它控制着如何把检索到的文档块和元数据格式化后放入最终的提示词。这里我们选择包含source和page元数据这样LLM在回答时可以引用来源比如“根据《XX产品手册》第5页的内容...”增加了可信度。PromptTemplate提示词模板是RAG的“指挥棒”。清晰的指令能极大提升回答质量。这里明确要求LLM基于上下文、不胡编并指定了回答语言。4.2 实现流式输出SSE用户不想等整个答案生成完再看到结果。Spring WebFlux的SseEmitter或更地道的ServerSentEvent可以轻松实现逐词或逐句的流式推送。RestController RequestMapping(/api/chat) Slf4j public class ChatController { Autowired private ConversationalRetrievalChain chatChain; PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString streamChat(RequestBody ChatRequest request) { return Flux.create(sink - { // 使用LangChain4j的流式API chatChain.executeAsync(request.getQuestion()) .onNext(token - { // 每生成一个token/片段就通过SSE发送出去 sink.next(ServerSentEvent.builder(token).build()); }) .onComplete(() - { sink.next(ServerSentEvent.builder([DONE]).build()); sink.complete(); }) .onError(sink::error) .start(); }).doOnSubscribe(sub - log.info(开始流式对话问题{}, request.getQuestion())) .doFinally(signal - log.info(流式对话结束)); } }前端配合前端需要使用EventSourceAPI来监听这个/api/chat/stream端点并不断将收到的data追加到页面上。这样就能实现类似ChatGPT的打字机效果。性能注意流式响应会保持一个长时间的HTTP连接。需要合理设置Web容器的超时时间如Tomcat的connection-timeout并考虑在网关层做连接管理和负载均衡。5. 进阶功能实现工具调用与多模态扩展一个只会聊天的助手是有限的。真正的智能体Agent应该能“动手”操作外部系统。这就是工具调用Function Calling的价值。同时支持多模态如图像生成能极大丰富应用场景。5.1 定义与注册工具在LangChain4j中一个工具就是一个实现了Tool接口的类或者一个带有Tool注解的方法。假设我们需要一个查询天气的工具Component public class WeatherTool { Tool(根据城市名称查询当前天气情况) public String getWeather(P(城市名称例如北京、上海) String city) { // 这里模拟调用外部天气API log.info(查询城市 {} 的天气, city); // 实际应调用如和风天气、OpenWeatherMap的API MapString, String mockData Map.of( 北京, 晴25℃微风, 上海, 多云28℃东南风3级 ); return mockData.getOrDefault(city, 暂未找到该城市天气信息); } }然后我们需要在配置中将这个工具“喂”给LLM。LangChain4j提供了ToolExecutor和AiServices来简化这个过程。Configuration public class ToolConfig { Bean public ToolExecutor toolExecutor(ListTool tools) { // 将所有工具包装成一个执行器 return new DefaultToolExecutor(tools); } Bean public AiServicesAssistant aiServices(ChatLanguageModel chatModel, ToolExecutor toolExecutor) { return AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .toolExecutor(toolExecutor) .build(); } // 定义一个助理接口声明对话方法 public interface Assistant { String chat(String userMessage); } }现在当你通过aiServices的Assistant实例进行对话时如果用户问“北京天气怎么样”LLM会自动识别出需要调用getWeather工具并将“北京”作为参数传入执行得到结果后再将结果整合进最终的回复中。这一切都是自动的。5.2 多模态图像合成集成除了文本工具我们还可以集成图像生成模型如OpenAI的DALL-E或Stable Diffusion的API。首先定义一个图像生成的工具Component public class ImageGenerationTool { Autowired private OpenAiImageModel openAiImageModel; // 需要引入langchain4j-open-ai Tool(根据文本描述生成一张图片) public String generateImage(P(详细的图片描述例如一只戴着眼镜、在敲代码的卡通猫) String description) { ImageResponse response openAiImageModel.generate(description); // 返回图片的URL或Base64数据 return response.content().imageUrl(); } }将这个工具像天气工具一样注册后当用户说“画一只在太空漫步的兔子”LLM就会调用这个工具并将生成的图片URL返回给用户。前端需要额外处理这种多媒体响应。MCP模型上下文协议的体现你可能注意到在上面的RAG链和工具调用中我们并没有显式地处理复杂的上下文管理比如长对话中的历史工具调用结果。这就是LangChain4j底层通过ChatMemory和PromptTemplate实现的MCP思想的一部分——它帮助模型维护一个结构化的对话历史将之前的用户输入、AI回复、工具调用及结果都记录下来作为下一次对话的上下文使得模型能进行连贯的、有状态的交互。我们通过配置MessageWindowChatMemory.withMaxMessages(10)就简单实现了这一点。6. 部署、监控与性能调优开发完成最后一步是让系统稳定可靠地跑起来。6.1 应用配置与部署application.yml关键配置示例spring: application: name: smart-chatbot datasource: url: jdbc:mysql://localhost:3306/chatbot_db?useUnicodetruecharacterEncodingutf8 username: root password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: update show-sql: true openai: api: key: ${OPENAI_API_KEY} model: gpt-4o-mini embedding-model: text-embedding-3-small rag: chunk: size: 1000 overlap: 200 retrieval: top-k: 5 # 检索返回的文档块数量 min-score: 0.7 # 相似度最低阈值 chroma: base-url: http://${CHROMA_HOST:localhost}:8000 collection-name: prod_knowledge_base server: port: 8080 # 针对流式响应调整超时设置 (Tomcat) tomcat: connection-timeout: 600000 # 10分钟 max-keep-alive-requests: 100部署建议容器化编写Dockerfile将SpringBoot应用打包成镜像。同时用docker-compose.yml编排应用、MySQL、Chroma三个服务一键启动。环境变量所有敏感信息API Key、数据库密码必须通过环境变量注入不要写在配置文件中。健康检查为SpringBoot Actuator添加健康端点并确保/actuator/health能正确反映Chroma和MySQL的连接状态。6.2 监控与日志智能对话系统的监控至关重要尤其是涉及API调用计费的部分。关键指标Token消耗监控每次对话请求的输入/输出token数预估成本。响应延迟区分“检索生成”总耗时和纯LLM生成耗时。延迟过高会影响用户体验。检索质量记录每次检索返回的文档块数量及最高相似度分数用于分析知识库覆盖度和分割策略效果。错误率统计API调用失败、解析失败、空检索结果的比例。实现方式可以使用Spring AOP面向切面编程在ChatService的方法上添加注解统一收集这些指标并输出到日志或推送到监控系统如PrometheusGrafana。Aspect Component Slf4j public class ChatMetricsAspect { Around(annotation(org.springframework.web.bind.annotation.PostMapping)) public Object logChatMetrics(ProceedingJoinPoint joinPoint) throws Throwable { long startTime System.currentTimeMillis(); String question // 从请求参数中获取问题 Object result joinPoint.proceed(); long duration System.currentTimeMillis() - startTime; // 这里可以记录到日志或Metrics Registry log.info(Chat request - Question: {}, Duration: {}ms, question, duration); return result; } }6.3 性能调优实战经验向量检索优化索引类型Chroma默认使用HNSW索引在速度和精度之间取得了很好的平衡。对于千万级以上的向量可以研究一下IVF等索引的调参。批量操作在构建知识库时使用embeddingStore.addAll()批量添加向量而不是循环单条添加速度有数量级提升。缓存对于高频且不变的知识库可以考虑将向量数据加载到应用内存缓存中如果内存足够但更常见的做法是使用Redis缓存频繁被检索的(query, results)对。LLM调用优化设置超时与重试网络不稳定或OpenAI服务偶发故障时必须设置合理的超时如30秒和有限次数的重试如2次。异步非阻塞对于非流式对话使用chatModel.generateAsync()避免阻塞Web容器线程提升并发能力。速率限制Rate Limiting严格遵守OpenAI等服务的速率限制在应用侧实现令牌桶等算法进行限流防止意外超限导致服务被禁。内存与GC优化解析大文档尤其是PDF时容易引发Full GC。确保JVM堆内存设置合理如-Xmx4g并监控GC日志。流式响应时注意及时释放已发送的数据引用避免在内存中堆积整个响应内容。这个项目从零到一的搭建过程充满了权衡和抉择。没有银弹最好的架构永远是适合自己业务场景和团队技术栈的那一个。LangChain4j和SpringBoot的组合为Java开发者提供了一个强大而灵活的起点让你能更专注于业务逻辑和创新而不是底层基础设施的搭建。希望这篇详尽的实战记录能帮你避开我踩过的那些坑更快地构建出属于自己的智能对话系统。本文还有配套的精品资源点击获取
返回列表