Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用

Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用

作者:鱼宵 | Spring AI 实战精通营 · 第 2 篇

上周拿第 1 课那个翻译接口改需求:产品说"同一句话,要能翻商务版、口语版、文艺版"。我的第一反应是再写两个接口,把 system() 里的提示词复制三遍、改两个字。写到第三个风格时我自己都烦了------提示词骨架一个字没动,就"风格"两个字不一样,凭什么复制三遍?

这就是第 2 课要收拾的事:把写死的提示词改造成带 {变量} 的模板,骨架写死一次、参数按次填;顺手再玩个 few-shot------不讲规则,直接甩 3 个"输入→输出"示例,让模型自己照着样例说话。代码全在仓库 lesson-02/ 目录里,clone 下来照着命令重跑一遍,十分钟你手里就有两个翻译接口。

一、核心原理:提示词不是写死的字符串,是带坑的表格

1. 一次请求 = 一张对话记录表

先把底层概念掰清楚:Message(消息) 是发给模型的最小单位,每条消息就两个属性------谁说的(role)+ 说了什么(content)。模型不是"读一整段话",而是捧着一本对话记录本,一行一行往下看:

角色 内容
system "你是专业中英翻译助手,只输出译文" ← 员工守则
user "把【今天天气真不错】翻译成商务正式风格的英文" ← 这一次的问题
assistant "The weather is quite pleasant today." ← 模型回话,下一轮进表

system() 必须放在 user() 前面------顺序就是记录本上的先后,模型按行读,写反了它就先答题再读守则。

2. 参数化模板:合同为什么不用每次重写

公司的劳动合同不会来一个人重写一份,都是一份模板写死"甲方:、乙方:、岗位:____",新人入职就把三个空填上。提示词模板一个思路:

text 复制代码
模板:  "把下面这句话翻译成英文,译文风格要【{style}】:{question}"
填值:  style=商务正式,  question=今天天气真不错
成品:  "把下面这句话翻译成英文,译文风格要【商务正式】:今天天气真不错"

对照两种写法,一眼看出差别:

写法 代码 问题
错误(字符串拼接) "风格是" + style + ",句子是" + question 又臭又长,引号转义全靠自己擦屁股
正确(模板+填坑) .user(u -> u.text("...{style}...").param("style", style)) 骨架复用,按名字填值

这里埋了本课最大的一个坑:直接 .user("...{question}...") 传字符串,占位符不会被替换 ------模板引擎只在 lambda 小括号模式(u -> u.text(...).param(...))里才启动。你老老实实把模板写进 text()、把值交给 param(),坑才会被填上。两边名字还得逐字符一致,{Question} 和 "question" 大小写对不上,坑就原样留在句子里发给模型,模型当场懵。

3. few-shot:教新人不写 SOP,甩 3 份优秀聊天记录

few-shot(少样本提示):不写一堆规则,直接在 system 里塞 2~3 个"输入→输出"对照示例,模型自己归纳规律并模仿。给 0 个示例叫 zero-shot(第 1 课就是),给几个示例就是 few-shot。

对比 zero-shot(讲规则) few-shot(给样例)
写法 system 里写"你是客服翻译,语气简短客气" system 里塞 3 条"用户:xxx / 翻译:yyy"
适合 规则说得清的任务 风格类、格式类任务(你说不清"客服口吻"是几个字)
成本 输入短 示例常驻每次请求,token 略涨

类比带新人:教客服新人,与其写 500 字 SOP,不如甩 3 份优秀聊天记录让他照着聊------他自己就悟到"要短、要客气"。大模型吃这套,第四节实测给你看证据。

二、动手:十分钟跑通两个翻译接口

环境:Windows + JDK 17 + Maven 3.9+,会 @RestController 就行。

第 1 步:30 秒检查环境。

powershell 复制代码
java -version                       # 期望 True
[bool][Environment]::GetEnvironmentVariable('DEEPSEEK_API_KEY') # 期望 True
Get-NetTCPConnection -LocalPort 8094 -State Listen -ErrorAction SilentlyContinue  # 无输出=端口空闲

第 2 步:编译 + 启动。

powershell 复制代码
cd spring-ai-journey\lesson-02
$env:JAVA_HOME="C:\Program Files\Java\jdk-17"   # Maven 必须跑在 JDK 17 上
mvn clean install -DskipTests         # 结尾看到 BUILD SUCCESS
mvn spring-boot:run                  # 看到 Tomcat started on port 8094 即成功

第 3 步:调两个接口(中文参数先 URL 编码)。

powershell 复制代码
[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(936)   # 防控制台中文乱码

# 接口一:参数化模板,question + style 两个变量
$q = [uri]::EscapeDataString('今天天气真不错')
$s = [uri]::EscapeDataString('商务正式')
Invoke-RestMethod "http://localhost:8094/translate?question=$q&style=$s"

# 接口二:few-shot,只传 question,风格靠 system 里的示例带出来
$q2 = [uri]::EscapeDataString('我想买两件衬衫')
Invoke-RestMethod "http://localhost:8094/translate-fewshot?question=$q2"

浏览器直接开 http://localhost:8094/translate?question=你好&style=商务正式 也行,浏览器会自动编码中文。

三、关键代码:两个接口,逐行拆解

工程还是标准 Spring Boot 项目,真正要看的就两处:yml 配置和那个控制器。

第一段:application.yml------只改端口和 token 上限。

yaml 复制代码
server:
  port: 8094                            # 端口按课程分配表:spring-ai 系列 lesson-02 = 8094
spring:
  ai:
    openai:
      base-url: ${LLM_BASE_URL:https://api.deepseek.com}   # 环境变量优先,默认 DeepSeek
      api-key: ${DEEPSEEK_API_KEY}                          # Key 只从环境变量读,文件里永远没有明文
      chat:
        options:
          model: ${LLM_MODEL:deepseek-chat}                 # 模型名,默认 deepseek-chat
          max-tokens: 400                                    # 翻译输出比一问一答略长,教学控成本
          temperature: 0.7                                   # 温度 0.7:翻译要稳但不死板

配置套路和第 1 课一模一样------这就是 yml 自动装配的好处,加新课不改配置习惯,就动了 port 和 max-tokens 两个数。

第二段:ChatClientTemplateDemo.java------本课主菜,完整可运行。

java 复制代码
package com.springai.lesson02;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 第 2 课核心:翻译助手控制器。
 * 两个接口:
 *   /translate         ------ 参数化模板:模板里写 {question}/{style} 占位,运行时 .param() 填值
 *   /translate-fewshot ------ few-shot:system 里塞 3 个"中英对照"示例,让模型模仿风格
 */
@RestController
public class ChatClientTemplateDemo {

    /** ChatClient 实例:和第 1 课一样,Builder 由 starter 自动装配,零手写模型代码 */
    private final ChatClient chatClient;

    public ChatClientTemplateDemo(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 接口一:参数化模板翻译。
     * GET /translate?question=今天天气真不错&style=商务正式
     * 重点看 .user(u -> u.text("...{question}...").param(...)) 这段:
     *   text() 里放模板,占位符 {question}/{style} 不会原样发给模型,填完才发;
     *   param() 按名字填坑,名字必须和模板里的占位符逐字符一致。
     */
    @GetMapping("/translate")
    public String translate(@RequestParam("question") String question,
                            @RequestParam(value = "style", defaultValue = "日常口语") String style) {
        return chatClient.prompt()
                // system:人设------只给译文,别啰嗦
                .system("你是专业中英翻译助手。只输出英文译文本身,不要解释、不要加引号、不要多余的话。")
                // user:带变量的模板,运行时填 question 和 style
                .user(u -> u.text("把下面这句话翻译成英文,译文风格要【{style}】:\n{question}")
                        .param("style", style)
                        .param("question", question))
                .call()
                .content();
    }

    /**
     * 接口二:few-shot(给示例让模型模仿)。
     * GET /translate-fewshot?question=我想买两件衬衫
     * system 里写死 3 个"中文→英文"对照示例(客服场景的简短客气口吻),
     * user 只放真正要翻的那句话,尾巴故意留个"翻译:"让模型顺着示例续写。
     */
    @GetMapping("/translate-fewshot")
    public String translateFewShot(@RequestParam("question") String question) {
        return chatClient.prompt()
                // system:人设 + 3 个示例(示例就是模型的"模仿对象")
                .system("你是外贸客服的中英翻译。下面是翻译示例,请严格模仿示例的简短客气语气,只输出英文译文:\n"
                        + "示例1:\n用户:早上好\n翻译:Good morning!\n"
                        + "示例2:\n用户:这件商品包邮吗?\n翻译:Is shipping free for this item?\n"
                        + "示例3:\n用户:麻烦帮我退一下货,谢谢\n翻译:I'd like to return this, please. Thank you.")
                // user:只留 {question} 一个坑,尾巴的"翻译:"引导模型续写
                .user(u -> u.text("用户:{question}\n翻译:")
                        .param("question", question))
                .call()
                .content();
    }
}

这段代码讲了三件事:u -> u.text(...) 进了 user 的小括号模式(第 1 课是直接传字符串,本课要填变量所以得进 lambda);{style}、{question} 是占位符不是发给模型的原文;defaultValue = "日常口语" 让 style 不传也能跑。

第三段:Lesson02Application.java------启动类,三行。

java 复制代码
package com.springai.lesson02;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 第 2 课启动类。启动后访问:
 *   GET http://localhost:8094/translate?question=今天天气真不错&style=商务正式
 *   GET http://localhost:8094/translate-fewshot?question=我想买两件衬衫
 */
@SpringBootApplication
public class Lesson02Application {

    public static void main(String[] args) {
        SpringApplication.run(Lesson02Application.class, args);
    }
}

四、实测输出:同一个模板,换个填法就换个风格

以下是 2026-10-05 本机真实运行(DeepSeek,端口 8094,HTTP 200)。先调 /translate,question 不变,style 换两次:

text 复制代码
# style=商务正式
HTTP 200
The weather is quite pleasant today.

# style=古代诗人李白风格
HTTP 200
The weather today is truly fine.

模板里的 {style} 真的被填进去了------商务版用了 "quite pleasant" 这种书面词,李白版明显凝练。风格有影响但不会天翻地覆(temperature 0.7 下模型偏稳妥),面试时别吹成"换个词就脱胎换骨"。

再调 /translate-fewshot,只传 question,看模型怎么模仿示例:

text 复制代码
# question=我想买两件衬衫
HTTP 200
I'd like to buy two shirts.

# question=请问什么时候发货?
HTTP 200
When will this be shipped?

看第一句------I'd like to buy two shirts. 用了 I'd like to... 开头,这正是 system 里示例 3(I'd like to return this, please)的句式。你没写"要用 I'd like to 开头"这条规则,模型自己从三个示例里学去了。这就是 few-shot 的力量:风格类任务,甩样例比讲规则准。

排查提示:回答里如果残留 {question} 字样,说明 param 的 key 和模板占位名对不上(大小写/拼写),两边逐字符核对即可。

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

  1. ⭐ 不传 style :浏览器里只开 http://localhost:8094/translate?question=你好,style 参数故意不给------看模型用了什么风格回答。答案就在源码 ChatClientTemplateDemo.java 的方法参数上,跑出来才知道默认值真的生效。
  2. ⭐⭐ 不用小括号模式 :把 /translate 里的 .user(u -> u.text("...").param(...)) 改回 .user("把下面这句话翻译成英文,译文风格要【商务正式】:今天天气真不错")------再进一步,试试 .user("...{question}...") 带占位符但不加 param()。看 {question} 会不会被替换。这题的答案藏在踩坑节里,跑一遍你这辈子都忘不了。
  3. ⭐⭐ 示例换成法语:把 system 里 3 个示例改成"中译法"(用户:早上好 / 翻译:Bonjour! 这种),user 还是中文------验证模型会不会照猫画虎吐出法语。看你改完示例后它"学歪"还是"学对"。

六、生产环境进阶:三个加分项

1. 提示词和代码分离。 模板骨架抽到 application.yml 或常量类里,Controller 只负责填变量。提示词改起来不用动代码、不用重新编译,产品提需求改一段 yml 就行。

2. few-shot 示例算 token 账。 示例是常驻输入,每次调用都陪着你的问题一起发给模型------示例太多既加钱又可能稀释重点。2~3 个高质量示例性价比最高,别贪多。

3. 占位符命名即契约。 {style} 在模板里写一次、param("style", ...) 在代码里填一次,两边靠字符串名字绑死。建议占位符名和业务参数名保持一致,加个常量类集中管理,别散落在 Controller 里手写。

七、面试回答模板

面试官:一次 ChatClient 请求,到底发给模型几段东西?

一句话:不是一整段字符串,是一条消息列表(Message List)。展开说:每条消息 = 角色(system/user/assistant)+ 内容;system() 是员工守则放在最前,user() 是这一轮问题,call() 把整本记录本发给模型。顺序就是记录本上的先后,system 必须在 user 前面。(指向本课第一节)
追问:提示词模板的变量怎么填?为什么我直接 user("...{x}...") 不替换?

一句话:填变量必须走 lambda 小括号模式 user(u -> u.text("...{x}...").param("x", v))。展开说:text() 里写模板、param() 按名字填坑;直接给字符串不走模板引擎,占位符原样发出去。param 的 key 和占位名逐字符一致,大小写不同就填不上。(指向本课第三节)
追问:few-shot 和 zero-shot 怎么选?

一句话:规则说得清用 zero-shot,风格/格式类说不清规则的任务用 few-shot 给 2~3 个示例。展开说:示例是模型的"模仿对象",必须和目标任务同分布;示例常驻输入会涨 token,2~3 个性价比最高。本课实测:给三个客服示例后,模型自己学会了用 I'd like to... 开头。(指向本课第四节)

八、总结表

坑 现象 解法
占位名和 param 对不上 句子里残留 {question} 原样发给模型 两边名字逐字符一致,区分大小写
直接 user(字符串带占位符) 变量不替换,坑留在句子里 走 lambda 小括号模式 text()+param()
few-shot 示例跑题 模型学歪成别的风格 示例必须和目标任务同分布
示例堆太多 输入 token 涨钱、重点被稀释 控制在 2~3 个高质量示例
中文 URL 参数乱码 浏览器/curl 直接带中文乱码 [uri]::EscapeDataString() 编码
端口占用 Port 8094 already in use Get-NetTCPConnection -LocalPort 8094 查占用

九、关于这个系列

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

👉 Spring AI 实战精通营(10 课):https://gitee.com/j67mk2/spring-ai-journey

  • 本文对应源码位置 :lesson-02/(内含 ChatClientTemplateDemo 双接口------参数化模板 + few-shot,配 application.yml 端口 8094)

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

篇 主题
1 Spring AI 初体验:配好 yml 就能聊,ChatClient 四步链式调用
2 Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用
3 Spring AI 结构化输出:entity() 把模型回答解析成 JavaBean,别再手撕 JSON
4 Spring AI 工具调用:@Tool 让大模型自己查订单查库存
5 Spring AI 流式输出:Flux + SSE 打字机,回答不再干等三秒
6 Spring AI 多模态:给大模型一双眼睛,图片它也能看懂
7 Spring AI 向量检索:本地 ONNX 嵌入,文本秒变坐标,知识库零成本起步
8 Spring AI RAG 问答助手:回答带引用,AI 不再睁眼说瞎话
9 Spring AI Advisor 编排:记忆 + 工具 + RAG 三合一,一个接口全搞定
10 Spring AI 企业智能客服:RAG + 工具 + 记忆 + 流式 + 兜底,十课收官

下一篇预告:《Spring AI 结构化输出:entity() 把模型回答解析成 JavaBean,别再手撕 JSON》------本课翻出来的是一坨英文译文,人看得懂,但程序没法拿它算钱;下一课上主菜,.entity(Order.class) 一句话让模型直接吐一个 Java 对象,商品清单、数量、总价自动装进 Order。

跑完有任何报错,把终端输出发评论区,一起排查。


标签建议 :SpringAI、提示词工程、few-shot

摘要建议(≤256 字):第 1 课的提示词是写死在代码里的字符串,改个风格就得复制三遍。本文把它改造成带 {变量} 的参数化模板------text() 写模板、param() 按名填值,骨架复用;再塞 3 个输入输出示例玩 few-shot,实测模型自己学会了 I'd like to 开头。逐行拆解 lambda 小括号模式这个新手坑,附 3 道挑战题与面试回答模板,源码在 gitee lesson-02 可 clone 直接跑。

相关推荐
蜗牛互联网1 小时前
Python消费Responses SSE事件:增量文本、超时与取消
java·开发语言·人工智能·后端·python
架构师那点事儿1 小时前
大模型如何私有化部署到生产环境
人工智能·架构·llm
HeyAI人工智能1 小时前
官网内容优化 vs 企业级 RAG:AI 搜索时代,企业到底该先做什么?
人工智能·aigc
wanderist.1 小时前
线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数
java·数据结构·算法
黑妹天下第一乖2 小时前
第09讲 · 多媒体与音频 SDK:硬件编解码与端侧语音
人工智能·嵌入式硬件·深度学习·机器人·音视频·iot
jeffsonfu2 小时前
扩散模型(Diffusion Models)崛起:GANs之后的下一个顶流?
人工智能
我不会起名字3222 小时前
Redis 入门(三):Spring Boot 整合 Redis,RedisTemplate 实战与序列化乱码
redis·spring·工具类·jedis·lettuce
老李的安全笔记本2 小时前
Agent 工具调用为什么越改越差:两种方向的失败,Prompt 修不了
人工智能
土豆3592 小时前
那个叫 fix bug 的提交,改了 140 万行
人工智能