LangChain4j 第一个对话程序:手写 LLM 调用链路,看清模型怎么"开口"
作者:鱼宵 | LangChain4j实战精通营 · 第 1 篇
上个月有个学员跑通 Spring AI 第一个 AI 接口后懵了:整个工程没出现一个"模型"类,配好 yml 就聊上天了。他反问我:那模型到底怎么被调起来的?我没急着解释,直接把另一个工程丢给他——没有 yml,模型是我手写@Beannew 出来的,每个参数都摆在你眼前。他跑了一遍,说这下看清"模型怎么开口"了。
这个工程就是 LangChain4j。和 Spring AI"配好 yml 拎包入住"不同,这门课第一课就把模型对象亲手new出来:HTTP 请求怎么发、Key 怎么管、温度和 maxTokens 是干嘛的,一个都藏不住。代码全在仓库lesson-01/,clone 下来重跑一遍,十分钟后你也有一个能对话的控制台程序——最后那两道挑战题,不亲手改一次,你真以为调模型有多玄乎。
一、核心原理:一次 LLM 调用背后发生了什么
一句话先撂这儿:你写的那行chatModel.chat("你好"),背后是框架帮你拼了一个 HTTP POST 请求,模型在远端推理完,再把 JSON 答案解析成 Java 字符串递回给你。
1. 框架帮你干了哪段脏活
不写框架直接调模型,你得自己干这些事:拼 JSON 请求体、管 HTTP 连接、处理各家厂商的接口差异、把返回 JSON 里choices[0].message.content那串字抠出来。就像自己做饭全流程包圆,用框架就是点菜——你只管下单(写业务),后厨脏活归它。
一次调用的链路是这样:
你的 Java 代码 chatModel.chat("你好") ↓ 框架把消息打包成 JSON HTTP POST https://api.deepseek.com/chat/completions ↓ 模型在远端推理(这里花 token、花钱、花时间) HTTP 响应 JSON(里面装着回答文本) ↓ 框架解析 你的 Java 代码拿到 String 回答面试能把这条链路讲顺,就等于讲清了"框架到底帮我封装了什么"。
2. 为什么 DeepSeek 能直接用 OpenAI 的客户端
这是这门课第一个"反直觉"的点:我代码里写的是OpenAiChatModel,连的却是 DeepSeek。原因是协议兼容——DeepSeek 对外的接口路径、请求体、响应体格式,跟 OpenAI 一模一样。所以 OpenAI 的客户端换个baseUrl就能连它。
打个比方:OpenAiChatModel像万能充电线,插座规格是 OpenAI 协议,插谁家都行,你只要换"充电头"三件套——base-url(服务地址)+ api-key(密钥)+ model(模型名)。这也是为什么这门课全程只教你连 DeepSeek,切模型却只改三个环境变量。
3. 手写 Bean vs yml 自动装配:两门课的对照点
Spring AI 那门课(第 1 篇)是"往 classpath 丢 starter,yml 写spring.ai.openai.*,框架自动把ChatClient创建好"。这门课反过来——模型对象是我在LlmConfig里手写@Bean造出来的:
| Spring AI(yml 派) | LangChain4j(手写 Bean 派) | |
|---|---|---|
| 模型在哪声明 | yml 里spring.ai.openai.* | Java 代码OpenAiChatModel.builder()...build() |
| 参数看得见吗 | 藏在配置绑定里 | 一行一行摆在 builder 上 |
| 换模型 | 改 yml + 重启 | 改环境变量 / builder 那几行 |
| 适合 | 纯 Spring 栈、想省心 | 想看清每个参数、多模型灵活切 |
说白了,yml 自动装配是"精装房拎包入住",手写 Bean 是"自己挑家具摆一遍"。教学阶段先摆一遍,以后住进去才不心慌。
4. SystemPrompt:给模型发一本入职手册
这门课第二个重点概念是SystemPrompt——发给模型的第一段"人设说明书",比用户问题优先级还高。新员工上岗先读手册就知道自己是谁,模型也一样:没手册是通用助手,给了手册就"入戏"。玩法二就是让它演 Java 面试官,第四节有实测。
二、动手:十分钟跑通第一个对话程序
环境:Windows + JDK 17 + Maven 3.9+。会
@SpringBootApplication、看得懂@Bean就行。
第 1 步:30 秒检查环境。
java-version# 期望 True(JDK17 在不在)[bool][Environment]::GetEnvironmentVariable('DEEPSEEK_API_KEY')# 期望 True(Key 配了没,只看存在性别打印值)$env:JAVA_HOME="C:\Program Files\Java\jdk-17"# 本机用户级 JAVA_HOME 指向 JDK8,必须覆盖第 2 步:编译工程。
cd langchain4j-journey\lesson-01$env:JAVA_HOME="C:\Program Files\Java\jdk-17"mvn clean install-DskipTests看到BUILD SUCCESS即通过。第一次会下载 LangChain4j 1.21.0 的依赖,耐心等。
第 3 步:跑玩法一(交互问答)。
java-jar target\lesson-01-1.0.0.jar看到你问:就敲问题回车,最多问 3 轮,输exit提前退。
第 4 步:跑玩法二(角色扮演:Java 面试官)。
java-jar target\lesson-01-1.0.0.jar roleplay注意区别只在启动参数roleplay,代码一个字没改。
三、关键代码:两个类,看清模型怎么被"造"出来
工程是纯控制台 Spring Boot 项目(没加 web 依赖,控制台课不需要 HTTP 接口),真正要看的就两个类。
第一段:LlmConfig.java——模型是怎么被手写出来的。
packagecom.langchain4j.lesson01;importdev.langchain4j.model.openai.OpenAiChatModel;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/** * 模型配置类:全课程统一套路——用 @Bean 手动把模型对象 new 出来。 * 和 Spring AI 的 yml 自动装配正好相反,这里每个参数都看得见。 * 安全红线:API Key 只从环境变量读,绝不写进代码 / yml / README / 日志。 */@ConfigurationpublicclassLlmConfig{@BeanpublicOpenAiChatModelopenAiChatModel(){returnOpenAiChatModel.builder()// 服务地址:默认 DeepSeek,环境变量 LLM_BASE_URL 可覆盖(换 OpenAI/通义).baseUrl(System.getenv().getOrDefault("LLM_BASE_URL","https://api.deepseek.com"))// API Key:只从环境变量读,写死 Key = 把钱包密码贴墙上.apiKey(System.getenv("DEEPSEEK_API_KEY"))// 模型名:默认 deepseek-chat,环境变量 LLM_MODEL 可覆盖.modelName(System.getenv().getOrDefault("LLM_MODEL","deepseek-chat"))// 输出上限 200 token:教学演示控成本,长回答会被拦腰截断(实测里能看到).maxTokens(200)// 温度 0.7:0=严谨死板,1=天马行空,聊天用 0.7 比较自然.temperature(0.7).build();}}这段就是"手写派"的精髓:builder 上每行都是一个你能叫出名字的参数。getOrDefault是故意的——环境变量优先、默认值兜底,这就是"切模型只改配置不改代码"。
第二段:ChatDemoApplication.java——一次 LLM 调用到底长什么样。
packagecom.langchain4j.lesson01;importdev.langchain4j.model.chat.ChatModel;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importjava.util.Scanner;/** * 第 1 课主程序。两种玩法用启动参数区分: * 无参 / chat —— 交互问答(控制台敲问题) * roleplay —— 角色扮演(Java 面试官,演示 SystemPrompt) */@SpringBootApplicationpublicclassChatDemoApplicationimplementsCommandLineRunner{// 注入的是接口 ChatModel(1.21.0 起由旧名 ChatLanguageModel 改名),// 实际对象是 OpenAiChatModel——面向接口,以后换实现业务代码一行不动。privatefinalChatModelchatModel;publicChatDemoApplication(ChatModelchatModel){this.chatModel=chatModel;}publicstaticvoidmain(String[]args){SpringApplication.run(ChatDemoApplication.class,args);}@Overridepublicvoidrun(String...args){Stringmode=args.length>0?args[0]:"chat";if("roleplay".equalsIgnoreCase(mode)){rolePlayDemo();}else{chatDemo();}}/** 玩法一:交互问答,最多 3 轮,输 exit 退出。 */privatevoidchatDemo(){System.out.println("========== LangChain4j 第 1 课:交互问答 ==========");System.out.println("(输入你的问题回车即可,输入 exit 退出,最多问 3 轮)");Scannerscanner=newScanner(System.in);intround=0;while(round<3){System.out.print("你问:");System.out.flush();// 不 flush 的话,输入前看不到提示语if(!scanner.hasNextLine())break;// 管道关闭就退出,保证程序能自然结束Stringquestion=scanner.nextLine().trim();if(question.isEmpty()||"exit".equalsIgnoreCase(question))break;round++;System.out.println(question);// ★★ 全课程最关键的一行:问进去,答出来,背后是一次完整的 HTTP 调用Stringanswer=chatModel.chat(question);System.out.println("AI 答:"+answer);System.out.println("---------- 第 "+round+" 轮结束 ----------");}System.out.println("========== 对话结束 ==========");}/** 玩法二:角色扮演。SystemPrompt 就是这里拼进去的。 */privatevoidrolePlayDemo(){System.out.println("========== LangChain4j 第 1 课:角色扮演(Java 面试官) ==========");// 人设说明书:先于用户问题发给模型,优先级最高StringsystemPrompt="你是资深 Java 技术面试官,语气专业但友好。"+"每次回答先用一句话点评候选人的回答,再追问一个更深入的问题。";String[]questions={"请问什么是 Java 里的 final 关键字?","HashMap 的底层数据结构是什么?"};for(inti=0;i<questions.length;i++){System.out.println("面试官问:"+questions[i]);// 把"人设 + 问题"拼在一段发给模型(第 2 课会用正规的消息体系替代这种手拼)Stringanswer=chatModel.chat(systemPrompt+"\n问题:"+questions[i]);System.out.println("AI 答:"+answer);System.out.println("---------- 第 "+(i+1)+" 题结束 ----------");}System.out.println("========== 面试演练结束 ==========");}}盯着chatModel.chat(question)这一行看三秒——你以后写的所有 AI 业务,底层都是它。框架在这行里把拼请求、发 HTTP、解析 JSON 全包了。
四、实测输出:同一份代码,加一段人设判若两人
以下是 2026-10-05 本机真实运行(DeepSeek 实测,已省略 Spring Boot 启动 banner)。
玩法一:交互问答。
========== LangChain4j 第 1 课:交互问答 ========== (输入你的问题回车即可,输入 exit 退出,最多问 3 轮) 你问:用一句话介绍你自己 AI 答:我是DeepSeek,由深度求索公司创造的AI助手,随时准备为你提供帮助! ---------- 第 1 轮结束 ---------- 你问:今天郑州天气适合做什么? AI 答:我无法直接查询今天的实时天气,所以没法告诉你郑州今天具体适不适合出门。不过你可以这样快速判断: **先看天气,再决定做什么:** | 天气情况 | 适合做的事 | |---------|-----------| | ☀️ 晴/多云,温度适宜 | 去公园(人民公园、碧沙岗)、黄河风景区、逛街、骑行 | | 🌧️ 下雨 | 博物馆(河南博物院)、商场、咖啡馆、看电影、在家休息 | | 🌫️ 雾霾/空气差 | 室内活动为主,减少户外运动 | | 🌡️ 高温(35℃+) | 室内场馆、傍晚再出门、注意防暑 | | 🥶 寒冷/大风 | 室内活动,出门注意保暖 | **建议你打开手机天气App看一眼:** - 温度多少? - 有没有雨/霾? - 风力大不大? **郑州比较值得去的地方(按天气选): ---------- 第 2 轮结束 ---------- ========== 对话结束 ==========看出两个门道没:
- 第二问末尾那句加粗的"郑州比较值得去的地方(按天气选):"下面没下文了——这不是 bug,是
.maxTokens(200)生效,输出到 200 token 被拦腰截断。生产环境按业务调大就行。 - 模型张口就说"我无法直接查询今天的实时天气"——它不联网、不知道今天天气,第 6 课给它装工具(Function Calling)才能查真实数据。
玩法二:同一个模型,只多了一段 systemPrompt。
========== LangChain4j 第 1 课:角色扮演(Java 面试官) ========== 面试官问:请问什么是 Java 里的 final 关键字? AI 答:你的回答可以从 final 修饰变量、方法、类这三个基本用法展开,这是很扎实的切入点。 那我追问一下:当 final 修饰一个引用类型变量时,比如 `final List<String> list = new ArrayList<>();`,为什么我们仍然可以往 list 里 add 元素?这跟 final 的语义到底是怎么对应的? ---------- 第 1 题结束 ---------- 面试官问:HashMap 的底层数据结构是什么? AI 答:点评:这是很基础的问题,答好了能体现你对集合框架的基本功。 追问:那你能具体说说,JDK 1.8 之后 HashMap 在什么条件下会把链表转成红黑树,又会在什么条件下退化回链表吗? ---------- 第 2 题结束 ---------- ========== 面试演练结束 ==========对比玩法一的"热情助手"口吻,这里它先点评一句、再追着问——SystemPrompt 的约束力实测摆在这。同一个模型、同一套代码,差别就是chat()前多拼了一段人设字符串。
排查提示:报 401 是 Key 环境变量没生效;402 是 DeepSeek 余额不足;连接超时多半是公司代理挡了 api.deepseek.com。
五、挑战题:改个参数,看看会怎样
- ⭐把温度拨到 0 和 1.5:
LlmConfig里.temperature(0.7)分别改成0.0和1.5,重编译,同一个问题"用 10 个字介绍你自己"连跑两次。你会发现 0.0 的回答几乎每次一字不差,1.5 的每次说法都不一样。答案就在LlmConfig.java那行 builder 上。 - ⭐把 maxTokens 那行删掉:注释掉
.maxTokens(200)重跑,再问一次"郑州天气适合做什么"——这回小作文能写完了。体会一下为什么生产环境反而要设上限(成本失控)。 - ⭐⭐加一道面试题:在
rolePlayDemo的questions数组里加第三题(比如"什么是 volatile?"),重跑roleplay,看面试官是不是还保持"先点评再追问"的派头。答案就在ChatDemoApplication.java那个数组里。
六、生产环境进阶:三个加分项
1. Key 只走环境变量,这是红线。整个工程 grep 一遍,代码、yml、日志里都不该出现真实 Key,只能出现System.getenv("DEEPSEEK_API_KEY")。Key 是钱袋子,贴进代码提交到 Git,等于把钱包照片发群里。
2. maxTokens 是天然成本刹车。它是单次输出上限,回答超长会被截断。演示用 200 控成本,生产按业务调大,同时也是防止模型"没完没了"的闸。
3. 切模型三件套,代码零改动。OpenAI =https://api.openai.com/v1+gpt-4o-mini;通义 =https://dashscope.aliyuncs.com/compatible-mode/v1+qwen-plus。三家都兼容 OpenAI 协议,改LLM_BASE_URL/LLM_MODEL加对应 Key 就能换供应商。
七、面试回答模板
面试官:一次 LLM 调用背后发生了什么?
一句话:你的代码调
chatModel.chat(问题),框架把消息打包成 JSON,HTTP POST 到模型服务,模型推理完返回 JSON,框架再解析成字符串还给你。展开讲:拼请求、发 HTTP、解析响应三段全是框架封装的,业务代码只管给问题、拿答案。DeepSeek 兼容 OpenAI 协议,OpenAI 客户端换个 baseUrl 就能连它。(指向本课第一节 + lesson-01 的ChatDemoApplication.java)
追问:为什么 DeepSeek 能直接用 OpenAI 的 Java 客户端?
因为它的接口路径、请求体、响应体格式和 OpenAI 完全一致,这叫"兼容 OpenAI 协议"。换模型只动三处:base-url、api-key、model 名,业务代码一行不改。(指向本课第 2 小节)
追问:LangChain4j 和 Spring AI 在模型装配上有什么区别?
Spring AI 靠 starter + yml 自动装配,配好
spring.ai.openai.*注入 ChatClient 就能用,像精装房拎包入住;LangChain4j 手写@Beanbuilder 出模型,每个参数看得见,更轻、厂商覆盖更广。纯 Spring 栈图省心选前者,要多模型灵活切选后者。(指向本课第 3 小节 + Spring AI 系列第 1 篇)
追问:temperature 和 maxTokens 各管什么?
temperature 管"创造性":0 严谨固定(适合抽取分类)、0.7 自然闲聊、1+ 发散易跑偏;maxTokens 管"输出上限",超了就截断,同时是成本刹车。(指向本课
LlmConfig.java)
八、总结表
| 坑 | 现象 | 解法 |
|---|---|---|
| JAVA_HOME 指向 JDK8 | mvn 跑在 Java 8 上报错 | 构建前$env:JAVA_HOME="C:\Program Files\Java\jdk-17" |
| 控制台中文乱码 | 中文输出变??? | 运行加-Dfile.encoding=UTF-8,文件 UTF-8 无 BOM |
| 401 Unauthorized | Key 环境变量没生效 | 检查$env:DEEPSEEK_API_KEY,设完重启终端 |
| 旧教程找不到类 | 编译报 cannot find symbol ChatLanguageModel | 1.21.0 已改名ChatModel,方法是chat() |
| 回答突然半截 | 不是 bug,是 maxTokens 截断 | 教学用 200 控成本,生产按需调大 |
| fat jar 报 no main manifest | java -jar起不来 | pom 里配start-class,别删 |
九、关于这个系列
本文是「Java 后端实战精通营」系列第 1 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。
👉LangChain4j 实战精通营(10 课):https://gitee.com/j67mk2/langchain4j-journey
- 本文对应源码位置:
lesson-01/(纯控制台工程,内含手写模型 Bean 的LlmConfig+ 交互/角色扮演两个玩法的ChatDemoApplication)
系列文章一览(按发布顺序):
| 篇 | 主题 |
|---|---|
| 1 | LangChain4j 第一个对话程序:手写 LLM 调用链路,看清模型怎么"开口" |
| 2 | LangChain4j 提示词工程:模板 + few-shot + 温度,提示词从写死到复用 |
| 3 | LangChain4j 结构化输出:让模型吐出 Java 对象,JSON 不再手写解析 |
| 4 | LangChain4j 对话记忆:让模型记住聊过什么,三种方式实测对比 |
| 5 | LangChain4j 流式输出:打字机效果 + SSE,回答不再干等三秒 |
| 6 | LangChain4j 工具调用:Function Calling 让模型动手查数据,多工具连调实测 |
| 7 | LangChain4j 向量检索:文本怎么变成可搜索的坐标 |
| 8 | LangChain4j RAG 流水线:手写问答全流程,给 AI 开卷考试 |
| 9 | LangChain4j Agent:多步推理自主调用,放手让"实习生"自己安排工作 |
| 10 | LangChain4j 企业智能客服:RAG + 工具 + 记忆 + 流式 + 兜底,验收 5 项全过 |
下一篇预告:《LangChain4j 提示词工程:模板 + few-shot + 温度,提示词从写死到复用》——本课你还在
systemPrompt + "\n问题:" + question这种手拼字符串,下一课把它升级成带{{占位符}}的正规模板,再塞几个示例(few-shot)让模型照猫画虎。
跑完有任何报错,把终端输出发评论区,一起排查。
标签建议:LangChain4j、大模型、DeepSeek
摘要建议(≤256 字):调大模型非得配 yml 黑魔法?LangChain4j 这门课反着来——手写 @Bean 把模型对象造出来,base-url / api-key / temperature 每个参数都摊在你眼前。本文带你用最小控制台工程跑通第一个对话程序,逐行拆解一次 LLM 调用的 HTTP 链路,实测对比 SystemPrompt 怎么让同一个模型从"热情助手"秒变"Java 面试官",附 3 道改参数挑战题与面试回答模板,源码在 lesson-01 可 clone 直接跑。