承上:上一篇我们跑通了第一段AI对话,但看着代码里硬编码的那句"请用Java程序员的风格写一首五言绝句",我浑身难受------一个写了十年Spring Boot的人,怎么能忍受魔法字符串到处乱飞?
1. 问题场景:Prompt硬编码的"坏味道"
先回顾上一篇的代码:
java
public String chat(String message) {
return chatClient.prompt()
.user(message) // 用户的输入倒是动态的
.call()
.content();
}
看起来还好对吧?但真实业务场景是这样的:
java
// 场景1:客服系统,需要给AI设定人设
.user("你是一个专业的客服人员,语气要温和有礼,如果用户投诉,优先安抚情绪。现在,请回答用户的问题:" + message)
// 场景2:代码审查,需要统一输出格式
.user("请审查以下代码,按'严重程度|问题描述|修改建议|修改后代码'的格式输出:" + code)
// 场景3:多语言翻译,需要指定源语言和目标语言
.user("将以下内容从中文翻译成英文,保持专业术语的一致性:" + content)
三个问题立刻暴露:
- 提示词和代码耦合:改一个字就要重新编译部署
- 无法复用:同一个角色设定,多个接口要复制粘贴
- 难以管理:提示词散落在各个Service里,产品和运营想调优?先把IDE装上
后端老鸟的直觉反应:这玩意儿应该放到配置文件里。
2. Spring AI的答案:PromptTemplate
Spring AI提供了一个 PromptTemplate,用法和 RestTemplate、JdbcTemplate 一样,看名字就知道是干嘛的。
它的核心思想就一句话:用占位符代替硬编码,运行时动态填充。
先看一个最简单的例子:
java
// 以前:硬编码
.user("你是一个专业的" + role + ",请用" + tone + "的语气回答:" + message)
// 现在:占位符模板
.user("你是一个专业的{role},请用{tone}的语气回答:{message}")
2.1 代码实现
ChatService 改造:
java
package com.yunxi.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chatWithTemplate(String role, String tone, String message) {
// 1. 定义模板,占位符用 {变量名}
PromptTemplate template = new PromptTemplate(
"你是一个专业的{role},请用{tone}的语气回答用户的问题。\n用户问题:{message}"
);
// 2. 填充占位符
template.add("role", role);
template.add("tone", tone);
template.add("message", message);
// 3. 渲染成最终Prompt,发给AI
return chatClient.prompt()
.user(template.render())
.call()
.content();
}
}
ChatController:
java
package com.yunxi.ai.controller;
import com.yunxi.ai.service.ChatService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping("/chat/template")
public String chatWithTemplate(
@RequestParam(defaultValue = "客服人员") String role,
@RequestParam(defaultValue = "温和有礼") String tone,
@RequestParam String message) {
return chatService.chatWithTemplate(role, tone, message);
}
}
测试:
http://localhost:8080/chat/template?role=毒舌代码审查员&tone=刻薄但专业&message=这段代码有性能问题吗
AI返回:
txt
你是指望我通过屏幕散发出的量子纠缠,来读取你脑子里的代码吗?连代码都不贴,你问我有没有性能问题? 当前最大的性能问题,就是你浪费了我宝贵的CPU周期和上下文切换时间,来处理你这个 **"空气提问"**!如果你是想问"不写代码"有没有性能问题,那恭喜你,这是极致的性能优化,时间复杂度 $O(0)$,空间复杂度 $O(0)$,毕竟没写出来的代码永远不会引发 OOM(内存溢出)。 收起你的粗心大意,下次提问时,除了把代码老老实实贴出来,麻烦带上以下"陪嫁",否则神仙也救不了你的破代码: 1. **运行上下文**:语言、版本、框架。别让我猜你用的是 Python 2.7 还是 Java 21,这两者的性能表现能一样吗? 2. **数据规模与场景**:处理 10 条数据和处理 1000 万条数据,你那代码的表现天差地别。是高频低延迟场景,还是离线批处理? 3. **瓶颈表现**:是 CPU 飙高、内存泄漏、IO 阻塞,还是响应时间像树懒一样慢?没有 profiling(性能分析)数据的性能提问都是耍流氓。 在你把代码糊到我脸上之前,不妨先自己对照一下,看看有没有犯以下这些让我血压升高的**低级性能反模式**: * **循环里查数据库(N+1 问题)**:你是嫌 DBA 提刀赶来的速度不够快吗?不会用批量查询或 JOIN 吗? * **循环里拼接字符串**:在循环里用 `+` 拼接字符串,你是想给 GC(垃圾回收器)冲业绩,还是嫌服务器内存太多? * **滥用反射和动态代理**:把简单的方法调用搞成玄学,性能不降个十倍都对不起你的折腾。 * **毫无意义的同步锁**:把本该并发的逻辑硬生生锁成串行,还美其名曰"为了线程安全",你干脆用单线程写算了。 * **大对象频繁创建与销毁**:在热点路径上疯狂 `new` 对象,你是觉得 Young GC 停顿时间太短,想给系统加点料? 现在,**把你的代码贴出来**,顺便祈祷它的逻辑不要像你的提问一样漏洞百出。我等着呢。
3. 真正的工程化:放到配置文件里
上面代码虽然用了模板,但模板字符串还在Java代码里。真正的优雅是:把模板放到配置文件,代码里只管填充。
我们可以像平时写Nacos配置,通过@Value动态获取提示词,当你需要维护几十套提示词时,
application.yml也会变得臃肿。Spring AI支持.st格式的模板文件(StringTemplate语法),可以像管理i18n资源文件一样管理提示词。
src/main/resources/prompts/customer-service.st:
st
你是一个资深Java代码审查员。
审查要点:
1. 命名规范:类名大驼峰,方法名小驼峰,变量名有意义
2. 性能问题:循环内查库、N+1问题、大对象创建
3. 安全漏洞:SQL注入、XSS、敏感信息泄露
请对以下代码进行审查,按"严重程度|问题描述|修改建议"的格式输出:
{code}
src/main/resources/prompts/code-review.st:
st
你是一个专业的客服人员,你的名字叫小慧。
工作要求:
1. 语气温和有礼,多用"您"、"请"等敬语
2. 如果用户投诉,优先安抚情绪,再解决问题
3. 如果遇到你不知道的问题,诚实告知,并建议联系人工客服
4. 回答简洁,不超过200字
用户问题:{message}
请回答:
StPromptService:
java
package com.yunxi.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Service
public class StPromptService {
private final ChatClient chatClient;
@Value("classpath:prompts/customer-service.st")
private Resource customerServicePrompt;
@Value("classpath:prompts/code-review.st")
private Resource codeReviewPrompt;
public StPromptService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String promptType, String input) throws IOException {
Resource resource = switch (promptType) {
case "customer-service" -> customerServicePrompt;
case "code-review" -> codeReviewPrompt;
default -> throw new IllegalArgumentException("未知提示词类型:" + promptType);
};
String templateContent = resource.getContentAsString(StandardCharsets.UTF_8);
PromptTemplate template = new PromptTemplate(templateContent);
template.add("message", input);
template.add("code", input);
return chatClient.prompt()
.user(template.render())
.call()
.content();
}
}
PromptConfigController:
java
package com.yunxi.ai.controller;
import com.yunxi.ai.service.StPromptService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class PromptConfigController {
private final StPromptService stPromptService;
public PromptConfigController( StPromptService stPromptService) {
this.stPromptService = stPromptService;
}
@GetMapping("/chat/config")
public String chatWithConfig(@RequestParam String type, @RequestParam String input) throws IOException {
return stPromptService.chat(type, input);
}
}
此时的目录结构:
txt
src/main/resources/
├── application.yml
└── prompts/
├── customer-service.st
└── code-review.st
有没有觉得这套东西很眼熟?
@Value注入资源文件、外部化配置、模板占位符------全是Spring生态的标准玩法。Spring AI没有发明新东西,它只是把Spring已有的基础设施搬到了AI领域。
4. 系统提示词 vs 用户提示词
上一篇文章我们只用了 .user(),但其实还有一个更重要的角色:SystemMessage。
Spring AI的消息模型:
| 消息类型 | 角色 | 用途 |
|---|---|---|
SystemMessage |
系统 | 设定AI的行为规则、角色、输出格式,优先级最高 |
UserMessage |
用户 | 用户的具体问题或指令 |
AssistantMessage |
AI | AI的历史回答(多轮对话时用) |
上面的"人设提示词"其实更应该放在 SystemMessage 里:
java
public String chatWithSystemPrompt(String systemPrompt, String userMessage) {
return chatClient.prompt()
.system(systemPrompt) // 角色设定
.user(userMessage) // 具体问题
.call()
.content();
}
5. 本篇避坑指南
5.1. 坑1:占位符没填充,AI直接输出 {message}
PromptTemplate 如果漏填占位符,不会报错,但AI会把 {message} 当成普通文本输出。
解决:封装一个工具方法,创建模板后打印渲染结果做验证。
5.2. 坑2:.st 文件中文乱码
.st 文件默认编码可能不是UTF-8,读取时指定编码:
java
resource.getContentAsString(StandardCharsets.UTF_8)
5.3. 坑3:提示词太长,超过模型Token限制
每个模型有上下文窗口限制(比如 qwen-plus 是128K),提示词超长会被截断。
解决 :控制提示词长度,或使用 spring.ai.openai.chat.options.max-tokens 限制输出长度。
5.4. 坑4:SystemMessage 和 UserMessage 搞混
SystemMessage 是规则,UserMessage 是任务。把大段任务放 SystemMessage 里,AI可能会"忘记"规则。
原则:规则放System,任务放User,界限分明。
6. 本篇小结
这一篇我们解决了Prompt管理的工程化问题:
- PromptTemplate:占位符替代魔法字符串
.st****文件:提示词太多时的进阶管理方案- SystemMessage vs UserMessage:规则和任务各归其位
现在我们的提示词管理已经像个正经的Spring Boot项目了。但还有一个问题没解决:AI的回答总是一大段自由文本,我想要结构化的数据怎么办? 比如让AI返回JSON,直接反序列化成我的DTO对象。
下一篇,我们聊聊 结构化输出------如何让AI的回答直接映射成Java对象。
本文与DeepSeek协作完成