Spring AI提示词模板与动态变量替换

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);
    }
}

总结

提示词模板与动态变量替换的关键技术点:

  1. 变量替换: {{variable}}语法 + Map参数
  2. 模板引擎: FreeMarker/Mustache支持条件、循环
  3. A/B实验: 一致性哈希分流 + 效果对比
  4. 版本控制: 草稿/发布/回滚完整生命周期
  5. 多语言: MessageSource + 地域化变量

参考博客: https://blog.csdn.net/badao_liumang_qizhi

相关推荐
Object_SC1 小时前
java 中的泛型
java
民乐团扒谱机1 小时前
【超详细】PyQt6 实现 AU 风格波形图编辑器:多级缩放与采样点逐级渲染,附全过程代码
开发语言·python·编辑器·pyqt·audition·时域·频谱图
CTA量化套保1 小时前
先跑清楚小流程,再让量化功能变复杂
人工智能·python
Csvn1 小时前
🐍 Day 10: 依赖管理 — 从 requirements.txt 到 pyproject.toml
后端·python
s_w.h1 小时前
【 linux 】线程互斥与同步
java·linux·服务器·开发语言
学长毕业设计1 小时前
基于SpringBoot的校园二手物品交易系统(源码+文档+讲解视频)
java·spring boot·后端
2601_962294051 小时前
Python 操作 Word:如何查找、替换和批量修改文本
python·正则表达式·word·文档处理·查找替换
观无2 小时前
若依AOP
java
是2的10次方啊2 小时前
日志不是越多越好:开发优化、常见坑与线上排查命令
java