
提示工程这个词这两年几乎被说烂了但真正能把它的来龙去脉讲清楚、并且落到代码里的人并不多。我见过太多团队模型选型讨论得热火朝天接口调通了Demo 也跑起来了结果一上真实业务就翻车——输出格式飘忽不定、多轮对话记不住上下文、模型该调工具的时候不调、不该调的时候乱调。问题往往不在模型本身而在于提示词这一层根本没设计过全靠“感觉写一句”。这篇内容我想做的是把 Prompt Engineering 从概念到落地完整走一遍重点不是教你背几个“魔法咒语”而是让你理解提示词背后的结构逻辑并且能把它和 Spring AI、Spring Boot 这套 Java 生态真正接起来。Function Calling 怎么配合提示词、Agent 场景下提示词怎么分层、多轮对话里系统提示怎么保持稳定这些都会讲到。适合已经能跑通大模型接口、但输出质量不稳定想系统化改进的后端同学也适合刚接触提示工程想建立完整认知的开发者。1. 提示工程到底在解决什么问题1.1 模型不是不会是你没说清楚很多人第一次用大模型会有个错觉这玩意儿时灵时不灵。同一个问题换个说法结果天差地别。于是有人得出结论——提示词是玄学。这个判断其实偷懒了。大模型的本质是一个根据上文预测下文的概率系统。你给它的输入就是它推断“你想要什么”的全部依据。它没有读心术也没有你脑子里的业务背景。你写“帮我总结一下”它不知道总结给谁看、要多长、保留哪些信息、用什么语气。它只能按训练数据里最常见的“总结”模式来猜。猜对了你觉得它聪明猜错了你觉得它笨。提示工程要做的就是把这个“猜”的空间压缩到最小。你不是在跟一个聪明人对话你是在给一个能力极强但完全没有你业务上下文的执行者写任务说明书。任务说明书越精确执行结果越可控。这个认知转变非常关键后面所有的技巧本质上都是围绕“如何写出精确的任务说明书”展开的。1.2 提示工程的四个可控维度把提示词拆开看能控制的其实就四件事我习惯把它们叫做四个维度。第一个是角色与身份。你让模型扮演谁会直接影响它的用词、知识调用倾向和判断标准。让它扮演“资深法律顾问”和扮演“科普作者”同一个法律问题的回答风格完全不同。角色设定不是装饰它是在给模型划定一个输出分布范围。第二个是任务与目标。你要它做什么做到什么程度。这里最容易犯的错是任务描述太宽泛。“分析这段代码”和“找出这段代码中可能导致空指针的三处风险并按严重程度排序”后者才是可执行的任务。第三个是约束与格式。输出多长、用什么结构、必须包含什么、绝对不能出现什么。格式约束是提示工程里性价比最高的投入因为它是可验证的——你写“输出 JSON”模型给你 JSON你就能用代码解析你写“随便说说”那你就得自己从一堆自然语言里抠信息。第四个是示例与参考。给一两个输入输出样例比写一百字描述都管用。这叫 Few-shot后面会专门讲。这四个维度不是孤立的实际写提示词的时候它们是交织在一起的。但你在调试的时候一定要能分清这次改动动的是哪个维度否则就是瞎调。1.3 为什么现在必须系统学而不是靠感觉早几年模型能力弱提示词技巧确实有点像玄学因为模型本身不稳定你怎么调都那样。但现在不一样了模型能力上来了提示词的边际收益变得非常明显。同一批任务结构化提示词和随手写的提示词准确率差距能到百分之二三十。这个差距在 Demo 里看不出来在生产环境里就是事故率。更现实的原因是现在大模型已经进入工程化阶段了。Spring AI 这类框架把模型调用标准化了Function Calling 让模型能操作外部系统Agent 让模型能自主规划多步任务。这些能力全都依赖提示词来驱动。你提示词写得糊Function Calling 的参数就填不对Agent 的规划就跑偏。提示工程不再是“调优技巧”而是工程链路里的一个必要环节。2. 提示词的结构化写法从一句话到一套模板2.1 系统提示与用户提示的分工在 Spring AI 里消息是分角色的最常见的是 System、User、Assistant 三种。System 消息就是系统提示User 消息是用户输入。很多人把两者混着写把所有要求都塞进用户消息里这是第一个要改的习惯。系统提示负责的是“长期稳定的规则”角色设定、输出格式、行为边界、全局约束。用户提示负责的是“这一次的具体任务”具体问题、具体数据、具体指令。打个比方系统提示是员工手册用户提示是今天的工作单。员工手册不会每天变工作单每次都不同。这样分工的好处很直接。系统提示写一次所有请求复用既省 token 又保证一致性。用户提示只放变量部分逻辑清晰。如果你把所有东西都塞用户消息里每次请求都要重复一大段规则token 浪费不说还容易因为用户输入里的内容干扰规则执行。在 Spring AI 里配置系统提示大概是这样ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem( 你是一名严谨的技术文档助手。 回答必须基于事实不确定的内容要明确说明。 输出使用 Markdown代码块标注语言类型。 ) .build();defaultSystem设的就是系统提示后续每次调用都会自动带上。这个设计就是鼓励你把稳定规则和动态输入分开。2.2 用分隔符把指令和数据隔开这是最容易被忽略、但效果立竿见影的一个技巧。当你的提示词里既有指令又有用户提供的数据时模型有时候会分不清哪些是指令、哪些是数据。尤其是数据里如果包含类似指令的句子模型可能被带跑。解决办法是用明确的分隔符把数据包起来。常见的有三引号、XML 标签、Markdown 代码块。我个人最推荐 XML 标签因为结构清晰模型对标签的识别也很稳。请根据下面的用户反馈判断情感倾向正面/负面/中性。 feedback {{user_feedback}} /feedback 只输出一个词不要解释。用feedback标签把变量包起来模型就知道标签里是待处理数据标签外是指令。这个习惯在数据来源不可控的场景下几乎是必须的比如用户评论分析、日志解析、文档问答。2.3 输出格式约束让结果可被程序消费如果你要把模型输出接到下游代码里格式约束就不是可选项是必选项。最常用的两种是 JSON 和固定字段的文本。JSON 的好处是结构化、可解析。但要注意光写“输出 JSON”不够模型可能给你带 Markdown 代码块标记的 JSON也可能字段名对不上。更稳的写法是把 schema 写清楚输出严格的 JSON不要包含任何额外文字或代码块标记。 格式如下 { sentiment: positive | negative | neutral, confidence: 0.0 到 1.0 之间的数字, reason: 一句话说明判断依据 }这里有个实操经验即使你写了“不要包含代码块标记”模型偶尔还是会加。所以下游解析的时候最好先做一次清洗把可能的json 和去掉再解析。不要假设模型 100% 听话工程上永远要有兜底。另外Spring AI 本身提供了结构化输出的支持可以配合提示词一起用。提示词负责告诉模型“要什么结构”框架负责把结果映射成 Java 对象。两者配合比单靠一边更稳。2.4 Few-shot给例子比讲道理管用有些任务你用文字描述很难说清楚但给两个例子模型立刻就懂了。这就是 Few-shot 的价值。比如你要模型把非结构化的地址拆成省市区与其写一堆规则不如直接给样例输入北京市海淀区中关村大街1号 输出{province:北京市,city:北京市,district:海淀区,detail:中关村大街1号} 输入浙江省杭州市西湖区文三路100号 输出{province:浙江省,city:杭州市,district:西湖区,detail:文三路100号} 输入{{address}} 输出样例的选择有讲究。要覆盖边界情况比如直辖市、没有区的情况、地址里有特殊字符的情况。样例不用多两到五个通常就够太多反而占 token 且可能让模型过度模仿样例的细节。还有一个细节样例的格式必须和你期望的输出格式完全一致。你样例里 JSON 有空格模型输出也会有空格你样例里字段顺序是 A、B、C模型大概率也按这个顺序。所以样例本身就是一种强约束写的时候要当成正式输出来对待。3. 提示工程和 Function Calling 怎么配合3.1 Function Calling 的本质是提示词驱动的决策Function Calling 听起来很高级但它的底层机制其实和提示工程紧密相关。模型并不是真的“调用”了函数它是根据你提供的工具描述判断当前该不该用工具、用哪个工具、参数填什么然后输出一个结构化的调用意图。真正执行函数的是你的代码。所以工具描述写得好不好直接决定模型能不能正确选择工具。工具描述本质上就是一种特殊的提示词。它要回答三个问题这个工具是干什么的、什么时候该用、参数是什么含义。在 Spring AI 里定义工具大概是这样public class WeatherService { Tool(description 查询指定城市的当前天气。当用户询问天气、气温、是否下雨时使用此工具。) public String getWeather( ToolParam(description 城市名称例如北京、上海) String city) { // 实际查询逻辑 return ...; } }注意description的写法。它没有写“天气查询函数”而是写了“当用户询问天气、气温、是否下雨时使用此工具”。这是在告诉模型触发条件而不只是功能名称。这个区别很关键模型是靠语义匹配来决定用不用工具的触发条件描述得越具体误触发和漏触发就越少。3.2 工具描述里的常见坑第一个坑是描述太笼统。比如“处理订单”模型不知道什么情况下该用。改成“当用户要查询订单状态、修改收货地址、申请退款时使用”触发边界就清楚了。第二个坑是多个工具职责重叠。如果你有两个工具都能“查询信息”模型就会犹豫甚至选错。解决办法是在描述里明确区分场景比如一个写“查询实时数据”一个写“查询历史归档数据”。第三个坑是参数描述缺失。模型不知道参数该填什么格式就会瞎填。日期参数要写清楚“格式 YYYY-MM-DD”枚举参数要把可选值列出来。这些细节直接决定调用成功率。第四个坑是工具太多。一次给模型几十个工具它的选择准确率会下降。实践中如果工具数量多要么做分组要么在系统提示里先做一轮意图路由缩小候选工具范围。3.3 用系统提示约束工具使用策略除了工具本身的描述系统提示里也可以加一段工具使用策略。比如你可以使用工具来获取实时信息。 规则 1. 涉及实时数据天气、股价、库存时必须调用工具不要凭记忆回答。 2. 涉及常识和通用知识时直接回答不要调用工具。 3. 如果工具返回错误向用户说明情况不要编造结果。这段策略的作用是给模型一个决策框架。尤其是第一条和第二条明确划出了“必须调”和“不要调”的边界能显著减少该调不调、不该调乱调的情况。第三条则是防止模型在工具失败时幻觉出一个结果这在生产环境里是必须防的。4. 多轮对话与 Agent 场景下的提示词管理4.1 多轮对话里系统提示为什么不能丢多轮对话最容易出的问题是“聊着聊着就忘了规矩”。第一轮你让它输出 JSON第三轮它开始输出自然语言了。原因通常是系统提示没有在每一轮都带上或者上下文被截断了。正确的做法是每一轮请求都把系统提示放在最前面然后按顺序拼接历史消息。Spring AI 的 ChatClient 配合 ChatMemory 可以自动管理这个过程但你要确认系统提示确实在每次请求里都生效。ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();这里defaultSystem保证系统提示每轮都在MessageChatMemoryAdvisor负责维护历史消息。两者配合模型既有稳定规则又有对话记忆。但要注意上下文长度。历史消息太长会挤占空间也可能让早期的不相关内容干扰当前任务。实践中要么限制保留的轮数要么对历史做摘要压缩。摘要本身也可以用模型来做把前 N 轮对话压缩成一段简短背景这样既保留信息又控制长度。4.2 Agent 场景下的提示词分层Agent 比普通对话复杂因为它要自主规划多步任务、调用多个工具、根据中间结果调整策略。这种场景下把所有要求写在一段提示词里会非常混乱。我的做法是分层。第一层是角色与总目标这个 Agent 是干什么的最终要达成什么。第二层是可用工具与使用规则有哪些工具什么场景用什么。第三层是执行流程约束先做什么后做什么什么时候该停下来问用户什么时候该自己决策。第四层是输出规范最终结果怎么呈现。分层的好处是调试的时候能定位问题。Agent 跑偏了你能判断是角色层没写清楚还是工具层描述有歧义还是流程层缺了约束。如果全糊在一起你只能整体重写效率极低。4.3 提示词版本管理这件事生产环境里提示词是会迭代的改一版效果可能变好也可能变差。如果没有版本管理出了问题你都不知道回滚到哪一版。我的建议是把提示词当代码管。具体做法是把提示词抽到配置文件或者独立的模板文件里不要硬编码在 Java 代码里。每次修改记录版本号和修改原因。有条件的话建一个小型的评测集每次改完提示词跑一遍看关键指标有没有退化。这个投入在项目初期看起来多余但一旦业务量上来它能帮你避免很多线上事故。Spring AI 支持从资源文件加载提示词模板配合参数填充既方便管理又方便复用。模板文件用.st后缀放在 resources 目录下代码里通过PromptTemplate加载。这样产品和运营也能参与提示词调整不用每次都改代码重新发版。5. 提示工程在 Spring Boot 项目里的落地实践5.1 把提示词从代码里抽出来前面提到提示词要当代码管具体落地第一步就是抽离。硬编码在 Java 字符串里的提示词改一个字就要重新编译打包这在快速迭代阶段是灾难。推荐的结构是每个业务场景一个模板文件放在src/main/resources/prompts/下。比如sentiment-analysis.st、order-assistant.st。文件里用占位符表示变量代码里填充。你是一名电商客服助手。 user_input {userInput} /user_input 请判断用户意图输出 JSON {intent: 咨询 | 投诉 | 售后 | 其他, summary: 一句话概括}代码里这样加载Value(classpath:prompts/sentiment-analysis.st) private Resource promptResource; PromptTemplate template new PromptTemplate(promptResource); Prompt prompt template.create(Map.of(userInput, input));这样做的好处是提示词改动不需要动 Java 代码测试和运维都能独立调整。而且模板文件可以纳入 Git 管理改动有记录、可回滚。5.2 提示词效果的验证不能靠肉眼提示词改完你怎么知道变好了还是变差了靠手动试几条是不够的样本太少结论不可靠。你需要一个评测集。评测集不用很复杂准备二三十条有代表性的输入每条标注期望输出。每次改完提示词批量跑一遍统计准确率、格式合规率这些指标。这个流程可以写成一个单元测试或者独立的测试脚本。关键指标我一般看三个任务准确率结果对不对、格式合规率输出能不能被解析、稳定性同样的输入跑多次结果是否一致。第三个尤其重要生产环境最怕的就是同一个输入今天对明天错。如果稳定性差说明提示词约束不够模型在自由发挥。5.3 和业务系统对接时的边界处理模型输出接到业务系统里边界处理必须做。模型不是数据库它可能返回不存在的枚举值、超出范围的数字、格式对但语义错的内容。这些都要在代码层拦截。比如模型返回的 JSON 里有个confidence字段你期望是 0 到 1但模型可能返回 1.5 或者 high。解析的时候要做校验不合法就触发降级逻辑——要么重试要么走规则兜底要么直接报错让上游处理。还有超时和重试。模型调用是有延迟的网络也可能抖动。Spring Boot 里可以用 Resilience4j 或者 Spring Retry 做重试但要注意重试次数不能太多否则延迟叠加。一般重试一到两次配合超时控制就够了。5.4 成本与延迟的平衡提示词越长token 消耗越大延迟也越高。这不是让你把提示词写短而是要有意识地管理。系统提示里的规则如果有些是通用的、有些是特定场景的可以把通用的放系统提示特定的按需拼接而不是每次都全量带上。Few-shot 的样例也是成本大头。如果样例很长可以考虑只在复杂场景用简单场景用零样本。或者把样例压缩只保留最关键的示范信息。还有一个技巧是把稳定的长提示词利用模型的上下文缓存能力。有些模型服务对重复的前缀有缓存优化相同系统提示的多次请求能降低延迟和成本。这个具体要看所用服务的特性但设计上把稳定内容放前面、变化内容放后面是个通用原则。6. 几个我踩过的坑和对应的解法6.1 提示词里的否定指令经常失效你写“不要输出解释”模型还是给你加一句“好的以下是结果”。你写“不要编造”它该编还是编。否定指令的效果普遍弱于肯定指令。解法是把否定转成肯定。不说“不要输出解释”说“只输出结果本身”。不说“不要编造”说“如果信息不足输出‘无法确定’”。给模型一个明确该做什么的指令比告诉它不要做什么有效得多。6.2 中文提示词和英文提示词的差异同一个任务中文提示词和英文提示词的效果可能不一样。这不是绝对的取决于模型训练数据的分布。我的经验是涉及中文语义理解的任务比如中文情感分析、中文文本摘要中文提示词通常更自然涉及代码、结构化输出、逻辑推理的任务英文提示词有时候更稳。实际项目里不用纠结两种都试用评测集说话。如果团队英文没问题关键任务可以准备双语版本做对比。6.3 温度参数和提示词的关系温度参数控制输出的随机性。温度高输出多样但可能跑偏温度低输出稳定但可能死板。很多人调提示词的时候忘了温度这个变量结果改了提示词效果变差其实是温度不合适。经验值结构化输出、分类任务用低温度0 到 0.3创意生成、头脑风暴用高温度0.7 到 1.0一般对话 0.5 左右。调提示词的时候先把温度固定住只改提示词这样才能判断改动本身的效果。6.4 模型升级后提示词要重新验证模型版本更新是常事但新版本不一定对所有提示词都更友好。有时候新模型对格式的遵循更严格你原来那种模糊的写法就失效了有时候新模型更“聪明”会自作主张补充你没要求的内容。所以每次模型升级评测集必须重跑。不要假设升级一定变好用数据说话。如果发现退化先看是不是提示词需要适配新模型的特性再决定是改提示词还是回退版本。7. 从提示工程到提示架构的思维转变写到这里我想把最开始那个认知再往前推一步。提示工程这个词容易让人以为它是“写提示词的技巧”但真正做下来你会发现它更像是一种架构设计——你是在设计模型和业务系统之间的接口契约。这个契约包含几层模型扮演什么角色、接收什么输入、输出什么结构、什么情况下调用什么工具、异常怎么处理、结果怎么验证。这些内容一旦确定下来就应该是稳定的、可版本管理的、可测试的。提示词只是这个契约的文本表达形式。所以我的建议是不要把它当成一个“调参”的活而是当成接口设计来做。先想清楚输入输出契约再写提示词去实现这个契约最后用评测集验证契约有没有被遵守。这个顺序反过来先写提示词再想契约就会陷入反复试错、无法沉淀的循环。Spring AI 这类框架其实就是在帮你把契约的工程部分标准化——消息角色、工具定义、结构化输出、记忆管理这些都有现成的抽象。你要做的是把业务契约想清楚然后用提示词把它表达准确。框架负责管道你负责契约这个分工想明白了整个链路就顺了。最后分享一个我自己的习惯每接手一个新场景我会先手写十条真实输入手动跑一遍看模型裸奔的表现记录下它在哪些地方出错。这些错误就是提示词要重点约束的地方。先看问题再写提示词比先写提示词再找问题效率高得多。这个习惯看起来笨但每次都能帮我省下大量反复调试的时间。