work hard work smart

专注于AI+Java后端开发。 不断总结,举一反三。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

摘要:在实际项目中,提示词(Prompt)往往冗长复杂且频繁调整。本文基于真实项目代码,深入讲解 Spring AI 的 PromptTemplate 机制,从基础变量替换到外部文件加载,全面掌握提示词模板的最佳实践。

一、为什么需要提示词模板?

硬编码提示词的痛点

在实际开发中,我们经常看到这样的代码:

// ❌ 错误示范:提示词硬编码在业务逻辑中
@GetMapping("/recommend")
public String recommend(String topic) {
    String prompt = """
        你是一个专业的技术顾问,请推荐 5 个关于%s的开源项目。
        要求:
        1. 项目必须是活跃的(最近 3 个月有更新)
        2. GitHub Star 数大于 1000
        3. 提供项目简介和适用场景
        4. 用 Markdown 表格形式输出
        """.formatted(topic);
    
    return chatClient.prompt(prompt).call().content();
}

问题

  • 🔴 提示词与业务代码耦合,难以维护
  • 🔴 调整提示词需要修改 Java 代码并重新编译
  • 🔴 无法复用相似的提示词结构
  • 🔴 长提示词影响代码可读性

提示词模板的价值

关注点分离:提示词与业务逻辑解耦
动态替换:通过变量实现提示词复用
外部管理:支持从文件加载,便于版本控制
团队协作:非技术人员也能参与提示词优化


二、PromptTemplate 核心概念

什么是 PromptTemplate?

PromptTemplate 是 Spring AI 提供的提示词模板类,类似于 Java 的 String.format() 或前端模板引擎,支持:

  1. 变量占位符:使用 {variable} 语法
  2. 动态赋值:运行时替换变量内容
  3. 模板复用:同一模板适配不同场景

核心 API

// 1. 创建模板
PromptTemplate template = new PromptTemplate("请介绍{topic}的基本概念");

// 2. 添加变量
template.add("topic", "Spring AI");

// 3. 生成 Prompt
Prompt prompt = template.create();

// 4. 或使用链式调用
Prompt prompt = PromptTemplate.builder()
    .resource(templateResource)
    .variables(Map.of("topic", "Spring AI"))
    .build()
    .create();

三、实战一:内联模板与变量替换

基础用法

@RestController
@RequestMapping("/prompt/template")
public class PromptTemplateController implements InitializingBean {

    @Autowired
    private ChatModel dashScopeChatModel;
    
    private ChatClient chatClient;

    @GetMapping("stream")
    public Flux<String> stream(String topic, HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        
        // 1. 定义模板(使用 {topic} 占位符)
        String template = """
                请给我推荐几个关于{topic}的开源项目
                """;

        // 2. 创建模板对象
        PromptTemplate promptTemplate = new PromptTemplate(template);
        
        // 3. 替换变量
        promptTemplate.add("topic", topic);

        // 4. 发起对话(SSE 流式输出)
        return chatClient.prompt(promptTemplate.create()).stream().content();
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .temperature(0.7)
                                .build()
                )
                .build();
    }
}

测试效果

# 请求 1:推荐 Java 相关项目
curl "http://localhost:8080/prompt/template/stream?topic=Java%20Web框架"

# 请求 2:推荐 Python 相关项目
curl "http://localhost:8080/prompt/template/stream?topic=Python%20机器学习"

同一模板,不同变量,输出完全不同!

简化写法

Spring AI 还支持更简洁的一行代码写法:

@GetMapping("stream1")
public Flux<String> stream1(String topic, HttpServletResponse response) {
    response.setCharacterEncoding("UTF-8");
    
    String template = """
            请给我推荐几个关于{topic}的开源项目
            """;
    
    // 一行代码:创建模板 + 替换变量 + 生成 Prompt
    return chatClient.prompt(
        new PromptTemplate(template).create(Map.of("topic", topic))
    ).stream().content();
}

对比

