Spring AI提示词模板与动态变量替换
前置知识
- Spring AI基础
- 了解Prompt Engineering基础概念
- Java字符串处理
- Mustache/FreeMarker等模板引擎基础
核心概念
提示词模板(Prompt Template)用于动态构建发送给LLM的提示词。Spring AI提供PromptTemplate抽象,支持变量替换、条件渲染、循环等高级功能,使提示词管理变得灵活且可复用。
模板处理流程
模板定义(带{{变量}}) → 渲染上下文(Map) → PromptTemplate.render() → 最终Prompt
完整实现
1. 基础提示词模板
java
@Configuration
public class PromptTemplateConfig {
/**
* 系统提示词模板
*/
@Bean
public PromptTemplate systemPromptTemplate() {
return new PromptTemplate("""
你是一个专业的{domain}助手。
你的特点:
- 风格: {style}
- 语言: {language}
- 回答长度: {length}
用户信息: {user_name}({user_level}会员)
请根据用户问题提供专业、准确的回答。
""");
}
/**
* 问答模板
*/
@Bean
public PromptTemplate qaPromptTemplate() {
return new PromptTemplate("""
上下文信息:
{context}
用户问题: {question}
请基于上下文回答问题。如果上下文中没有相关信息,请明确告知。
回答要求:
- 简洁准确
- 引用来源(如有)
- 列出关键点
""");
}
/**
* 摘要模板
*/
@Bean
public PromptTemplate summaryTemplate() {
return new PromptTemplate("""
请将以下{type}内容压缩为{style}风格的摘要。
摘要要求:
- 长度: {length}
- 保留关键信息
- 保持原文逻辑结构
原文:
{content}
""");
}
}
2. 模板渲染服务
java
@Service
@Slf4j
public class PromptRenderService {
/**
* 简单变量替换
*/
public String render(String template, Map<String, Object> variables) {
PromptTemplate promptTemplate = new PromptTemplate(template);
return promptTemplate.render(variables);
}
/**
* 安全渲染 - 防止注入
*/
public String renderSafe(String template, Map<String, Object> variables) {
// 清理变量值,防止Prompt注入
Map<String, Object> safeVariables = variables.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> sanitizeValue(e.getValue())
));
return new PromptTemplate(template).render(safeVariables);
}
/**
* 嵌套模板渲染
*/
public String renderNested(String outerTemplate,
Map<String, Object> outerVars,
Map<String, String> nestedTemplates) {
// 先渲染嵌套模板
Map<String, Object> resolvedVars = new HashMap<>(outerVars);
for (Map.Entry<String, String> entry : nestedTemplates.entrySet()) {
String templateStr = entry.getValue();
String rendered = render(templateStr, resolvedVars);
resolvedVars.put(entry.getKey(), rendered);
}
// 再渲染外层模板
return render(outerTemplate, resolvedVars);
}
/**
* 批量渲染多个模板
*/
public List<Prompt> renderBatch(List<String> templates,
List<Map<String, Object>> contextList) {
List<Prompt> results = new ArrayList<>();
for (int i = 0; i < templates.size(); i++) {
String template = templates.get(i);
Map<String, Object> context = i < contextList.size()
? contextList.get(i) : Map.of();
results.add(new Prompt(render(template, context)));
}
return results;
}
private Object sanitizeValue(Object value) {
if (value instanceof String str) {
// 移除可能的Prompt注入尝试
return str.replace("{{", "{{")
.replace("}}", "}}")
.replace("{#", "{#")
.replace("{%", "{%");
}
return value;
}
}
3. 模板引擎集成 - FreeMarker
java
/**
* FreeMarker模板引擎集成
*/
@Component
public class FreeMarkerPromptService {
private final Configuration freemarkerConfig;
private final ObjectMapper objectMapper;
public FreeMarkerPromptService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.freemarkerConfig = new Configuration(Configuration.VERSION_2_3_31);
freemarkerConfig.setDefaultEncoding("UTF-8");
freemarkerConfig.setTemplateLoader(new ClassTemplateLoader(
this.getClass(), "/prompts"));
freemarkerConfig.setTemplateExceptionHandler(
TemplateExceptionHandler.RETHROW_HANDLER);
}
/**
* 使用FreeMarker模板渲染
*/
public String renderFromTemplate(String templateName,
Map<String, Object> dataModel) {
try {
Template template = freemarkerConfig.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
return writer.toString();
} catch (IOException | TemplateException e) {
log.error("FreeMarker模板渲染失败: {}", templateName, e);
throw new RuntimeException("模板渲染失败", e);
}
}
/**
* 带条件渲染的模板
*
* FTL模板示例:
* <#if userLevel == "vip">
* 尊敬的VIP用户 {userName},享受专属服务!
* <#else>
* 用户 {userName},欢迎!
* </#if>
*/
public String renderConditional(String templateName, UserContext context) {
Map<String, Object> data = Map.of(
"userName", context.getUserName(),
"userLevel", context.getLevel(),
"isVip", "vip".equals(context.getLevel()),
"question", context.getQuestion(),
"language", context.getLanguage()
);
return renderFromTemplate(templateName, data);
}
/**
* 带循环的模板渲染
*
* FTL:
* <#list items as item>
* ${item_index + 1}. ${item.name}: ${item.description}
* </#if>
*/
public String renderList(String templateName, List<ContextItem> items) {
Map<String, Object> data = Map.of(
"items", items.stream()
.map(item -> Map.of(
"name", item.name(),
"description", item.description()
))
.collect(Collectors.toList())
);
return renderFromTemplate(templateName, data);
}
}
4. A/B测试模板管理
java
@Service
@Slf4j
public class PromptExperimentService {
private final PromptRepository promptRepository;
private final MeterRegistry meterRegistry;
/**
* 创建A/B测试实验
*/
public Experiment createExperiment(ExperimentConfig config) {
Experiment experiment = new Experiment();
experiment.setId(UUID.randomUUID().toString());
experiment.setName(config.name());
experiment.setVariants(config.variants());
experiment.setTrafficSplit(config.trafficSplit());
experiment.setStatus("RUNNING");
experiment.setStartTime(LocalDateTime.now());
return promptRepository.saveExperiment(experiment);
}
/**
* 根据用户ID选择模板变体 (一致性哈希)
*/
public PromptTemplate selectVariant(String experimentId, String userId) {
Experiment experiment = promptRepository.findExperiment(experimentId);
if (experiment == null || !"RUNNING".equals(experiment.getStatus())) {
return getDefaultTemplate();
}
int bucket = Math.abs(userId.hashCode()) % 100;
int cumulative = 0;
for (int i = 0; i < experiment.getVariants().size(); i++) {
cumulative += experiment.getTrafficSplit().get(i);
if (bucket < cumulative) {
PromptVariant variant = experiment.getVariants().get(i);
meterRegistry.counter("prompt.experiment.variant",
"experiment", experimentId,
"variant", variant.getName()).increment();
return new PromptTemplate(variant.getTemplate());
}
}
return new PromptTemplate(
experiment.getVariants().get(0).getTemplate());
}
/**
* 分析实验结果
*/
public ExperimentResult analyzeExperiment(String experimentId) {
Map<String, List<Double>> variantScores =
promptRepository.getVariantScores(experimentId);
VariantResult bestVariant = null;
double bestScore = 0;
for (Map.Entry<String, List<Double>> entry : variantScores.entrySet()) {
double avgScore = entry.getValue().stream()
.mapToDouble(Double::doubleValue)
.average().orElse(0);
if (avgScore > bestScore) {
bestScore = avgScore;
bestVariant = new VariantResult(entry.getKey(), avgScore);
}
}
return new ExperimentResult(experimentId, bestVariant, variantScores);
}
public record ExperimentConfig(
String name,
List<PromptVariant> variants,
List<Integer> trafficSplit
) {}
public record PromptVariant(String name, String template) {}
public record VariantResult(String name, double avgScore) {}
public record ExperimentResult(
String experimentId,
VariantResult winner,
Map<String, List<Double>> allScores
) {}
}
5. 提示词版本控制与热更新
java
@Service
@Slf4j
public class PromptVersionService {
private final PromptRepository repository;
private final Map<String, CachedPrompt> promptCache = new ConcurrentHashMap<>();
private final ScheduledExecutorService refreshScheduler =
Executors.newScheduledThreadPool(1);
@PostConstruct
public void init() {
// 每60秒刷新缓存
refreshScheduler.scheduleAtFixedRate(this::refreshAll, 0, 60, TimeUnit.SECONDS);
}
/**
* 获取当前生效版本的模板
*/
public PromptTemplate getCurrentPrompt(String promptKey) {
CachedPrompt cached = promptCache.get(promptKey);
if (cached == null || cached.isExpired()) {
PromptEntity entity = repository.findActiveByKey(promptKey);
if (entity == null) {
throw new NotFoundException("Prompt模板不存在: " + promptKey);
}
cached = new CachedPrompt(entity.getTemplate(),
entity.getVersion(), System.currentTimeMillis() + 60000);
promptCache.put(promptKey, cached);
}
return new PromptTemplate(cached.template);
}
/**
* 创建新版本
*/
@Transactional
public PromptEntity createVersion(String promptKey, String template,
String changeNote) {
PromptEntity entity = repository.findActiveByKey(promptKey);
int newVersion = entity != null ? entity.getVersion() + 1 : 1;
PromptEntity newEntity = new PromptEntity();
newEntity.setId(UUID.randomUUID().toString());
newEntity.setKey(promptKey);
newEntity.setTemplate(template);
newEntity.setVersion(newVersion);
newEntity.setStatus("DRAFT");
newEntity.setChangeNote(changeNote);
newEntity.setCreatedAt(LocalDateTime.now());
return repository.save(newEntity);
}
/**
* 发布指定版本 (热更新)
*/
@Transactional
public boolean publishVersion(String promptKey, int version) {
PromptEntity entity = repository.findByKeyAndVersion(promptKey, version);
if (entity == null) return false;
// 下线旧版本
repository.deactivateAllVersions(promptKey);
// 激活新版本
entity.setStatus("ACTIVE");
entity.setPublishedAt(LocalDateTime.now());
repository.save(entity);
// 清除缓存
promptCache.remove(promptKey);
log.info("Prompt模板已更新: {} -> v{}", promptKey, version);
return true;
}
/**
* 回滚到上一版本
*/
@Transactional
public boolean rollback(String promptKey) {
PromptEntity current = repository.findActiveByKey(promptKey);
if (current == null || current.getVersion() <= 1) return false;
return publishVersion(promptKey, current.getVersion() - 1);
}
private void refreshAll() {
promptCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
record CachedPrompt(String template, int version, long expiryAt) {
boolean isExpired() {
return System.currentTimeMillis() > expiryAt;
}
}
}
6. 多语言提示词管理
java
@Service
public class MultiLanguagePromptService {
private final MessageSource messageSource;
private final PromptVersionService versionService;
/**
* 基于 locale 获取多语言提示词
*/
public PromptTemplate getLocalizedPrompt(String promptKey, Locale locale,
Map<String, Object> variables) {
String template = messageSource.getMessage(
"prompt." + promptKey, null, locale);
if (template == null) {
// 回退到默认语言
template = messageSource.getMessage(
"prompt." + promptKey, null, Locale.ENGLISH);
}
// 渲染模板
renderWithLocale(template, variables, locale);
return new PromptTemplate(template);
}
/**
* 语言特定的回答风格适配
*/
public String buildSystemPrompt(String basePrompt, Locale locale) {
String styleHint = switch (locale.getLanguage()) {
case "zh" -> "使用中文回答,语气专业且友好";
case "en" -> "Respond in English, professional and clear";
case "ja" -> "日本語で回答してください、丁寧に";
case "es" -> "Respuesta en español, profesional";
default -> "Answer in the user's language";
};
return basePrompt + "\n\n" + styleHint;
}
/**
* 地域化模板变量
*/
public Map<String, Object> localizeContext(Map<String, Object> context,
Locale locale) {
Map<String, Object> localized = new HashMap<>(context);
// 日期格式本地化
if (context.containsKey("date")) {
DateTimeFormatter fmt = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(locale);
LocalDateTime date = (LocalDateTime) context.get("date");
localized.put("date", date.format(fmt));
}
// 货币符号本地化
if (context.containsKey("amount")) {
NumberFormat fmt = NumberFormat.getCurrencyInstance(locale);
BigDecimal amount = (BigDecimal) context.get("amount");
localized.put("amount", fmt.format(amount));
}
return localized;
}
}
7. Prompt模板组合与复用
java
/**
* 模板片段组合服务
*/
@Service
public class PromptCompositionService {
private final PromptVersionService versionService;
/**
* 组合多个模板片段
*/
public PromptTemplate compose(String... templateKeys) {
StringBuilder template = new StringBuilder();
for (String key : templateKeys) {
String content = versionService.getCurrentPrompt(key).getTemplate();
template.append(content).append("\n\n");
}
return new PromptTemplate(template.toString().trim());
}
/**
* 基于角色的模板组合
*/
public PromptTemplate composeForRole(UserRole role) {
return switch (role) {
case ADMIN -> compose(
"common:base",
"common:safety",
"role:admin",
"output:structured");
case DEVELOPER -> compose(
"common:base",
"style:technical",
"output:code");
case ANALYST -> compose(
"common:base",
"style:analytical",
"output:data");
case USER -> compose(
"common:base",
"style:friendly",
"output:concise");
};
}
/**
* 模板继承机制
*/
public PromptTemplate extendTemplate(String baseTemplate,
Map<String, String> overrides) {
// 解析基础模板中的可覆盖块
String extended = baseTemplate;
for (Map.Entry<String, String> entry : overrides.entrySet()) {
String placeholder = "{{" + entry.getKey() + "}}";
extended = extended.replace(placeholder, entry.getValue());
}
return new PromptTemplate(extended);
}
}
总结
提示词模板与动态变量替换的关键技术点:
- 变量替换: {{variable}}语法 + Map参数
- 模板引擎: FreeMarker/Mustache支持条件、循环
- A/B实验: 一致性哈希分流 + 效果对比
- 版本控制: 草稿/发布/回滚完整生命周期
- 多语言: MessageSource + 地域化变量