孔蓝拿着原型说:"不同专业的课程推荐要不一样!计算机系和人文系看到的推荐肯定不同啊。"白歌说:"用 PromptTemplate,占位符
{major}动态注入,一次编写模板,适配所有专业。"
提示词模板与动态构建
定义与作用
PromptTemplate 是 Spring AI 提供的 占位符模板引擎,支持 {variable} 语法,运行时用实际值替换。核心方法:
PromptTemplate template = new PromptTemplate("""
推荐适合 {major} 专业大{year}学生的 {count} 门选修课
""");
template.add("major", "计算机科学");
template.add("year", "二");
template.add("count", "3");
Prompt prompt = template.create();
核心原理:从模板到 Prompt 的转换流程
图释:PromptTemplate 内部使用 StringTemplate 做占位符替换,生成最终的 Message 并包装为 Prompt。
与 RAG 配合:动态注入检索上下文
// RAG 场景的标准模板
PromptTemplate template = new PromptTemplate("""
基于以下文档内容回答用户问题。
如果文档中没有相关信息,请明确说明"未找到相关信息"。
相关文档片段:
{context}
用户问题:{question}
请用中文回答,引用时注明来源页码:
""");
template.add("context", vectorSearchResult); // 向量检索结果
template.add("question", userQuestion);
Prompt prompt = template.create();
完整示例一:根据学生专业动态生成课程推荐
场景说明
孔蓝需要学生管理系统根据登录学生的专业,动态生成个性化课程推荐。
关键代码
@RestController
public class CourseRecommendController {
private final ChatClient chatClient;
public CourseRecommendController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是飞翔科技大学的课程推荐专家。")
.build();
}
@GetMapping("/course-recommend")
public String recommend(@RequestParam String major,
@RequestParam(defaultValue = "二") String year) {
// 步骤1:准备模板
PromptTemplate template = new PromptTemplate("""
请为{major}专业大{year}学生推荐3门选修课。
要求:
1. 每门课程说明适合什么方向的学生
2. 列出课程的先修条件
3. 给出课程编号(格式:DEPT-编号)
请用结构化格式输出。
""");
// 步骤2:动态填充参数
template.add("major", major);
template.add("year", year);
// 步骤3:使用模板
return chatClient.prompt()
.user(template.create().getInstructions().get(0).getContent())
.call()
.content();
}
}
运行结果
GET /course-recommend?major=计算机科学&year=二
回复:
1. CS310 人工智能导论
适合方向:对机器学习、自然语言处理感兴趣的学生
先修条件:CS201 离散数学、CS202 数据结构
2. CS320 软件工程
适合方向:想进入企业级开发的工程型学生
先修条件:CS101 程序设计基础
3. MATH305 概率论与数理统计
适合方向:数据科学、AI 方向必备
先修条件:MATH101 高等数学A
GET /course-recommend?major=汉语言文学&year=二
回复:
1. CHN210 中国现代文学
适合方向:对现当代文学流派感兴趣的学生
先修条件:CHN101 文学概论
2. CHN215 古代汉语
适合方向:古典文献研究方向
先修条件:无
3. CHN220 创意写作
适合方向:文学创作方向
先修条件:CHN105 基础写作
完整示例二:RAG 场景的动态 Prompt 构建
场景说明
飞翔科技的智能客服需要先检索产品手册,再基于检索结果回答问题。
关键代码
@Service
public class ProductQAService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public ProductQAService(ChatClient.Builder builder,
VectorStore vectorStore) {
this.chatClient = builder
.defaultSystem("你是飞翔科技产品顾问。")
.build();
this.vectorStore = vectorStore;
}
public String ask(String question) {
// 1. 从向量库检索相关文档
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(3)
.build()
);
// 2. 拼接检索结果
String context = docs.stream()
.map(doc -> doc.getContent() + "(来源:"
+ doc.getMetadata().get("source") + ")")
.collect(Collectors.joining("\n---\n"));
// 3. 用模板构造 Prompt
PromptTemplate template = new PromptTemplate("""
你是飞翔科技的产品顾问。基于以下产品手册内容回答用户问题。
手册相关内容:
{context}
用户问题:{question}
回答要求:
- 如果手册中有明确信息,直接引用并注明来源
- 如果手册中信息不完整,说明哪些是确定的,哪些需要进一步确认
- 绝对不要编造手册中没有的信息
""");
template.add("context", context);
template.add("question", question);
// 4. 发送给 LLM
return chatClient.prompt()
.user(template.create().getInstructions().get(0).getContent())
.call()
.content();
}
}
System Prompt 最佳实践
在飞翔科技的项目实践中,白歌总结了 System Prompt 的设计准则:
DO(推荐做法):
1. 角色 + 边界:"你是飞翔科技大学的课程顾问,只回答课程相关问题"
2. 格式约束:"回答用 Markdown 表格,字段为:课程名、编号、学分、时间"
3. 行为规范:"如果不确定,明确说'我不确定',不要编造"
4. 语气设定:"使用正式礼貌的学术语气"
DON'T(避免做法):
1. 不要写过长:"你是...(500字角色描述)"——超过 200 字 AI 容易忽略细节
2. 不要自相矛盾:"回答要详细...但每句不超过 10 字"
3. 不要覆盖模型安全机制——尝试让 AI 绕过安全限制会被拒绝
易错场景与面试考点
易错场景一:占位符未填充导致原始模板发给 AI
// ❌ 错误:忘记填充 {major}
PromptTemplate template = new PromptTemplate("推荐{major}专业的课程");
// 忘记调用 template.add("major", "计算机科学")
Prompt prompt = template.create();
// AI 收到:"推荐{major}专业的课程" → 回复"请问你指的是什么专业?"
问题分析
PromptTemplate.create() 不会对未填充的占位符报错或跳过——它会将原始 {variable} 文本原封不动发送给 AI,造成奇怪的对话体验。
// ✅ 正确:确保所有占位符已填充
PromptTemplate template = new PromptTemplate("推荐{major}专业的课程");
template.add("major", major); // 从方法参数获取
Prompt prompt = template.create();
防御性编程建议:在模板创建前检查所有必填参数。
public Prompt createPrompt(String templateStr, Map<String, Object> params) {
PromptTemplate template = new PromptTemplate(templateStr);
params.forEach((k, v) -> {
if (v == null || v.toString().isBlank()) {
throw new IllegalArgumentException("参数 " + k + " 不能为空");
}
template.add(k, v);
});
return template.create();
}
面试高频题
Q1:PromptTemplate 和 String.format 有什么区别?
PromptTemplate 基于 Antlr 的 StringTemplate 引擎,支持更丰富的占位符语法和集合迭代,且与 Spring AI 的消息体系集成。String.format 只能做简单的 %s 替换,不适用于复杂的 RAG 场景。
Q2:RAG 场景中如何组织 PromptTemplate 以减少幻觉?
关键三点:①明确约束"只基于提供的上下文回答";②要求引用来源("注明来源页码");③明确兜底策略("如果文档中没有相关信息,请说'未找到'")。
本章小结
- PromptTemplate 使用
{variable}占位符语法,template.add(key, value)填充 - 与 RAG 配合时,将向量检索结果拼接到
{context}占位符中 - 未填充的占位符不会报错,会把原始
{xxx}发给 AI,需要防御性编码 - System Prompt 遵循"角色 + 边界 + 格式 + 兜底"四要素
- 模板让一次编写、多场景复用成为可能