
说实话Java后端接大模型这件事过去一直很拧巴。我身边不少朋友在Python圈用LangChain用得风生水起回到Java项目里就只能手写HTTP请求封装自己搞prompt拼接再流着泪处理流式输出和上下文管理。直到我认真用上LangChain4j这种感觉才彻底改变。LangChain4j是专门为Java生态设计的LLM应用开发框架核心思想是把你熟悉的Spring式编程习惯迁移到AI领域依赖引入、接口定义、注解声明、链式装配一套流程下来非常顺滑。这篇文章我直接从新手视角出发带你完整跑一遍LangChain4j的实战链路——从环境搭建、第一个对话Demo到工具调用、RAG知识库问答最后把生产环境容易踩的坑一并梳理清楚。适合刚接触大模型开发、想在Java项目里落地AI能力的后端工程师也适合看了大量LangChain文档却不知道怎么翻译成Java代码的同学。1. 项目冷启动先把环境搞干净1.1 版本选择JDK 17起步别再用Java 8裸奔LangChain4j官方标注的最低要求是Java 8但我强烈建议直接用JDK 17或21原因有两点。第一框架内部大量使用了动态代理、反射和java.lang.invoke相关能力在老JDK上跑不是不行而是遇到诡异问题基本查不到资料。第二现在主流Spring Boot 3.x已经把基线抬到JDK 17你的AI模块大概率要嵌在Spring Boot项目里没必要为了兼容Java 8给自己埋坑。我刚接触时曾在Java 8环境跑过0.35版本碰到过一次方法句柄初始化异常换到JDK 17后问题彻底消失。构建工具Maven和Gradle都可以本文统一用Maven写示例。你不需要马上建立一个多复杂的工程一个普通的Maven项目就足够跑通前几个Demo。如果你打算集成到Spring Boot建议新建干净的空项目避免后面依赖冲突时还要怀疑是不是自己业务代码的问题。1.2 用BOM统一版本别一个个手写版本号LangChain4j不是一个单包而是拆成了几十个模块核心模块、模型提供商适配模块、嵌入模型、存储扩展、流式API等等。新手最容易犯的错误就是去GitHub上各模块仓库找最新版本号然后手动填version结果因为版本不一致出现ClassNotFoundException。正确做法是引入它的BOM让版本号统一由父级POM管理。dependencyManagement dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-bom/artifactId version0.36.2/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement引入BOM之后加依赖就不用再写版本号了。比如先加核心包和一个模型适配包dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId /dependency /dependencies这里选langchain4j-open-ai而不是某个厂商专用模块是因为它支持通过baseUrl指向任何兼容OpenAI协议的端点。国内模型服务像通义千问、DeepSeek、Kimi、智谱都提供了OpenAI兼容模式你只需要换一个baseUrl和modelName就能切换模型供应商业务代码完全不用动。这个兼容层带来的便利等你真正需要做多模型切换时体会最深。1.3 三个必须知道的模块划分新手面对一堆模块名容易懵我把常用模块按用途分成三组模块分类典型artifactId用途核心抽象langchain4j模型接口、消息模型、AiServices、RAG框架模型适配langchain4j-open-ai、langchain4j-ollama、langchain4j-dashscope对接不同大模型或本地模型存储与工具langchain4j-pgvector、langchain4j-milvus、langchain4j-easy-rag向量库与开箱即用的RAG组件记住一个原则核心包必加模型适配包按需选一个存储包等做到RAG阶段再加。别一次性把整个生态全部引入依赖冲突和启动耗时都会教你做人。2. 核心概念拆解LangChain4j的五个关键抽象2.1 ChatLanguageModel一切对话的入口ChatLanguageModel是整个框架的门面任何模型供应商最终都会实现这个接口。你只需要面向它编程不用关心底层是OpenAI的Chat Completion还是通义千问的DashScope协议。最基础的方法是generate(String userMessage)传入一句话返回模型生成的字符串。这个接口适合快速验证连通性。实际开发时我的建议是再往上走一层使用统一的消息列表方式。因为真实对话永远是多轮的单字符串方法只适合跑通链路不适合做产品。chat(ListChatMessage)能表达完整的历史对话和系统设定后续所有高级功能都建立在这套消息模型之上。2.2 Message与ChatMemory让模型“记住”上下文大模型本身无状态它每次只根据你传进去的文字做预测。所谓记忆本质是把历史消息再塞回上下文。LangChain4j用四类消息表达对话SystemMessage系统设定、UserMessage用户输入、AiMessage模型回复、ToolMessage工具调用结果。ChatMemory接口负责管理这些消息。最常用的实现是WindowChatMemory你可以指定最多保留多少条ChatMemory chatMemory WindowChatMemory.builder() .maxMessages(20) .build();为什么是20而不是50或者100因为上下文窗口是稀缺资源。保留的历史越多能用于生成回复的token越少响应越慢费用越高。而且超出模型上下文长度会直接报错不会自动裁剪。我的经验是普通客服场景保留10到20条足够涉及代码分析或长文总结时再按需调大。2.3 AiServices面向任务的声明式编程AiServices是整个框架里我最喜欢的部分它把AI调用封装成定义一个接口声明要做什么然后拿代理对象直接用。底层原理是动态代理框架在运行时替你组装提示词、管理消息历史、触发工具调用你只负责定义业务方法。interface CustomerAssistant { String chat(String userMessage); }配合注解可以定制系统提示词interface CustomerAssistant { SystemMessage(你是一个耐心、专业的电商客服助手。回答要简洁准确不要编造订单信息。) String chat(String userMessage); }然后一行代码拿到代理对象CustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .build(); String answer assistant.chat(我的订单为什么还没发货);看起来只是个普通接口实际调用时已经经历了完整的提示词组装和模型调用。这种模式的好处是业务代码零侵入后续加工具、加记忆、加RAG都只是链式配置而已。2.4 Tool让模型调用你的Java方法ToolFunction Calling是把LLM从聊天机器人变成执行者的关键能力。你可以把任意Java方法暴露给模型当用户提问需要真实数据时模型会在回答里附加一个函数调用意图框架拦截后反射执行你的方法再把返回值交还给模型生成最终答案。LangChain4j里加一个工具极其简单。在任意类的公有方法上打Tool注解描述里写清楚这个方法什么时候用、参数是什么含义class OrderTool { Tool(根据订单号查询订单状态当用户询问订单进度时调用) String queryOrderStatus(String orderId) { return 订单 orderId 已发货预计3天内送达; } }然后注册进AiServicesCustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .tools(new OrderTool()) .build();工具调用的底层流程是框架把方法名、描述、参数Schema转换成JSON结构在模型请求里声明你有这些函数可用。模型判断用户问题匹配某个函数时返回一个特殊消息而不是普通文本框架解析后反射调用Java方法再把结果作为ToolMessage送回模型最终得到面向用户的回答。这里有一个新手高频翻车点Tool描述写得太模糊。描述越模糊模型越容易误判或漏判。比如查询订单就远不如根据订单号查询订单当前物流状态适用于用户询问包裹发货、配送进度、异常件等情况时调用来得可靠。工具描述就是给模型的说明书值得多花几分钟打磨。2.5 Embedding与RAG给模型外挂知识库Embedding是把一段文本转成一串高维向量语义相近的文本向量距离也近。RAG检索增强生成的思路是先把私域知识文档切块并向量化存入向量库用户提问时先在库里检索最相关的几段内容连同问题一起交给模型让模型基于这些事实来回答。这样既不用微调模型又能让模型知道你私有数据里的细节。LangChain4j的RAG链路设计得很清晰文档分割器DocumentSplitter、嵌入模型EmbeddingModel、向量库EmbeddingStore、检索器ContentRetriever各司其职。接入方式我会在第5章完整演示这里先记住一个核心结论检索结果的质量主要取决于文档切分策略和向量召回阈值而不是生成模型本身的强弱。3. 实战5分钟跑通第一个对话应用3.1 注册模型服务并配置API Key这一节我用通义千问的OpenAI兼容模式作为示例其他兼容服务商的配置几乎一模一样。先去阿里云百炼或DashScope控制台申请一个API Key然后选择模型比如qwen-plus注意确认服务支持compatible-mode端点。API Key建议通过环境变量传入而不是硬编码在代码里防止误提交到Git仓库。这里我使用一个简单的读取方式export LLM_API_KEYsk-xxxxxxxxxxxx代码里用System.getenv(LLM_API_KEY)读取。即便只是做Demo也要养成这个习惯后面上生产会少踩一次泄漏事故的坑。3.2 用OpenAiChatModel构建模型发起第一次对话LangChain4j的OpenAI模块本身实现的就是OpenAI协议所以配置好baseUrl就能指向任何兼容端点import dev.langchain4j.model.openai.OpenAiChatModel; import java.time.Duration; OpenAiChatModel model OpenAiChatModel.builder() .apiKey(System.getenv(LLM_API_KEY)) .modelName(qwen-plus) .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .timeout(Duration.ofSeconds(60)) .logRequests(true) .logResponses(true) .build(); String answer model.generate(你好请用一句话说明LangChain4j是什么); System.out.println(answer);这里我特别把logRequests(true)和logResponses(true)打开了新手阶段这是最好的调试工具。你能直接在控制台看到发给模型的结构化请求和模型返回的原始内容尤其是后面调试工具调用和RAG时看不到原始报文根本没法定位问题。跑起来以后控制台会输出请求里的messages、temperature等参数日志格式非常完整。第一次看到这些日志不要嫌烦它们是你排查一切问题的基础。如果这一步能正常输出文字说明你的工程搭建、API Key、网络连通性、模型参数全部通过可以继续往下走。3.3 流式输出让回复一个字一个字地蹦出来对话类产品里流式输出几乎是刚需。用户发完消息希望立刻看到回复正在生成而不是傻等完整结果返回。LangChain4j的流式调用同样建立在模型对象上model.chat(给我讲一个程序员的幽默段子, new StreamingResponseHandlerAiMessage() { Override public void onPartialResponse(String token) { System.out.print(token); } Override public void onCompleteResponse(ChatResponseAiMessage completeResponse) { System.out.println(\n--- 流式输出完成 ---); } Override public void onError(Throwable error) { error.printStackTrace(); } });注意onPartialResponse回调里的token是增量内容你要做的就是逐段推送到前端。在Spring Boot里配合SseEmitter或WebFlux的FluxString都能轻松实现打字机效果。我的实践是先把流式单独封装成一个接口不要和普通同步请求混在同一个Service方法里否则后面接日志、接限流都会很别扭。3.4 加入聊天记忆让它记住你是谁一个只能单轮问答的Demo没有产品意义。给它加上WindowChatMemory让多轮对话连续起来。这里直接把记忆和AiServices配合使用代码量最小ChatMemory chatMemory WindowChatMemory.builder() .maxMessages(20) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build(); System.out.println(assistant.chat(我叫小王记住这个名字)); System.out.println(assistant.chat(我刚才告诉你我叫什么));第二次提问时模型会准确回答你叫小王因为历史消息已经被注入上下文。到这里你已经拥有了一个具备基础记忆能力的对话机器人。4. 实战进阶用AiServices 工具调用做一个订单查询助手4.1 需求拆解哪些信息必须走工具假设你要做一个电商客服助手。用户会问我的订单发货了没什么时候到帮我改地址这类问题。这里有个关键判断订单状态是存储在业务数据库里的实时数据不可能让模型凭空编造所以必须由工具方法查询真实数据。而改地址这类写操作更建议用工具加人工确认的方式不要让模型直接执行。我们把需求收窄成两个明确动作查订单状态输入订单号返回发货状态和预计到达时间。查物流轨迹输入订单号和物流单号返回最近几条轨迹信息。这两个动作都对应数据库或第三方物流API查询是典型的工具调用场景。4.2 定义业务接口和工具实现先定义AI服务接口interface CustomerAssistant { String chat(String userMessage); }再实现工具类class OrderTool { Tool(根据订单号查询订单当前状态、是否发货以及预计送达时间) String queryOrderStatus(String orderId) { // 这里替换成真实的订单服务调用 if (O20240315001.equals(orderId)) { return 订单状态已发货预计送达3天内承运商顺丰; } return 未查询到订单 orderId 请确认订单号是否正确; } Tool(根据订单号和物流单号查询最近三条物流轨迹适用于用户询问包裹到哪了、配送进度等场景) String queryLogisticsTrace(String orderId, String trackingNo) { // 模拟物流查询 return 轨迹1: 3月14日 10:00 快件已从杭州发出轨迹2: 3月15日 08:30 到达上海转运中心轨迹3: 等待下一站更新; } }注意方法返回类型我用了String而不是复杂对象。工具调用的最终结果要拼进模型上下文复杂对象在序列化时容易出意外也容易把无关字段灌给模型。如果查询结果本身是对象建议在工具方法里整理成简明字符串再返回。组装并测试CustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .tools(new OrderTool()) .build(); String reply assistant.chat(帮我查一下订单 O20240315001 现在到哪了); System.out.println(reply);模型会看到用户问题里的订单号匹配到queryOrderStatus或queryLogisticsTrace。因为这个问题没提供物流单号它有较高概率先调用订单状态查询再基于返回的真实状态组织回答。4.3 从日志里看懂工具调用过程打开logRequests(true)后你能看到请求体里多了一个tools字段里面是每个方法的名称、描述、参数JSON Schema。模型判断需要调用时响应里会出现toolCalls包含方法名和参数。框架接着执行方法随后带着ToolMessage再请求一次模型。整个过程报文顺序是请求系统消息 用户问题 可用工具定义。响应模型说我要调用queryOrderStatus参数是O20240315001。框架执行Java方法拿到真实结果。请求系统消息 用户问题 工具调用记录 工具返回结果。响应模型基于真实结果组织最终回答。新手最容易懵的是第一步看到工具定义被塞进请求就以为框架把工具代码传给了模型。其实模型只知道方法名、描述和参数格式并不知道方法内部逻辑。边界在这里模型负责决策Java负责执行。这个边界理解到位工具调用就没什么神秘可言了。4.4 工具调用的三个设计建议第一参数尽量用简单类型。String、Long、Integer这类模型从用户话里提取参数的准确率最高。如果你定义一个QueryOrderRequest对象模型虽然也能填字段但复杂嵌套结构容易填错。第二方法名要动词开头描述里明确触发条件和参数含义。第三工具不是越多越好。每次请求都会把所有工具定义发给模型工具太多既费token又降低模型识别准确率。建议单场景下工具控制在10个以内功能相近的合并成一个方法用参数区分。5. 实战进阶RAG本地知识库问答5.1 场景与数据准备很多企业知识库场景并不需要模型记住全部文档只需要模型看过相关文档后回答问题。比如员工手册、产品FAQ、内部规章制度文档更新频繁不可能每次更新都微调模型。RAG就是最优解。我先准备一个简单的测试文档handbook.txt内容取员工手册节选放在src/main/resources/docs下。内容不用太长几百字足够验证链路通了。关键是要有明确的事实点比如试用期三个月年假每年5天这类可被检索和核对的表述。5.2 引入嵌入模型与向量库依赖RAG链路里有两个新组件嵌入模型和向量库。这里我继续使用兼容OpenAI协议的接口通义千问的文本嵌入模型text-embedding-v3可以走同一个baseUrl。向量库为了演示直接使用内存实现InMemoryEmbeddingStore它适合Demo和小数据量场景生产环境再替换成PGVector或Milvus。EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(LLM_API_KEY)) .modelName(text-embedding-v3) .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .build(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore();5.3 文档分割切得好不好直接决定回答质量文档不能整个丢给嵌入模型因为单次请求有长度限制且一个段落里混着多个主题会稀释语义。所以要先切块。LangChain4j提供了DocumentSplitter我用递归切分器按每块500字符、重叠100字符的方式切。DocumentSplitter splitter DocumentSplitters.recursive(500, 100);为什么要有重叠切块会让原本相邻的句子被分到不同块重叠部分相当于给每块补上了切缝处的上下文。为什么是500字符太小了语义不完整太大了检索时浪费token且容易命中不相关内容。这两个参数需要根据文档类型微调技术文档可以适当放大到800FAQ类问题可以缩小到300。导入文档EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); Document document Document.fromFile(Paths.get(src/main/resources/docs/handbook.txt)); ingestor.ingest(document);真正执行时框架会把文档切成多个块逐个调用嵌入接口把向量和原文一起存入向量库。执行完可以打印总数确认切分是否正确System.out.println(embeddingStore.countAll());5.4 配置ContentRetriever完成RAG问答检索器负责在向量库里找出和用户问题最相关的块再把它作为上下文拼进模型请求。我用EmbeddingStoreContentRetrieverContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.7) .build();maxResults控制检索几块内容minScore是相似度阈值低于这个分的内容不返回。阈值太高会漏掉相关内容导致回答变成我不知道阈值太低会混入无关内容导致模型答非所问。我建议先在0.6到0.75之间试再根据业务反馈微调。然后在AiServices里挂上检索器interface Assistant { SystemMessage(你是一个严谨的企业知识库助手只能依据提供的资料回答问题资料中没有的信息要明确说明不知道。) String chat(String question); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String ans assistant.chat(员工试用期是多久); System.out.println(ans);系统提示词里那句只能依据提供的资料回答非常重要。没有这句模型经常会自由发挥把检索来的资料当参考而不是事实依据幻觉满天飞。加上这句之后回答边界立刻清晰。5.5 RAG链路性能的关键先召回还是先生成这里补充一个容易踩的误区。有人觉得RAG慢是生成模型的问题其实大部分时候瓶颈在向量检索和文档处理。如果文档几千块每次都全量检索内存向量库会越来越吃力。生产环境的方案是把InMemoryEmbeddingStore换成PGVector并给向量列建索引。如果检索召回的内容相关性差优先调整切分策略和minScore而不是换更强的生成模型。生成模型再强喂进去的资料错了也无济于事。6. 常见问题与排查技巧实录6.1 依赖冲突slf4j与jackson的版本风暴很多Spring Boot项目引入LangChain4j之后启动报NoSuchMethodError或ClassNotFoundException九成是日志或JSON库冲突。LangChain4j底层用SLF4J做日志、Jackson做JSON序列化而Spring Boot对这两个库的版本有强管控。解决思路是在dependencyManagement里让Spring Boot BOM优先管理版本或者针对langchain4j相关传递依赖排除冲突项。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId exclusions exclusion groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId /exclusion /exclusions /dependency不过要注意排除slf4j-api之后要确保项目中显式引入了Spring Boot能接受的slf4j版本否则日志崩得莫名其妙。6.2 请求超时跟重试怎么配才算稳大模型接口不像普通REST接口那么快复杂模型生成几千字可能要几十秒。默认超时往往只有10秒大概率会超时。我建议至少配60秒长文生成场景直接给120秒。OpenAiChatModel.builder() .timeout(Duration.ofSeconds(60)) .maxRetries(2) .build();maxRetries不要配大。模型调用失败后重试相当于再花一次钱而且很多接口虽然幂等但绝不免费。1到2次足够更多次数造成的延迟和费用都不划算。特别提醒如果你的工具方法里有副作用操作比如写数据库模型调用失败重试可能导致工具被重复执行这类操作要自己保证幂等。6.3 Token超限上下文窗口塞爆了怎么办报错信息里经常出现context length exceeded或maximum context length。这是ChatMemory窗口设大了或者单轮输入本身超过了模型限制。处理思路有三步先缩小WindowChatMemory.maxMessages再检查系统提示词和工具描述是否冗长精简掉无关内容最后如果长对话产品必须保留大量上下文可以引入摘要式记忆——把达到一定长度的历史对话压缩成一句摘要再塞进系统提示词。这是LangChain4j里自定义压缩工具的活效果比简单截断好很多。6.4 工具调用不生效从报文里找真相我调试工具调用时的标准动作先打开logRequests(true)和logResponses(true)再看原始请求里tools字段是否存在。如果工具定义没进请求说明工具注册没生效检查是否忘了调.tools()。如果工具定义在但模型始终不调用大概率是方法描述和用户问题的匹配度不够或者模型本身不支持function calling部分小参数模型确实不支持。如果模型调用了但Java方法没执行检查方法是否是public以及返回类型能否被正确序列化。把这几层排查完绝大部分问题都能定位。6.5 问题排查速查表症状可能原因优先排查动作启动冲突异常slf4j/jackson版本不一致检查依赖树统一BOM管理请求超时默认超时太短调大timeout检查到模型端点延迟Token超限上下文窗口占用过多缩小记忆窗口精简提示词工具从不被调用描述模糊或模型不支持打开logRequests核对tools字段工具被调用但Java没执行方法非public或返回类型异常检查工具方法可见性、返回类型RAG回答不准确切分粒度不合适或阈值不合理调整splitter参数和minScore日志中文乱码控制台编码问题设置-Dfile.encodingUTF-87. 性能优化与工程化落地建议7.1 控制并发模型API不是你家的线程池项目一上线第一个问题是每秒能处理多少个AI请求。这里要冷静大模型API的单次响应耗时通常在1到10秒如果直接开50个线程并发调不仅API端会限流自己机器的线程池也会被占满。建议用独立的线程池管理模型调用核心线程数根据API配额和业务量评估别和普通RPC请求共用线程池。同步调用场景用ThreadPoolTaskExecutor流式场景注意连接数是OkHttp线程池的两倍左右否则高并发下连接建立会拖垮响应。7.2 缓存相同问题不该重复烧钱很多咨询类问题其实是重复的尤其知识库场景试用期是多久这种问题一天可能被问几百次。给AiServices套一层缓存是非常有效的降本手段。用Caffeine按问题内容做短期缓存命中则直接返回。但要注意涉及实时数据的工具调用结果不该长期缓存比如订单状态变了缓存旧状态会误导用户。我的做法是纯知识问答缓存5分钟涉及实时数据查询的不缓存。CacheString, String answerCache Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(5)) .maximumSize(10000) .build(); String answer answerCache.getIfPresent(question); if (answer null) { answer assistant.chat(question); answerCache.put(question, answer); }7.3 模型路由让每个请求用最合适的模型预算充足不等于可以浪费。简单问题用便宜快速的轻量模型复杂推理用重量模型文档嵌入统一用专用嵌入模型。LangChain4j里可以封装一个ChatLanguageModelProvider根据问题的关键词、长度、业务线动态返回不同模型实例。判断规则不用搞得多复杂先按业务场景分客服闲聊走轻量模型专业分析走重量模型。这一层路由做好账单能降三分之一。7.4 安全与合规API Key和敏感数据API Key必须走环境变量或配置中心禁止硬编码。日志打印时模型请求响应里可能包含用户隐私生产环境不要把logRequests一直开着或者只对部分调试账号开启。工具方法返回的内容如果是用户订单、手机号在递给模型之前先做脱敏因为模型生成文本时可能会原样复述这些信息一旦日志外泄就是事故。另外对话数据留存要遵循业务合规要求该删的删该加密的加密不要让AI项目成为数据安全的突破口。7.5 可观测性给AI调用加上完整链路AI调用比普通接口更需要可观测性因为你不知道每次调用到底花费了多少token、响应时间多长、模型是否走了工具调用。建议每次调用都记录用户ID、模型名称、输入token数、输出token数、耗时、是否使用了工具、最终回答质量评估。LangChain4j有ChatModelListener机制可以在自定义Listener里采集这些指标再打到日志或监控系统。成本统计、异常告警、prompt效果迭代全靠这份数据支撑。8. 从Demo到生产的个人体会跑通这几个Demo不难真正难的是认清AI应用开发的边界在哪。我见过不少团队把业务逻辑一股脑写进提示词里结果模型一升级全部行为漂移。用LangChain4j的正确姿势是模型负责理解意图和生成自然语言工具负责确定动作和获取事实RAG负责提供私域知识三者各管一段互不越界。查询助手场景里订单状态必须走工具制度问答必须走RAG只有寒暄和总结才让模型自由发挥。从成本角度看建议所有接AI的业务先想三个问题这个问题一定要大模型参与吗能用规则解决吗能缓存吗AI不是银弹用在该用的地方才划算。我在实际项目中已经踩过把所有问题都丢给模型的坑后来改成规则过滤加模型生成账单和响应速度都明显改善。还有一个细节想分享调试AI功能时一定要学会利用框架日志而不是盯着模型返回的结果看。模型输出是经过包装的只有原始请求响应才能告诉你它到底有没有理解你的工具定义、检索到了什么内容、哪一步消耗了token。打开日志、读懂报文多花十分钟换来的是后面少排查几小时。现在LangChain4j已经是我Java后端项目的标配依赖了。它把AI这层复杂度封装得足够像普通Java框架给不熟悉Python、又不想引入第二套技术栈的团队提供了一个很舒服的落地方案。照着这篇文章的步骤你完全可以先跑通第一个对话应用再慢慢往里面加工具、加知识库、加路由策略。这条路我已经替你趟过了剩下的交给你的业务想象力。