一、最直接的方式:.system()
在每次调用时传入系统提示词:
java
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public String chat(String message) {
return chatClient.prompt()
// ← 每次请求都执行
.system("你是一位专业的气象助手,说话简洁,只回答与天气相关的问题。")
.user(message)
.tools(weatherTools)
.call()
.content();
}
}
特点:
-
每次调用都要写一遍,适合不同请求用不同角色的场景。
-
可以和
.user()、.tools()自由组合。 -
系统提示词会作为
system角色的消息发给模型,模型会据此调整回答风格。
二、全局默认方式:defaultSystem()
如果你希望这个 ChatClient 的所有调用都用同一个角色,在构建时设置:
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
// ← 只执行一次,全局共享
.defaultSystem("你是一位专业的气象助手,说话简洁,只回答与天气相关的问题。")
.build();
}
}
之后在 ChatService 里就可以省略 .system():
java
public String chat(String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools)
.call()
.content();
}
特点:
-
适合"这个 ChatClient 就是干这个的"场景。
-
如果某次调用想覆盖,仍然可以在
.prompt()后再次调用.system(),就近原则,后者覆盖前者。
两种方式的区别:
.system() 可以传变量,defaultSystem() 不行
场景:根据用户身份切换角色
假设你的系统有普通用户和 VIP 用户,VIP 用户希望模型回答更详细:
java
public String chat(String message, boolean isVip) {
String role = isVip
? "你是一位资深气象专家,回答要专业、详细,包含数据来源"
: "你是一位简洁的气象助手,回答不超过两句话";
return chatClient.prompt()
.system(role) // ← 每次请求根据参数决定角色
.user(message)
.call()
.content();
}
这就是 .system() 的意义:角色内容由代码在运行时决定,而不是写死在配置里。
场景:多语言
java
public String chat(String message, String lang) {
String role = switch (lang) {
case "en" -> "You are a professional weather assistant.";
case "ja" -> "あなたはプロの天気アシスタントです。";
default -> "你是一位专业的气象助手。";
};
return chatClient.prompt()
.system(role)
.user(message)
.call()
.content();
}
场景:从数据库读取角色配置
java
public String chat(String message, Long userId) {
User user = userService.findById(userId);
String role = user.getAiRole(); // 从数据库读,每个用户可能不同
return chatClient.prompt()
.system(role)
.user(message)
.call()
.content();
}
这三种场景,defaultSystem() 都做不到,因为它只在构建时执行一次,拿不到运行时的请求参数。
那 defaultSystem() 什么时候用?
当你确定这个 ChatClient 的角色永远不变时!!!
因为:
-
业务代码更干净,不用每次都写
.system() -
角色定义集中在一处,好维护
三、用模板参数:defaultSystem(Consumer<SystemSpec>)
如果系统提示词里有动态部分,可以用占位符 + 参数:
java
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem(s -> s
.text("你是一位专业的{role}助手,服务对象是{audience},说话风格:{style}。")
.param("role", "气象")
.param("audience", "普通用户")
.param("style", "简洁"))
.build();
}
之后每次调用时可以覆盖参数:
java
chatClient.prompt()
.system(s -> s
.text("你是一位专业的{role}助手,服务对象是{audience},说话风格:{style}。")
.param("role", "气象")
.param("audience", "农民")
.param("style", "详细"))
.user(message)
.call()
.content();
特点:
-
适合提示词模板化的场景,比如多语言、多角色切换。
-
参数用
{name}占位,.param()填值。
四、从文件加载提示词(推荐生产环境用)
系统提示词往往很长(几百字),写在 Java 代码里不优雅。Spring AI 支持从 classpath 加载:
1. 在 src/main/resources/prompts/ 下建文件 weather-system.st:
java
你是一位专业的气象助手。
要求:
1. 只回答与天气相关的问题
2. 回答简洁,不超过 3 句话
3. 如果用户问与天气无关的问题,礼貌拒绝
当前日期:{currentDate}
2. 在代码中引用:
java
@Value("classpath:/prompts/weather-system.st")
private Resource systemPrompt;
public String chat(String message) {
return chatClient.prompt()
.system(s -> s
.text(systemPrompt)
.param("currentDate", LocalDate.now().toString()))
.user(message)
.call()
.content();
}
特点:
-
提示词和代码分离,改提示词不用重新编译。
-
适合长提示词、需要版本管理的场景。
-
.st是 Spring AI 推荐的扩展名(String Template),其实.txt也行。
五、system 消息的底层长什么样?
无论用哪种方式,最终发给模型的消息列表大致是:
java
[
{ "role": "system", "content": "你是一位专业的气象助手..." },
{ "role": "user", "content": "北京天气怎么样?" }
]
模型看到 system 消息后,会把它当作最高优先级的行为准则。
注意:不同模型的"听话程度"不一样。Qwen2.5 对 system prompt 的遵循度不错,但如果发现模型不按角色说话,可以试试:
把关键约束放在 system prompt 的开头或结尾(中间容易被忽略)
用更强的措辞,比如"必须""严禁"
减少 system prompt 长度,越短越聚焦
六、多种方式对比
| 方式 | 作用范围 | 适用场景 |
|---|---|---|
.system() |
单次调用 | 不同请求不同角色 |
defaultSystem() |
整个 ChatClient | 固定角色 |
defaultSystem(Consumer) |
整个 ChatClient,带模板 | 模板化提示词 |
| Resource 文件 | 单次或全局 | 长提示词、版本管理 |
| system + user 组合 | 单次调用 | 复杂对话 |
七、实战:完整示例
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient weatherChatClient(
ChatClient.Builder builder,
@Value("classpath:/prompts/weather-system.st") Resource systemPrompt) {
return builder
.defaultSystem(s -> s
.text(systemPrompt)
.param("currentDate", LocalDate.now().toString()))
.build();
}
}
java
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatClient weatherChatClient;
private final WeatherTools weatherTools;
public String chat(String message) {
return weatherChatClient.prompt()
.user(message)
.tools(weatherTools)
.call()
.content();
}
}
八、一个容易踩的坑
system prompt 和 tool description 要协调,不能冲突。
比如你的 system prompt 写"你只回答天气问题",但用户问"帮我订机票",模型可能会因为角色限制而拒绝调用 bookFlight 工具。
正确做法是让 system prompt 描述角色和风格 ,工具 description 描述能力,两者不重叠:
java
// system prompt
你是一位专业的旅行助手,说话简洁礼貌。
// weather tool description
查询指定城市的当前天气情况
// flight tool description
根据出发地、目的地和日期预订航班
模型会自然地把"助手角色"和"可用工具"结合起来。
总结
-
单次调用用
.system() -
全局固定用
defaultSystem() -
长提示词放 Resource 文件,用
.param()填动态值 -
system prompt 管"角色和风格",tool description 管"能力"