第三章:Prompt 管理——别再拼字符串了
前两章我们把 Hello World 跑通了,代码长这样:
chatClient.prompt()
.user("帮我写一段冒泡排序的代码")
.call()
.content();
如果只是写个 Demo,这足够了。但上了生产,Prompt 这样写就是在给自己埋雷。 这一章教你从"拼字符串"进化到"像管 SQL 一样管 Prompt"。
3.1 告别原始人写法
先说问题。你在项目里八成见过这种代码:
// ❌ 噩梦级 Prompt 管理
String prompt = "你是一个" + role + ",请根据以下" + contextType +
"回答问题。注意:" + rules1 + "," + rules2 + "。" +
"如果用户的问题涉及" + sensitiveTopic + ",请委婉拒绝。" +
"问题:" + userQuestion;
String answer = chatClient.prompt().user(prompt).call().content();
这段代码什么问题?
正确方式:PromptTemplate
// ✅ 用模板
String template = """
你是一个{role},请根据以下{contextType}回答问题。
注意:{rules1},{rules2}。
如果用户的问题涉及{sensitiveTopic},请委婉拒绝。
问题:{question}
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
// 填空
Map<String, Object> variables = Map.of(
"role", "资深Java工程师",
"contextType", "代码规范",
"rules1", "只输出代码不解释",
"rules2", "使用Java 17语法",
"sensitiveTopic", "系统漏洞",
"question", userQuestion
);
Prompt prompt = promptTemplate.create(variables);
String answer = chatClient.prompt(prompt).call().content();
就多了 PromptTemplate 这一层,但带来的好处立竿见影:
- Prompt 文本和变量分离:模板是模板,数据是数据
- 可读性飙升:一眼能看懂 Prompt 长什么样,不会被 + 号淹没
- 变量校验:如果少传了 {question},create() 直接抛异常,而不是运行时生成一段半截子 Prompt 发给 AI
3.2 把 Prompt 放进资源文件——像管 SQL 一样管 Prompt
PromptTemplate 解决了拼接问题,但模板本身还是嵌在 Java 代码里。对于一个正经项目,更好的做法是把 Prompt 放到资源文件里。
目录结构
src/main/resources/
└── prompts/
├── code-review.st # 代码审查的 Prompt
├── bug-analysis.st # Bug 分析的 Prompt
└── meeting-summary.st # 会议纪要的 Prompt
.st 是 Spring AI 默认的 Prompt 模板后缀(StringTemplate),你也可以用其他模板引擎。
code-review.st 内容
你是一个{role},负责审查以下代码。
审查重点:
1. 潜在的空指针异常
2. 不合理的异常处理
3. 性能问题(循环中的数据库调用、不必要的大对象创建)
4. 并发安全问题
请按以下格式输出:
– 严重程度:[高/中/低]
– 问题行号:[行号]
– 问题描述:[描述]
– 修改建议:[建议代码]
要审查的代码:
```java
{code}
### Java 代码中加载
```java
@Service
public class CodeReviewService {
private final ChatClient chatClient;
private final Resource promptResource;
public CodeReviewService(ChatClient.Builder builder,
// 直接从 classpath 加载 Prompt 文件
@Value("classpath:/prompts/code-review.st")
Resource promptResource) {
this.chatClient = builder.build();
this.promptResource = promptResource;
}
public ReviewResult review(String code) {
// 读取模板文件
PromptTemplate template = new PromptTemplate(promptResource);
// 填参数
Prompt prompt = template.create(Map.of(
"role", "资深Java后端开发工程师",
"code", code
));
return chatClient.prompt(prompt)
.call()
.entity(ReviewResult.class); // 直接映射到对象,下章细讲
}
}
这样做的好处
| 修改成本 | 改代码 → 编译 → 部署 | 改文件 → 重启(或热加载) |
| 版本控制 | 混在 Java diff 里看不清 | Git 里独立追踪每次 Prompt 变更 |
| 协作门槛 | 只有开发能改 | 产品、运营也能提 PR 改 Prompt |
| 复用性 | 靠复制粘贴 | 一个模板多处引用 |
| 国际化 | 几乎不可能 | 加个 _zh、_en 后缀就行 |
实战经验:我们团队现在的新规则——Prompt 超过 3 行就必须放资源文件,不允许写在 Java 代码里。
3.3 System Message / User Message / Assistant Message 的区别
前面用的 .user() 其实只是冰山一角。跟大模型对话时,消息分三种角色:
┌──────────────────────────────────────────────────────────┐
│ 一次完整的对话请求 │
├──────────────────────────────────────────────────────────┤
│ System Message ←"你是资深工程师,只回答Java问题" │
│ 设定 AI 的人设、行为规则、输出格式 │
├──────────────────────────────────────────────────────────┤
│ User Message ←"Java里HashMap和ConcurrentHashMap │
│ 有什么区别?" │
│ 用户真正想问的问题 │
├──────────────────────────────────────────────────────────┤
│ Assistant Message ←"上次你说要学并发,建议先看这个…" │
│ 多轮对话时,之前的 AI 回答 │
├──────────────────────────────────────────────────────────┤
│ User Message ←"好的,那具体怎么用?" │
│ 新一轮用户问题 │
└──────────────────────────────────────────────────────────┘
三句话讲清楚:
- System Message:给 AI “定规矩"的,AI 会把它当铁律。比如"你只回答 Java 问题”、“用中文回答”、“输出 JSON 格式”。
- User Message:用户问的问题。这是对话的主体。
- Assistant Message:AI 之前的回答。多轮对话时,把历史回答喂回去,AI 才知道"刚才聊到哪了"。
代码怎么写
String answer = chatClient.prompt()
.system(systemPrompt) // 定规矩
.user(userPrompt) // 问问题
.advisors(new MessageChatMemoryAdvisor(...)) // 自动管理历史消息
.call()
.content();
系统消息的实战套路
大部分人用 System Message 只写一句"你是一个XX专家"。太浪费了。我总结了一个"三段式"模板:
你是一个{role}。
## 工作规则
1. {rule1}
2. {rule2}
3. {rule3}
## 输出格式
请严格按以下JSON格式输出,不要加任何额外内容:
{
"field1": "…",
"field2": "…"
}
## 禁止事项
– 不要输出HTML标签
– 不要反问用户
– 遇到不确定的事情,直接说"我不确定",不要编造
三个关键点:
实战经验:禁止事项里一定要加"不要编造"。没有这一条,AI 在不知道答案的时候会瞎编一段看起来很像那么回事的内容——这就是"幻觉"。
3.4 实战经验:Prompt 版本化 & A/B 测试
前面解决了怎么写 Prompt 的问题。接下来是更进阶的话题:怎么知道你改的 Prompt 比之前好?
问题场景
你上线了一个"客服自动回复"功能,Prompt v1 效果还行。有一天你灵光一闪觉得改一下措辞会让回答更温柔,改了之后就上线了。
三天后投诉量翻了 3 倍。但你根本不知道——因为看不出是哪个改动导致的。
解决方案:Prompt 版本化管理
最简单的版本化:文件名带版本号 + Git 打 Tag。
src/main/resources/prompts/
└── customer-service/
├── v1/
│ └── reply.st
└── v2/
└── reply.st
代码中通过配置决定用哪个版本:
app:
prompt:
customer-service-version: v2 # 改这里就切版本
@Service
public class CustomerServiceBot {
private final String promptPath;
public CustomerServiceBot(
@Value("${app.prompt.customer-service-version}") String version) {
this.promptPath = "classpath:/prompts/customer-service/" + version + "/reply.st";
}
// …
}
改 Prompt 的流程变成:
A/B 测试思路
如果你想验证"新 Prompt 是否真的比旧的好",可以做 A/B 测试:
@Service
public class AbTestBotService {
private final ChatClient chatClient;
private final Resource promptA; // 旧版
private final Resource promptB; // 新版
public String reply(String userId, String question) {
// 根据用户 ID 哈希决定用哪个版本
boolean useVersionB = Math.abs(userId.hashCode() % 100) < 50; // 50% 流量
Resource prompt = useVersionB ? promptB : promptA;
PromptTemplate template = new PromptTemplate(prompt);
String answer = chatClient.prompt(template.create(Map.of("question", question)))
.call()
.content();
// 记录用了哪个版本,方便后续分析
log.info("User {} served by prompt version: {}",
userId, useVersionB ? "B" : "A");
return answer;
}
}
然后对比两组的指标:
| 回答质量 | 用户"有帮助"/"没帮助"按钮的点击率 |
| 用户满意度 | 对话后的评分(👍/👎) |
| 任务完成率 | 对话后用户是否还需要转人工 |
| 响应效率 | 平均对话轮数 |
实战经验:大部分团队不需要上来就搞 A/B 测试。先把 Prompt 从代码里拆出来、做到版本化管理,就已经超越了 90% 的团队。A/B 测试是锦上添花,不是必需品。
第三章小结
下一章讲结构化输出——教你用 BeanOutputConverter 让 AI 直接返回 Java 对象,再也不用手写正则解析 AI 的回答。这是 RAG 和 Function Calling 的前置基础,很重要的。





