资讯动态

LangChain4j实战:Java开发者的大模型应用集成指南

发布时间:2026/10/20 6:21:59 来源:尧图企业网站定制
1. 为什么Java开发者终于可以舒舒服服地玩LLM了过去这两年大语言模型火得一塌糊涂但如果你是个Java后端工程师大概率会有一种“热闹是Python的我什么也没有”的尴尬。想在公司项目里接一个智能对话、文档总结、结构化抽取之类的功能打开官方文档一看全是Python示例。想自己写HTTP调用封装又要处理流式输出、Token统计、消息历史管理、JSON结构化解析……一套下来少说两三天多则一两周而且写出来的代码还未必健壮。LangChain4j就是冲着这个痛点来的。它是Java生态里的LLM应用集成框架把调用大模型所需的基础能力都做了标准化封装对话模型统一接口、提示词模板管理、聊天记忆存储、结构化输出、文本切分与嵌入、向量存储对接还有一套实用的AI服务接口让你像写Spring Service一样去写一个“会聊天”的方法。我是在一个内部知识库问答项目里正式把它用起来的当时团队里有人提议用Python写个独立服务再让Java后端去调。想法没错但运维、监控、链路追踪全要多出一套体系想想就头大。后来我花了半天时间调研LangChain4j发现它走的是纯Java路线能和Spring Boot无缝集成直接作为项目的一个模块存在这才下定决心把它引入。实践证明这个选择非常正确——几百行代码就搞定了一整套带引用来源的问答链路。这篇文章我会从头梳理LangChain4j的入门路径从依赖引入、模型对接到提示词模板、对话记忆、结构化输出再到一个完整的RAG问答小案例最后把我踩过的坑和排查经验一并分享。不管你是刚听说这个框架还是已经写了几段示例代码但没跑透这篇应该都能帮你省点时间。2. 核心概念和配置准备先搞清楚框架替你做了什么很多初学者上来就找“三步快速接入”的教程结果跑通了也不知道原理换个模型、换个场景立刻抓瞎。我的建议是动手之前先把LangChain4j的几个核心抽象理解透因为这决定了你后续写代码时是在“用框架”还是在“被框架用”。2.1 模型统一抽象别被各家SDK绑架LangChain4j最底层、也最重要的接口是ChatLanguageModel。OpenAI、通义千问、文心一言、Ollama本地模型都有对应的实现类。你面向这个接口写业务代码换模型厂商基本就是换依赖和换配置的事。这种设计理念很像JDBC——Java程序员写SQL不需要关心底层是MySQL还是PostgreSQL。不过有个区别得说清楚JDBC规范了SQL语义而不同大模型的能力边界和响应格式并不一致所以LangChain4j的抽象更多是“统一调用方式”不是“保证能力一致”。例如某些模型不支持函数调用你要在代码里做降级处理不能无脑替换。2.2 依赖引入与版本选择Maven坐标可以到官方仓库查最新版本号。我建议使用某个稳定的正式版本避免追新因为LangChain4j的迭代节奏非常快一个月能发好几个版本。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency这个核心包自带了一套SPI加载机制你还需要引入具体的模型实现。如果你用OpenAI兼容接口比如很多国内云厂商提供的兼容网关或者本地Ollama都可以用OpenAI模块dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency2.3 模型初始化几种写法构建模型最简单的方式是直接newChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .temperature(0.3) .build();但如果你用的是Spring Boot建议把模型声明成BeanBean ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(https://你的模型网关地址/v1) .apiKey(你的密钥) .modelName(qwen-plus) .temperature(0.3) .logRequests(true) .logResponses(true) .build(); }logRequests和logResponses两个参数在联调阶段非常有用能看到实际发出去的请求头和响应内容。不过生产环境建议关掉一是日志量太大二是可能把用户问题明文打进日志有隐私风险。注意如果你用Ollama跑本地模型baseUrl填http://localhost:11434/v1就行完全不需要改代码这就是OpenAI兼容协议的好处。3. 提示词模板与结构化输出从只会聊天到能干活模型建好之后最简单的用法就是model.generate(你好)。但真实业务里你不可能把用户请求直接扔给模型而是要构造一套精心设计的提示词。LangChain4j的PromptTemplate就是干这个的。3.1 PromptTemplate用模板代替字符串拼接假设你要做一个商品评论情感分类提示词模板可以这样写PromptTemplate template PromptTemplate.from( 你是一个电商评论分析助手。请判断以下评论的情感倾向。 评论内容{{text}} 只输出一个词positive / negative / neutral ); Prompt prompt template.apply(Map.of(text, 物流很快但包装有点破损)); ChatResponseAiMessage response model.chat(prompt.toUserMessage()); System.out.println(response.aiMessage().text());这里{{text}}是占位符用apply(Map)传入参数。对比用String.format拼接提示词PromptTemplate的好处有三个一是占位符语义清晰不会出现%s顺序搞错的问题二是模板里的特殊字符不需要转义三是模板本身可以外置到配置文件运营同学也能维护。有一点要提醒提示词里的花括号容易踩坑。模型输入输出里如果包含JSON你写模板时{{会被当成占位符需要留意转义规则必要时把JSON部分的括号换成全角或者拆成多个变量传入。3.2 结构化输出让模型乖乖返回JSON情感分类可能还能只用纯文本但很多场景需要模型返回字段完整的JSON。LangChain4j提供了优雅的AiServices方案你只要定义一个Java接口框架自动帮你完成模型调用和结果解析。interface SentimentAnalyzer { UserMessage(分析这条评论的情感{{text}}) SentimentResult analyze(String text); } Data class SentimentResult { private String sentiment; private int confidence; // 0-100 private ListString keywords; }然后这样绑定SentimentAnalyzer analyzer AiServices.builder(SentimentAnalyzer.class) .chatLanguageModel(model) .build(); SentimentResult result analyzer.analyze(屏幕显示效果很好电池略不耐用); System.out.println(result.getSentiment()); // negative这个过程中框架会自动生成一段提示词要求模型返回JSON格式并通过Gson反序列化成SentimentResult对象。如果模型返回的JSON字段和Java属性对不上会抛异常这点你在定义字段时要多留神。我自己用下来的感受是AiServices是LangChain4j里最“值钱”的功能。它把复杂的PromptTemplate 模型对话 JSON解析 重试全部藏起来了。你看到的就是一个普通的接口方法调用测试的时候甚至可以Mock掉这种开发体验在Java生态里简直梦回MyBatis。3.3 结构化输出时的提示词技巧如果发现模型经常把JSON格式返回错或者字段解析失败你可以自己用SystemMessage加强约束。比如在接口上加上SystemMessage( 你是一个严谨的评论分析助手。 只输出JSON对象不要包含任何额外文字或Markdown标记。 )另一个技巧是把示例放在UserMessage里让模型模仿格式。比如附上一个真实的输入输出对。这一点在模型能力较弱时效果特别明显实测下来可以让格式正确率从七成提升到九成以上。4. 聊天记忆与会话管理让AI记得住上下文单轮问答和真正的“对话”之间差的正是记忆。如果你直接把用户的每一句话单独发给模型那它永远不知道你上一句说了什么。LangChain4j把这块抽象成了ChatMemory使用起来非常直接。4.1 最简单的消息记忆ChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); ChatLanguageModel model OpenAiChatModel.builder() .apiKey(...) .modelName(gpt-4o-mini) .build(); AiServicesAssistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build();MessageWindowChatMemory是滑动窗口式的记忆保留最近N条消息。它的优点是简单省Token缺点是超过窗口后早期信息直接消失。适合客服问答、闲聊机器人这类对近期上下文敏感的场景。4.2 持久化会话把记忆存起来MessageWindowChatMemory默认在内存中服务一重启就没了。真实项目里你会希望每个用户的会话历史能存进数据库。LangChain4j提供了ChatMemoryStore接口。实现它把消息列表序列化成JSON存入表里public class JdbcChatMemoryStore implements ChatMemoryStore { Override public ListChatMessage getMessages(Object memoryId) { // 按memoryId从数据库查出消息反序列化为ChatMessage } Override public void updateMessages(Object memoryId, ListChatMessage messages) { // 全量覆盖先删除该会话旧数据再批量插入新数据 } }然后构建记忆时指定这个StoreChatMemory chatMemory PersistentChatMemory.builder() .chatMemoryStore(new JdbcChatMemoryStore()) .maxMessages(50) .build();memoryId就是你的会话ID可以是Long或String。在Web应用里通常从登录态中取出用户ID或会话ID传进去这样每个用户各自有独立的对话上下文互不串线。注意updateMessages是“全量覆盖”语义不是增量追加。如果你自己实现Store直接在数据库里delete by memoryId再insert all就行。别画蛇添足去做增量比较既容易出错也没有必要。4.3 多轮对话的隐藏陷阱加了记忆之后第一个坑就是Token费用飞涨。你说20轮的话里可能有一半是历史消息而且模型要处理的信息量越大响应越慢。所以在业务上一定要限制对话轮数或总Token量。LangChain4j里可以设置maxTokens但它控制的是模型生成长度不是输入长度。要控制输入长度只能靠maxMessages。第二个坑是“上下文污染”。如果用户在这个会话里先聊了A项目又聊B项目模型容易把两个项目的信息混在一起。解决思路是当话题明显切换时让前端调用“清空会话”接口重置ChatMemory。或者你在提示词里强调“仅基于当前问题回答不要参考无关历史”但效果只能说在一定程度上有效。5. RAG问答落地让大模型读你自己的文档很多企业用LangChain4j做知识库问答。它的核心逻辑是“先检索后生成”先把文档切成小块转成向量存起来用户提问时把问题转成向量在库里找最相似的几个片段再把片段拼进提示词让模型基于这些片段作答。这就是RAGRetrieval-Augmented Generation。5.1 文档切分与向量化首先把文档拆成适合向量检索的块。LangChain4j内置了多种切分器最简单的用DocumentSplitterDocumentSplitter splitter new DocumentByParagraphSplitter(500, 100); ListTextSegment segments splitter.split(document);500是每段最大字符数100是相邻分段之间的重叠字符数。重叠很重要否则一句话恰好被从中间截断语义就残了。实际项目中我习惯按标题层级切分而不是纯按段落长度切分这样每个片段都有相对完整的主题。底层用哪个嵌入模型决定了向量检索的效果。EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(...) .modelName(text-embedding-3-small) .build(); ListEmbedding embeddings embeddingModel.embed(segments).content();然后将文本片段和向量一起存入向量存储。LangChain4j支持的内存实现是InMemoryEmbeddingStore适合原型验证生产环境一般接PGVector或Redis存储容量和查询性能都比内存方案强很多。5.2 构建问答链路EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); // 检索器根据用户问题找到最相关的片段 EmbeddingStoreRetriever retriever EmbeddingStoreRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.6) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .contentRetriever(retriever) .build();maxResults控制取回几个片段minScore是相似度下限。这两项要配合着调——maxResults太小可能漏内容太大会让模型看太多无关片段反而降低准确性minScore设太高问题稍微换种问法就搜不到东西设太低又会把不相关内容塞给模型。我自己的经验值maxResults4到6minScore0.5到0.7左右具体要看你的文档质量和嵌入模型能力。跑一批真实问题看召回率再微调。5.3 引用来源让回答有据可循如果只让模型基于片段回答用户会觉得“你说了算”。加一个引用来源可信度立刻拉满。做法是在提示词里要求模型在回答末尾列出来源段落编号SystemMessage( 你是一个知识库助手。你必须基于给定的资料片段回答不要编造。 回答末尾用「来源[片段编号]」标注依据。 )当然这个实现有点粗糙更规范的办法是自己维护TextSegment的metadata存原文链接或文档标题再在返回值里带上引用。不管用哪种方式RAG的核心原则是一致的模型只能基于给它的资料回答不要让它自由发挥。5.4 本地自测把链路跑通如果你不想一开始就花钱调云API建议先用Ollama跑一个qwen2.5:7b这样的本地模型做功能验证。链路完全一样只是API地址换成本地。实测下来7B模型做中文知识库问答的可用性还不错虽然推理速度慢点但用来开发调试足够了等逻辑稳定之后再切成更强的大模型。6. 实战示例30分钟搭一个带记忆的客服助手前面概念说了不少现在串一个完整的实例。我要做一个小型客服助手要求能记住同一用户会话内的上下文能回答基于内部文档的问题模型不知道的内容不能瞎编6.1 定义接口与数据类public interface CustomerServiceAssistant { SystemMessage( 你是某公司的客服助手。请用温和、专业的语气回答用户问题。 如果资料中找不到答案请明确回复“暂未查询到相关信息”。 ) String chat(MemoryId String sessionId, UserMessage String userMessage); }MemoryId告诉框架这个参数是会话IDUserMessage直接标注哪句话是用户输入。这个思路很接近Spring MVC的注解风格Java后端工程师接受起来几乎没有门槛。6.2 初始化组件ChatLanguageModel model OpenAiChatModel.builder() .apiKey(sk-xxx) .modelName(gpt-4o-mini) .temperature(0.2) .build(); EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(sk-xxx) .modelName(text-embedding-3-small) .build(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); // 实际项目中DataInitializer里启动时加载文档切块、向量化、存入EmbeddingStore CustomerServiceAssistant assistant AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .chatMemoryProvider(memoryId - MessageWindowChatMemory.builder() .maxMessages(20) .build()) .contentRetriever(EmbeddingStoreRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(4) .minScore(0.6) .build()) .build();这里用了chatMemoryProvider而非chatMemory区别在于Provider是按memoryId动态创建记忆实例非常适合多用户并发场景。如果只用共享的chatMemory所有用户会读到彼此的聊天记录那是严重的事故级Bug。6.3 业务层调用RestController RequestMapping(/api/chat) public class ChatController { private final CustomerServiceAssistant assistant; public ChatController(CustomerServiceAssistant assistant) { this.assistant assistant; } PostMapping public ResultString chat(RequestBody ChatRequest request) { String sessionId request.getSessionId() ! null ? request.getSessionId() : UUID.randomUUID().toString(); String answer assistant.chat(sessionId, request.getMessage()); return Result.success(answer); } }一个Controller方法搞定。没有手写HTTP调用、没有乱七八糟的消息数组拼接、没有JSON序列化问题——这些框架全替你做完了。6.4 文档离线索引任务文档索引不能每次请求都做一遍应该在启动时或者有文档更新事件时执行。代码结构类似这样Component public class DocumentIndexInitializer implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 读入MD或TXT文档 // 切分成TextSegment // embeddingModel.embed(segments) // embeddingStore.addAll(embeddings, segments) } }这里强烈建议文档维护走版本管理比如放到Git仓库或OSS上记录每个文档的hash。检测到hash变化时只更新变化的文档对应向量而不是全量重建。全量重建在文档量大了以后非常慢而且会清掉其他来源的知识片段。7. 常见问题与排查实录开发中一定会踩的坑讲理论的时候觉得啥都清楚写代码的时候总有一两个问题磨你一下午。我把在LangChain4j开发中遇到的典型问题整理一下给后面的人提个醒。7.1 请求超时与网络通信问题问题现象模型调用偶尔报SocketTimeoutException或者特别慢。排查思路先看你的baseUrl是否走的是公网公网到大模型的链路延迟不稳定。OpenAI类客户端的默认超时时间在底层HTTP客户端里可以先放宽建连和读取超时。OpenAiChatModel.builder() .timeout(Duration.ofSeconds(60)) .build();还要注意代理配置。如果公司网络环境要求走代理确认HttpClient有正确的代理设置否则请求会一直卡在连接阶段。7.2 Token用量超预期问题现象跑了一个月账单数字比预估的高不少。原因通常是对话记忆无上限增长或者RAG查询每次把大量文档片段塞进提示词。解决方案严格限制maxMessages用Token计量工具估算输入长度对RAG查询调低maxResultsLangChain4j提供了TokenCountEstimator接口你可以对每次对话的Token数做监控。实测下来接入这个监控之后我们对成本的控制心里就有底了。7.3 模型乱返回JSON反序列化失败问题现象结构化输出接口偶尔抛Gson的JsonSyntaxException。排查思路开启logResponses(true)看看模型到底返回了什么。极有可能是模型在JSON外面加了Markdown代码块标记或者字段名对不上。解决办法加强SystemMessage约束在系统提示词里明确写明“不要带json标记”自己写一个OutputParser做兜底把多余内容剥离7.4 Spring Boot集成后Bean注入失败问题现象启动报NoSuchBeanDefinitionException。原因通常是模型Bean没有被Spring扫描到。检查配置类是否加了Configuration模型Bean的Bean方法是否在配置类中是否由于某个依赖版本冲突导致自动配置失效如果用了langchain4j-spring-boot-starter还要检查配置文件里的前缀是否拼写正确。很多参数是带连字符的比如langchain4j.open-ai.api-key少写一点就会静默跳过加载。7.5 向量检索效果差答非所问问题现象RAG能跑通但回答质量一塌糊涂。大多数时候问题不在模型而在文档切分和嵌入模型切分太大导致一个片段包含多个主题检索出来的内容一半无关切分太小片段语义不完整嵌入模型太弱中文语义区分度不足我踩过最深的一次坑是把PDF转文本时页码水印也切进了片段导致检索结果全是噪声。清洗数据这一步直接影响RAG效果怎么重视都不为过。8. 在真实项目中落地的几点个人体会最后聊聊我的实际感受。LangChain4j解决的不只是“调API”的问题它让Java团队有能力把LLM当做一个常规组件来开发、测试和运维。我在实际项目里体会到几个明显优势调试成本低本地起个Spring Boot服务Ollama拉个模型断点随便打。这对Python方案来说简直就是降维打击。代码可维护性高AI服务就是Java接口改提示词不用改业务代码换模型不用改调用方项目成员接力维护没有心理负担。框架边界清晰LangChain4j没有把“链”搞成了重流程引擎面向简单场景时你可以不用链直接调用模型复杂场景再一步步入局。当然它也不是银弹。如果你想做非常精细的Agent流程编排LangChain4j的灵活性相比Python生态略逊一筹毕竟后者的生态更丰富。但话说回来绝大多数企业级应用追求的恰恰是稳定可控而不是快速原型。Java LangChain4j在这方面挺对味的。一个小建议如果你团队里有人对LangChain4j有抵触说“不如直接用HTTP调用”别急着吵。你花半天时间写一个对比Demo一边是手写消息构造、解析、记忆、重试的老路子一边是AiServices的接口化实现。Demo放到代码评审会上效果比任何PPT都管用。后续我计划把RAG那部分单独展开写一篇包括如何选嵌入模型、如何调优切分策略、如何评估检索质量。这次先到这里希望对正在入门LangChain4j的朋友们有帮助。

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

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

免费获取报价 →
↑