Skip to content

Spring AI · 提示词的使用

前两篇讲清了 Spring AI 的定位(springAi-介绍)和 Model 抽象(springAi-Model核心概念)。这一篇把最贴近日常业务的一块拆开:提示词(Prompt)到底怎么写。围绕 ChatClient 的链式 API,讲透四件事——普通提示词、系统提示词、用户提示词、以及提示词模板(PromptTemplate),并给出对应的 Controller 落地示例。

1. 先厘清概念:Prompt 与消息角色

在 Spring AI(以及几乎所有对齐 OpenAI 协议的框架)里,一次对话的输入不是"一段字符串",而是一个有序的消息(Message)列表。每条消息带一个角色(role)

角色Spring AI 类型语义谁来写
systemSystemMessage设定模型的身份、风格、约束("你是谁、要怎么答")开发者
userUserMessage用户当前这轮真正的问题 / 指令终端用户
assistantAssistantMessage模型历史回复(多轮上下文)模型
toolToolResponseMessage工具调用的返回结果程序

Prompt 就是这些 Message 的容器:

java
// Prompt 本质上就是 List<Message> + 可选的 ChatOptions
Prompt prompt = new Prompt(List.of(
    new SystemMessage("你是一个严谨的中文技术助手"),
    new UserMessage("解释一下什么是幂等")
));

所以本文标题里的"普通提示词 / 系统提示词 / 用户提示词",本质上都是往这个消息列表里塞不同角色的消息。下面逐个看在 ChatClient 里怎么写。

2. 普通提示词:直接把一段话丢给模型

最朴素的用法——不区分角色,直接把用户输入交给模型:

java
@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,它会被当作系统级的整体指令注入:

java
@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(...)

java
this.chatClient.prompt()
        .system("你是一个客服,回答保持礼貌、简洁,不确定时主动说明")
        .user(userInput)
        .call()
        .content();

4.2 全局默认:defaultSystem(...)

如果整个 ChatClient 都用同一个人设,在构建时设一次即可,后续每次调用自动带上:

java
public ChatController(ChatClient.Builder builder) {
    this.chatClient = builder
            .defaultSystem("你是一个严谨的中文技术写作助手。")
            .build();
}

优先级:单次调用的 .system(...)覆盖/追加defaultSystem(...) 之上,用来做"全局人设 + 局部微调"。

5. 用户提示词:.user(...) 承载真正的问题

用户提示词就是终端用户这一轮的输入。除了直接传字符串,.user(...) 还支持 lambda 形式,用来在用户消息里嵌入占位符和参数(下一节的模板能力就靠它):

java
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

java
// 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,让系统消息和用户消息各自参数化:

java
@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 读取外部文件:

java
@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",提示词的组织方式一字不用改

java
@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核心概念.mdChatClient / ChatModel 的完整抽象)
  • 提示词工程通用方法:5-AI手记/3-提示词工程/提示词工程 / 提示词优化 / 提示词框架
  • Spring AI Reference — Prompts:https://docs.spring.io/spring-ai/reference/api/prompt.html