Prompt工程五要素与Spring AI模板化落地:让提示词成为可复用代码资产
1. 为什么我把 Prompt 工程拆成五要素做 AI 应用开发这一年多我最大的感受是模型能力本身在快速拉平真正拉开差距的反而是你怎么把话说清楚。同样的 Qwen 模型有人一句话就能拿到结构化 JSON有人写一整段还是得到一堆废话。差别不在模型在 Prompt。我是从乱写 Prompt 的坑里爬出来的。最早写 Prompt 全靠灵感今天加一句请用中文回答明天删掉半段介绍效果时好时坏。后来我把手头几十个 Prompt 全部摊开做了个对比发现真正在起作用的维度其实只有五个角色、任务、上下文、约束、输出格式。把自然语言收敛到这五个维度之后Prompt 才开始变得可以管理、可以复用、可以测试。这也是我在团队里推广时坚持用框架这个词的原因。五要素不是某种神秘口诀而是一张检查清单。每次写新 Prompt我按这五个维度逐个过一遍缺哪个补哪个时间久了就成了一种肌肉记忆。这篇内容适合两类人一是正在用 Java 做 AI 应用、但 Prompt 还停留在手写阶段的同学二是已经在用 Dify、Coze 这类可视化平台编排 Prompt、想往代码化迁移的同学。我会把五要素的拆解逻辑、Spring AI 里的模板化写法、以及一个从 Dify 工作流迁移到 Java 代码的真实案例完整讲一遍。整个过程不绕弯子直接给结论和代码。1.1 五要素是怎么提炼出来的最初是我在对比不同 Prompt 在同一个模型上的表现时发现的规律。当时拿三个业务场景做实验摘要类、抽取类、问答类。每个场景写三版 Prompt——一版极简一版带角色和格式说明一版把背景和约束全写进去。跑完的结果很有意思完整版未必最好但缺要素的版本一定有稳定的短板。少了输出格式说明的回答解析起来一定费劲少了上下文做背景补充的抽取任务漏字段的几率高得吓人。把这几轮实验的变量归纳一下就成了后来的五要素框架。1.2 五要素框架适合谁如果你只是在网页里跟模型聊聊天完全不需要这套框架。但如果你在写代码、做自动化、或者把 Prompt 集成进业务流程五要素几乎是刚需。尤其是 Java 后端团队你们迟早要把 Prompt 从调试工具里的实验品变成生产环境里的基础配置——那时候一个结构清晰的模板比一百个经验丰富的老手管用。我自己在团队里推这套框架时最明显的效果是代码评审时讨论的重点变了从这段提示词写得对不对变成了这个 Prompt 参数该用什么值。后者才是工程化该有的讨论方式。2. 五要素逐个拆解每句话到底在控制什么这一节我把五个要素一个一个讲透。重点不是定义而是为什么这样写和怎么写才有效。每个要素我都见过正反两个极端的写法理解起来会很快。2.1 角色设定给模型一个明确的岗位角色要素的本质是给模型一个视角。模型训练时见过海量文本不同语境激活的是不同能力。让模型扮演资深 Java 工程师和让模型扮演零基础新手它给出的代码评审意见是两种完全不同的东西。我常用的角色要素长这样你是一位有五年经验的运维工程师擅长诊断 Linux 服务器上的性能问题。请根据下面的监控数据给出排查建议。注意角色描述不能太虚。光说你是专家没用要加上擅长什么这相当于告诉模型调用哪个知识子集。在五要素里我通常把角色放在最前面因为它决定了后续所有内容的解读方式。2.2 任务描述把需求翻译成可执行指令任务要素是五要素里最容易写错的部分。很多人把任务和背景混在一起让模型自己从一堆文字里猜你到底要我干什么。正确的写法是让任务必须是动词开头的祈使句。比如分析这个订单就是含糊任务模型可能给你输出一段描述改成从订单文本中提取客户名、商品名、金额并判断订单状态输出质量立刻提升。有个小技巧是给任务加一个明确的完成标志比如提取并整理成表格比提取信息更容易得到规整的结果。任务里还可以顺带说明处理顺序比如先分类再对每类给出原因但这要和约束要素做好区分。2.3 上下文管理信息取舍比堆砌更重要上下文是最容易走极端的要素。一种极端是不给背景模型全靠猜测输出结果就是瞎编另一种极端是拼命塞信息把一万字文档原文丢进去模型被无关信息干扰反而分不清重点。我的经验是上下文只保留完成任务所需的最小集。比如做工单分类任务需要的是工单描述和产品线分类规则而不是整个知识库。另外上下文要给结构化的暗示比如用以下是本期需要分类的工单工单内容这种形式明确告诉模型哪部分是要处理的数据。在 Spring AI 模板里上下文往往是唯一一个频繁变动的变量把它单独作为一个参数位是值得的。2.4 约束条件用边界对抗幻觉约束条件是整个框架里防错价值最大的一个。模型天生喜欢自由发挥不限定它就发挥给你看。约束一般包括三类第一类是硬性规则比如只能使用给定的工具列表第二类是禁选项比如如果上下文中没有信息请明确回答未找到不要编造第三类是风格和长度比如回答不超过 200 字不要使用专业术语。我最看重的是禁选项这一类因为 AI 幻觉的典型表现就是编造不存在的细节。用一句不要编造加上信息不足时请说明能大幅减少胡说八道的概率。提示约束不是越多越好太多互相矛盾的约束会降低模型的执行效果。我的原则是每条约束都要能对应到一个具体的风险点写不出一条现实风险的约束就删掉。2.5 输出格式让结果直接能用输出格式是我后期才重视起来的。最早写 Prompt 从不规定输出格式每次拿到的结果都要靠正则表达式兜底清洗维护成本极高。后来改成在 Prompt 里明确写请以 JSON 格式输出字段如下清洗代码直接删了一半。格式说明要足够具体最好把字段名、类型、甚至示例都写出来。不要只写输出 JSON模型猜不透你要的是数组还是对象、是下划线命名还是驼峰。在 Spring AI 里这一步可以交给 BeanOutputConverter 生成结构化描述效果比自己手写格式说明书要稳定得多这个后面会讲到。到这里五要素的单点用法讲完了。但单点会用和能在系统里用是两回事。你手写 Prompt 可以靠感觉可一旦要接进 Java 服务、被多个业务方复用就必须把 Prompt 变成一种结构化资产。下一步就是把它们装进 Spring AI 的模板体系里做成可传参、可测试、可版本管理的代码资产。3. Spring AI 模板化落地把五要素变成代码资产3.1 为什么选 Spring AI三个理由和一条建议Java 生态里接大模型的路子很多最原始是直接 HttpClient 调 API灵活但代码量大后来有人封装一层 SDK省事但跟 Spring 的整合全靠自己再后来是 Spring AI。我选 Spring AI 有三个理由第一它把 ChatModel、Prompt、PromptTemplate 这些概念做成了 Spring 风格的对象可以像配置数据源一样配置模型供应商第二它内置了输出解析能力省掉一大坨 JSON 处理的胶水代码第三项目本身迭代快跟 Spring Boot 3.x 配合得不错。当然Spring AI 的 API 变动也比较频繁我从 1.0 用到 2.0.1 经历了好几次方法签名调整。我的建议是不要追最新版本选一个稳定的版本把官方文档的迁移说明读一遍再动手。如果你用的是阿里云百炼平台可以直接走 Spring AI Alibaba 项目Maven 依赖引入 spring-ai-starter-model-dashscope就能配置 Qwen 系列模型。社区里偶尔有人问这个项目是不是停了判断活跃与否最靠谱的方式是去看官方仓库的提交记录和 release 时间线而不是听转述。适配项目跟着上游版本走节奏慢不代表不维护有疑问直接翻 GitHub release notes 最准确。3.2 模板结构设计把五要素变成占位符把五要素装进模板核心思路是结构固定变量动态。模板里放的是五要素的框架文字具体内容全部用占位符表示。我习惯把模板字符串放到 resources 目录下方便在不重新编译代码的情况下微调提示词。项目里常用的模板骨架长这样你是 {role}。 你的任务是{task}。 参考以下上下文信息{context}。 必须遵守的约束{constraints}。 输出要求{outputFormat}。这个骨架解决了两个问题一是每个调用方的 Prompt 都是同一个结构不会有人把角色和任务的顺序写反二是调试时可以只替换一个变量方便做 A/B 对比。注意模板里的占位符语法是{变量名}但模板引擎对特殊字符敏感变量值里如果带大括号要先转义这个我在后面的常见问题里会细说。3.3 核心代码实现一个极简的模板服务先添加依赖。以 Spring AI 加百炼为例需要引入 BOM 和对应模块。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId /dependency然后在配置文件里配好 api-key 和默认模型百炼平台的 key 从控制台申请模型名一般用 qwen-plus 或 qwen-max。spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus定义一个参数对象把五要素固定成五个字段。这里我用 record 类型简洁也方便生成不可变对象。public record PromptParams( String role, String task, String context, String constraints, String outputFormat ) { public MapString, Object toVariables() { return Map.of( role, role, task, task, context, context, constraints, constraints, outputFormat, outputFormat ); } }然后是渲染和调用的核心类。ChatClient 是 Spring AI 1.0 之后主推的调用入口用起来比直接操作 ChatModel 顺手。Service public class PromptService { private final ChatClient chatClient; public PromptService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String call(PromptParams params) { String template loadTemplate(); // 从 resources/prompts/five-elements.st 加载 return chatClient.prompt() .user(u - u.text(template).params(params.toVariables())) .call() .content(); } }有个细节值得注意user 消息里传入的变量名必须和模板里的占位符完全一致少一个字符都会导致渲染异常。我在变量名拼错这种事上栽过跟头后来干脆让参数对象统一提供 toVariables() 方法键名集中管理问题才根治。loadTemplate() 你可以直接从 classpath 读取也可以配合 Spring 的 ResourceLoader 做环境隔离。4. 实战把 Dify 工作流迁移到 Spring AI Java 代码4.1 迁移前的工作流拆解现在不少团队先用 Dify 把流程跑通再往 Java 服务里迁移。Dify 是可视化编排跑 demo 很快但一旦要嵌入现有业务系统、做精细权限控制、或者写单元测试可视化节点反而成了瓶颈。我最近把一个客服质检的 Dify 工作流迁移到了 Spring AI正好拿这个例子讲。原工作流大概长这样一个知识检索节点先查订单表一个 LLM 节点写死了一段很长的提示词最后是一个参数提取节点把大模型输出转成结构化字段。拆开看真正有价值的只有 LLM 节点里的提示词——但它在 Dify 里是一段没有版本管理的文本没法做单元测试也没法在代码里复用。4.2 五要素模板设计从一段话到结构化参数原提示词是这么写的你是一个客服质检专家请分析订单信息如果有问题请指出并给建议。三个要素都有但任务和约束糊在一起输出格式完全没定义。我把提示词拆成了五要素版本角色是资深客服质检专员任务是分析订单对话记录输出问题类型、严重程度、处理建议上下文是订单对话原始文本加上售后政策关键条款摘要约束是只基于给定上下文分析、不得引入外部信息、信息不足时输出 UNKNOWN、建议必须可执行输出格式则直接复用 BeanOutputConverter 生成的 JSON Schema 描述。4.3 结构化输出与调用实现我用一个 OrderAnalysisResult 记录类型定义输出结构字段有 issueType、severity、suggestion、confidence。配合 BeanOutputConverter模型输出可以直接映射成对象不需要再用正则抠字段。public record OrderAnalysisResult( String issueType, String severity, String suggestion, double confidence ) {} Service public class OrderAnalysisService { private final ChatClient chatClient; private final BeanOutputConverterOrderAnalysisResult converter; public OrderAnalysisService(ChatClient.Builder builder) { this.chatClient builder.build(); this.converter new BeanOutputConverter(OrderAnalysisResult.class); } public OrderAnalysisResult analyze(String orderText, String policyText) { String format converter.getFormat(); PromptParams params new PromptParams( 资深客服质检专员, 分析下面的订单对话提取问题类型、严重程度、处理建议。, 订单对话 orderText \n售后政策摘要 policyText, 只基于给定上下文分析不得引入外部信息信息不足时输出 UNKNOWN建议必须可执行。, format ); String content chatClient.prompt() .user(u - u.text(loadTemplate()).params(params.toVariables())) .call() .content(); return converter.convert(content); } }迁移完最直观的变化有两个一是之前 Dify 里没法跑的单元测试现在用一个 Mock ChatModel 就能在 CI 里跑二是后续换模型供应商的时候业务代码一行不用改。这个过程本质上就是把靠感觉调出来的文本升级成了有结构、有版本、可测试的代码资产。如果你也在做类似迁移建议按这个顺序来先拆原提示词再定五要素最后写模板和解析层。5. 常见问题与排查技巧实录5.1 输出不稳定先分模板问题还是模型问题经常有人问同一个模板为什么上次好用下次就乱写我的排查方法很直接固定模板换模型和固定模型换模板交叉对比。如果在同一个模型下换了几种 Prompt 结构结果都飘那是模型的问题要调 temperature 或者换更强的型号如果只有提示词风格变化时结果才波动那就是模板里某些表述在诱导不稳定优先检查约束要素和输出格式要素。另外temperature 这个参数值得单独说一句它控制的是采样的随机性不是模型创造性。绝大多数业务场景0.2 到 0.4 就够了拉到 0.8 以上你会得到一个话痨。5.2 模板渲染异常的经典来源第一个是占位符拼写不一致。Spring AI 默认模板语法用{变量名}变量名区分大小写。第二个是变量值里本身包含大括号比如你塞了一段 JSON 作为上下文模板引擎会把 JSON 的花括号当成占位符去解析。解决办法是提前对参数值做转义或者把模板改成引用的方式比如用上下文见 session.xxx这种间接引用让模型从预设的 system 消息里拿内容。第三种常见情况是模板文件编码问题——中文注释或者全角标点在某些环境下会乱码写模板文件时统一用 UTF-8 基本能避开。5.3 JSON 输出解析失败的三个高频原因BeanOutputConverter 转换失败我遇到最多的是三种模型返回了 markdown 代码块包着的 JSON模型多输出了解释文字把 JSON 夹在中间模型输出里存在非法转义字符。处理方案是在模板里明确写只输出 JSON不要使用代码块不要附带任何说明文字。如果还是不行就写一层兜底解析用正则把第一个{到最后一个}之间的内容截出来再做转换。这个兜底逻辑虽然丑但生产环境里值得留着它能救你很多次。5.4 模型供应商怎么选百炼、OpenAI 还是本地 Ollama选供应商我的建议是优先看数据合规和成本而不是模型效果。国内业务优先走阿里云百炼的 Qwen 系列在 Spring AI 里通过 dashscope starter 接入很顺。部署环境不允许出网的场景用 Ollama 跑本地模型Spring AI 也支持。OpenAI 适合做海外业务或者需要最强模型打底的时候。强调一句Spring AI 抽象层做得不错换供应商多数时候只改配置代码层面影响很小。这也是我推荐在 Spring AI 基础上做模板化的原因——你的 Prompt 资产不会绑定在某个具体模型上。5.5 排查问题速查表这部分是我在支持同事时常用的速查表直接放出来大家参考。症状最可能原因处理办法输出总是重复同一句话temperature 过低或模板太死板提高 temperature 到 0.4 以上检查约束是否互相冲突结果经常漏字段输出格式描述不够具体改用 BeanOutputConverter把字段类型写清楚模板变量没被替换变量名拼写或大小写不一致统一使用参数对象的 toVariables() 方法输出夹杂解释文字缺少只输出结果约束模板末尾显式声明不要多余文字模型编造数据缺少信息不足请说明约束在约束要素里加 UNKNOWN 机制6. 最后补几个我自己踩过的坑先说版本。Spring AI 版本迭代确实快网上教程用的 API 新旧不一照着抄容易编译不过。我的做法是把官方文档里对应版本的迁移说明先通读一遍再动手写代码。第二个坑是模板文件里的变量别乱用特殊符号。我有一次把一段 SQL 模板塞进 Prompt 参数里花括号被模板引擎解析得支离破碎排查了一下午。方案是参数化之前先做转义或者干脆避免在参数值里携带大括号。第三个心得是 Prompt 模板一定要纳入版本管理并且要过代码评审。模板换个标点符号线上效果都可能变它跟普通配置一样值得被认真对待。6.1 版本、转义和代码评审把模板当作代码来对待是这套方案能不能在团队里落地生根的关键。我现在项目里的模板文件都放在src/main/resources/prompts下跟 Controller、Service 一样打 commit、走 review改动时明确标注影响范围。因为模板直接决定了模型行为比普通配置面向的用户更广一旦改坏影响是线上的。建议团队里指定一个人专门维护模板基线其他人提 PR 修改时说明改动动机最好附带一两条对比测试的输出样例。这样坚持两个月模板质量会肉眼可见地变稳。6.2 一个让我少踩很多坑的习惯最后分享一个我坚持了很久的习惯每次上线新的 Prompt 模板先在测试环境用一小批真实数据做回归把输出格式和历史版本做 diff。这个习惯帮我挡掉过不少看起来没改什么、实际输出全变的坑。如果你也在做 Java 端的 AI 应用建议从今天开始把手头的 Prompt 按五要素理一遍再挑一个简单场景落到 Spring AI 模板里。这套组合拳打下来你的 AI 功能会从靠感觉调试变成可测试、可回归、可复用。等你在生产环境跑一个月再回头看最初那些靠猜的 Prompt会觉得自己当初浪费了不少好模型。