写法 优点 适用场景
add() 分步赋值 逻辑清晰,便于调试 变量多、需要条件判断
create(Map) 一步到位 代码简洁 变量少、直接传递

四、实战二:外部文件加载模板

为什么需要外部文件?

当提示词变得非常长时(如包含角色设定、少样本示例、输出格式要求),硬编码在代码中会严重影响可读性。

示例:一个完整的提示词可能包含

你是一个专业的 GitHub 项目收集专家,具备以下能力:
1. 熟悉主流编程语言生态
2. 了解开源项目的评价标准
3. 能够根据技术栈推荐合适的项目

请按照以下格式输出:
| 项目名称 | GitHub 地址 | Star 数 | 简介 | 适用场景 |
|---------|------------|---------|------|---------|

参考示例:
Input:Java Web 框架
Output:| Spring Boot | github.com/spring-projects/spring-boot | 75k+ | ... | ... |

现在请推荐关于{topic}的开源项目,编程语言限定为{language}。

解决方案:使用外部模板文件

步骤 1:创建模板文件

resources/templates/ 目录下创建 open_source_system_prompt.st

请给我推荐几个关于{topic}的开源项目,要求是和编程语言{language}相关的。

文件命名建议

  • .st 后缀(String Template 缩写)
  • 语义化命名:open_source_system_prompt.st
  • 按功能分类存放

步骤 2:在 Controller 中加载

@RestController
@RequestMapping("/prompt/template")
public class PromptTemplateController implements InitializingBean {

    @Autowired
    private ChatModel dashScopeChatModel;
    
    private ChatClient chatClient;

    // 1. 使用 @Value 注解加载资源文件
    @Value("classpath:/templates/open_source_system_prompt.st")
    private Resource systemPrompt;

    @GetMapping("/file")
    public Flux<String> file(@RequestParam(value = "message") String message, 
                             HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");

        // 2. 定义多个变量
        HashMap variables = new HashMap();
        variables.put("language", "Java");
        variables.put("topic", message);
        
        // 3. 使用 Builder 模式加载文件模板
        PromptTemplate promptTemplate = PromptTemplate.builder()
                .resource(systemPrompt)          // 加载外部文件
                .variables(variables)            // 替换变量
                .build();

        // 4. 发起对话(可同时设置 system 角色)
        return chatClient.prompt(promptTemplate.create())
                .system("你是一个专业的的github项目收集人员")
                .stream()
                .content();
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .temperature(0.7)
                                .build()
                )
                .build();
    }
}

测试效果

# 请求:推荐 Java 相关的微服务框架
curl "http://localhost:8080/prompt/template/file?message=微服务框架"

# 输出示例
| 项目名称 | GitHub 地址 | Star 数 | 简介 | 适用场景 |
|---------|------------|---------|------|---------|
| Spring Cloud | github.com/spring-cloud/spring-cloud | 15k+ | 基于 Spring Boot 的微服务框架集合 | 企业级 Java 微服务 |
| Apache Dubbo | github.com/apache/dubbo | 40k+ | 高性能 RPC 框架 | 分布式服务治理 |
| ... | ... | ... | ... | ... |

Builder 模式的优势

// ❌ 传统方式:功能有限
PromptTemplate template = new PromptTemplate("模板内容");
template.add("key", "value");

// ✅ Builder 模式:功能强大
PromptTemplate template = PromptTemplate.builder()
        .resource(systemPrompt)              // 支持文件加载
        .variables(Map.of("key", "value"))   // 支持批量变量
        .build();

