☰
LangChain4j 第一个对话程序:手写 LLM 调用链路,看清模型怎么“开口”
2026/10/22 7:21:39 网站建设 项目流程

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 轮结束 ---------- ========== 对话结束 ==========

看出两个门道没:

  1. 第二问末尾那句加粗的"郑州比较值得去的地方(按天气选):"下面没下文了——这不是 bug,是.maxTokens(200)生效,输出到 200 token 被拦腰截断。生产环境按业务调大就行。
  2. 模型张口就说"我无法直接查询今天的实时天气"——它不联网、不知道今天天气,第 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。

五、挑战题:改个参数,看看会怎样

  1. ⭐把温度拨到 0 和 1.5:LlmConfig里.temperature(0.7)分别改成0.0和1.5,重编译,同一个问题"用 10 个字介绍你自己"连跑两次。你会发现 0.0 的回答几乎每次一字不差,1.5 的每次说法都不一样。答案就在LlmConfig.java那行 builder 上。
  2. ⭐把 maxTokens 那行删掉:注释掉.maxTokens(200)重跑,再问一次"郑州天气适合做什么"——这回小作文能写完了。体会一下为什么生产环境反而要设上限(成本失控)。
  3. ⭐⭐加一道面试题:在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 指向 JDK8mvn 跑在 Java 8 上报错构建前$env:JAVA_HOME="C:\Program Files\Java\jdk-17"
控制台中文乱码中文输出变???运行加-Dfile.encoding=UTF-8,文件 UTF-8 无 BOM
401 UnauthorizedKey 环境变量没生效检查$env:DEEPSEEK_API_KEY,设完重启终端
旧教程找不到类编译报 cannot find symbol ChatLanguageModel1.21.0 已改名ChatModel,方法是chat()
回答突然半截不是 bug,是 maxTokens 截断教学用 200 控成本,生产按需调大
fat jar 报 no main manifestjava -jar起不来pom 里配start-class,别删

九、关于这个系列

本文是「Java 后端实战精通营」系列第 1 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。

👉LangChain4j 实战精通营(10 课):https://gitee.com/j67mk2/langchain4j-journey

  • 本文对应源码位置:lesson-01/(纯控制台工程,内含手写模型 Bean 的LlmConfig+ 交互/角色扮演两个玩法的ChatDemoApplication)

系列文章一览(按发布顺序):

篇主题
1LangChain4j 第一个对话程序:手写 LLM 调用链路,看清模型怎么"开口"
2LangChain4j 提示词工程:模板 + few-shot + 温度,提示词从写死到复用
3LangChain4j 结构化输出:让模型吐出 Java 对象,JSON 不再手写解析
4LangChain4j 对话记忆:让模型记住聊过什么,三种方式实测对比
5LangChain4j 流式输出:打字机效果 + SSE,回答不再干等三秒
6LangChain4j 工具调用:Function Calling 让模型动手查数据,多工具连调实测
7LangChain4j 向量检索:文本怎么变成可搜索的坐标
8LangChain4j RAG 流水线:手写问答全流程,给 AI 开卷考试
9LangChain4j Agent:多步推理自主调用,放手让"实习生"自己安排工作
10LangChain4j 企业智能客服: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 直接跑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询