Prompt 工程实战:五要素框架与 Spring AI 模板化落地
Prompt 工程实战:五要素框架与 Spring AI 模板化落地
Prompt 不是随便写一句话发给模型就完事——它直接决定了大模型能力的上限。本文从一个真实的 Code Review 场景出发,演示"差 Prompt"与"好 Prompt"的效果差距,提炼出 Prompt 五要素框架(角色、任务、约束、格式、示例),并通过 Spring AI 的多种模板化手段将其工程化落地。面向有 Spring Boot 基础、正在用 Spring AI 做大模型应用开发的后端工程师。
一、Prompt 为什么决定模型能力上限
很多人对 Prompt 的第一印象是"随便写两句话丢给 AI"。但在真实项目中你会发现,同一个模型、同一段输入,Prompt 写法不同,输出质量可以差出一个数量级。
1.1 一个 Code Review 的对比实验
假设有这样一段代码:
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 消息都写在代码里。
@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} 占位符,运行时动态填充:
@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 上:
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 加载:
@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:
@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 一次性注入:
@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)的用法:
@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 检索增强生成:让模型接入企业私有知识库,解决"模型不知道公司内部信息"的问题
敬请关注。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)