Builder 模式支持

  • ✅ 从 Resource 加载模板
  • ✅ 批量设置变量(Map
  • ✅ 链式调用,代码优雅

五、PromptTemplate 高级用法

1. 多变量复杂模板

String template = """
        你是一个{role},请用{tone}的语气回答。
        
        请分析以下{subject}:
        {content}
        
        输出要求:
        1. 字数不超过{max_words}字
        2. 使用{language}语言
        3. 包含{num_examples}个具体示例
        """;

PromptTemplate promptTemplate = PromptTemplate.builder()
        .resource(new ByteArrayResource(template.getBytes()))
        .variables(Map.of(
                "role", "技术专家",
                "tone", "专业但通俗易懂",
                "subject", "Spring AI 的优势",
                "content", "用户提供的技术文档...",
                "max_words", "500",
                "language", "中文",
                "num_examples", "3"
        ))
        .build();

2. 条件渲染模板

// 根据条件动态选择模板
Resource template = userLevel.equals("beginner") 
        ? new ClassPathResource("templates/beginner_prompt.st")
        : new ClassPathResource("templates/advanced_prompt.st");

PromptTemplate promptTemplate = PromptTemplate.builder()
        .resource(template)
        .variables(Map.of("topic", topic))
        .build();

3. 模板继承与组合

// 基础模板
String baseTemplate = "你是一个{role},";

// 扩展模板
String extendedTemplate = baseTemplate + """
        请完成以下任务:
        1. 分析{topic}
        2. 提供{num}个示例
        3. 输出格式:{format}
        """;

PromptTemplate template = new PromptTemplate(extendedTemplate);
template.add("role", "数据分析师");
template.add("topic", "用户行为数据");
template.add("num", "5");
template.add("format", "Markdown 表格");

六、模板文件管理的最佳实践

1. 目录结构规范

src/main/resources/
└── templates/
    ├── system_prompts/          # 系统角色提示词
    │   ├── tech_expert.st
    │   ├── teacher.st
    │   └── analyst.st
    ├── user_prompts/            # 用户任务提示词
    │   ├── code_review.st
    │   ├── bug_fix.st
    │   └── feature_design.st
    └── output_formats/          # 输出格式模板
        ├── json_output.st
        ├── markdown_table.st
        └── report.st

2. 模板文件命名规范

命名模式 示例 说明
{场景}_{角色}_prompt.st code_review_expert_prompt.st 语义清晰
{功能}_{语言}.st recommendation_java.st 按功能+语言分类
{版本}_{描述}.st v2_detailed_analysis.st 版本化管理

3. 模板文件版本控制

// 在文件名中体现版本
@Value("classpath:/templates/v2_recommendation_prompt.st")
private Resource promptV2;

// 或使用配置文件管理版本
@ConfigurationProperties(prefix = "prompt")
public class PromptConfig {
    private String version = "v2";
    private String basePath = "templates/";
}

七、常见陷阱与解决方案

❌ 陷阱 1:变量名拼写错误

// 模板中:{topc}  (拼写错误)
// 代码中:template.add("topic", "Java")

// 结果:变量不会被替换,输出原始占位符

解决方案

// 1. 使用常量定义变量名
public class PromptVariables {
    public static final String TOPIC = "topic";
    public static final String LANGUAGE = "language";
}

// 2. 使用 Map 批量替换(IDE 可检查拼写)
Map<String, String> variables = Map.of(
        PromptVariables.TOPIC, "Java",
        PromptVariables.LANGUAGE, "Java"
);

❌ 陷阱 2:模板文件未找到

// 错误路径
@Value("classpath:/template/open_source.st")  // 少了一个 's'
private Resource systemPrompt;

// 启动时报错:Resource not found

解决方案

// 1. 添加空值检查
if (!systemPrompt.exists()) {
    throw new IllegalStateException("模板文件不存在:open_source_system_prompt.st");
}

// 2. 使用 @PostConstruct 验证
@PostConstruct
public void validateTemplates() {
    assert systemPrompt.exists() : "系统提示词模板缺失";
}

❌ 陷阱 3:特殊字符未转义

// 模板中包含 JSON 格式
String template = """
        输出格式:{"topic": "{topic}", "count": 5}
        """;

// 大括号可能与变量占位符冲突

解决方案

// 1. 使用双大括号转义(如果模板引擎支持)
String template = """
        输出格式:{{"topic": "{topic}", "count": 5}}
        """;

// 2. 将格式说明放在变量中
template.add("format", "{\"topic\": \"<topic>\", \"count\": 5}");

八、完整 Controller 示例

package cn.hollis.llm.llmentor.controller;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/prompt/template")
public class PromptTemplateController implements InitializingBean {

    @Autowired
    private ChatModel dashScopeChatModel;
    
    private ChatClient chatClient;

    /**
     * 方式一:内联模板 + add() 分步赋值
     */
    @GetMapping("stream")
    public Flux<String> stream(String topic, HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        
        String template = """
                请给我推荐几个关于{topic}的开源项目
                """;

        PromptTemplate promptTemplate = new PromptTemplate(template);
        promptTemplate.add("topic", topic);

        return chatClient.prompt(promptTemplate.create()).stream().content();
    }

    /**
     * 方式二:内联模板 + create(Map) 一步到位
     */
    @GetMapping("stream1")
    public Flux<String> stream1(String topic, HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        
        String template = """
                请给我推荐几个关于{topic}的开源项目
                """;
        return chatClient.prompt(
            new PromptTemplate(template).create(Map.of("topic", topic))
        ).stream().content();
    }

    /**
     * 方式三:外部文件加载模板
     */
    @Value("classpath:/templates/open_source_system_prompt.st")
    private Resource systemPrompt;

    @GetMapping("/file")
    public Flux<String> file(@RequestParam(value = "message") String message, 
                             HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");

        HashMap variables = new HashMap();
        variables.put("language", "Java");
        variables.put("topic", message);
        
        PromptTemplate promptTemplate = PromptTemplate.builder()
                .resource(systemPrompt)
                .variables(variables)
                .build();

        return chatClient.prompt(promptTemplate.create())
                .system("你是一个专业的的github项目收集人员")
                .stream()
                .content();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .temperature(0.7)
                                .build()
                )
                .build();
    }
}

