资讯详情

资讯详情

Java工程师AI应用开发指南:LangChain4j从入门到生产实践

1. 从Java工程师到AI应用开发为什么LangChain4j值得你花时间如果你是一名写了几年Java的开发者最近大概率会有一种隐隐的焦虑身边做Python的同事开始聊RAG、聊Agent、聊大模型应用开发而自己还在写CRUD、调MyBatis的XML、处理各种业务接口。不是说这些工作没价值而是当AI应用开发成为新的技术增量时Java工程师如果一直站在门外看迟早会发现自己能碰到的项目类型越来越窄。LangChain4j就是在这个背景下值得认真对待的一个东西。它是LangChain的Java版本把大模型调用、提示词模板、对话记忆、Embedding向量化、向量库检索、Agent工具调用这些能力用Java开发者熟悉的方式封装了起来。你不需要切到Python生态不需要重新学一套工程化体系用Maven引入依赖、用Spring Boot组织服务、用你熟悉的接口和实现类思维就能把一个大模型应用跑起来。我写这篇东西的出发点很简单网上关于LangChain4j的中文资料要么是官方文档的翻译要么是跑一个Hello World就结束真正从Java工程师视角讲清楚“这东西怎么用、为什么这么设计、实际项目里会遇到什么”的内容很少。所以下面我会按照一个真实项目的推进节奏来展开——从环境搭建、模型接入、提示词工程到Embedding与RAG、Agent工具调用再到实际踩过的坑和性能调优。每一部分都会解释设计意图而不是只给代码。这篇文章适合两类人一类是有Java基础、想快速进入AI应用开发领域的工程师另一类是在项目里已经接到“接入大模型”需求、但不知道从哪下手的开发者。如果你连Maven和Spring Boot都没用过建议先把Java Web的基础打一打再来看不然中间一些工程化的部分会有点吃力。2. 环境准备与依赖选型别一上来就被版本问题卡住2.1 JDK版本与构建工具的实际选择LangChain4j对JDK的最低要求是JDK 8但我在实际项目里强烈建议用JDK 17或更高。原因有两个一是LangChain4j的很多新特性比如Record类型的消息封装、Stream流式处理的优化在JDK 17上体验更好二是现在主流的Spring Boot 3.x本身就要求JDK 17起步你如果还在用JDK 8后面集成Spring Boot的时候会非常别扭。构建工具用Maven还是Gradle都行但考虑到国内Java团队Maven的普及率更高我下面的示例统一用Maven。如果你用Gradle依赖坐标是一样的只是写法不同。Maven的核心依赖其实就几个我列一个最小可用的配置dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency这里有个细节要注意langchain4j是核心包包含了提示词模板、对话记忆、链式调用这些基础能力langchain4j-open-ai是模型接入包用来对接兼容OpenAI接口的大模型服务。如果你用的是其他模型比如通义千问、智谱、DeepSeek它们大多提供了兼容OpenAI协议的接口所以这个包基本是通用的。版本号方面LangChain4j迭代很快0.35.0是我写这篇文章时比较稳定的一个版本。不建议用太老的版本因为早期版本在流式输出和工具调用上有一些已知问题。也不建议盲目追最新版新版本偶尔会有API变动如果你是在生产项目里用锁定一个稳定版本更稳妥。2.2 API Key的配置方式与安全实践很多教程会把API Key直接硬编码在代码里这在本地跑Demo没问题但一旦进入团队协作或者生产环境就是安全隐患。我推荐的做法是用环境变量或者配置文件加占位符的方式。本地开发时可以在IDE的运行配置里设置环境变量比如OPENAI_API_KEY。代码里这样读String apiKey System.getenv(OPENAI_API_KEY);如果是Spring Boot项目更规范的做法是放在application.yml里通过Value或者ConfigurationProperties注入langchain4j: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.example.com/v1 model-name: gpt-4o-mini这里base-url很关键。如果你用的是国内的大模型服务需要把它改成对应服务商的接口地址。很多新手在这一步卡住是因为直接用了默认的OpenAI地址结果网络不通或者认证失败。记住LangChain4j本身不绑定任何一家模型服务它只是按照OpenAI的接口协议去发请求具体请求发到哪里由base-url决定。提示API Key不要提交到Git仓库。即使是私有仓库也建议用.gitignore排除本地配置文件团队共享的配置用环境变量或者配置中心管理。2.3 第一个可运行的最小示例环境准备好之后先跑一个最小示例验证链路是否通。不要一上来就搞RAG和Agent先把“能调用模型并拿到回复”这件事跑通。import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.chat.ChatLanguageModel; public class FirstDemo { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .baseUrl(https://api.example.com/v1) .modelName(gpt-4o-mini) .build(); String answer model.generate(用一句话解释什么是Java的面向对象编程); System.out.println(answer); } }这段代码跑通之后你会看到模型返回的一段文字。如果报错大概率是三个原因API Key不对、base-url写错、模型名称服务商不支持。排查的时候先把异常信息完整打印出来LangChain4j的异常信息通常比较清晰会告诉你到底是认证失败还是连接超时。我个人的经验是第一次跑通这个Demo之后不要急着往下走先花十分钟把OpenAiChatModel.builder()里的参数看一遍。除了apiKey、baseUrl、modelName还有temperature、maxTokens、timeout这些参数它们直接影响模型输出的风格和成本。比如temperature设成0.0会让输出更确定适合做信息抽取设成0.8会让输出更发散适合做创意生成。这些参数的含义和OpenAI官方一致LangChain4j只是做了Java侧的封装。3. 提示词模板与对话记忆让模型输出可控、可复用3.1 为什么需要提示词模板而不是字符串拼接刚开始用的时候很多人会直接写model.generate(帮我翻译这段话 text)。这样写能跑但有两个问题一是提示词散落在代码各处改起来麻烦二是当提示词里有多个变量时字符串拼接容易出错尤其是变量里包含特殊字符的时候。LangChain4j提供了PromptTemplate来解决这个问题。它的思路和MyBatis的#{}占位符很像用{{变量名}}来标记需要替换的部分import dev.langchain4j.model.input.PromptTemplate; import java.util.Map; PromptTemplate template PromptTemplate.from( 你是一名资深Java面试官请针对以下知识点出一道面试题{{topic}}。 题目难度为{{level}}并给出参考答案。 ); Prompt prompt template.apply(Map.of( topic, Java并发编程中的volatile关键字, level, 中级 )); String result model.generate(prompt.text());这样写的好处是提示词和业务逻辑分离了。你可以把常用的提示词模板抽到一个专门的类或者配置文件里业务代码只负责传参。团队协作的时候产品经理或者运营同学想调整提示词也不需要改Java代码改配置就行。3.2 对话记忆的几种实现与选型单轮对话很简单但真实场景里用户往往需要多轮交互。比如用户先问“Java有哪些集合类”接着问“ArrayList和LinkedList有什么区别”再问“那它们在多线程环境下安全吗”。如果每次请求都是独立的模型就不知道“它们”指的是什么。LangChain4j的ChatMemory就是解决这个问题的。它会把历史对话保存下来每次请求时把历史消息一起发给模型。常用的实现有两种MessageWindowChatMemory按消息条数保留最近N条超出就丢弃最早的。适合大多数对话场景实现简单内存占用可控。TokenWindowChatMemory按Token数量保留更精确地控制上下文长度但需要模型支持Token计数。我一般用MessageWindowChatMemory设置窗口大小为10到20条。太小了模型记不住上下文太大了会增加Token消耗而且模型对太早的历史信息本来就不敏感。import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.service.AiServices; interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build(); assistant.chat(Java有哪些集合类); assistant.chat(ArrayList和LinkedList有什么区别); assistant.chat(那它们在多线程环境下安全吗);这里用到了AiServices它是LangChain4j里非常核心的一个类。你定义一个接口它帮你生成实现把模型调用、记忆管理、工具调用这些逻辑都串起来。这种“声明式”的用法对Java开发者来说很亲切有点像Spring Data JPA的Repository接口。3.3 系统提示词与角色设定的实战技巧系统提示词System Message决定了模型的“人设”和回答风格。很多人忽略这一点结果模型回答得过于笼统或者跑题。一个好的系统提示词应该包含三部分角色定义、任务边界、输出格式要求。举个例子如果你要做一个Java面试题生成器系统提示词可以这样写你是一名有十年经验的Java技术面试官。 你的任务是针对用户给出的知识点生成面试题。 要求 1. 题目要贴近真实面试场景不要过于理论化 2. 每道题附带参考答案答案要简洁准确 3. 如果知识点范围太大先追问用户想聚焦哪个方向 4. 输出格式为题目、参考答案、考察点在LangChain4j里系统提示词可以通过SystemMessage注解来设置interface InterviewAssistant { SystemMessage(你是一名有十年经验的Java技术面试官...) String generateQuestion(String topic); }实测下来系统提示词写得越具体模型输出越稳定。但也不要写得太长超过500字之后边际收益就下降了而且会占用上下文窗口。我的经验是控制在200到400字之间把最关键的约束说清楚就行。4. Embedding与RAG让模型能回答私有知识4.1 Embedding到底是什么用生活化的方式理解Embedding这个词听起来很技术但它的本质很简单把一段文字转换成一串数字向量这串数字代表了这段文字的“语义坐标”。语义相近的文字它们的向量在空间里的距离也近。打个比方假设我们把所有文字都放在一个巨大的地图上“Java”和“编程语言”这两个词的位置会比较近“Java”和“咖啡”的位置会比较远虽然Java确实也是一种咖啡。Embedding模型做的就是给每段文字算出这个地图上的坐标。为什么需要Embedding因为大模型本身只知道它训练时见过的知识。你公司内部的文档、产品手册、客服话术这些私有知识模型是不知道的。你当然可以把整篇文档塞进提示词里让模型读但文档一长Token消耗巨大而且模型对超长上下文的理解能力也会下降。Embedding加向量检索的思路是先把文档切成小块每块算出向量存起来用户提问时把问题也算出向量然后找出最相似的几块文档只把这些相关片段发给模型。这样既省Token又提高了回答的准确性。4.2 文档切分策略切得好不好直接决定RAG效果文档切分Chunking是RAG里最容易被低估的环节。很多人随便按固定长度切结果切出来的片段语义不完整检索效果很差。LangChain4j提供了DocumentSplitter常用的策略有按段落切、按句子切、按固定字符数切。我的经验是对于结构清晰的文档比如Markdown、技术手册优先按标题层级切每个小节作为一个片段。对于连续文本比如小说、论文按段落切每段控制在300到500字。如果段落本身就很长再按句子切但不要切得太碎否则丢失上下文。import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.segment.TextSegment; import java.util.List; Document document Document.from(yourText); ListTextSegment segments DocumentSplitters.recursive(500, 50).split(document);这里的recursive(500, 50)表示每段最大500个字符相邻段之间重叠50个字符。重叠是为了避免一个完整的句子被切断后前后两段都丢失关键信息。这个参数不是固定的要根据你的文档特点调。技术文档可以设小一点叙事类文档可以设大一点。注意切分粒度太细会导致检索到的片段缺乏上下文模型回答时容易断章取义切分粒度太粗会导致检索精度下降因为一个片段里混了太多不相关的信息。实际项目里我一般会准备两三组参数用真实问题测一下检索效果再定。4.3 向量库选型从内存到生产环境LangChain4j支持多种向量库选哪个取决于你的数据量和部署环境。我列一个对比向量库适用场景优点缺点InMemoryEmbeddingStore本地开发、Demo零配置启动即用重启数据丢失不适合生产Chroma中小规模、快速验证部署简单社区活跃大规模性能一般Milvus大规模生产环境性能强支持分布式运维复杂度高PostgreSQL pgvector已有PG的团队复用现有数据库事务支持好向量检索性能不如专用库Redis已有Redis的团队部署简单速度快向量功能相对基础新手入门我建议先用InMemoryEmbeddingStore把流程跑通理解RAG的完整链路。等要上生产了再根据团队的技术栈选一个。如果团队已经在用PostgreSQLpgvector是个很务实的选择不用额外维护一套向量数据库。import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 50)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(document);EmbeddingStoreIngestor把切分、向量化、存储三步串起来了一行代码完成入库。这种封装对新手很友好但你要知道它背后做了什么不然出了问题不好排查。4.4 检索增强生成的完整链路与常见瓶颈RAG的完整链路是用户提问 → 问题向量化 → 向量库检索相似片段 → 把片段和问题一起组装成提示词 → 发给模型 → 返回答案。这个链路里最常见的瓶颈有三个第一个是检索不准。用户问“怎么配置数据库连接池”检索出来的却是“数据库索引优化”的片段。原因可能是Embedding模型对中文技术术语的理解不够好或者切分粒度不合适。解决办法是换一个中文效果更好的Embedding模型或者调整切分策略。第二个是检索到了但模型没用上。有时候检索结果明明包含答案但模型回答时却忽略了。这通常是提示词的问题需要在提示词里明确要求“请基于以下参考资料回答如果参考资料中没有相关信息请如实说明”。第三个是Token超限。检索回来的片段太多加上历史对话超过了模型的上下文窗口。解决办法是限制检索返回的片段数量一般3到5个或者用更激进的摘要策略。import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; interface KnowledgeAssistant { String answer(String question); } KnowledgeAssistant assistant AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(model) .contentRetriever(EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build()) .build();maxResults(5)表示最多返回5个片段minScore(0.7)表示相似度低于0.7的片段直接丢弃。这两个参数是控制检索质量的关键建议根据实际数据调优。5. Agent与工具调用让模型从“会说”到“会做”5.1 Agent的本质模型决定调用哪个工具普通的大模型调用是“你问我答”模型只能输出文字。Agent不一样它可以让模型决定调用外部工具比如查数据库、调API、执行计算。模型输出的不再是最终答案而是一个“我要调用某个工具”的指令LangChain4j解析这个指令执行对应的Java方法把结果再喂给模型模型再决定下一步。这个机制的核心价值在于模型的知识是静态的但工具可以访问实时数据。比如你问“今天北京的天气怎么样”模型本身不知道但如果有一个查天气的工具Agent就能调用它拿到实时数据。在LangChain4j里定义一个工具非常简单用Tool注解标记一个方法就行import dev.langchain4j.agent.tool.Tool; public class WeatherTool { Tool(查询指定城市的当前天气) public String getWeather(String city) { // 实际项目中这里调用天气API return city 今天晴气温25度; } }然后把工具注册到AiServices里interface WeatherAssistant { String chat(String message); } WeatherAssistant assistant AiServices.builder(WeatherAssistant.class) .chatLanguageModel(model) .tools(new WeatherTool()) .build(); assistant.chat(北京今天天气怎么样);模型会识别出需要调用getWeather工具传入“北京”拿到结果后再组织语言回复用户。5.2 工具设计的粒度与边界工具设计有个常见的误区把工具做得太粗一个工具干很多事。比如一个executeSql(String sql)工具让模型直接生成SQL执行。这样做很危险模型可能生成删库的SQL也可能生成性能极差的查询。正确的做法是把工具做细每个工具只做一件明确的事并且在工具内部做好参数校验和权限控制。比如queryOrderStatus(String orderId)只查订单状态不接受任意SQLcalculateShippingFee(String province, double weight)只算运费参数有明确类型searchProduct(String keyword, int page)只搜商品分页参数有上限这样设计的好处是模型不容易误用而且每个工具的输入输出都是可控的。我在实际项目里还会给工具加日志记录每次调用的参数和结果方便排查问题。5.3 多工具协作与调用链的调试当Agent注册了多个工具时模型需要决定调用哪个、按什么顺序调用。比如用户问“帮我查一下订单12345的状态如果已发货就查一下物流”模型需要先调queryOrderStatus根据结果再决定是否调queryLogistics。这个过程中最容易出问题的是模型调用了错误的工具或者传了错误的参数。调试的时候我一般会打开LangChain4j的日志把每次工具调用的请求和响应都打出来。LangChain4j用的是SLF4J配置一下日志级别就行logging: level: dev.langchain4j: DEBUG日志里会显示模型输出的工具调用指令、实际执行的Java方法、返回的结果。如果模型调错了工具通常是工具的描述不够清晰。Tool注解里的描述要写得具体告诉模型这个工具是干什么的、什么时候用。描述写得越清楚模型选对的概率越高。提示工具方法的参数类型尽量用String、int、double这些基础类型避免用复杂的自定义对象。模型对复杂参数的理解能力有限容易传错。6. 实战踩坑记录那些文档里不会写的问题6.1 流式输出的线程安全问题流式输出Streaming是提升用户体验的重要手段模型生成一个字就返回一个字不用等全部生成完。LangChain4j支持流式调用但在Web环境里用的时候要注意线程安全。我遇到过一个典型问题在Spring Boot的Controller里用流式输出多个用户同时请求时回复内容会串。原因是流式回调是在模型客户端的线程里执行的如果直接往HttpServletResponse里写没有做好隔离就会出现并发问题。解决办法是用StreamingResponseHandler把每个请求的流式响应绑定到独立的上下文model.generate(你的问题, new StreamingResponseHandler() { Override public void onNext(String token) { // 把token推送到当前请求的SSE连接 emitter.send(token); } Override public void onComplete(ResponseString response) { emitter.complete(); } Override public void onError(Throwable error) { emitter.completeWithError(error); } });配合Spring的SseEmitter使用每个请求有自己的emitter实例就不会串了。6.2 Token消耗的监控与成本控制大模型调用是按Token计费的如果不加监控月底账单可能会吓你一跳。LangChain4j的响应对象里包含了Token使用信息ResponseString response model.generate(你的问题); int inputTokens response.tokenUsage().inputTokenCount(); int outputTokens response.tokenUsage().outputTokenCount();我一般会在项目里做一个简单的Token统计按用户、按接口维度记录消耗。如果发现某个接口的Token消耗异常高通常是提示词写得太长或者RAG检索返回的片段太多。控制成本的手段有几个一是用更便宜的模型处理简单任务复杂任务再用贵模型二是压缩提示词去掉不必要的说明三是限制RAG返回的片段数量和长度四是设置maxTokens上限防止模型生成过长的回复。6.3 模型返回格式不稳定的处理让模型输出JSON格式是常见需求但模型有时候会在JSON外面加一段解释文字或者JSON格式不合法。LangChain4j提供了AiServices的返回类型支持可以直接把模型输出映射成Java对象interface JsonAssistant { Person extractPerson(String text); } class Person { String name; int age; String occupation; }LangChain4j会自动在提示词里加入格式说明并尝试把模型输出解析成Person对象。但实测下来模型偶尔还是会返回不合法的JSON。我的做法是在解析失败时做一次重试重试时在提示词里强调“只输出JSON不要任何其他文字”。如果重试还失败就降级到人工处理或者返回默认值。6.4 中文场景下的Embedding模型选择做中文RAG的时候Embedding模型的选择非常关键。OpenAI的text-embedding-ada-002对中文的支持一般检索中文文档时经常出现语义相近但检索不到的情况。国内有几个中文效果不错的Embedding模型比如智谱的embedding-2、百度的Embedding-V1在中文语义相似度任务上表现更好。LangChain4j支持自定义Embedding模型只要实现EmbeddingModel接口就行。如果你用的模型服务商提供了兼容OpenAI的Embedding接口也可以直接用OpenAiEmbeddingModel改一下base-url和modelName即可。选Embedding模型的时候不要只看排行榜要用你自己的数据测。准备20到30个真实问题人工标注每个问题应该检索到哪些文档片段然后对比不同模型的召回率。这个方法虽然土但最靠谱。7. 从Demo到生产工程化落地的几个关键决策7.1 模型调用的超时与重试策略生产环境里模型调用超时是常态。网络抖动、服务商限流、模型负载高都可能导致请求失败。如果不做超时和重试用户体验会很差。LangChain4j的模型构建器支持设置超时OpenAiChatModel model OpenAiChatModel.builder() .apiKey(apiKey) .baseUrl(baseUrl) .modelName(modelName) .timeout(Duration.ofSeconds(30)) .maxRetries(2) .build();timeout设成30秒是个比较平衡的值太短了正常请求也可能超时太长了用户等不起。maxRetries(2)表示失败后重试2次加上首次请求一共3次。重试的时候要注意如果是因为限流失败重试间隔最好递增避免加重服务端压力。7.2 提示词版本管理与A/B测试提示词是AI应用的核心资产之一但很多团队把它散落在代码里改了就改了没有版本管理。我建议把提示词抽到独立的配置文件或者数据库里每次修改都记录版本号和修改原因。更进一步可以做A/B测试同一批用户随机分配两组提示词对比回答质量和用户满意度。LangChain4j本身不提供A/B测试功能但你可以自己在业务层实现——根据用户ID哈希取模决定用哪个版本的提示词。7.3 日志、监控与问题追溯AI应用的日志比普通应用更重要因为模型的行为有不确定性。我一般会记录这几类信息每次请求的完整提示词包括系统提示词、历史对话、RAG检索结果模型的原始输出Token消耗耗时如果涉及工具调用记录调用的工具名和参数这些日志在排查问题时非常有用。比如用户反馈“回答不准确”你可以回溯当时的提示词和检索结果判断是检索没找到相关文档还是模型理解错了。注意记录日志的时候要注意脱敏提示词里可能包含用户隐私信息不要明文存储。7.4 并发场景下的资源隔离如果你的应用同时服务多个用户要考虑资源隔离。比如给VIP用户分配更快的模型给免费用户用便宜的模型或者限制每个用户的并发请求数防止某个用户把配额占满。LangChain4j的模型实例本身是线程安全的可以在多个线程间共享。但如果你用了ChatMemory每个用户需要有独立的记忆实例不能共享。我一般用ConcurrentHashMap按用户ID管理记忆实例并设置过期时间避免内存泄漏。8. 一些个人体会与后续可扩展的方向LangChain4j给我的最大感受是它没有试图把Java工程师变成Python工程师而是用Java开发者熟悉的方式来解决AI应用开发的问题。接口、注解、构建器、依赖注入这些概念你本来就会学习成本主要花在理解大模型的特性和RAG、Agent这些新范式上而不是花在学一门新语言上。如果你已经跟着上面的内容把Demo跑通了下一步可以尝试这几个方向一是把RAG接到真实的业务文档上比如公司内部的技术Wiki或者产品手册看看检索效果如何二是给Agent加更多工具比如查数据库、调内部API让它能处理更复杂的任务三是研究一下模型微调如果你的场景对回答风格有特殊要求微调可能比提示词工程更有效。我在实际项目里踩过的坑远不止上面写的这些但那些坑大多和具体的业务场景绑定写出来参考价值有限。真正通用的经验是不要迷信模型的能力该做的校验、该设的边界、该留的降级方案一个都不能少。模型是概率性的工程是确定性的把两者结合好才能做出靠谱的AI应用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →