Spring AI · 提示词的使用
前两篇讲清了 Spring AI 的定位(
springAi-介绍)和 Model 抽象(springAi-Model核心概念)。这一篇把最贴近日常业务的一块拆开:提示词(Prompt)到底怎么写。围绕ChatClient的链式 API,讲透四件事——普通提示词、系统提示词、用户提示词、以及提示词模板(PromptTemplate),并给出对应的 Controller 落地示例。
1. 先厘清概念:Prompt 与消息角色
在 Spring AI(以及几乎所有对齐 OpenAI 协议的框架)里,一次对话的输入不是"一段字符串",而是一个有序的消息(Message)列表。每条消息带一个角色(role):
| 角色 | Spring AI 类型 | 语义 | 谁来写 |
|---|---|---|---|
| system | SystemMessage | 设定模型的身份、风格、约束("你是谁、要怎么答") | 开发者 |
| user | UserMessage | 用户当前这轮真正的问题 / 指令 | 终端用户 |
| assistant | AssistantMessage | 模型历史回复(多轮上下文) | 模型 |
| tool | ToolResponseMessage | 工具调用的返回结果 | 程序 |
而 Prompt 就是这些 Message 的容器:
// Prompt 本质上就是 List<Message> + 可选的 ChatOptions
Prompt prompt = new Prompt(List.of(
new SystemMessage("你是一个严谨的中文技术助手"),
new UserMessage("解释一下什么是幂等")
));所以本文标题里的"普通提示词 / 系统提示词 / 用户提示词",本质上都是往这个消息列表里塞不同角色的消息。下面逐个看在 ChatClient 里怎么写。
2. 普通提示词:直接把一段话丢给模型
最朴素的用法——不区分角色,直接把用户输入交给模型:
@GetMapping("/normal")
public String normal(@RequestParam String userInput) {
return this.chatClient.prompt() // 开启一次调用
.user(userInput) // 只有一条 user 消息
.call() // 同步调用
.content(); // 取纯文本结果
}要点:
chatClient.prompt()起一个链式构建器,.user(...)追加一条用户消息,.call().content()拿到字符串结果。- 这里没有系统提示词,模型行为完全由它自己的默认对齐决定——适合"随便问问",但不适合有明确角色/风格要求的业务。
3. 普通提示词的另一种形态:new Prompt(...)
ChatClient.prompt() 也可以直接接收一个构造好的 Prompt 对象。当你传入一个字符串型的 Prompt,它会被当作系统级的整体指令注入:
@GetMapping("/prompt")
public String prompt(@RequestParam String userInput) {
return this.chatClient.prompt(
// 直接传 String 也可以:等价于 new Prompt("...")
new Prompt("你是一个智能助手,可以帮忙整理材料、写代码、写文章等")
)
.user(userInput)
.call()
.content();
}对比 §2:
.prompt(new Prompt("..."))给模型设定了"人设 / 能力边界"。.user(userInput)才是用户真正这轮的问题。- 这两者叠加,就构成了"带背景约束的一次问答"。
记忆点:
chatClient.prompt()无参 = 从零构建;chatClient.prompt(new Prompt(x))= 用x作为起始(整体/系统级)指令,再往后.user()/.system()追加。
4. 系统提示词:.system(...) 设定角色与规则
系统提示词是开发者对模型的"出厂设置":身份、语气、格式约束、安全边界。它不随用户输入变化,是最该被认真打磨的一段。
在 ChatClient 里有两种写法。
4.1 单次调用级别:.system(...)
this.chatClient.prompt()
.system("你是一个客服,回答保持礼貌、简洁,不确定时主动说明")
.user(userInput)
.call()
.content();4.2 全局默认:defaultSystem(...)
如果整个 ChatClient 都用同一个人设,在构建时设一次即可,后续每次调用自动带上:
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个严谨的中文技术写作助手。")
.build();
}优先级:单次调用的
.system(...)会覆盖/追加在defaultSystem(...)之上,用来做"全局人设 + 局部微调"。
5. 用户提示词:.user(...) 承载真正的问题
用户提示词就是终端用户这一轮的输入。除了直接传字符串,.user(...) 还支持 lambda 形式,用来在用户消息里嵌入占位符和参数(下一节的模板能力就靠它):
this.chatClient.prompt()
.user(u -> u.text("用户问题:{question}")
.param("question", userInput))
.call()
.content();.text("...{question}...")定义带占位符的文本;.param("question", userInput)把占位符替换成实际值;.system(...)也有同样的 lambda 形态,可以对系统消息做参数化。
6. 提示词模板:PromptTemplate
当提示词里有动态变量(用户等级、语言、日期、检索到的上下文……),手工拼字符串既易错又难维护。Spring AI 提供 PromptTemplate 做模板渲染。
6.1 独立使用 PromptTemplate
// 1. 定义带占位符的模板(默认用 {} 作为占位符语法)
PromptTemplate promptTemplate = new PromptTemplate(
"当前级别:{level},用户问题:{question}");
// 2. 传入参数
Map<String, Object> params = Map.of(
"level", "VIP",
"question", userInput
);
// 3. 渲染成一个 Prompt
Prompt prompt = promptTemplate.create(params);
// 4. 交给模型
String answer = this.chatClient.prompt(prompt).call().content();create(Map) 把占位符替换后生成 Prompt,可以直接进 ChatClient。
6.2 在链式 API 里内联模板(推荐)
更常见的落地写法:不单独 new PromptTemplate,而是在 .system(...) / .user(...) 的 lambda 里直接写模板 + param,让系统消息和用户消息各自参数化:
@GetMapping("/userTemplate")
public String userTemplate(@RequestParam String userInput) {
return this.chatClient.prompt(new Prompt("你是一个客服"))
// 系统消息带上元数据(客服等级)
.system(s -> s.text("当前客服等级:{level}").param("level", "VIP"))
// 用户消息带上参数化的问题
.user(u -> u.text("用户问题:{question}").param("question", userInput))
.call()
.content();
}这样一条调用里同时用到了:整体指令(new Prompt("你是一个客服"))+ 参数化系统消息 + 参数化用户消息,是模板能力最实用的组合形态。
6.3 从外部文件加载模板
提示词一旦复杂,就该从代码里挪出去,做版本管理。PromptTemplate 可配合 Spring 的 Resource 读取外部文件:
@Value("classpath:/prompts/customer-service.st")
private Resource systemPrompt;
// ...
PromptTemplate template = new PromptTemplate(systemPrompt);
Prompt prompt = template.create(Map.of("level", "VIP"));好处:提示词与代码解耦,可以独立评审、A/B、灰度不同版本,而不必改 Java 代码重新编译。
7. 流式返回:提示词写法完全一致
上面所有例子把 .call() 换成 .stream(),就从"等全部生成完再返回"变成"边生成边吐 token",提示词的组织方式一字不用改:
@GetMapping(value = "/streamPrompt", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamPrompt(@RequestParam String userInput) {
return this.chatClient.prompt(new Prompt("你是一个客服"))
.system(s -> s.text("当前客服等级:{level}").param("level", "VIP"))
.user(u -> u.text("用户问题:{question}").param("question", userInput))
.stream() // 唯一的区别:call() -> stream()
.content();
}返回 Flux<String>,配合 Spring WebFlux + SSE(text/event-stream)即可做打字机效果。流式的更多细节见 springAi-Model核心概念 §9。
8. 四种提示词写法速查
| 你想做的事 | 写法 | 角色 |
|---|---|---|
| 随手问一句 | .prompt().user(x) | user |
| 给一段整体/背景指令 | .prompt(new Prompt("...")) | system 级 |
| 设定模型人设/规则(单次) | .system("...") | system |
| 设定模型人设/规则(全局) | builder.defaultSystem("...") | system |
| 承载用户当轮问题 | .user(userInput) | user |
| 消息里插动态变量 | .user(u -> u.text("{q}").param("q", x)) | user/system |
| 复用/版本化模板 | new PromptTemplate(text/Resource).create(params) | 任意 |
| 流式输出 | 把 .call() 换成 .stream() | 不影响提示词 |
9. 落地建议
- 系统提示词是投入产出比最高的一段:模型答得好不好,八成取决于 system 写得清不清楚。优先把角色、格式、边界写死在
defaultSystem或外部模板里。 - 用户输入永远走
.user(...),不要把用户内容拼进系统提示词——既是安全(防提示词注入),也是职责清晰。 - 有动态变量就上
PromptTemplate/.param(...),不要用String.format或+手拼,占位符写法统一、可读、可复用。 - 提示词复杂到一定程度就外置成文件,用
Resource加载,让提示词能独立于代码演进。
10. 进一步阅读
- 本目录上一篇:
springAi-Model核心概念.md(ChatClient/ChatModel的完整抽象) - 提示词工程通用方法:
5-AI手记/3-提示词工程/(提示词工程/提示词优化/提示词框架) - Spring AI Reference — Prompts:
https://docs.spring.io/spring-ai/reference/api/prompt.html
