资讯动态

LangChain4j实战:Java工程师如何快速构建AI应用与RAG知识库

发布时间:2026/10/26 21:35:44 来源:尧图企业网站定制
1. 从Java工程师到AI应用开发LangChain4j到底解决了什么问题如果你是一名写了几年Spring Boot的Java工程师最近大概率会有一种被时代推着走的焦虑感。Python那边LangChain、LlamaIndex玩得风生水起各种AI Agent、RAG知识库的教程铺天盖地而Java生态里能拿得出手的大模型应用开发框架前两年几乎是空白。LangChain4j就是在这个背景下出现的——它不是LangChain的Java翻译版而是一套从零设计的、符合Java工程师思维习惯的LLM应用开发框架。我最初接触LangChain4j是在一个企业内部知识库项目上。当时团队的技术栈清一色是Spring Boot MyBatis-Plus运维体系也是围绕Java构建的。如果为了接一个大模型能力就引入Python服务意味着要多维护一套运行环境、多一套依赖管理、多一套部署流程这对小团队来说成本太高。LangChain4j的价值就在于它让Java工程师可以用自己熟悉的注解、接口、依赖注入方式把大模型能力直接嵌进现有的Spring Boot工程里。具体来说LangChain4j解决的核心问题有三个层面。第一层是统一抽象不同厂商的大模型APIOpenAI、通义千问、文心一言、Ollama本地模型等接口格式各不相同LangChain4j用ChatLanguageModel、EmbeddingModel、StreamingChatLanguageModel等接口把它们统一起来切换模型只需要换一个实现类。第二层是能力编排AI应用不是简单调一次API就完事往往涉及提示词模板、对话记忆、工具调用、检索增强等多个环节LangChain4j提供了AiServices这种声明式的方式来编排这些能力。第三层是工程集成它天然适配Spring Boot可以通过AiService注解把AI能力注入到你的Service层和现有的业务代码无缝衔接。这篇文章适合谁看如果你有Java基础会用Maven或Gradle写过Spring Boot项目但对大模型应用开发完全没有头绪那这篇内容就是为你准备的。我会从环境搭建开始一步步带你跑通第一个AI服务然后深入到RAG知识库、Embedding向量检索、AI Agent工具调用这些进阶场景。整个过程不需要你懂Python不需要你了解深度学习原理只需要你有一颗愿意动手的心。提示LangChain4j的版本迭代非常快本文基于1.x稳定版本编写。如果你在实操时发现某些API对不上先去官方GitHub的Release Notes确认版本差异这是新手最容易踩的第一个坑。2. 环境准备Java工程师上手LangChain4j前必须理清的几件事2.1 JDK版本与构建工具的选择逻辑LangChain4j要求JDK 17及以上这一点没有商量余地。原因不是框架作者故意设门槛而是它大量使用了Record、Sealed Class、Pattern Matching这些Java 17才正式稳定的特性。如果你所在的公司还在用JDK 8那要么推动升级要么只能放弃这个框架。我实测过在JDK 11上强行编译会在dev.langchain4j.internal包下报一堆语法错误改都改不动。构建工具方面Maven和Gradle都支持但我建议用Maven。原因很实际LangChain4j的模块拆分非常细langchain4j-core、langchain4j-open-ai、langchain4j-ollama、langchain4j-spring-boot-starter等等每个模块的依赖关系在Maven的dependencyManagement里管理起来更直观。Gradle虽然灵活但在处理这种多模块BOMBill of Materials时新手容易搞混implementation和api的区别导致运行时找不到类。一个典型的pom.xml核心依赖配置如下properties langchain4j.version1.0.0/langchain4j.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies !-- 核心抽象层 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- OpenAI兼容接口实现国内模型大多兼容此协议 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- Spring Boot集成 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency /dependencies这里有个细节值得展开为什么推荐用langchain4j-open-ai而不是直接找某个国内厂商的专用模块因为目前国内主流大模型厂商通义千问、DeepSeek、智谱等都提供了OpenAI兼容的API端点。你只需要改baseUrl和apiKey就能用同一套代码切换模型。这比每换一个模型就换一套SDK要省事得多。2.2 API Key的安全管理别把密钥写死在代码里新手最容易犯的错误就是把API Key直接硬编码在Java类里然后一不小心提交到了Git仓库。我见过不止一个团队因为这个原因被刷爆了账单。正确的做法是利用Spring Boot的配置体系把密钥放在环境变量或配置中心里。在application.yml中这样配置langchain4j: open-ai: chat-model: base-url: ${AI_BASE_URL:https://api.openai.com/v1} api-key: ${AI_API_KEY} model-name: ${AI_MODEL_NAME:gpt-4o-mini} temperature: 0.7 timeout: PT60S embedding-model: base-url: ${AI_BASE_URL:https://api.openai.com/v1} api-key: ${AI_API_KEY} model-name: ${AI_EMBEDDING_MODEL:text-embedding-3-small}然后在启动脚本或IDE的运行配置里设置环境变量AI_API_KEY。这样做的好处是代码仓库里永远不会有明文密钥不同环境开发、测试、生产可以用不同的Key互不干扰。注意temperature参数控制输出的随机性。做知识库问答时建议调到0.2以下让回答更稳定、更贴近检索到的原文做创意文案生成时可以调到0.8以上。这个参数没有绝对标准要根据你的业务场景反复调试。2.3 本地模型还是云端API一个务实的选型判断很多教程一上来就让你用Ollama跑本地模型理由是免费、数据不出内网。这话没错但有个前提你的机器得有足够的显存。一个7B参数的模型量化后大概需要6-8GB显存13B的模型需要12GB以上。如果你的开发机是16GB内存的轻薄本跑本地模型会卡到让你怀疑人生。我的建议是分阶段来开发调试阶段用云端API因为响应快、效果好能让你快速验证业务逻辑生产部署时再根据数据敏感性和成本预算决定是否切换到本地模型或私有化部署。LangChain4j的好处就在于切换模型只需要改配置代码一行不用动。如果你确实需要本地跑Ollama是最省事的选择。安装好Ollama后执行ollama pull qwen2.5:7b拉取模型然后在LangChain4j里这样配置ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .temperature(0.3) .build();这里有个实测经验Ollama默认的上下文窗口是2048个token做RAG的时候很容易不够用。你需要在Modelfile里调整num_ctx参数或者创建模型时指定ollama run qwen2.5:7b --parameter num_ctx 8192。这个坑我在第一次做文档问答时踩过检索出来的内容稍微长一点模型就忘记了前面的对话。3. 第一个AI服务从ChatLanguageModel到AiServices的演进3.1 最原始的调用方式与它的局限理解LangChain4j的最好方式是先看最底层、最笨的写法然后一步步看它如何抽象。最原始的调用长这样ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(AI_API_KEY)) .modelName(gpt-4o-mini) .build(); String answer model.generate(用一句话解释什么是Java的反射机制); System.out.println(answer);这段代码能跑通但它暴露了几个问题。第一提示词和业务代码耦合在一起改个措辞就要重新编译。第二没有对话记忆每次调用都是独立的用户问那它有什么缺点时模型根本不知道它指的是什么。第三返回的是纯字符串如果你想让它返回结构化数据比如JSON还得自己写解析逻辑模型稍微不听话就解析失败。这些问题在真实项目里会被放大。想象一下你要做一个客服机器人需要维护多轮对话、需要根据用户意图调用不同的后端接口、需要把回答格式化成前端要的结构——用原始的generate方法写代码会变成一团乱麻。3.2 用AiServices声明式地定义AI接口LangChain4j的AiServices是解决上述问题的核心武器。它的思路很像MyBatis的Mapper接口你只定义接口和方法签名框架帮你生成实现。看一个例子interface CustomerServiceBot { SystemMessage(你是一名专业的电商客服回答要简洁、礼貌不确定的信息不要编造。) String chat(MemoryId String userId, UserMessage String message); } CustomerServiceBot bot AiServices.builder(CustomerServiceBot.class) .chatLanguageModel(model) .chatMemoryProvider(userId - MessageWindowChatMemory.withMaxMessages(20)) .build(); String reply bot.chat(user-001, 我上周买的鞋子想退货怎么操作);这段代码里有几个关键点值得拆解。SystemMessage定义的是系统提示词相当于给模型设定角色和边界这部分内容不会出现在对话历史里但每次调用都会带上。UserMessage标记的参数是用户输入。MemoryId是对话记忆的隔离键——不同用户的对话历史互不干扰这在多用户场景下是必须的。MessageWindowChatMemory.withMaxMessages(20)表示只保留最近20条消息。为什么要限制因为大模型的上下文窗口是有限的而且按token计费。如果不限制一个用户聊了几百轮之后每次请求都会带上全部历史成本会指数级上升而且超出窗口后模型会直接报错。20条是一个经验值大概能覆盖10轮左右的问答对大多数客服场景够用了。3.3 结构化输出让模型返回Java对象而不是一堆文本实际项目里我们往往需要模型返回结构化的数据。比如从用户的一段话里提取订单号、退货原因、期望处理方式。LangChain4j支持直接把返回值定义成Java Record或POJOrecord ReturnRequest( String orderId, String reason, String expectedAction ) {} interface ReturnParser { UserMessage(从以下用户描述中提取退货信息{{it}}) ReturnRequest parse(String userInput); } ReturnParser parser AiServices.create(ReturnParser.class, model); ReturnRequest request parser.parse(订单号是20241125001鞋子尺码不对想换大一码);框架会自动把返回值类型的信息转换成提示词的一部分告诉模型请按这个JSON结构输出然后自动反序列化成Java对象。实测下来GPT-4o-mini这种级别的模型结构化输出的成功率在95%以上。如果遇到解析失败框架会抛出OutputParsingException你可以在业务层做重试或降级处理。提示结构化输出对字段名很敏感。建议用英文命名Record的字段并在UserMessage里用中文明确说明每个字段的含义。我试过用中文拼音做字段名模型经常搞混改成英文后准确率明显提升。4. RAG知识库实战让模型回答它没见过的内容4.1 RAG要解决的核心矛盾与基本流程大模型有两个天生的短板一是知识有截止日期训练数据之后发生的事情它不知道二是它不知道你公司的内部文档、产品手册、历史工单。RAGRetrieval-Augmented Generation检索增强生成就是用来补这个短板的。它的基本流程分两步。离线阶段把你的文档切分成小块chunk用Embedding模型把每块转成向量存进向量数据库。在线阶段用户提问时把问题也转成向量在数据库里找最相似的几个chunk把它们拼进提示词里让模型基于这些内容回答。这个流程听起来简单但每一步都有坑。文档怎么切切太大检索出来的内容包含太多无关信息干扰模型切太小上下文不完整模型理解不了。Embedding模型选哪个不同模型对中文的支持差异很大。检索出来多少条合适太多会超出上下文窗口太少可能漏掉关键信息。4.2 文档切分策略按语义切而不是按字数切LangChain4j提供了几种DocumentSplitter。最常用的是DocumentSplitters.recursive(maxSegmentSizeInChars, maxOverlapSizeInChars)。它的逻辑是优先按段落切段落太长再按句子切句子还长再按字符切。maxOverlapSizeInChars是相邻块之间的重叠字符数设置重叠是为了避免关键信息刚好被切在边界上导致丢失。我的经验值是中文文档maxSegmentSizeInChars设在500-800之间maxOverlapSizeInChars设在50-100之间。为什么是这个范围因为中文一个字符大约对应1.5个token800字符大概是1200个token。检索时取top 3-5个块总共3000-6000个token加上系统提示词和对话历史控制在主流模型8K-16K的上下文窗口内比较安全。DocumentSplitter splitter DocumentSplitters.recursive(600, 80); ListDocument documents FileSystemDocumentLoader.loadDocuments( Paths.get(/data/knowledge-base), new ApachePdfBoxDocumentParser() ); ListTextSegment segments splitter.splitAll(documents);这里有个容易忽略的点PDF解析出来的文本经常带有页眉页脚、页码、乱码。直接切分会把噪声也存进向量库检索时匹配到这些噪声回答质量会大打折扣。建议在切分前加一步清洗用正则把纯数字行、重复出现的页眉文本过滤掉。4.3 Embedding模型的选择与向量库的落地Embedding模型的选择直接决定检索质量。目前中文场景下我实测过几个主流选项OpenAI的text-embedding-3-small效果稳定但需要联网通义千问的text-embedding-v3对中文支持很好本地的bge-large-zh系列在离线场景下表现不错但需要自己部署推理服务。向量数据库方面LangChain4j支持很多种。开发阶段我强烈建议用内存向量库InMemoryEmbeddingStore零配置、启动快适合验证流程。数据量大了之后超过几万条向量再换成PostgreSQL的pgvector扩展或Milvus。pgvector的好处是你不需要额外维护一个数据库复用现有的PG实例就行。EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(AI_API_KEY)) .modelName(text-embedding-3-small) .build(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(documents);4.4 把检索能力接入AiServices最后一步是把检索到的内容喂给模型。LangChain4j提供了ContentRetriever接口最常用的是EmbeddingStoreContentRetrieverContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build(); interface KnowledgeBot { String answer(UserMessage String question); } KnowledgeBot bot AiServices.builder(KnowledgeBot.class) .chatLanguageModel(model) .contentRetriever(retriever) .build();maxResults(5)表示取最相似的5个块minScore(0.7)是相似度阈值低于这个分数的块直接丢弃。这个阈值很关键设太低会检索到不相关的内容模型容易被误导设太高可能什么都检索不到模型只能凭自己的知识瞎编。0.7是一个比较稳妥的起点实际项目中要根据你的文档质量和Embedding模型的表现来微调。注意RAG最常见的失败模式是检索到了但模型没用。表现是回答里出现了文档中没有的信息。这通常是因为系统提示词没有强调只基于提供的上下文回答。在SystemMessage里明确写上如果上下文中没有相关信息请直接说不知道不要编造能显著降低幻觉率。5. AI Agent工具调用让模型从会说变成会做5.1 工具调用的本质模型输出结构化指令AI Agent这个词被炒得很热但剥开概念的外壳它的核心机制其实很朴素模型在回答时如果判断需要调用外部能力就输出一段结构化的指令通常是JSON告诉程序我要调用哪个函数、传什么参数。程序执行完函数后把结果再喂回给模型模型基于结果生成最终回答。LangChain4j用Tool注解来标记可被模型调用的方法。看一个查天气的例子class WeatherTools { Tool(根据城市名称查询当前天气) String getWeather(P(城市名称如北京、上海) String city) { // 实际项目中这里调用天气API return city 今天晴气温15-22度; } } interface WeatherAgent { String chat(UserMessage String message); } WeatherAgent agent AiServices.builder(WeatherAgent.class) .chatLanguageModel(model) .tools(new WeatherTools()) .build(); String result agent.chat(北京今天天气怎么样);当用户问天气时模型会先输出一个调用getWeather(北京)的指令LangChain4j拦截这个指令执行方法拿到结果再让模型生成自然语言回答。整个过程对调用方是透明的你只需要调chat方法。5.2 工具设计的三条实战原则工具能不能被正确调用很大程度上取决于你怎么定义它。我踩过不少坑总结出三条原则。第一工具描述要写清楚什么时候用而不只是这是什么。比如你有两个工具一个查订单状态一个查物流轨迹。如果描述只写查询订单和查询物流模型经常搞混。改成当用户询问订单是否已付款、是否已发货时使用此工具和当用户询问包裹当前在哪个城市、预计何时送达时使用此工具准确率会高很多。第二参数类型尽量用String避免复杂对象。模型生成JSON参数时对嵌套对象的支持不稳定。如果确实需要多个参数拆成多个String参数在方法内部再组装。我试过让模型直接生成一个包含List的复杂对象十次里有三次格式是错的。第三工具方法内部要做好异常处理。模型调用工具时传的参数可能不符合预期比如传了一个不存在的城市名。如果方法直接抛异常整个对话就中断了。正确的做法是捕获异常返回一个友好的错误信息字符串让模型自己决定怎么跟用户解释。5.3 多工具协作与调用链的观察当你有多个工具时模型可能会连续调用多个。比如用户说帮我查一下北京天气如果下雨就提醒我带伞模型会先调天气工具拿到结果后判断是否下雨再决定要不要调提醒工具。这种链式调用是Agent能力的体现但也容易出问题——如果第一个工具返回的结果格式不清晰模型可能无法正确判断下一步。调试多工具协作时打开LangChain4j的日志很有帮助。在application.yml里加上logging: level: dev.langchain4j: DEBUG这样你能看到每次请求的完整提示词、模型的原始输出、工具调用的参数和返回值。我第一次调Agent时发现模型反复调用同一个工具看了日志才知道是工具返回的内容里包含了触发再次调用的关键词调整返回格式后就正常了。6. 从Demo到生产那些教程不会告诉你的坑6.1 超时与重试大模型API不是永远可靠的云端大模型API的响应时间波动很大快的时候一两秒慢的时候十几秒甚至超时。如果你的服务没有设置合理的超时和重试用户体验会很差。LangChain4j的模型配置里可以设置timeout但重试逻辑需要自己实现。我的做法是在Service层包一层重试用Spring Retry或者手写一个简单的循环。重试次数设2-3次每次间隔递增比如1秒、3秒、5秒。注意不要对所有异常都重试——如果是API Key无效这种认证错误重试多少次都没用只会浪费时间。只对超时和5xx错误重试。Retryable( retryFor {TimeoutException.class, WebClientException.class}, maxAttempts 3, backoff Backoff(delay 1000, multiplier 2) ) public String askWithRetry(String question) { return bot.chat(question); }6.2 Token成本控制别让账单吓到你按token计费意味着每一次调用都是真金白银。一个没优化好的RAG应用单次问答消耗上万token是常有的事。控制成本有几个抓手限制对话记忆条数前面提到的withMaxMessages控制检索块数量maxResults不要设太大压缩系统提示词能一句话说清就别写三段对简单问题走缓存相同问题直接返回上次的结果。我做过一个统计一个日活1000的客服机器人如果每次问答平均消耗3000 token用GPT-4o-mini的话一个月成本大概在几十美元。但如果对话记忆不限制、检索块设成10个成本会翻好几倍。这些参数在开发阶段感觉不到上线后账单会教你做人。6.3 流式输出提升用户体验的关键一步大模型生成一段完整回答可能需要好几秒用户盯着空白屏幕等待的体验很糟糕。流式输出让文字像打字机一样逐字出现感知上的等待时间大大缩短。LangChain4j支持StreamingChatLanguageModel配合Spring的SseEmitter可以很方便地实现服务端推送。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(RequestParam String message) { SseEmitter emitter new SseEmitter(60_000L); streamingModel.generate(message, new StreamingResponseHandler() { Override public void onNext(String token) { try { emitter.send(token); } catch (IOException e) { emitter.completeWithError(e); } } Override public void onComplete(ResponseAiMessage response) { emitter.complete(); } Override public void onError(Throwable error) { emitter.completeWithError(error); } }); return emitter; }这里有个细节流式输出和工具调用、RAG检索的配合需要额外注意。检索必须在流式生成开始之前完成否则你没法把检索结果拼进提示词。工具调用在流式模式下也更复杂因为模型可能在流的中途决定调用工具。我的建议是如果场景涉及复杂的Agent编排先用非流式跑通逻辑再考虑是否值得改成流式。6.4 测试策略怎么测一个不确定的系统传统Java项目的单元测试是确定性的输入A断言输出B。但大模型的输出是不确定的同样的输入可能得到不同的回答。这让很多Java工程师很不适应。我的测试策略分三层。第一层是契约测试不测具体内容只测格式。比如结构化输出能不能成功反序列化、工具调用有没有被触发、返回结果是不是非空。第二层是回归测试准备一组标准问题和期望答案的关键词每次修改提示词或换模型后跑一遍看命中率有没有下降。第三层是人工评估定期抽样真实对话人工判断回答质量。自动化测试能保证系统不崩但回答好不好最终还是得人来判断。7. 一些个人体会与后续可以探索的方向写到这里LangChain4j从入门到能跑通一个带RAG和工具调用的AI服务核心路径基本覆盖了。回顾我自己的学习过程最大的感受是Java工程师做AI应用开发优势不在算法而在工程。模型能力是厂商提供的提示词效果是可以调优的但如何把AI能力稳定、高效、可维护地集成进现有系统这才是Java工程师的主场。LangChain4j这个框架还在快速演进我目前关注的两个方向一个是和Spring AI的生态融合另一个是Agent编排能力的增强。如果你已经跑通了本文的基础流程下一步可以试试把知识库换成pgvector做持久化、给Agent加上更多业务工具、或者研究一下如何用LangChain4j做多Agent协作。这些内容展开又是另一篇长文了有机会再单独聊。最后分享一个我踩过的坑作为收尾不要在Tool方法里做耗时操作。我一开始把一个需要调用外部HTTP接口、平均响应3秒的方法标成了工具结果模型调用它之后整个对话卡住用户以为系统挂了。后来改成异步执行加轮询查询状态体验才好起来。工具方法要快最好在几百毫秒内返回这是Agent设计的一条铁律。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