Spring AI Prompt模板:动态提示词工程的完整实战指南
2026/4/30大约 8 分钟
Spring AI Prompt模板:动态提示词工程的完整实战指南
适读人群:用Spring AI做AI应用、想让Prompt更规范可维护的Java工程师 阅读时长:约18分钟
我见过最乱的Prompt代码
半年前我接手了一个项目的AI模块,打开代码的第一眼就傻了。
整个系统的Prompt全部是Java字符串硬编码,散落在20多个Service类里。有的用+拼接,有的用String.format(),有的直接是"""..."""的多行文本。最夸张的一个方法里,一段Prompt字符串有200行,里面还嵌套着各种条件判断,拼不拼某段话取决于七八个参数。
业务方说想改一个措辞,我找了半小时才找到那段字符串在哪里。
改的时候小心翼翼,因为有些参数占位符用的是%s,有些是{placeholder},还有些干脆是硬编码的中文字符,直接替换掉就完了。
那个项目让我意识到:Prompt不只是字符串,它是业务逻辑的一部分,需要被认真管理。
Spring AI 1.0提供了完整的Prompt模板体系,今天我把这套体系完整拆给你看。
Spring AI的Prompt抽象层次
基础:PromptTemplate的正确用法
Spring AI使用{变量名}语法作为占位符,底层用StringTemplate4引擎。
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptTemplateService {
private final ChatClient chatClient;
/**
* 方式1:代码内联模板(适合简单场景)
*/
public String simpleTemplate(String productName, String userQuestion) {
PromptTemplate template = new PromptTemplate("""
你是{product_name}的专业客服,负责解答用户关于产品的问题。
用户问题:{question}
请给出专业、友好的回答,如果不确定请告知用户联系人工客服。
""");
Prompt prompt = template.create(Map.of(
"product_name", productName,
"question", userQuestion
));
return chatClient.prompt(prompt).call().content();
}
/**
* 方式2:从resources加载模板文件(推荐,便于管理)
* 文件放在 src/main/resources/prompts/ 目录下
*/
@Value("classpath:prompts/customer-service.st")
private Resource customerServiceTemplate;
public String resourceTemplate(String productName, String userQuestion,
String customerName) {
PromptTemplate template = new PromptTemplate(customerServiceTemplate);
Prompt prompt = template.create(Map.of(
"product_name", productName,
"question", userQuestion,
"customer_name", customerName,
"current_date", LocalDate.now().toString()
));
return chatClient.prompt(prompt).call().content();
}
/**
* 方式3:SystemPromptTemplate(专门处理System消息)
*/
@Value("classpath:prompts/system-role.st")
private Resource systemRoleTemplate;
public String withSystemPrompt(String userRole, String userMessage) {
SystemPromptTemplate systemTemplate = new SystemPromptTemplate(systemRoleTemplate);
Message systemMessage = systemTemplate.createMessage(Map.of(
"role", userRole,
"constraints", "不得讨论政治、宗教等敏感话题"
));
UserMessage userMsg = new UserMessage(userMessage);
Prompt prompt = new Prompt(List.of(systemMessage, userMsg));
return chatClient.prompt(prompt).call().content();
}
}模板文件管理规范
把Prompt拆成模板文件,项目结构清晰,业务改Prompt不需要动Java代码。
推荐目录结构:
src/main/resources/prompts/
├── system/
│ ├── base-assistant.st # 基础助手角色
│ ├── customer-service.st # 客服角色
│ └── code-reviewer.st # 代码审查角色
├── user/
│ ├── qa-template.st # 问答模板
│ ├── summary-template.st # 摘要模板
│ └── translate-template.st # 翻译模板
└── structured/
├── json-extract.st # JSON提取模板
└── analysis-report.st # 分析报告模板示例模板文件(customer-service.st):
你是{product_name}的专业售后客服助手。
当前客户:{customer_name}
咨询日期:{current_date}
你的职责:
1. 解答关于{product_name}的使用问题
2. 处理退换货咨询
3. 收集用户反馈
回答规范:
- 语气:{tone}(默认:专业友好)
- 语言:简洁清晰,避免专业术语
- 如遇到超出权限的问题,引导用户联系{escalation_contact}
客户问题:
{question}进阶:动态Prompt构建器
实际项目中,Prompt通常需要根据条件动态组合不同的片段。下面是我们团队的动态Prompt构建方案:
/**
* 动态Prompt构建器
* 支持条件片段、多角色组合、变量继承
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class DynamicPromptBuilder {
private final ResourceLoader resourceLoader;
private final Map<String, String> templateCache = new ConcurrentHashMap<>();
/**
* 构建带条件判断的复杂Prompt
* 解决"根据用户类型显示不同内容"的问题
*/
public Prompt buildAdaptivePrompt(AdaptivePromptContext ctx) {
StringBuilder systemBuilder = new StringBuilder();
// 基础角色设定(始终包含)
systemBuilder.append(loadTemplate("system/base-assistant.st", ctx.getBaseVars()));
systemBuilder.append("\n\n");
// 根据用户等级追加不同权限说明
if (ctx.getUserLevel() == UserLevel.VIP) {
systemBuilder.append(loadTemplate("system/vip-permissions.st", ctx.getBaseVars()));
systemBuilder.append("\n\n");
}
// 根据业务场景追加专业知识
if (ctx.getScenario() == BusinessScenario.TECHNICAL_SUPPORT) {
systemBuilder.append(loadTemplate("system/tech-support-knowledge.st", ctx.getBaseVars()));
} else if (ctx.getScenario() == BusinessScenario.SALES) {
systemBuilder.append(loadTemplate("system/sales-guide.st", ctx.getBaseVars()));
}
// 如果有历史对话,追加上下文提示
if (!ctx.getChatHistory().isEmpty()) {
systemBuilder.append("""
注意:以下是你与用户的历史对话,请保持上下文一致性:
""");
}
SystemMessage systemMessage = new SystemMessage(systemBuilder.toString());
// 构建消息列表
List<Message> messages = new ArrayList<>();
messages.add(systemMessage);
// 追加历史对话(最近N轮)
List<ChatMessage> history = ctx.getChatHistory();
int historyStart = Math.max(0, history.size() - ctx.getMaxHistoryTurns() * 2);
for (int i = historyStart; i < history.size(); i++) {
ChatMessage histMsg = history.get(i);
if (histMsg.getRole() == ChatRole.USER) {
messages.add(new UserMessage(histMsg.getContent()));
} else {
messages.add(new AssistantMessage(histMsg.getContent()));
}
}
// 当前用户消息
messages.add(new UserMessage(ctx.getUserMessage()));
return new Prompt(messages, buildChatOptions(ctx));
}
/**
* 链式Prompt:多步推理场景
* 先让LLM思考,再基于思考结果生成最终答案
*/
public String chainOfThoughtPrompt(String complexQuestion) {
// 第一步:让LLM先分析问题
String analysisPrompt = """
请分析以下问题,列出解答所需的关键信息点(不要直接回答):
问题:{question}
请用结构化的方式列出:
1. 需要了解的背景信息
2. 解答的关键步骤
3. 可能的歧义点
""";
PromptTemplate analysisTemplate = new PromptTemplate(analysisPrompt);
String analysis = chatClient.prompt(
analysisTemplate.create(Map.of("question", complexQuestion))
).call().content();
log.debug("CoT分析阶段完成: {}", analysis);
// 第二步:基于分析结果,生成最终答案
String finalPrompt = """
基于以下分析框架,请给出完整、准确的回答:
原始问题:{question}
分析框架:
{analysis}
现在请给出完整答案:
""";
PromptTemplate finalTemplate = new PromptTemplate(finalPrompt);
return chatClient.prompt(
finalTemplate.create(Map.of(
"question", complexQuestion,
"analysis", analysis
))
).call().content();
}
private String loadTemplate(String templatePath, Map<String, Object> vars) {
String template = templateCache.computeIfAbsent(templatePath, path -> {
try {
Resource resource = resourceLoader.getResource("classpath:prompts/" + path);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("加载模板失败: {}", path);
return "";
}
});
// 简单变量替换
String result = template;
for (Map.Entry<String, Object> var : vars.entrySet()) {
result = result.replace("{" + var.getKey() + "}",
var.getValue() != null ? var.getValue().toString() : "");
}
return result;
}
private ChatOptions buildChatOptions(AdaptivePromptContext ctx) {
return OpenAiChatOptions.builder()
.withModel(ctx.getModelName())
.withTemperature(ctx.getTemperature())
.withMaxTokens(ctx.getMaxTokens())
.build();
}
}结构化输出:让LLM返回JSON
Prompt工程里的一大挑战:让LLM稳定地返回结构化数据。Spring AI 1.0对此有专门支持。
/**
* 结构化输出Prompt
* 让LLM返回JSON,Spring AI自动解析成Java对象
*/
@Service
@RequiredArgsConstructor
public class StructuredOutputService {
private final ChatClient chatClient;
// 定义期望的输出结构
record ProductAnalysis(
String productName,
List<String> pros,
List<String> cons,
double overallScore, // 0.0-10.0
String recommendation, // "推荐" / "谨慎购买" / "不推荐"
String targetAudience
) {}
/**
* 分析产品评价,返回结构化结果
* Spring AI的entity()方法会自动将JSON响应映射到Java类
*/
public ProductAnalysis analyzeProductReview(String reviewText) {
return chatClient.prompt()
.system("""
你是一个专业的产品分析师。
请根据用户评价,提取结构化信息。
所有分析必须基于评价原文,不要添加主观判断。
""")
.user("""
请分析以下产品评价:
{review}
按照指定格式输出JSON结果。
""".replace("{review}", reviewText))
.call()
.entity(ProductAnalysis.class); // Spring AI自动解析JSON
}
/**
* 批量处理,并发提取结构化数据
*/
public List<ProductAnalysis> batchAnalyze(List<String> reviews) {
return reviews.parallelStream()
.map(review -> {
try {
return analyzeProductReview(review);
} catch (Exception e) {
log.error("分析失败: {}", e.getMessage());
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}Few-Shot Prompt:通过示例引导输出
对于格式要求严格的场景,Few-Shot(少样本示例)是最稳定的方式:
/**
* Few-Shot Prompt示例
* 通过给LLM看几个示例,引导它输出期望的格式
*/
@Component
public class FewShotPromptBuilder {
private static final String FEW_SHOT_TEMPLATE = """
你的任务是从客户反馈中提取关键信息,输出标准化的工单。
以下是一些示例:
示例1:
客户反馈:我买的手机昨天突然黑屏了,按电源键没反应,充电也没用,已经影响正常使用了。
输出:
{
"issue_type": "硬件故障",
"product_category": "手机",
"urgency": "高",
"symptoms": ["黑屏", "无法开机", "充电无效"],
"impact": "影响正常使用"
}
示例2:
客户反馈:耳机的左耳声音比右耳小很多,听歌体验很差。
输出:
{
"issue_type": "音质问题",
"product_category": "耳机",
"urgency": "中",
"symptoms": ["左右声道不平衡"],
"impact": "影响使用体验"
}
现在请处理以下客户反馈:
客户反馈:{customer_feedback}
输出:
""";
public String buildFewShotPrompt(String customerFeedback) {
PromptTemplate template = new PromptTemplate(FEW_SHOT_TEMPLATE);
return template.create(Map.of("customer_feedback", customerFeedback)).toString();
}
}Prompt版本管理与A/B测试
生产环境里,不同版本的Prompt效果可能差异很大,需要有版本管理和A/B测试能力:
/**
* Prompt版本管理
* 支持A/B测试,自动统计不同版本的效果
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptVersionManager {
private final PromptVersionRepository versionRepo;
private final PromptEffectivenessMetrics metrics;
@Entity
@Data
@Table(name = "prompt_versions")
public static class PromptVersion {
@Id
private String versionId;
private String templateName;
private String content;
private String status; // ACTIVE / SHADOW / ARCHIVED
private double trafficWeight; // A/B测试流量权重
private LocalDateTime createdAt;
}
/**
* 根据流量权重选择Prompt版本(A/B测试)
*/
public PromptVersion selectVersion(String templateName) {
List<PromptVersion> activeVersions = versionRepo
.findByTemplateNameAndStatus(templateName, "ACTIVE");
if (activeVersions.isEmpty()) {
throw new RuntimeException("没有可用的Prompt版本: " + templateName);
}
if (activeVersions.size() == 1) {
return activeVersions.get(0);
}
// 按权重随机选择版本
double random = Math.random();
double cumulative = 0;
for (PromptVersion version : activeVersions) {
cumulative += version.getTrafficWeight();
if (random <= cumulative) {
return version;
}
}
return activeVersions.get(activeVersions.size() - 1);
}
/**
* 记录版本使用效果(结合用户反馈)
*/
public void recordEffectiveness(String versionId, boolean positive) {
metrics.record(versionId, positive);
log.debug("Prompt版本反馈: version={}, positive={}", versionId, positive);
}
}Prompt工程的黄金法则
| 原则 | 错误做法 | 正确做法 |
|---|---|---|
| 明确角色 | 不设定角色,直接问问题 | 用System消息明确AI的身份和职责 |
| 结构化指令 | 一段话描述所有要求 | 用编号列表分点说明 |
| 示例驱动 | 只说"输出JSON格式" | 给出1-3个完整的输入输出示例 |
| 约束明确 | "回答要简洁" | "回答不超过200字" |
| 模板化管理 | 代码里硬编码字符串 | 放在resources文件,用变量占位 |
| 版本控制 | 直接修改,无版本记录 | 每次改动建新版本,A/B测试验证 |
