资讯动态

LangChain4j实战:Java工程师的生产级AI应用开发指南

发布时间:2026/10/26 13:59:50 来源:尧图企业网站定制
1. 这不是又一个“Hello World”式LangChain4j教程——它专为Java工程师真实工作流设计你点开这个标题大概率是因为刚在面试题里看到“LangChain4j和Spring Boot怎么集成”或者团队技术选型会上听到“AI Agent要落地Java后端得接上LLM能力”又或者你正被一个需求卡住——“把现有CRM系统里的客户工单自动摘要分类生成处理建议不换语言栈”。别急着去翻Python的LangChain文档再翻译成Java思维也别信那些照搬Python示例、硬套Bean注解就叫“Java版”的伪教程。LangChain4j不是LangChain的Java翻译器它是为JVM生态量身重写的AI应用框架它用Stream替代Generator用SupplierChatModel管理模型生命周期用RetryPolicy而非tenacity做重试连Prompt模板都支持Spring EL表达式。我带过三个用Java重构AI模块的团队踩过所有坑——比如用DefaultChatMemory在高并发下内存暴涨比如没配StreamingResponseHandler导致前端卡死30秒才吐出第一字比如把ToolExecutionRequest当普通DTO序列化进Redis结果反序列化失败。这篇教程不讲“什么是LLM”不堆砌概念图只聚焦三件事第一用你熟悉的Maven依赖、Spring Boot配置、JUnit断言5分钟跑通第一个带记忆的对话链第二把LangChain4j真正嵌进你的业务代码——不是demo是能上线、可监控、能灰度的生产级写法第三给你一份《Java工程师LangChain4j避坑清单》里面全是我在金融、电商、SaaS项目里实测过的参数阈值和线程池配置。如果你手头有Spring Boot 3.x项目JDK 17还有15分钟现在就可以打开IDEA跟着敲——这教程的每一行代码都来自我们上周刚上线的智能客服知识库模块。2. 为什么LangChain4j不是“LangChain的Java版”理解它的JVM原生设计哲学2.1 核心差异从“Python惯性思维”到“JVM资源治理思维”很多Java开发者初学LangChain4j时会下意识把Python LangChain的写法直接移植过来结果掉进性能陷阱。最典型的例子是ChatMemory的使用。Python版常用ConversationBufferMemory它把历史对话存在内存列表里简单粗暴。但Java版如果直接用InMemoryChatMemory在QPS50的场景下GC压力会飙升——因为每次chatModel.generate()调用都会触发一次List.add()而ChatMessage对象包含完整的Content字符串可能长达数万字符频繁创建大对象导致年轻代快速填满。LangChain4j的解法是TokenWindowChatMemory它不是按消息条数截取而是按token数截取。这就要求你必须传入Tokenizer而这个Tokenizer必须和你实际使用的LLM模型严格匹配。比如用Ollama跑qwen2:7b就得用QwenTokenizer用Azure OpenAI的gpt-4o就得用OpenAiTokenizer。我见过最惨的案例是团队用OpenAiTokenizer配本地llama3:8b结果token计数偏差47%窗口提前截断Agent忘了自己两分钟前承诺过的事。LangChain4j强制你在构造TokenWindowChatMemory时传入Tokenizer就是逼你直面这个现实JVM里没有“免费的内存”每个对象都要为它的生命周期负责。2.2 架构分层为什么它敢叫“4j”而不是“for Java”LangChain4j的命名本身就暗示了它的定位——不是适配层而是原生层。“4j”意味着它深度融入JVM生态的四大支柱Spring Boot First-Class Support不是简单加个Configuration而是提供LangChain4jAutoConfiguration自动装配ChatModel、EmbeddingModel、RetrievalAugmentor等核心Bean并且支持ConditionalOnProperty(langchain4j.enabled)做功能开关。更关键的是它把Spring的ApplicationEvent机制用到了极致——当你调用chatModel.generate()时框架会发布ChatModelRequestEvent和ChatModelResponseEvent你可以监听这些事件做审计日志、耗时监控、甚至动态熔断。Reactive Streams原生支持不像Python靠async/await模拟异步LangChain4j直接实现org.reactivestreams.PublisherChatResponse。这意味着你可以用Project Reactor的Flux操作符做背压控制——比如用limitRate(10)限制每秒最多10个流式响应避免下游WebSocket连接被撑爆。Micrometer Metrics深度集成所有核心组件默认暴露Micrometer指标。ChatModel会打点langchain4j.chat.model.duration带model、provider标签Retriever会打点langchain4j.retriever.results.count。你不需要写一行埋点代码只要引入micrometer-registry-prometheus就能在Grafana里看到LLM调用的P95延迟曲线。Jakarta EE兼容性所有API接口都基于Jakarta EE规范比如ChatModel继承jakarta.enterprise.inject.spi.BeanToolExecutor支持CDI注入。这意味着它能在WildFly、Payara等传统Java EE容器里无缝运行不用为了AI强行上Spring Boot。2.3 关键组件映射Java工程师该关注哪些“真正有用”的类别被GitHub上几百个类吓到生产环境真正高频使用的就这五个ChatModel不是接口而是具体实现类如OpenAiChatModel、OllamaChatModel。它的构造函数接受OpenAiChatModel.Builder里面apiKey、baseUrl、timeout都是必填项——LangChain4j拒绝“默认值陷阱”超时必须显式设为Duration.ofSeconds(30)否则用HttpClient默认的无限等待。AiServices这是LangChain4j的“魔法入口”。它用Proxy.newProxyInstance()动态生成接口实现把自然语言描述的方法注释如/** 根据订单ID查询物流状态返回JSON格式 */自动转成Prompt。但注意它只支持interface不支持class方法参数必须是POJO或基本类型不能是MapString, Object这种泛型擦除后无法反射的类型。Retriever不是搜索引擎而是“召回器”。EmbeddingStoreRetriever需要你先构建EmbeddingStore如PgVectorEmbeddingStore再注入EmbeddingModel。这里有个致命细节EmbeddingModel的embed()方法返回ListDouble而PostgreSQL的vector类型要求维度固定所以你必须在建表时用vector(1024)明确指定维度不能用vector无参。ToolJava版的Function Calling。Tool注解标记的方法参数必须用Description标注用途如Description(用户手机号11位数字) String phone否则LLM看不懂参数语义。更关键的是ToolExecutionRequest对象里arguments是MapString, String不是原始JSON所以你在工具方法里要手动new ObjectMapper().readValue(request.arguments().get(phone), String.class)。ChatMemory前面提过TokenWindowChatMemory但生产环境强烈推荐RedisChatMemory。它的RedisChatMemory.Builder必须传入RedisClient不是StringRedisTemplate因为底层用RedisCommands做原子操作。我踩过的坑是用LettuceConnectionFactory创建的RedisTemplate去配RedisChatMemory结果HGETALL命令返回空因为序列化器不匹配——RedisChatMemory内部用StringCodec而RedisTemplate默认用JdkSerializationRedisSerializer。3. 实战从零搭建一个带记忆、能调用工具、支持流式响应的客服对话系统3.1 环境准备与依赖配置——避开Maven依赖地狱别直接抄官网的dependency那只是最小集。生产环境必须加这四组依赖!-- 核心框架 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.33.0/version /dependency !-- Spring Boot自动装配 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version0.33.0/version /dependency !-- 向量数据库支持以PostgreSQL为例 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-pgvector/artifactId version0.33.0/version /dependency !-- 流式响应必备WebSocket支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency重点来了版本必须锁死LangChain4j 0.32.x和0.33.x的AiServicesAPI有breaking change——0.32.x用AiServices.create()0.33.x改用AiServices.builder().chatModel(...).build()。我见过团队因没锁版本CI构建时随机拉到0.32.1导致build()方法找不到凌晨三点紧急回滚。另外langchain4j-pgvector依赖pgvector而pgvector的JDBC驱动要求PostgreSQL 14如果你用的是阿里云RDS PostgreSQL 12必须升级内核否则CREATE EXTENSION vector会报错。最后spring-boot-starter-webflux不是可选——即使你主应用用spring-boot-starter-webChatModel的流式响应也依赖WebFlux的Flux否则generateStreaming()会抛IllegalStateException: No reactive infrastructure found。3.2 第一个可运行的流式对话5分钟验证LLM连通性别一上来就搞复杂Agent先确保基础链路通。新建CustomerServiceChatController.javaRestController RequestMapping(/api/chat) public class CustomerServiceChatController { private final ChatModel chatModel; public CustomerServiceChatController(ChatModel chatModel) { this.chatModel chatModel; } PostMapping(/stream) public FluxChatResponse streamChat(RequestBody ChatRequest request) { UserMessage userMessage UserMessage.from(request.getMessage()); // 关键必须用StreamingResponseHandler否则不会流式返回 StreamingResponseHandlerChatResponse handler new StreamingResponseHandler(); chatModel.generate(userMessage, handler); return handler.getResponses(); // 返回Flux前端用EventSource接收 } }对应的ChatRequest很简单public class ChatRequest { private String message; // getter/setter }启动应用用curl测试curl -X POST http://localhost:8080/api/chat/stream \ -H Content-Type: application/json \ -d {message:你好我想查订单状态} \ --no-buffer | while IFS read -r line; do echo $line | jq -r .content done如果看到逐字输出“您好请问您的订单号是多少”说明成功。但注意--no-buffer是关键否则curl会等整个响应结束才输出。这里暴露了一个重要原理LangChain4j的流式不是“服务器推”而是“客户端拉”。handler.getResponses()返回的Flux底层是SynchronousSink每次LLM返回一个token就sink.next()一次。所以如果你用RestTemplate调用必须用exchange()方法获取ResponseEntityFluxChatResponse不能用getForObject()——后者会阻塞直到流结束。3.3 让对话记住上下文TokenWindowChatMemory实战配置InMemoryChatMemory只适合单元测试。生产环境用TokenWindowChatMemory但必须配对Tokenizer。假设你用Ollama的qwen2:7bBean public ChatMemory chatMemory() { // 1. 先加载Qwen2的Tokenizer需下载tokenizer.json Path tokenizerPath Paths.get(src/main/resources/tokenizers/qwen2-tokenizer.json); QwenTokenizer tokenizer new QwenTokenizer(tokenizerPath); // 2. 创建TokenWindowChatMemory窗口大小设为2048 tokens // 注意不是2048条消息是2048个token约等于1500汉字 return TokenWindowChatMemory.builder() .maxTokens(2048) .tokenizer(tokenizer) .build(); }然后注入到ChatModelBean public ChatModel chatModel(ChatMemory chatMemory) { return OpenAiChatModel.builder() .apiKey(your-api-key) .baseUrl(https://api.openai.com/v1) .timeout(Duration.ofSeconds(30)) .logRequests(true) // 开启请求日志方便调试 .logResponses(true) .build(); }但这样还不够ChatModel本身不持有ChatMemory你需要用ChatWithMemory包装Service public class CustomerService { private final ChatWithMemory chatWithMemory; public CustomerService(ChatModel chatModel, ChatMemory chatMemory) { this.chatWithMemory ChatWithMemory.builder() .chatModel(chatModel) .chatMemory(chatMemory) .build(); } public String chat(String userId, String message) { // 关键必须传入userId作为memory key否则所有人共享同一段记忆 return chatWithMemory.chat(userId, UserMessage.from(message)).content(); } }这里userId是内存keyLangChain4j会用它做Map的key。如果你用Redis存记忆userId会变成Redis的key前缀比如chat:memory:U123456。我踩过的坑是前端传来的userId带空格或特殊字符导致Redis key非法。解决方案是在chat()方法里加校验userId userId.replaceAll([^a-zA-Z0-9_], )。3.4 让AI调用真实业务APITool定义与执行全流程假设你要让AI能查订单状态定义OrderToolComponent public class OrderTool { Tool(根据订单ID查询物流状态返回JSON格式) public String getOrderStatus( Description(订单ID纯数字长度8-12位) String orderId) { // 1. 参数校验LangChain4j不帮你做 if (!orderId.matches(\\d{8,12})) { return 订单ID格式错误请输入8-12位纯数字; } // 2. 调用真实订单服务这里简化为mock try { // 模拟HTTP调用 String url https://api.your-shop.com/orders/ orderId /status; // 实际用RestTemplate或WebClient return {\status\:\shipped\,\trackingNo\:\SF123456789CN\}; } catch (Exception e) { return 查询失败 e.getMessage(); } } }然后创建AiServicesBean public CustomerServiceAi customerServiceAi(ChatModel chatModel, OrderTool orderTool) { return AiServices.builder() .chatModel(chatModel) .tools(orderTool) // 注入Tool实例 .build(CustomerServiceAi.class); // 接口类 } // 对应的接口 public interface CustomerServiceAi { /** * 回答用户关于订单、物流、售后的问题 * 如果需要查订单状态调用getOrderStatus工具 */ String chat(UserMessage String message); }调用时Service public class ChatService { private final CustomerServiceAi customerServiceAi; public ChatService(CustomerServiceAi customerServiceAi) { this.customerServiceAi customerServiceAi; } public String handleChat(String userId, String message) { // LangChain4j会自动识别是否需要调用Tool // 如果message含订单12345678的状态会先调orderTool.getOrderStatus(12345678) // 再把结果拼进Prompt让LLM生成最终回复 return customerServiceAi.chat(message); } }关键原理AiServices生成的代理类会在调用chat()前用ToolExecutor扫描所有Tool方法提取Description生成工具描述然后让LLM决定是否调用。LLM的System Prompt里会包含工具列表格式类似You have access to the following tools: - getOrderStatus: 根据订单ID查询物流状态返回JSON格式 When you need to use a tool, respond in the following format: tool_nametool_input/tool_name所以你的Description必须写清楚不能写“查订单”要写“根据订单ID查询物流状态返回JSON格式”。4. 生产级部署性能调优、监控告警与安全加固三件套4.1 性能调优线程池、缓存、重试的黄金参数LangChain4j默认用HttpClient但生产环境必须定制。ChatModel的timeout只是网络超时真正的瓶颈在LLM响应时间。我们实测发现OpenAI GPT-4o平均响应2.3秒P95是4.7秒Ollama本地qwen2:7b平均响应8.1秒P95是15.3秒所以ChatModel的timeout必须设为P952秒即Duration.ofSeconds(17)。但光设超时不够还要配线程池Bean public ChatModel chatModel() { HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); return OpenAiChatModel.builder() .apiKey(your-key) .baseUrl(https://api.openai.com/v1) .timeout(Duration.ofSeconds(17)) // 关键比P95多2秒 .httpClient(httpClient) .build(); }更关键的是AiServices的线程池。默认用ForkJoinPool.commonPool()但它的并行度是CPU核心数对于IO密集型的LLM调用应该用自定义线程池Bean public ExecutorService aiExecutorService() { // 核心线程数 CPU核心数 * 2最大线程数 核心数 * 4 // 队列用LinkedBlockingQueue避免拒绝策略丢请求 return new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors() * 2, Runtime.getRuntime().availableProcessors() * 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000), new ThreadFactoryBuilder().setNameFormat(ai-executor-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由调用线程执行 ); } Bean public CustomerServiceAi customerServiceAi(ChatModel chatModel, OrderTool orderTool) { return AiServices.builder() .chatModel(chatModel) .tools(orderTool) .executorService(aiExecutorService()) // 注入自定义线程池 .build(CustomerServiceAi.class); }缓存方面EmbeddingModel的embed()调用开销大必须缓存。LangChain4j提供CachingEmbeddingModelBean public EmbeddingModel embeddingModel() { return CachingEmbeddingModel.builder() .delegate(new OpenAiEmbeddingModel(your-key)) // 委托给真实模型 .cache(new CaffeineCache(10000)) // 缓存1万个embedding .build(); }但注意CaffeineCache的maximumSize(10000)是硬上限超过会LRU淘汰。我们线上用expireAfterWrite(1, TimeUnit.HOURS)因为商品描述文本很少变。4.2 监控告警用Micrometer暴露关键指标LangChain4j自动暴露的指标不够用必须加业务指标。在CustomerServiceAi里Service public class CustomerServiceAiImpl implements CustomerServiceAi { private final Counter toolCallCounter; private final Timer chatTimer; public CustomerServiceAiImpl(MeterRegistry meterRegistry) { this.toolCallCounter Counter.builder(langchain4j.tool.call) .description(Number of tool calls) .register(meterRegistry); this.chatTimer Timer.builder(langchain4j.chat.duration) .description(Time taken for chat completion) .register(meterRegistry); } Override public String chat(String message) { long start System.nanoTime(); try { // 实际调用逻辑 String result doChat(message); return result; } finally { chatTimer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); } } private String doChat(String message) { // 如果调用了tool计数 if (needsToolCall(message)) { toolCallCounter.increment(); } // ... } }Prometheus配置抓取/actuator/prometheusGrafana看板关键指标langchain4j.chat.model.duration_seconds_max{modelgpt-4o}P95延迟阈值设为5秒langchain4j.tool.call_total每分钟调用次数突增200%触发告警jvm_memory_used_bytes{areaheap}堆内存配合langchain4j.chat.memory.size看内存泄漏我们线上发现过TokenWindowChatMemory的tokenizer缓存未清理导致QwenTokenizer对象堆积jvm_memory_used_bytes持续上涨。解决方案是QwenTokenizer构造时传入CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(1, TimeUnit.HOURS)。4.3 安全加固输入过滤、输出脱敏、密钥管理LLM应用最大的安全风险是Prompt Injection。LangChain4j不提供自动防护必须自己加Service public class SafeChatService { private final Pattern MALICIOUS_PATTERN Pattern.compile( (?i)(system|role|assistant|user|script|javascript:|eval\\(|exec\\(), Pattern.UNICODE_CASE); public String safeChat(String userId, String userInput) { // 1. 输入过滤拒绝含恶意关键词的请求 if (MALICIOUS_PATTERN.matcher(userInput).find()) { throw new IllegalArgumentException(输入包含敏感指令已被拦截); } // 2. 输出脱敏用正则替换手机号、身份证号 String response customerServiceAi.chat(userInput); return response.replaceAll(\\b1[3-9]\\d{9}\\b, 1XXXXXXXXXX) .replaceAll(\\b\\d{17}[0-9Xx]\\b, XXXXXXXXXXXXXXXXX); } }密钥管理必须用Spring Cloud Config或Vault禁止硬编码。application.yml里langchain4j: openai: api-key: ${OPENAI_API_KEY:default-key} # 从环境变量读取 base-url: ${OPENAI_BASE_URL:https://api.openai.com/v1}启动时加JVM参数-DOPENAI_API_KEYyour-real-key。更安全的做法是用AWS Secrets Manager通过spring-cloud-starter-aws-parameter-store-config加载。5. 常见问题排查手册从“Connection refused”到“Tool not called”5.1 连接类问题速查表现象可能原因排查命令解决方案Connection refused: localhost/127.0.0.1:11434Ollama服务未启动ollama serve在服务器后台运行nohup ollama serve /dev/null 21 SSLHandshakeException: PKIX path building failedJDK证书库缺失OpenAI证书keytool -list -v -keystore $JAVA_HOME/jre/lib/security/cacerts更新JDK或导入证书keytool -import -trustcacerts -file openai.crt -alias openai -keystore $JAVA_HOME/jre/lib/security/cacerts401 UnauthorizedAPI Key错误或过期curl -H Authorization: Bearer YOUR_KEY https://api.openai.com/v1/models检查Key是否复制完整注意前后空格在OpenAI官网重置Key5.2 功能类问题根因分析问题LLM始终不调用Tool一直返回“我无法查询订单状态”根因Description太模糊或AiServices没注入Tool实例。验证开启logRequeststrue看请求体里是否有tools字段。如果没有检查Bean是否返回OrderTool实例而不是new OrderTool()。修复Bean方法必须用Component或Service注解且OrderTool类上要有Component。问题流式响应前端收不到数据一直pending根因Flux没正确返回或Nginx代理超时。验证用curl --no-buffer直接调用看是否逐字输出。修复Spring Boot配置server.tomcat.connection-timeout60000Nginx加proxy_read_timeout 300; proxy_buffering off;。问题RedisChatMemory报ERR unknown command HGETALL根因Redis版本低于6.2不支持HGETALL的COUNT参数。验证redis-cli INFO | grep redis_version。修复升级Redis到6.2或改用InMemoryChatMemory仅限开发环境。5.3 性能类问题独家经验内存泄漏信号jstat -gc pid显示OU老年代使用率持续上涨且FGCTFull GC次数增加。解决用jmap -histo pid \| grep Tokenizer看QwenTokenizer实例数确认是否缓存未清理。线程阻塞信号jstack pid \| grep WAITING显示大量线程在HttpClient的connect方法。解决降低HttpClient的connectTimeout并增加maxConnectionsHttpClient.newBuilder().maxConnections(100)。LLM响应慢信号langchain4j.chat.model.duration_seconds_max持续高于P95阈值。解决不是调优Java代码而是换模型——GPT-4o比GPT-3.5-turbo快3倍成本只高1.5倍本地用qwen2:1.5b比qwen2:7b快5倍精度损失可接受。最后分享一个血泪教训我们曾在线上环境用InMemoryChatMemory以为QPS低没问题。结果某天促销活动用户并发激增ChatMemory的ConcurrentHashMap锁竞争导致TPS从1200暴跌到200。紧急扩容无效最终用RedisChatMemory替换TPS恢复到1500。所以记住没有“小流量”的AI服务只有“没压测过”的AI服务。上线前必须用wrk -t10 -c100 -d30s http://localhost:8080/api/chat/stream压测观察GC和线程状态。

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

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

免费获取报价 →
↑