Prompt 工程实战:五要素框架与 Spring AI 模板化落地
Prompt 不是随便写一句话发给模型就完事------它直接决定了大模型能力的上限。本文从一个真实的 Code Review 场景出发,演示"差 Prompt"与"好 Prompt"的效果差距,提炼出 Prompt 五要素框架(角色、任务、约束、格式、示例),并通过 Spring AI 的多种模板化手段将其工程化落地。面向有 Spring Boot 基础、正在用 Spring AI 做大模型应用开发的后端工程师。
一、Prompt 为什么决定模型能力上限
很多人对 Prompt 的第一印象是"随便写两句话丢给 AI"。但在真实项目中你会发现,同一个模型、同一段输入,Prompt 写法不同,输出质量可以差出一个数量级。
1.1 一个 Code Review 的对比实验
假设有这样一段代码:
java
for (int i = 0; i < 1000; i++) {
User user = userDao.findById(ids.get(i));
user.setName("test");
userDao.save(user);
}
Prompt A(随手写):
帮我看这段代码有没有问题。
模型返回:循环里 findById 如果返回 null,后续 setName 会抛空指针。然后就没了------只挖出一个问题。
Prompt B(精心设计):
你是一个资深 Java 工程师,专注于代码质量和性能优化,有 10 年实战经验。请 review 这段代码,重点检查:1. 性能问题 2. 潜在 Bug 3. 最佳实践违反 4. 代码风格问题。每个问题标注严重程度:严重 / 中等 / 建议。
模型返回:N+1 查询问题(严重)、空指针风险(严重)、硬编码问题(中等)、方法命名不规范(建议)------挖出四个问题,还带了分级和修改建议。
1.2 差距产生的根本原因
大模型的本质是一个概率生成器------它什么都知道,但不知道你需要什么。这跟一个刚入职的技术大牛很像:能力很强,但不了解业务上下文,你给的指令越模糊,他越容易"自由发挥"。
一个好的 Prompt 本质上在做三件事:
| 动作 | 作用 | 对应上面的例子 |
|---|---|---|
| 建立上下文 | 激活模型特定知识领域 | "你是资深 Java 工程师" |
| 明确目标 | 给模型一份工作清单 | "重点检查性能、Bug、最佳实践、风格" |
| 约束输出 | 规定格式,减少噪音 | "标注严重程度:严重/中等/建议" |
理解了这三件事,就可以引出更系统化的方法论了。
二、Prompt 五要素框架
告别"凭感觉写 Prompt",用工程化思维来拆解。一个高质量的 Prompt 由五个要素组成:
2.1 角色(Role)------激活特定知识域
角色的作用是告诉模型"用什么身份来思考"。不同的角色设定会激活模型不同的知识区域和表达风格。
最佳配方:职业身份 + 专业方向 + 经验背景
你是一个资深 Java 后端工程师,有 10 年 Spring Boot 开发经验,专注于高并发系统架构和性能优化。
注意:写"你是全中国最厉害的 Java 大神"这种虚的没有用。模型需要的是有具体方向和经验维度的锚点,不是夸张修辞。
同一个问题,不同角色的回答深度完全不同:
| 无角色 | 角色 = 初学者讲师 | 角色 = 性能优化专家 |
|---|---|---|
| "数据库索引是一种数据结构......"(教科书式回答) | "想象一本书的目录,有目录直接翻到,没目录就得一页页找" | "索引本质是 B+Tree / Hash 结构,将全表扫描 O(n) 降至 O(log n),关键在于减少回表次数" |
2.2 任务(Task)------给模型工作清单
任务是五要素中唯一一个必填项 。写法公式:动词 + 对象 + 期望结果。
❌ 处理一下这段代码
✅ Review 这段代码,找出潜在的空指针异常,给出具体位置和修复方案
拆步骤效果更好------模型会严格按步骤推进,输出更完整:
第一步:判断代码中是否存在空指针风险
第二步:提取性能瓶颈点
第三步:给出每个问题的修复建议
2.3 约束(Constraint)------限定边界,防止跑偏
不加约束的模型就像一匹脱缰的马:你问苹果好不好吃,它能给你写一篇苹果产地分布的论文。
常见的约束类型:
| 约束类型 | 示例 |
|---|---|
| 范围约束 | 遇到与产品无关的问题,回复"超出服务范围" |
| 长度约束 | 回答控制在 200 字以内,代码不超过 30 行 |
| 知识边界 | 不确定的内容说"我不确定,建议查阅文档",不要编造 |
| 风格约束 | 用口语化/学术/幽默的风格回答 |
最后一条尤其重要:不告诉模型"不知道就说不知道",它一定会编一个答案出来。在生产环境中,编造是最危险的行为。
2.4 格式(Format)------控制输出结构
当模型的输出要被下游代码解析时,格式约束就变成了刚需:
- JSON 格式:告诉模型按指定结构输出 JSON,不要包含额外内容
- Markdown 格式:表格、列表、代码块
- 纯文本格式:不要 Markdown 标记,输出自然流畅的中文(语音播报场景)
2.5 示例(Example)------用范例消除歧义
遇到以下情况时,示例比文字描述更高效:
- 输出格式特殊,语言描述不清
- 需要特定风格或语气
- 分类任务中类别边界模糊
- 模型反复理解错意图
单示例(One-Shot):
将代码注释翻译成英文。
示例输入:获取用户列表
示例输出:Get user list
请翻译:根据ID查询订单
多示例(Few-Shot):
判断评论所属问题类型:
- "快递三天还没到,太慢了" → 物流问题
- "收到商品有划痕,和图片不一样" → 商品质量
- "客服态度很差,问了半天没解决" → 服务问题
请判断:"包装都坏了,里面东西也破了"
2.6 五要素的优先级
并非所有场景都需要写满五个要素。根据场景做取舍:
| 场景 | 必须 | 建议 | 可选 |
|---|---|---|---|
| 简单问答 | 任务 | 约束 | 角色、格式、示例 |
| 智能客服 | 角色、任务、约束 | 格式 | 示例 |
| 数据提取 | 任务、格式 | 约束 | 角色、示例 |
| 内容创作 | 任务、格式 | 角色、示例 | 约束 |
| 代码生成 | 任务、约束 | 角色、格式 | 示例 |
三、Prompt 工程 vs 微调:什么时候够用
在动手写代码之前,先回答一个常见困惑:Prompt 工程和模型微调该选哪个?
| 需求 | Prompt 能搞定? | 说明 |
|---|---|---|
| 特定风格回答 | 能 | System Prompt 直接约束 |
| 输出 JSON | 能 | 格式约束 + 结构化输出 |
| 公司私有知识 | 挂 RAG | 不需要微调,检索增强即可 |
| 始终用特定语言 | 能 | System Prompt 约束 |
| 掌握行业术语(少量) | 能 | 通过示例注入 |
| 掌握行业术语(大量) | 需要微调 | Prompt 塞不下 |
| 学会全新任务形式(复杂) | 需要微调 | Few-Shot 覆盖不了 |
结论:大多数场景 Prompt 工程就够了,迭代成本低、见效快。优先 Prompt,不够再考虑 RAG,最后才上微调。
四、Spring AI 中的 Prompt 工程化实战
理论到此为止,下面全部是代码。Spring AI 提供了三种 Prompt 模板化手段,从简单到灵活逐级演进。
4.1 硬编码 Prompt------快速验证
最直接的方式:system 和 user 消息都写在代码里。
java
@RestController
public class CodeReviewController {
private final ChatClient chatClient;
public CodeReviewController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/review")
public String codeReview(@RequestParam String code) {
return chatClient.prompt()
.system("""
你是一个资深 Java 工程师,正在做 code review。
找出代码中的:
1. Bug(包括潜在的运行时错误)
2. 性能问题
3. 不符合 Java 最佳实践的写法
输出格式:
每个问题单独列出,格式为:
【问题类型】问题描述
原代码:...
建议修改:...
""")
.user("请 review 这段代码:\n" + code)
.call()
.content();
}
}
这里的 System Prompt 包含了角色(资深 Java 工程师)、任务(找 Bug/性能/最佳实践)、格式(问题类型+描述+建议),三个要素一次到位。
优点 :简单直接,适合原型验证。
缺点:Prompt 散落在 Controller 里,改一个字就要重新编译部署。
4.2 内联模板变量------一套模板覆盖多场景
Spring AI 的 ChatClient 原生支持 {variable} 占位符,运行时动态填充:
java
@Service
public class InlineTemplateService {
private final ChatClient chatClient;
public InlineTemplateService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String ask(String role, String domain, String concept) {
return chatClient.prompt()
.system(s -> s.text("你是一个 {role},擅长 {domain} 领域的问题。")
.param("role", role)
.param("domain", domain))
.user(u -> u.text("请解释 {concept} 的工作原理")
.param("concept", concept))
.call()
.content();
}
}
调用时传不同参数,一个接口覆盖所有场景:
GET /inline/ask?role=DBA&domain=数据库&concept=索引
GET /inline/ask?role=架构师&domain=微服务&concept=服务熔断
同样的思路用在 Code Review 上:
java
public String codeReview(String language, String code) {
return chatClient.prompt()
.system(s -> s.text("""
你是一个资深 {language} 工程师,做 code review。
找出 Bug、性能问题、代码风格问题,每个问题注明严重程度(高/中/低)。
""")
.param("language", language))
.user(u -> u.text("请 review 这段代码:\n```\n{code}\n```")
.param("code", code))
.call()
.content();
}
传 language=Java 它是 Java 工程师,传 language=Python 它就是 Python 工程师。角色和任务都实现了参数化。
4.3 PromptTemplate + 外部文件------Prompt 与代码彻底解耦
当 Prompt 变长、需要非开发人员(比如产品经理)维护时,把 Prompt 抽到外部文件是更好的选择。
第一步 :在 src/main/resources/prompts/ 下创建模板文件 code-review.st:
你是一个资深 {language} 工程师,有 {years} 年开发经验。
请 review 以下代码,找出:
1. Bug 和潜在风险
2. 性能优化点
3. 代码风格问题
代码:
{code}
请用中文回答,格式:每个问题独立列出,标注严重程度(高/中/低)。
第二步 :Service 层通过 PromptTemplate 加载:
java
@Service
public class CodeReviewService {
private final ChatClient chatClient;
public CodeReviewService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String review(String language, String code) {
PromptTemplate template = new PromptTemplate(
new ClassPathResource("prompts/code-review.st")
);
Prompt prompt = template.create(Map.of(
"language", language,
"years", "10",
"code", code
));
return chatClient.prompt(prompt)
.call()
.content();
}
}
第三步:Controller 只负责接参数和调 Service:
java
@RestController
@RequestMapping("/template")
public class TemplateCodeReviewController {
private final CodeReviewService codeReviewService;
public TemplateCodeReviewController(CodeReviewService codeReviewService) {
this.codeReviewService = codeReviewService;
}
@PostMapping("/review")
public String review(
@RequestParam(defaultValue = "Java") String language,
@RequestParam String code
) {
return codeReviewService.review(language, code);
}
}
这种方式的核心优势:
- Prompt 文件可以热更新(配合配置中心),不用重启应用
- 产品或运营人员可以直接编辑
.st文件,不碰 Java 代码 - 支持版本管理 :
code-review-v1.st、code-review-v2.st,做 A/B 测试时切换文件名即可
4.4 defaultSystem------全局角色 + 约束注入
对于某个 ChatClient 实例的所有调用都需要遵守的规则,用 defaultSystem 一次性注入:
java
@Service
public class JavaTechService {
private final ChatClient chatClient;
public JavaTechService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是一个专业的 Java 技术助手。
职责:
- 回答 Java、Spring Boot、Spring AI 相关的技术问题
- 帮助用户 debug 代码
- 提供最佳实践建议
规则:
- 代码示例使用 Java 17+ 语法
- 回答简洁,不要过度解释
- 不确定的内容要说明,不要编造
- 非技术问题礼貌拒绝
输出格式:
- 使用 Markdown 格式
- 代码用代码块包裹
""")
.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
注意这段 defaultSystem 的结构------它把角色、任务、约束、格式四个要素全部写进了 System Prompt。后续每次调用只需要传 user 消息,模型就会在这套规则下工作。这是五要素框架在 Spring AI 中最典型的工程化落地方式。
4.5 PromptTemplate 用于翻译场景
再看一个翻译场景,展示 PromptTemplate 处理纯用户消息(不区分 system/user)的用法:
java
@Service
public class TranslateService {
private final ChatClient chatClient;
public TranslateService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String translate(String text, String targetLanguage) {
PromptTemplate template = new PromptTemplate("""
请将下面这段文字翻译成 {targetLanguage},
保持原文的语气和风格,不要意译:
{text}
""");
Prompt prompt = template.create(Map.of(
"targetLanguage", targetLanguage,
"text", text
));
return chatClient.prompt(prompt).call().content();
}
}
这里的约束是"保持原文语气和风格,不要意译"------简单的一句话就把模型限定在了直译路线上。
4.6 三种模板化方式对比
| 方式 | Prompt 存放位置 | 适用场景 | 可维护性 |
|---|---|---|---|
| 硬编码 | Java 代码中 | 原型验证、简单场景 | 低 |
| 内联模板变量 | Java 代码中(参数化) | 多场景复用 | 中 |
| PromptTemplate + 外部文件 | .st 资源文件 |
生产环境、需要非开发人员维护 | 高 |
五、常见踩坑与最佳实践
在实际项目中,以下几个问题出现频率最高。
5.1 任务描述太模糊
❌ 帮我优化这段代码
✅ 帮我优化这段代码的查询性能,只改查询逻辑,不改接口签名,使用 Java Stream API
不给方向,模型可能从可读性、性能、内存、命名各个角度都改一遍,对项目的改动超出预期。
5.2 所有内容塞进一条 User Prompt
❌ user: "你是一个代码助手,只回答技术问题,用中文回答,代码用 Java 17 语法。帮我解释 Spring AOP 是什么。"
✅ system: "你是一个代码助手,只回答技术问题,用中文回答,代码用 Java 17 语法。"
user: "帮我解释 Spring AOP 是什么。"
System Prompt 和 User Prompt 的职责不同:system 管"你是谁、你怎么做",user 管"这次具体做什么"。混在一起会导致角色设定在多轮对话中丢失。
5.3 没有约束输出格式
让模型"分析文章优缺点",它可能给你写一篇散文。加上格式约束:
优点(3 条):
缺点(3 条):
总体评分(1-10):
改进建议(1 条):
输出立刻变得可解析、可对比。
5.4 过度依赖模型猜测意图
❌ 用中文写一篇 Spring Boot 的文章
✅ 用中文写一篇 Spring Boot 入门文章,2000 字左右,面向有 Java 基础的初学者,包含 3 个代码示例,语气轻松
字数、受众、深度、是否需要代码、语气风格------这些模型都能猜,但猜出来的一定不如你直接说清楚。
六、总结
本文的核心内容可以浓缩为三句话:
- Prompt 决定模型能力上限------不是模型不行,是你的指令不够精确。
- 五要素框架(角色、任务、约束、格式、示例)------把"凭感觉"变成可复制的工程方法。
- Spring AI 提供了从硬编码到外部模板文件的完整工具链------让 Prompt 管理融入正常的软件工程流程。
本系列后续内容
Prompt 工程解决了"怎么跟模型说话"的问题,但一个完整的 AI 应用远不止对话。后续将进入更深层的主题:
- Agent 智能体实战:让大模型具备自主规划和执行能力,从"被动回答"升级为"主动做事"
- MCP(Model Context Protocol)实战:标准化的工具调用协议,让 Agent 能够操作外部系统
- RAG 检索增强生成:让模型接入企业私有知识库,解决"模型不知道公司内部信息"的问题
敬请关注。