九、PromptTemplate 对比传统方式

维度 硬编码 String.format() PromptTemplate 外部文件 + Builder
可读性 ❌ 提示词与代码混杂 ✅ 模板独立 ✅ 完全分离
维护性 ❌ 修改需重新编译 ⚠️ 修改需重新编译 ✅ 修改无需编译
复用性 ⚠️ 需手动封装 ✅ 原生支持 ✅ 原生支持
团队协作 ❌ 仅开发人员可改 ⚠️ 需了解 Java ✅ 非技术也可改
版本控制 ✅ Git 管理 ✅ Git 管理 ✅ Git 管理
适用场景 简单提示词 中等复杂度 复杂提示词

十、总结

Spring AI 的 PromptTemplate 提供了从简单到复杂的完整提示词管理方案:

三种方式的选择指南

graph TD A[提示词复杂度?] -->|简单,1-2个变量| B[内联模板 + create Map] A -->|中等,需要调试| C[内联模板 + add 分步] A -->|复杂,超过50行| D[外部文件 + Builder] B --> E[一行代码搞定] C --> F[逻辑清晰,便于调试] D --> G[团队协作,独立管理]

最佳实践清单

  • ✅ 简单场景用内联模板,复杂场景用外部文件
  • ✅ 变量名使用常量定义,避免拼写错误
  • ✅ 模板文件按功能分类,语义化命名
  • ✅ 使用 Builder 模式加载文件,代码更优雅
  • ✅ 添加模板文件存在性检查,避免运行时错误
  • ✅ 模板文件纳入 Git 版本控制,便于追踪变更

核心 API 速查

// 1. 内联模板 + 分步赋值
PromptTemplate template = new PromptTemplate("模板{var}");
template.add("var", "value");

// 2. 内联模板 + 一步到位
Prompt prompt = new PromptTemplate("模板{var}")
        .create(Map.of("var", "value"));

// 3. 外部文件 + Builder
PromptTemplate template = PromptTemplate.builder()
        .resource(resource)
        .variables(Map.of("key", "value"))
        .build();

掌握 PromptTemplate,让你的提示词管理更加专业、高效、可维护!


参考资源