LangChain4j 提示词工程:模板 + few-shot + 温度,提示词从写死到复用
作者:鱼宵 | LangChain4j实战精通营 · 第 2 篇
上一课我们玩角色扮演,提示词是这么拼的:chatModel.chat(systemPrompt + "\n问题:" + question)。当时能跑,但你有没有意识到一个隐患——规则写死在代码里,问题一换就得重新拼字符串,变量一多就乱成一团浆糊。更要命的是,同一段翻译规则,我今天中译英、明天英译中,难道要写两遍?
这一课就是把这种"临时工式拼字符串"升级成正规军。三个家伙一起上:PromptTemplate 把提示词做成带占位符的模板、消息体系把人设有序排好、few-shot 给两个示例让模型照猫画虎、温度旋钮决定它严谨还是发散。代码全在仓库lesson-02/,clone 下来跑一遍,你会发现同一个模板换三组变量就能翻三个方向——末尾那两道挑战题,改完参数跑一次才知道温度旋钮有多玄。
一、核心原理:提示词为什么不能写死
一句话:提示词 = 固定规则 + 动态变量。规则该写一次复用 N 次,变量运行时再填。
1. PromptTemplate:一张印好的填空题答题卡
以前硬拼字符串,等于每次都拿白纸重画一遍题面。PromptTemplate反过来:你提前把提示词写成一段带{{占位符}}的模板,运行时再把变量填进去。
打个比方:老师(你)提前印好答题卡——“请把{{sourceLang}}的{{text}}翻译成{{targetLang}}”,考试(调模型)时只需要在空格里填具体值。规则印一次,N 次考试复用。
PromptTemplatetemplate=PromptTemplate.from("把{{text}}翻译成{{language}}");Promptprompt=template.apply(Map.of("text","你好","language","英文"));// prompt.text() 就是填好后的完整提示词注意是双大括号{{text}}——写成单大括号{text}占位符不会替换,这是本课第一个必踩坑(第七节表里有)。
2. 消息体系:模型眼里不是"一段话",是"一条消息列表"
这是本课最值钱的认知升级。很多人以为发给模型的是一整段拼接文本,其实是一个带角色标记的消息列表(List):
| 消息类型 | 谁的话 | 干嘛用 |
|---|---|---|
| SystemMessage | 系统(你给的人设) | 排在最前,优先级最高,就是上节课的 SystemPrompt |
| UserMessage | 用户 | 你问的话 |
| AiMessage | 模型 | 它上次回的话 |
类比你翻微信聊天记录:是一条条"对方说—你说—对方说"的消息流,不是一整坨文字。模型也按列表顺序读,并且知道每条是谁说的。没有角色标记,它就分不清"哪句是规则、哪句是要处理的数据"。
3. few-shot:开卷考试先给标准答案
few-shot(少样本示例):正式提问之前,先在消息列表里塞 2~3 组"用户问 → 模型答"的示例对,让模型照着示例的格式和风格回答。
你不是跟实习生讲一百字"该怎么翻译",而是直接甩给他 3 张"原文→译文"的样表,说"照这个样子做"。实习生照样表抄格式,往往比读规则理解得更准。什么时候特别有用?你要的输出格式很特殊、或者要模仿某种语气——给示例比写规则管用。简单任务不给示例(zero-shot)就够,示例是拿来"纠偏"的。
4. temperature:答题风格的旋钮
temperature是模型生成每个词时的"随机性旋钮",而且它是建模型时定死的参数,不是每次调用时传的。
0.0:学霸闭卷默写,每次答案几乎一样——适合信息抽取、分类、SQL 生成;0.7:自然闲聊,本课默认值;1.5:诗人即兴发挥,每次说法都不同,有灵感也容易跑题。
面试常被一起追问top_p:简单记,temperature 调"随机程度",top_p 调"只从概率最高的前百分之几的词里选词"。
二、动手:跑通三个 demo
环境与上节课完全一样:JDK 17、
DEEPSEEK_API_KEY已配好。
第 1 步:编译。
cd langchain4j-journey\lesson-02$env:JAVA_HOME="C:\Program Files\Java\jdk-17"mvn clean install-DskipTests第 2 步:默认三个 demo 依次全跑。
java-Dfile.encoding=UTF-8-jar target\lesson-02-1.0.0.jar第 3 步:单独跑某个(可选)。
java-Dfile.encoding=UTF-8-jar target\lesson-02-1.0.0.jar template# 只跑模板java-Dfile.encoding=UTF-8-jar target\lesson-02-1.0.0.jar fewshot# 只跑 few-shotjava-Dfile.encoding=UTF-8-jar target\lesson-02-1.0.0.jar temp# 只跑温度对比控制台中文容易乱码,想把输出存下来对照,用cmd /c "... > out.txt"重定向到 UTF-8 文件再打开看,最稳。
三、关键代码:一个主类讲透三件事
本课模型配置LlmConfig和上节课一模一样(三件套 +maxTokens(300)+temperature(0.7)),不重复贴。重点全在PromptDemoApplication这一个类里。
packagecom.langchain4j.lesson02;importdev.langchain4j.data.message.AiMessage;importdev.langchain4j.data.message.ChatMessage;importdev.langchain4j.data.message.SystemMessage;importdev.langchain4j.data.message.UserMessage;importdev.langchain4j.model.chat.ChatModel;importdev.langchain4j.model.chat.response.ChatResponse;importdev.langchain4j.model.input.Prompt;importdev.langchain4j.model.input.PromptTemplate;importdev.langchain4j.model.openai.OpenAiChatModel;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importjava.util.ArrayList;importjava.util.List;importjava.util.Map;/** * 第 2 课主程序:三个 demo 用启动参数切换,默认全跑。 * template —— PromptTemplate 模板变量(填空题答题卡) * fewshot —— 消息体系 + few-shot(给示例让模型模仿) * temp —— temperature 0.0 vs 1.5 对比 */@SpringBootApplicationpublicclassPromptDemoApplicationimplementsCommandLineRunner{privatefinalChatModelchatModel;// 注入接口(1.21.0 叫 ChatModel,不是旧名)publicPromptDemoApplication(ChatModelchatModel){this.chatModel=chatModel;}publicstaticvoidmain(String[]args){SpringApplication.run(PromptDemoApplication.class,args);}@Overridepublicvoidrun(String...args){Stringmode=args.length>0?args[0]:"all";switch(mode){case"template"->templateDemo();case"fewshot"->fewShotDemo();case"temp"->temperatureDemo();default->{templateDemo();fewShotDemo();temperatureDemo();}}System.out.println("========== LangChain4j 第 2 课全部演示结束 ==========");}/** Demo 一:模板变量。规则印一次,换两组变量翻两个方向。 */privatevoidtemplateDemo(){System.out.println("========== Demo 1:PromptTemplate 模板变量(翻译助手) ==========");// 模板:三个占位符,运行时才填值PromptTemplatetemplate=PromptTemplate.from("你是专业的{{sourceLang}}→{{targetLang}}翻译助手。规则:只输出译文,不要任何解释。\n"+"待翻译文本:{{text}}");// 第一组变量:中译英Promptprompt1=template.apply(Map.of("sourceLang","中文","targetLang","英文","text","今天天气真不错"));System.out.println("【填好后的提示词(第 1 次)】\n"+prompt1.text());System.out.println("【模型回答】"+chatModel.chat(prompt1.text()));// 第二组变量:同一个模板,换成英译中——模板复用,只换变量Promptprompt2=template.apply(Map.of("sourceLang","英文","targetLang","中文","text","Good morning, have a nice day"));System.out.println("【填好后的提示词(第 2 次)】\n"+prompt2.text());System.out.println("【模型回答】"+chatModel.chat(prompt2.text()));}/** Demo 二:消息列表 + few-shot。3 组示例之后甩一道新题。 */privatevoidfewShotDemo(){System.out.println("========== Demo 2:消息体系 + few-shot(中英对照示例) ==========");List<ChatMessage>messages=newArrayList<>();// 第 1 条:系统消息,整个会话的基调messages.add(SystemMessage.from("你是中英互译助手。严格模仿后面示例的格式和风格,只输出译文。"));// 3 组示例对:用户句 + 标准答案句,必须成对出现messages.add(UserMessage.from("你好,世界"));messages.add(AiMessage.from("Hello, world"));messages.add(UserMessage.from("我喜欢编程"));messages.add(AiMessage.from("I love programming"));messages.add(UserMessage.from("今天天气真不错"));messages.add(AiMessage.from("The weather is really nice today"));// 正式考题:模型会照前面三对示例的风格翻这句messages.add(UserMessage.from("我喜欢写 Java"));// chat(List) 返回 ChatResponse,要从 aiMessage().text() 取文本(和 chat(String) 返回 String 不一样)ChatResponseresponse=chatModel.chat(messages);System.out.println("【3 组示例之后的新问题】我喜欢写 Java");System.out.println("【模型模仿示例的翻译结果】"+response.aiMessage().text());}/** Demo 三:温度对比。温度是建模型时定的,所以临时建两个模型。 */privatevoidtemperatureDemo(){System.out.println("========== Demo 3:temperature 温度对比 ==========");Stringquestion="用三个词形容秋天";ChatModelcoldModel=buildModel(0.0);// 严谨ChatModelhotModel=buildModel(1.5);// 发散System.out.println("【问题】"+question);System.out.println("【temperature=0.0(严谨)】"+coldModel.chat(question));System.out.println("【temperature=1.5(发散)】"+hotModel.chat(question));}/** 临时建指定温度的模型,配置和 LlmConfig 一致,只换 temperature。 */privateOpenAiChatModelbuildModel(doubletemperature){returnOpenAiChatModel.builder().baseUrl(System.getenv().getOrDefault("LLM_BASE_URL","https://api.deepseek.com")).apiKey(System.getenv("DEEPSEEK_API_KEY")).modelName(System.getenv().getOrDefault("LLM_MODEL","deepseek-chat")).maxTokens(200).temperature(temperature)// ← 唯一变化的参数.build();}}这段代码有两个新手最容易混的点,标出来:
chat(String)直接返回String;chat(List<ChatMessage>)返回ChatResponse,要.aiMessage().text()才拿到文本。两个重载,别混。- 温度不是调用参数,是 builder 参数——所以温度对比实验得
buildModel临时建两个模型。
四、实测输出:三组实验一镜到底
以下为 2026-10-05 本机真实运行(DeepSeek 实测,已省略启动 banner)。
========== Demo 1:PromptTemplate 模板变量(翻译助手) ========== 【填好后的提示词(第 1 次)】 你是专业的中文→英文翻译助手。规则:只输出译文,不要任何解释。 待翻译文本:今天天气真不错 【模型回答】The weather is really nice today. 【填好后的提示词(第 2 次)】 你是专业的英文→中文翻译助手。规则:只输出译文,不要任何解释。 待翻译文本:Good morning, have a nice day 【模型回答】早上好,祝你今天愉快 ========== Demo 2:消息体系 + few-shot(中英对照示例) ========== 【3 组示例之后的新问题】我喜欢写 Java 【模型模仿示例的翻译结果】I love writing Java ========== Demo 3:temperature 温度对比 ========== 【问题】用三个词形容秋天 【temperature=0.0(严谨)】**丰饶、萧瑟、澄明** - **丰饶**:秋天是收获的季节,稻谷金黄、果实累累,大地回馈着一年辛勤的劳作。 - **萧瑟**:秋风起,落叶飘零,草木渐枯,带着一丝清冷与寂寥的诗意。 - **澄明**:秋日天空高远湛蓝,空气清爽通透,万物轮廓分明,令人心境开阔。 【temperature=1.5(发散)】**丰饶、萧瑟、澄明** 也可以用另一组:**金黄、凉爽、寂寥** ========== LangChain4j 第 2 课全部演示结束 ==========对照着看,三个结论一次看清:
- 模板复用:同一行
template.apply(...),换两组变量就完成中→英、英→中两个方向,规则部分(“只输出译文”)一个字没动。这就是不写死的好处。 - few-shot 的威力:新问题"我喜欢写 Java"翻成
I love writing Java——你对照示例看:首字母大写 I、句式 “I love …”、结尾不加句号,全是在模仿前面 3 组示例的风格。给示例比写规则更能把格式统一,眼见为实。 - 温度差异:0.0 给出规整、结构固定的答案;1.5 在同一组词之外主动补了一句"也可以用另一组:金黄、凉爽、寂寥"——严谨 vs 发散,肉眼可辨。
排查提示:
PromptTemplate占位符没被替换?检查你是不是写成了单大括号{text},LangChain4j 要双大括号{{text}}。few-shot 风格不对?检查示例对是不是 UserMessage / AiMessage 成对、顺序对不对。
五、挑战题:改参数,看看会怎样
- ⭐加第三组变量:在
templateDemo里复用同一个模板,再加一组"中→日"(targetLang填"日文"),重跑观察模板是不是真的一个字没改就翻了新方向。答案就在PromptDemoApplication.java的templateDemo里照抄一段。 - ⭐⭐温度连跑三次:把
temperatureDemo的 0.0 模型对同一问题连跑 3 次,记录输出是不是每次一字不差;再把 1.5 的连跑 3 次对比。答案在buildModel那个temperature参数上,跑出来你才信"0.0 几乎确定"。 - ⭐⭐把示例顺序对调:故意把 few-shot 里一对
UserMessage/AiMessage的顺序反过来,重跑看翻译变成什么样——体会"消息顺序为什么重要"。答案在fewShotDemo那几行messages.add。
六、生产环境进阶:三个加分项
1. 抽取类任务把温度压到 0。信息抽取、分类、SQL 生成要的是确定事实,不要文采——温度 0.7 会让模型"自由发挥"写出不合规结果。下一课结构化输出就把温度压到了 0.0。
2. 提示词模板进配置/数据库。演示里模板写死在代码里;生产环境规则经常运营同学改,把模板抽到配置中心或数据库,改规则不用发版。
3. few-shot 示例要挑有代表性的。示例不是越多越好,2~3 组覆盖典型情况即可;示例本身错了,模型会跟着错得很自信。
七、面试回答模板
面试官:PromptTemplate 相比硬拼字符串好在哪?
一句话:提示词拆成固定模板 + 动态变量,规则写一次复用 N 次。展开:
{{var}}双大括号占位,apply(Map)填值,同一个模板能 apply 出多份不同变量的提示词,便于统一维护和测试。硬拼字符串变量一多就乱,还没法复用规则。(指向本课第一节 + lesson-02 的templateDemo)
追问:为什么说 LLM 对话是"消息列表"而不是一段长文本?
因为每条消息带角色:SystemMessage(人设,优先级最高)、UserMessage(用户)、AiMessage(模型)交替排列。有了角色标记,模型才分得清哪句是规则、哪句是数据。第 4 课的"对话记忆"本质上就是在维护这个列表。(指向本课第 2 小节)
追问:few-shot 和 system prompt 怎么分工?什么时候 few-shot 更好用?
SystemMessage 写"人设和大规则",few-shot 给"具体格式和风格的样例"。格式要求特殊(固定 JSON、固定句式)或要模仿某种语气时,给示例比写一百字规则管用——模型照样表抄格式更准。简单任务 zero-shot 就够。(指向本课第 3 小节 +
fewShotDemo实测)
追问:temperature 和 top_p 的区别?
temperature 调"随机程度":0 死板(抽取/分类)、0.7 自然、1+ 发散易跑偏;top_p 是 nucleus sampling,按概率累积只从最高的前百分之几的词里选。它是建模型时的参数,不是每次调用传的。(指向本课第 4 小节)
八、总结表
| 坑 | 现象 | 解法 |
|---|---|---|
占位符写单大括号{text} | 填值不生效,原样输出 | LangChain4j 要双大括号{{text}},调试时打印prompt.text()看哪没填上 |
chat(List)返回值取错 | 编译报错/拿到对象不是 String | 传 List 返回ChatResponse,要.aiMessage().text();传 String 才直接拿 String |
| few-shot 示例不成对 | 模型模仿不到风格 | UserMessage / AiMessage 必须问→答成对出现,顺序别乱 |
| 换温度要新建模型 | 以为是chat()参数 | temperature 是 builder 参数,对比实验要buildModel新建两个模型 |
| 接口改名找不到类 | 编译报 cannot find symbol | 1.21.0 是ChatModel.chat(),旧教程的ChatLanguageModel自动翻译 |
九、关于这个系列
本文是「Java 后端实战精通营」系列第 2 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。
👉LangChain4j 实战精通营(10 课):https://gitee.com/j67mk2/langchain4j-journey
- 本文对应源码位置:
lesson-02/(控制台工程,主类PromptDemoApplication内含模板 / few-shot / 温度三个 demo)
系列文章一览(按发布顺序):
| 篇 | 主题 |
|---|---|
| 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 结构化输出:让模型吐出 Java 对象,JSON 不再手写解析》——这两课你拿到的回答都是"一大段自由文本",但业务要的是能塞进数据库的 Java 对象。下一课讲怎么让模型稳定吐 JSON,以及最优雅的
AiServices直接返回 JavaBean。
跑完有任何报错,把终端输出发评论区,一起排查。
标签建议:LangChain4j、提示词工程、PromptTemplate
摘要建议(≤256 字):还在用str + var硬拼提示词?LangChain4j 的 PromptTemplate 把规则写成带{{占位符}}的答题卡,运行时填变量,同一模板翻三个方向。本文用一个主类跑通模板变量、消息体系(System/User/AiMessage)、few-shot 示例模仿、temperature 0.0 vs 1.5 对比三个实测,看清"给示例比写规则更能把格式统一",附 3 道改参数挑战题与面试回答模板,源码在 lesson-02 可 clone 直接跑。