摘要:在实际项目中,提示词(Prompt)往往冗长复杂且频繁调整。本文基于真实项目代码,深入讲解 Spring AI 的 PromptTemplate 机制,从基础变量替换到外部文件加载,全面掌握提示词模板的最佳实践。
一、为什么需要提示词模板?
硬编码提示词的痛点
在实际开发中,我们经常看到这样的代码:
// ❌ 错误示范:提示词硬编码在业务逻辑中
@GetMapping("/recommend")
public String recommend(String topic) {String prompt = """你是一个专业的技术顾问,请推荐 5 个关于%s的开源项目。要求:1. 项目必须是活跃的(最近 3 个月有更新)2. GitHub Star 数大于 10003. 提供项目简介和适用场景4. 用 Markdown 表格形式输出""".formatted(topic);return chatClient.prompt(prompt).call().content();
}
问题:
- 🔴 提示词与业务代码耦合,难以维护
- 🔴 调整提示词需要修改 Java 代码并重新编译
- 🔴 无法复用相似的提示词结构
- 🔴 长提示词影响代码可读性
提示词模板的价值
✅ 关注点分离:提示词与业务逻辑解耦
✅ 动态替换:通过变量实现提示词复用
✅ 外部管理:支持从文件加载,便于版本控制
✅ 团队协作:非技术人员也能参与提示词优化
二、PromptTemplate 核心概念
什么是 PromptTemplate?
PromptTemplate 是 Spring AI 提供的提示词模板类,类似于 Java 的 String.format() 或前端模板引擎,支持:
- 变量占位符:使用
{variable}语法 - 动态赋值:运行时替换变量内容
- 模板复用:同一模板适配不同场景
核心 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 {@Autowiredprivate 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();}@Overridepublic 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}的开源项目""";// 一行代码:创建模板 + 替换变量 + 生成 Promptreturn 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 {@Autowiredprivate 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();}@Overridepublic 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 {@Autowiredprivate 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();}@Overridepublic 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 TDA[提示词复杂度?] -->|简单,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,让你的提示词管理更加专业、高效、可维护!
参考资源:
- Spring AI 官方文档 - Prompt Templates
- [项目源码 - PromptTemplateController](file:///D:/springaiworkspace/LLMentor/springai/src/main/java/cn/hollis/llm/llmentor/controller/PromptTemplateController.java)
- [提示词工程实战指南](file:///D:/springaiworkspace/LLMentor/springai/docs/提示词工程实战指南.md)
作者:Work Hard Work Smart
出处:http://www.cnblogs.com/linlf03/
欢迎任何形式的转载,未经作者同意,请保留此段声明!
