提示词工程实践指南
和大模型打交道,提示词的质量直接决定输出效果。同样一个问题,换个问法,结果可能天差地别。
前面的章节聊过提示词的基础知识,这篇文章专门讲讲在Spring AI中怎么把这些技巧落地。
系统提示词与用户提示词
Spring AI遵循OpenAI的设计规范,把提示词分成两类:
- 系统提示词(System):设定AI的角色、行为准则、回答风格
- 用户提示词(User):具体的问题或指令
代码示例
在ChatClient中设置这两类提示词非常直观:
@RestController
@RequestMapping("/prompt")
public class PromptController {
private final ChatClient chatClient;
@GetMapping("/interview")
public Flux<String> mockInterview(@RequestParam String question) {
return chatClient.prompt()
// 系统提示词:定义角色
.system("你是一位资深的Java技术面试官,有10年面试经验。" +
"回答问题时要专业严谨,适当追问细节," +
"对模糊的回答要指出不足之处。")
// 用户提示词:具体问题
.user(question)
.stream()
.content();
}
}
两种设置方式
方式一:构建ChatClient时设置默认值
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是电商平台的智能导购助手," +
"擅长根据用户需求推荐商品," +
"回答要简洁实用。")
.build();
}
这样每次调用都会自动带上这个系统提示词。
方式二:调用时动态设置
// 调用时的system会覆盖默认设置
chatClient.prompt()
.system("你现在是一个严格的代码审查员")
.user("请审查这段代码:" + code)
.call()
.content();
注意:如果使用的是Prompt对象中的SystemMessage,行为会有所不同——它是追加而不是覆盖:
chatClient.prompt().system("...")— 覆盖默认的系统提示词- 通过
Prompt对象传入SystemMessage— 追加到现有系统提示词之后
使用错误的方式可能导致系统提示词叠加,产生意料之外的模型行为。
// 这种方式是追加,不是覆盖
Prompt prompt = new Prompt(
List.of(
new SystemMessage("补充说明:重点关注安全性问题"),
new UserMessage("分析这段代码")
)
);
Few-shot:示例教学法
Few-shot是提示词工程中非常实用的技巧:在提示词里给几个示例,让模型学会处理模式。
场景:用户输入改写
假设我们要做一个搜索query优化功能,把用户的随意输入改写成规范的搜索词:
@GetMapping("/rewrite")
public String rewriteQuery(@RequestParam String input) {
return chatClient.prompt()
.system("""
你是一个搜索词优化专家,负责将用户的随意输入改写成更精准的搜索词。
改写规则:
1. 修正错别字
2. 去掉口语化表达,提取核心关键词
3. 补充必要的限定词
参考示例:
输入:想买个笔记本,玩游戏用的,预算一万左右
输出:游戏笔记本电脑 10000元价位
输入:苹果手机最新款多少钱
输出:iPhone 15 Pro Max 价格
输入:有没有好看的连衣裙推荐啊
输出:连衣裙 女装 时尚款
请直接输出改写后的搜索词,不需要解释。
""")
.user(input)
.call()
.content();
}
通过几个示例,模型就能学会你期望的改写风格。
Few-shot(少样本示例)是提示词工程中最有效的技巧之一。与其用长篇文字描述期望的行为,不如直接给出输入-输出的示例对,让模型"看懂"你的意图。示例越典型,模型的表现越稳定。
场景:格式化输出
Few-shot也很适合教模型特定的输出格式:
@GetMapping("/extract")
public String extractInfo(@RequestParam String text) {
return chatClient.prompt()
.system("""
从文本中提取结构化信息,按JSON格式输出。
示例1:
输入:张三,男,1990年出生,在阿里巴巴做程序员
输出:{"name":"张三","gender":"男","birthYear":1990,"company":"阿里巴巴","role":"程序员"}
示例2:
输入:李四是腾讯的产品经理,女,85年的
输出:{"name":"李四","gender":"女","birthYear":1985,"company":"腾讯","role":"产品经理"}
请严格按照示例格式输出JSON,不要添加其他内容。
""")
.user(text)
.call()
.content();
}
指定输出格式
很多时候我们需要大模型按特定格式输出,方便后续程序处理。
直接在提示词中约定
@GetMapping("/product-list")
public Flux<String> generateProductList(@RequestParam String category) {
return chatClient.prompt()
.system("你是一个商品数据生成器")
.user("""
请为"%s"类目生成3个虚构的商品信息。
要求以JSON数组格式输出,每个商品包含以下字段:
- productId: 商品ID(字符串)
- name: 商品名称
- price: 价格(数字,单位元)
- description: 一句话描述
直接输出JSON,不要包含markdown代码块。
""".formatted(category))
.stream()
.content();
}
限制回答格式
有时候需要AI用特定的方式组织回答:
@GetMapping("/analyze")
public String analyzeIssue(@RequestParam String issue) {
return chatClient.prompt()
.system("""
你是一位技术问题分析专家。分析问题时请按以下结构回答:
【问题定位】
简要说明问题的本质
【可能原因】
用编号列出可能的原因
【排查步骤】
给出具体的排查步骤
【解决方案】
给出推荐的解决方案
""")
.user(issue)
.call()
.content();
}
思维链:让AI一步步思考
Chain-of-Thought(思维链)是一种让模型"展示推理过程"的技巧,对复杂问题特别有效。
基本用法
只需要加一句"请一步一步思考":
@GetMapping("/calculate")
public String calculate(@RequestParam String problem) {
return chatClient.prompt()
.system("你是一个数学老师,擅长解决应用题")
.user(problem + "\n\n请一步一步思考,展示推理过程,最后给出答案。")
.call()
.content();
}
更精细的控制
可以明确指定思考步骤:
@GetMapping("/design")
public String designSolution(@RequestParam String requirement) {
return chatClient.prompt()
.system("你是一位系统架构师")
.user("""
请为以下需求设计技术方案:
%s
请按照以下步骤分析:
步骤1:分析需求的核心目标
步骤2:识别技术挑战和约束条件
步骤3:列举可选的技术方案
步骤4:对比各方案的优缺点
步骤5:给出推荐方案和理由
""".formatted(requirement))
.call()
.content();
}
PromptTemplate:模板化管理
当提示词比较复杂,或者需要动态替换部分内容时,PromptTemplate就派上用场了。
基本使用
PromptTemplate使用{变量名}作为占位符:
@GetMapping("/recommend")
public Flux<String> recommend(@RequestParam String category,
@RequestParam String budget) {
PromptTemplate template = new PromptTemplate(
"请推荐{count}个{category}领域的产品,预算在{budget}以内。" +
"要求给出产品名称、特点和适用场景。"
);
// 填充变量
Prompt prompt = template.create(Map.of(
"count", "3",
"category", category,
"budget", budget
));
return chatClient.prompt(prompt)
.stream()
.content();
}
Builder模式
更优雅的写法是用Builder:
@GetMapping("/review")
public String codeReview(@RequestParam String code,
@RequestParam String language) {
Map<String, Object> variables = new HashMap<>();
variables.put("language", language);
variables.put("code", code);
PromptTemplate template = PromptTemplate.builder()
.template("""
请审查以下{language}代码,从以下几个方面给出建议:
1. 代码规范性
2. 潜在的bug
3. 性能问题
4. 安全隐患
代码:
```{language}
{code}
""") .variables(variables) .build();
return chatClient.prompt(template.create()) .call() .content(); }
### 自定义分隔符
默认用大括号`{}`作为占位符,如果和内容冲突,可以换成其他符号:
```java
PromptTemplate template = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.template("请介绍<topic>的核心概念")
.build();
String prompt = template.render(Map.of("topic", "微服务架构"));
配置文件外置提示词
实际项目中,把提示词硬编码在Java代码里不是好习惯。更好的做法是放到配置文件中统一管理。
创建.st模板文件
在resources目录下创建提示词模板文件,比如prompts/product-recommend.st:
请为用户推荐{category}类目的商品。
用户偏好:
- 预算范围:{budgetMin}到{budgetMax}元
- 使用场景:{scenario}
要求:
1. 推荐3-5个商品
2. 说明推荐理由
3. 给出购买建议
在代码中引用
@RestController
@RequestMapping("/shop")
public class ShopController {
// 注入模板文件内容
@Value("classpath:prompts/product-recommend.st")
private Resource promptTemplate;
private final ChatClient chatClient;
@GetMapping("/recommend")
public String recommend(@RequestParam String category,
@RequestParam int budgetMin,
@RequestParam int budgetMax,
@RequestParam String scenario) {
Map<String, Object> variables = Map.of(
"category", category,
"budgetMin", budgetMin,
"budgetMax", budgetMax,
"scenario", scenario
);
PromptTemplate template = PromptTemplate.builder()
.resource(promptTemplate)
.variables(variables)
.build();
return chatClient.prompt(template.create())
.call()
.content();
}
}
这样做的好处
- 便于维护:修改提示词不需要改代码、重新编译
- 职责分离:提示词可以由产品或运营人员调整
- 版本管理:提示词文件可以独立进行版本控制
- 多环境支持:不同环境可以用不同的提示词
最佳实践总结
经过这么多项目实践,总结几条提示词工程的经验:
- 角色设定要具体:说明职位、经验年限、擅长领域、回答风格,越具体越好
- 给出清晰的约束条件:字数、格式、风格、禁忌项都要明确说明
- 用示例消除歧义:当你觉得描述不清楚时,给个例子往往比长篇解释更有效
- 分步骤处理复杂任务:让模型按步骤思考,能显著提升复杂任务的质量
- 迭代优化:提示词不是一次就能写好的,先跑起来,看效果,不断调整
小结
这篇文章介绍了Spring AI中提示词工程的实践方法:
- 系统提示词和用户提示词的分工与配合
- Few-shot示例让模型学会特定模式
- 指定输出格式方便程序解析
- 思维链提升复杂问题的推理质量
- PromptTemplate实现模板化管理
- 配置文件外置提升可维护性
提示词工程是个需要积累的技能,多尝试、多总结,你的提示词会越写越好。
下一篇我们来聊聊结构化输出,看看怎么让大模型直接返回Java对象。