第1905篇:Spring AI的PromptTemplate高级用法——动态组装与条件渲染
第1905篇:Spring AI的PromptTemplate高级用法——动态组装与条件渲染
提示词工程(Prompt Engineering)说白了就是"怎么跟 AI 说话才能让它做对事"。这里面有学问,但更有工程问题:在生产代码里,提示词是写死在字符串里,还是放配置文件,还是存数据库,还是动态生成?
很多项目一开始把 prompt 写死在代码里,问题马上来了:一个 AI 功能要支持中英双语,要根据用户角色变化措辞,要在不同场景下给不同的背景信息……硬编码的 prompt 改一个地方要改一堆地方,而且改完还得重新部署。
Spring AI 的 PromptTemplate 提供了模板化的 prompt 管理机制,本文从基础用法讲到复杂的动态组装,重点放在真实项目里会遇到的那些需求。
PromptTemplate 基础:不只是字符串替换
先看最基础的用法:
String templateText = """
你是一个{role},正在帮助{userName}解决问题。
用户的问题是:{question}
请用{language}回答,回答要{style}。
""";
PromptTemplate template = new PromptTemplate(templateText);
Map<String, Object> variables = Map.of(
"role", "资深 Java 工程师",
"userName", "小王",
"question", "Spring Boot 如何配置多数据源",
"language", "中文",
"style", "简洁专业,带代码示例"
);
Prompt prompt = template.create(variables);这个基础用法不稀奇,很多框架都有。Spring AI 真正有意思的地方在于:
1. 支持从 Resource 加载模板
// 从 classpath 加载
PromptTemplate template = new PromptTemplate(
new ClassPathResource("prompts/customer-service.st"));
// 从文件系统加载
PromptTemplate template = new PromptTemplate(
new FileSystemResource("/config/ai/prompts/analysis.st"));模板文件可以放在 Git 仓库里版本管理,不需要重新部署就能修改 prompt——发布时只需要替换配置文件。
2. SystemPromptTemplate:区分系统提示和用户消息
SystemPromptTemplate systemTemplate = new SystemPromptTemplate("""
你是{companyName}的专属客服助手,名字叫{assistantName}。
你的职责是:{responsibilities}
你不应该:{restrictions}
当前时间:{currentTime}
""");
Message systemMessage = systemTemplate.createMessage(Map.of(
"companyName", "云端科技",
"assistantName", "小云",
"responsibilities", "回答产品使用问题、处理退款申请、转接人工客服",
"restrictions", "讨论竞争对手产品、透露公司内部信息、承诺超出权限的赔偿",
"currentTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
));
// 用户消息也用模板
PromptTemplate userTemplate = new PromptTemplate(
"用户{userId}的问题:{userMessage}");
Message userMessage = userTemplate.createMessage(Map.of(
"userId", "U123456",
"userMessage", "我的订单什么时候发货?"
));
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
ChatResponse response = chatModel.call(prompt);动态 Prompt 组装:模块化设计
真实业务里的 prompt 往往很长,而且不同部分来自不同来源(数据库、配置、实时计算)。用模块化思路来组装 prompt,是比较成熟的做法。
@Service
public class DynamicPromptBuilder {
@Autowired
private PromptPartRepository partRepo;
@Autowired
private UserProfileService userProfile;
@Autowired
private RuleEngineService ruleEngine;
/**
* 根据场景动态组装 System Prompt
*/
public Message buildSystemMessage(String sceneType, String userId) {
StringBuilder promptBuilder = new StringBuilder();
// 1. 基础身份设定(固定部分)
promptBuilder.append(partRepo.getContent("base_identity")).append("\n\n");
// 2. 根据场景加载专业知识背景
String sceneKnowledge = partRepo.getContent("scene_" + sceneType);
if (sceneKnowledge != null) {
promptBuilder.append(sceneKnowledge).append("\n\n");
}
// 3. 根据用户等级调整措辞策略
UserLevel level = userProfile.getUserLevel(userId);
String communicationStyle = switch (level) {
case EXPERT -> "用户是专家,可以使用专业术语,不需要解释基础概念。";
case INTERMEDIATE -> "用户有一定基础,适当解释专业术语。";
case BEGINNER -> "用户是初学者,请用简单语言解释,避免行话,多举例子。";
};
promptBuilder.append("沟通风格:").append(communicationStyle).append("\n\n");
// 4. 根据业务规则引擎动态加载限制条件
List<String> activeRules = ruleEngine.getActiveRules(sceneType, userId);
if (!activeRules.isEmpty()) {
promptBuilder.append("当前业务规则:\n");
activeRules.forEach(rule -> promptBuilder.append("- ").append(rule).append("\n"));
promptBuilder.append("\n");
}
// 5. 时效性信息(每次都要最新的)
promptBuilder.append("当前时间:")
.append(LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")))
.append("\n");
return new SystemMessage(promptBuilder.toString());
}
/**
* 构建包含上下文文档的用户消息
*/
public Message buildUserMessageWithContext(String userQuery,
List<Document> retrievedDocs,
Map<String, Object> metadata) {
PromptTemplate template = new PromptTemplate("""
用户问题:{userQuery}
{#if hasContext}
以下是检索到的相关文档,请参考这些信息回答:
{contextDocs}
{/if}
{#if hasMetadata}
额外上下文信息:
{metadataInfo}
{/if}
请基于以上信息,准确回答用户的问题。如果文档中没有相关信息,请如实告知。
""");
Map<String, Object> vars = new HashMap<>();
vars.put("userQuery", userQuery);
vars.put("hasContext", !retrievedDocs.isEmpty());
vars.put("contextDocs", formatDocuments(retrievedDocs));
vars.put("hasMetadata", !metadata.isEmpty());
vars.put("metadataInfo", formatMetadata(metadata));
return template.createMessage(vars);
}
private String formatDocuments(List<Document> docs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < docs.size(); i++) {
sb.append("【文档 ").append(i + 1).append("】\n");
sb.append(docs.get(i).getContent()).append("\n");
if (i < docs.size() - 1) sb.append("\n---\n\n");
}
return sb.toString();
}
private String formatMetadata(Map<String, Object> metadata) {
return metadata.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.collect(Collectors.joining("\n"));
}
}注意上面我用了 {#if hasContext}...{/if} 这种条件渲染语法——Spring AI 的 PromptTemplate 底层用的是 StringTemplate 或者 Mustache 引擎,支持条件和循环:
条件渲染:让 Prompt 根据上下文自动变化
Spring AI 默认用 StringTemplate 引擎,支持条件渲染,但语法稍有不同。来看一个更完整的条件渲染示例:
// 条件渲染的模板(StringTemplate 语法)
String templateText = """
你是一个智能助手。
<if(isVip)>
当前用户是 VIP 会员,享有优先服务,可以咨询的问题范围更广。
<endif>
<if(hasPreviousIssue)>
用户之前反映过的问题:<previousIssue>
请在回答时关注这个问题是否已经解决。
<endif>
<if(isWorkingHours)>
当前是工作时间,如有需要可以帮用户转接人工客服。
<else>
当前是非工作时间(工作时间:周一至周五 9:00-18:00),人工客服暂不可用。
<endif>
用户问题:<userMessage>
""";实际上在 Spring AI 项目里,我更推荐切换到 Mustache 引擎,语法更通用:
// 配置使用 Mustache 引擎
@Configuration
public class PromptConfig {
@Bean
public PromptTemplateFactory promptTemplateFactory() {
return new MustachePromptTemplateFactory();
}
}Mustache 语法的条件渲染更直观:
{{#isVip}}
当前用户是 VIP 会员,享有以下额外权益:
{{#vipBenefits}}
- {{.}}
{{/vipBenefits}}
{{/isVip}}
{{^isVip}}
如果您想获得更好的服务,可以了解我们的 VIP 方案。
{{/isVip}}多语言 Prompt 管理
国际化是很多项目的需求,多语言 prompt 管理是个实际问题:
@Component
public class I18nPromptService {
// 按语言缓存模板
private final Map<String, Map<String, String>> templates = new ConcurrentHashMap<>();
@Autowired
private PromptTemplateRepository templateRepo;
@PostConstruct
public void loadTemplates() {
// 加载所有语言的模板
templateRepo.findAll().forEach(t ->
templates.computeIfAbsent(t.getLanguage(), k -> new HashMap<>())
.put(t.getKey(), t.getContent())
);
}
/**
* 获取指定语言的 Prompt
*/
public PromptTemplate getTemplate(String key, String language) {
Map<String, String> langTemplates = templates.getOrDefault(language,
templates.get("zh")); // 默认中文
String templateText = langTemplates.getOrDefault(key,
templates.get("zh").get(key)); // 找不到就用中文兜底
if (templateText == null) {
throw new TemplateNotFoundException("模板不存在: " + key);
}
return new PromptTemplate(templateText);
}
/**
* 动态刷新某个语言的模板(不重启服务)
*/
public void refreshTemplate(String language, String key, String newContent) {
templates.computeIfAbsent(language, k -> new HashMap<>())
.put(key, newContent);
log.info("模板已更新: language={}, key={}", language, key);
}
/**
* 构建多语言 Prompt
*/
public Prompt buildLocalizedPrompt(String sceneKey, String language,
Map<String, Object> variables) {
PromptTemplate systemTemplate = getTemplate("system_" + sceneKey, language);
PromptTemplate userTemplate = getTemplate("user_" + sceneKey, language);
// 加入语言相关的变量
Map<String, Object> enrichedVars = new HashMap<>(variables);
enrichedVars.put("language", getLanguageName(language));
enrichedVars.put("locale", language);
Message systemMessage = systemTemplate.createMessage(enrichedVars);
Message userMessage = userTemplate.createMessage(enrichedVars);
return new Prompt(List.of(systemMessage, userMessage));
}
private String getLanguageName(String language) {
return switch (language) {
case "zh" -> "中文";
case "en" -> "English";
case "ja" -> "日本語";
default -> "中文";
};
}
}提示词版本管理:A/B 测试的工程实现
提示词优化是持续的过程,A/B 测试可以帮你量化不同 prompt 版本的效果差异:
@Component
public class PromptVersionManager {
@Autowired
private PromptVersionRepository versionRepo;
@Autowired
private ExperimentService experimentService;
@Autowired
private MetricsService metricsService;
/**
* 根据实验分配获取 Prompt 版本
*/
public PromptTemplate getTemplate(String promptKey, String userId) {
// 查询当前是否有该 prompt 的 A/B 实验
ExperimentAssignment assignment = experimentService
.getAssignment("prompt_" + promptKey, userId);
String version;
if (assignment != null) {
version = assignment.getVariant(); // "control" 或 "treatment"
} else {
version = "production"; // 没有实验就用生产版本
}
PromptVersionEntity entity = versionRepo.findByKeyAndVersion(promptKey, version)
.orElseGet(() -> versionRepo.findByKeyAndVersion(promptKey, "production")
.orElseThrow());
// 记录使用了哪个版本(用于后续分析)
metricsService.recordPromptVersionUsage(promptKey, version, userId);
return new PromptTemplate(entity.getContent());
}
/**
* 发布新版本 Prompt
*/
public void publishVersion(String promptKey, String content, String author) {
PromptVersionEntity version = PromptVersionEntity.builder()
.key(promptKey)
.version("v" + System.currentTimeMillis())
.content(content)
.author(author)
.publishedAt(Instant.now())
.status(VersionStatus.DRAFT)
.build();
versionRepo.save(version);
log.info("新版本 Prompt 已保存: key={}, author={}", promptKey, author);
}
/**
* 将某版本切换为生产版本
*/
@Transactional
public void promoteToProduction(String promptKey, String versionId) {
// 将当前生产版本降级
versionRepo.findByKeyAndVersion(promptKey, "production")
.ifPresent(current -> {
current.setVersion("archived_" + System.currentTimeMillis());
versionRepo.save(current);
});
// 将指定版本提升为生产
PromptVersionEntity target = versionRepo.findById(versionId)
.orElseThrow(() -> new NotFoundException("版本不存在: " + versionId));
target.setVersion("production");
target.setStatus(VersionStatus.PRODUCTION);
versionRepo.save(target);
log.info("Prompt 版本已提升为生产: key={}, versionId={}", promptKey, versionId);
}
}Prompt 编排流程
踩过的坑
坑1:模板变量命名冲突
Spring AI 的 PromptTemplate 内部会自动处理一些保留变量名(比如 {input})。有次我的模板里用了 {input} 作为用户输入的变量名,结果发现有时候填充的值不对——框架内部把它当特殊变量处理了。
解决方案:避免使用 input、text、query 等通用名词作为变量名,改用更具体的名称如 userInput、searchQuery。
坑2:大 Prompt 导致首次响应慢
我有一个 prompt 组装了大量背景知识,内容超过 3000 个 token,结果 TTFT(首次 token 响应时间)从 500ms 变成了 3 秒多。用户体验很差。
解决方案:静态的背景知识用 RAG 方式按需检索,不要全量放进 system prompt。只把真正每次都需要的上下文放进去。
坑3:条件渲染的空白行问题
用 StringTemplate 做条件渲染时,如果条件为 false,对应内容被删掉后往往留下空白行,导致 prompt 里有很多空行。虽然不影响功能,但看起来不够整洁,而且也浪费 token。
解决方案:在模板里条件块前后不加空行,或者用后处理清理多余空白:
public Prompt buildPrompt(String templateText, Map<String, Object> vars) {
PromptTemplate template = new PromptTemplate(templateText);
Prompt prompt = template.create(vars);
// 清理多余空白行
String cleanedContent = prompt.getContents()
.replaceAll("\\n{3,}", "\n\n") // 3个以上连续换行换成2个
.trim();
return new Prompt(cleanedContent, prompt.getOptions());
}坑4:Prompt 里的特殊字符转义
当 prompt 包含用户输入时,用户可能输入包含 {、} 的内容(比如代码),这会被 StringTemplate 引擎误解析为变量占位符,导致报错或行为异常。
解决方案:对用户输入做转义,或者把用户输入单独放在一个不经过模板引擎的消息里:
// 不对用户消息做模板渲染,直接用字符串
Message userMessage = new UserMessage(userInput); // 直接用原始输入
// 只对系统提示做模板渲染
SystemPromptTemplate systemTemplate = new SystemPromptTemplate(systemTemplateText);
Message systemMessage = systemTemplate.createMessage(systemVars);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));小结
Spring AI 的 PromptTemplate 不只是字符串替换工具,配合动态组装、条件渲染、多语言支持和版本管理,可以构建出一套完整的 prompt 工程体系:
- 把 prompt 从代码里分离出来,放配置文件或数据库,支持热更新
- 模块化组装:身份设定、场景知识、用户策略、业务规则分层管理
- 条件渲染按上下文动态裁剪,减少无效 token 消耗
- A/B 测试能力让 prompt 优化有量化依据
这套体系建起来之后,AI 产品的迭代速度会明显加快——大多数效果调优只需要改 prompt 配置,不需要动代码。
