技术栈选择先说结论组件选什么为什么JDKJava 21虚拟线程对LLM的长耗时IO天然友好框架Spring Boot 3.3你最熟的框架不增加学习成本LLM框架LangChain4j 0.36Java生态最成熟的LLM框架社区活跃LLM模型DeepSeek国产、便宜、OpenAI兼容API、注册就送额度IDEIntelliJ IDEA不用解释生产实录为什么不是Spring AI你可能听说过Spring AI——Spring官方出品的AI框架。但在实际生产中LangChain4j的生态更成熟RAG工具链完整、支持的模型更多、社区迭代更快。我在生产项目中虽然使用的是内部SDK但其API设计思想与LangChain4j高度一致——Builder链式调用、流式回调、模型抽象层。LangChain4j学会了换任何SDK都能快速上手。第一步创建项目创建一个标准的Spring Boot 3.3项目。核心依赖如下propertiesjava.version21/java.versionlangchain4j.version0.36.2/langchain4j.version/propertiesdependencies!-- Spring Boot --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- LangChain4j 核心 --dependencygroupIddev.langchain4j/groupIdartifactIdlangchain4j/artifactIdversion${langchain4j.version}/version/dependency!-- LangChain4j Spring Boot 集成 --dependencygroupIddev.langchain4j/groupIdartifactIdlangchain4j-spring-boot-starter/artifactIdversion${langchain4j.version}/version/dependency!-- OpenAI兼容模型支持DeepSeek/Qwen/豆包都走这个 --dependencygroupIddev.langchain4j/groupIdartifactIdlangchain4j-open-ai-spring-boot-starter/artifactIdversion${langchain4j.version}/version/dependency/dependencies注意langchain4j-open-ai这个依赖——它不只是给OpenAI用的。国内主流大模型DeepSeek、通义千问、豆包、百度千帆现在都支持OpenAI兼容API格式所以一个依赖就能接所有模型。这点在生产中非常重要你不需要为每个模型厂商引入单独的SDK。第二步获取API Key去 DeepSeek开放平台 注册账号。新用户有免费额度足够你跑完整本小册的所有示例。拿到API Key后配置到application.ymllangchain4j:open-ai:chat-model:api-key:${DEEPSEEK_API_KEY:sk-xxxxx}# 建议用环境变量别硬编码model-name:deepseek-chatbase-url:https://api.deepseek.com# 关键改成DeepSeek的地址temperature:0.7max-tokens:2048看到了吗只改了base-url和model-name其他跟调OpenAI一模一样。这就是OpenAI兼容API的意义——所有厂商都说同一种协议。生产实录配置驱动的多模型路由在我参与的生产系统中不同业务场景会调用不同的模型。比如# 配置结构示例基于生产实践整理scene-model:knowledgeSummary:qwen3-max# 文档总结 → 通义千问质量优先knowledgeRetrieval:doubao-1-6# 知识检索 → 豆包速度优先internetSearch:qwen3-max-search# 联网搜索 → 通义搜索插件queryRewrite:doubao-1-6# 问题改写 → 豆包轻量任务业务代码里只写一个场景别名具体用哪个模型、哪个厂商完全由配置决定。这样切换模型只需要改YAML不需要改一行代码。我们另一个项目更进一步——做了一个LLM模型网关统一管理所有模型的调用入口包含多API Key容灾一个Key额度用完自动切下一个、按渠道分发、调用计费。你现在用一个DeepSeek就够了但架构思维要先建立模型和业务代码之间应该有一层路由抽象隔离。这跟你做支付系统时用策略模式隔离支付渠道是一回事。第三步你的第一次LLM调用同步调用——最简单的方式RestControllerRequestMapping(/ai)publicclassChatController{privatefinalChatLanguageModelchatModel;publicChatController(ChatLanguageModelchatModel){this.chatModelchatModel;}GetMapping(/chat)publicStringchat(RequestParamStringmessage){returnchatModel.generate(message);}}启动项目访问GET http://localhost:8080/ai/chat?message你好用一句话介绍一下Java返回Java是一种面向对象的、跨平台的编程语言以一次编写到处运行著称。就这么简单。Spring Boot自动注入了ChatLanguageModel基于你在application.yml里的配置你直接用就行。跟你注入JdbcTemplate查数据库没有任何区别。Java类比理解MyBatis LangChain4j ────────── ────────── DataSource配置 → model配置api-key, base-url SqlSessionFactory → ChatLanguageModel Mapper接口 → AiService接口 mapper.selectById(1) → chatModel.generate(你好) 返回Java对象 → 返回文本字符串ChatLanguageModel就是LangChain4j的SqlSession——你拿到它就能跟数据库大模型对话。第四步流式输出——让AI像人一样打字同步调用有个问题LLM生成回答需要几秒甚至十几秒用户看到的是一片空白然后突然弹出一大段文字。体验很差。流式输出的意思是LLM每生成一个词就立刻推送给前端用户看到的是AI在打字。就像ChatGPT那样。Java类比// 同步 攒够了一次给你ListOrderordersorderService.findAll();// 查完才返回returnorders;// 流式 来一条给你一条类似Java Stream的惰性求值StreamOrderordersorderService.streamAll();// 边查边返回orders.forEach(o-send(o));SSE实现SSEServer-Sent Events是流式输出最常用的协议——本质上就是一个长连接服务端可以持续向客户端推送数据。你可能没直接用过SSE但你一定见过WebSocketSSE比WebSocket简单得多单向推送基于HTTP不需要握手升级。RestControllerRequestMapping(/ai)publicclassStreamChatController{privatefinalStreamingChatLanguageModelstreamingModel;publicStreamChatController(StreamingChatLanguageModelstreamingModel){this.streamingModelstreamingModel;}GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstreamChat(RequestParamStringmessage){SseEmitteremitternewSseEmitter(60_000L);// 60秒超时streamingModel.generate(message,newStreamingResponseHandler(){OverridepublicvoidonNext(Stringtoken){try{emitter.send(SseEmitter.event().data(token));}catch(IOExceptione){emitter.completeWithError(e);}}OverridepublicvoidonComplete(ResponseAiMessageresponse){emitter.complete();}OverridepublicvoidonError(Throwableerror){emitter.completeWithError(error);}});returnemitter;}}访问http://localhost:8080/ai/stream?message用3句话介绍Spring Boot你会看到文字一个个蹦出来而不是等半天才整段显示。在application.yml中补充流式模型配置langchain4j:open-ai:streaming-chat-model:api-key:${DEEPSEEK_API_KEY:sk-xxxxx}model-name:deepseek-chatbase-url:https://api.deepseek.com生产实录流式输出的两个真实坑坑1客户端断连了LLM还在跑用户关掉了页面但服务端的LLM调用还在继续——白白烧token。在我们的生产系统中SSE的封装会检测客户端连接状态// 生产级做法封装一个安全的Emitter引用// 客户端断连时标记 clientGonetrueLLM回调继续执行但不再推送// 关键是 LLM的流不中断避免连接泄露只是不再发送给前端这是一个很容易忽略的点。LLM的流式调用一旦启动没有优雅的中断机制——你不能中途挂电话。最佳实践是让LLM流跑完内容可以直接丢弃或落库备用但在推送端做静默降级。坑2WebSocket vs SSE的选择在我参与的两个项目中对话主通道用的是WebSocket支持双向通信用户可以中途发新消息思维导图等辅助功能用的是SSE单向推送够用实现更简单。选择建议如果是纯对话用户问一句、AI答一句→ SSE足够实现简单如果需要双向交互用户中途打断、切换话题→ WebSocket更合适如果你是初学者 →先用SSE后面根据需要再升级第五步用AiService让代码更优雅直接用ChatLanguageModel.generate()可以工作但不够优雅。LangChain4j提供了一个更Java风格的方式——AiService本质上就是给LLM生成一个动态代理publicinterfaceAssistant{SystemMessage(你是一个Java技术专家擅长用简洁清晰的方式回答技术问题。)Stringanswer(UserMessageStringquestion);SystemMessage(你是一个代码审查助手。请审查以下代码指出潜在的问题。)StringreviewCode(UserMessageStringcode);}注册为Spring BeanConfigurationpublicclassAiConfig{BeanpublicAssistantassistant(ChatLanguageModelchatModel){returnAiServices.create(Assistant.class,chatModel);}}使用RestControllerRequestMapping(/ai)publicclassAssistantController{privatefinalAssistantassistant;publicAssistantController(Assistantassistant){this.assistantassistant;}GetMapping(/ask)publicStringask(RequestParamStringquestion){returnassistant.answer(question);}GetMapping(/review)publicStringreview(RequestParamStringcode){returnassistant.reviewCode(code);}}Java类比MyBatis的Mapper接口 LangChain4j的AiService接口 ────────────────── ────────────────────────── Select(SELECT * ...) SystemMessage(你是专家...) User findById(int id); String answer(String q); MapperProxy动态代理生成实现 AiServices动态代理生成实现 底层执行SQL 底层调用LLM API一模一样的设计思路——用接口定义契约框架负责实现。你定义一个接口LangChain4j通过动态代理帮你实现每个方法把SystemMessage和UserMessage拼成Prompt调LLM返回结果。就像MyBatis帮你把Select变成SQL执行一样。生产实录Builder链式API vs AiService在实际项目中我们使用的LLM调用方式更接近Builder模式// 生产级调用风格示例llmApi.chat(doubao-1-6)// 选模型配置别名.systemPrompt(systemPrompt)// 系统提示词.historyMessages(memoryList)// 历史对话记忆.userInput(userMessage)// 用户输入.contentConsumer(token-{// 流式回调outputChannel.send(token);}).callStream();// 触发流式调用这个Builder链式API跟LangChain4j的思路一致只是封装层次不同。生产系统选择Builder是因为灵活性更高——一行代码就能切换模型、注入不同的上下文、对接不同的输出通道WebSocket/SSE/直接落库。初学阶段用AiService足够代码干净等你做到生产级别需要精细控制时再考虑Builder模式。第六步用环境变量管理API Key一个严肃的提醒永远不要把API Key硬编码在代码里或提交到Git。# application.yml - 用环境变量引用langchain4j:open-ai:chat-model:api-key:${DEEPSEEK_API_KEY}本地开发时在IDEA的Run Configuration里设置环境变量或者创建一个.env文件记得加到.gitignore。这个习惯对Java工程师来说应该是常识了——跟你管理数据库密码一样。但在AI应用里API Key泄露的代价更高别人用你的Key调LLM一晚上能烧掉几千块。本讲完整代码src/main/java/com/example/ai/ ├── config/ │ └── AiConfig.java # AiService配置 ├── controller/ │ ├── ChatController.java # 同步对话接口 │ ├── StreamChatController.java # 流式对话接口 │ └── AssistantController.java # AiService风格接口 ├── service/ │ └── Assistant.java # AiService接口定义 └── Application.java src/main/resources/ └── application.yml # 模型配置本讲要点LangChain4j OpenAI兼容API 一套代码接所有国产大模型ChatLanguageModel 你的LLM版JdbcTemplate注入即用流式输出用SSEStreamingResponseHandler回调逐token推送AiService MyBatis Mapper的AI版接口定义 动态代理实现API Key用环境变量别硬编码别提交Git动手练习跑通同步调用和流式调用把base-url换成通义千问https://dashscope.aliyuncs.com/compatible-mode/v1体验只改配置就能切模型用AiService定义一个CodeHelper接口包含explainCode()和generateTest()两个方法想一想如果你要同时支持DeepSeek和通义千问主备切换配置和代码应该怎么设计提示回忆一下你做支付系统时怎么做通道切换的下一讲预告第3讲流式输出——LLM的分页查询——深入SSE协议的报文格式和生产级实现实现一个带打字机效果的完整对话页面前后端都有。还会分享真实生产项目中WebSocket/SSE双通道选型、客户端断连处理等真实踩坑经验。「Java转AI实战」完整目录共16讲讲次标题第1讲Java工程师的AI转型地图第2讲开发环境搭建与第一次LLM调用本文第3讲流式输出——LLM的分页查询第4讲多模型策略——LLM的负载均衡第5讲Prompt Engineering——给LLM写需求文档第6讲RAG全景——为什么LLM需要开卷考试第7讲文档解析——把非结构化数据变成可检索的文本第8讲智能切分——文档的分库分表第9讲Embedding向量化——文本的序列化第10讲检索策略——从SQL查询到语义搜索第11讲端到端RAG应用——把前5讲串起来第12讲从对话到行动——Agent是什么第13讲MCP协议——AI的标准API网关第14讲Agent实战——构建一个能执行任务的AI助手第15讲从Demo到生产——你的Java经验在这里爆发第16讲转型行动指南——从会了到找到AI工作前4讲免费阅读第5讲起进入RAG核心区和Agent实战在知识星球持续更新。获取完整课程这个系列的前4讲已经在CSDN免费发布帮你搞定LLM调用的基本功。但AI应用开发真正的技术壁垒在后面——第5讲的Prompt工程、第6-11讲的RAG全链路、第12-14讲的Agent实战——这些才是区分会调API和能做AI应用的分水岭。完整16讲 持续更新的实战内容都在知识星球**「Java转AI实战内参」**里真实生产级AI项目的完整架构拆解RAG Agent MCP每周更新的AI实战案例和踩坑记录Java转AI岗的面试题库与简历优化社群答疑有问必答