Spring AI Prompt 实战:把“提示词”做成可维护的工程资产
- 别再把 prompt 写成一坨字符串——用 Spring AI 的 Prompt/Message/PromptTemplate,把提示词变成可复用、可测试、可版本化的“工程资产”。
- 适用读者 & 前置知识:Spring Boot 后端开发;能写 Controller/Service;了解一点大模型对话即可。
1. 这节解决的真实问题是什么
- 你要做“可维护的提示词体系”:一个业务 30 个接口,prompt 到处散落、改一处影响十处,还没法回归测试;这时候你需要模板化和资源化。
- 你要做“有规矩的对话”:客服分诊、工单生成、报告结构化输出——必须区分 system/user/assistant/tool 角色,否则很容易跑偏、越权。
- 你要控成本与上限:RAG/长上下文一上来就塞很多内容,直接触发 token 上限或成本爆炸;需要“精简有效信息”的工程策略。 ([Home][1])
2. 官方文档里讲了什么
- Prompt 不是字符串,而是一组带角色的 Message + ChatOptions:
Prompt作为容器承载List<Message>与请求选项;每条消息有角色(MessageType)和内容,组合成更细腻的对话上下文。(API Overview / Prompt) ([Home][1]) - Message 自带 metadata:
Message除了 content,还有 metadata map;你可以把 traceId、业务标识等放进去做链路审计/调试(模型不一定“理解”metadata,但你的系统能用)。(API Overview / Message) ([Home][1]) - 角色(Roles)决定“谁在立规矩、谁在提需求、谁在返回结果”:System 负责约束行为与风格;User 是用户输入;Assistant 是模型输出;Tool 用于返回工具调用结果。(Roles) ([Home][1])
- PromptTemplate 是提示词工程的核心组件:用
TemplateRenderer把变量替换进模板;默认实现是StTemplateRenderer,基于 StringTemplate,引擎默认用{}识别变量。(PromptTemplate) ([Home][1]) - 分隔符可改,尤其当你要在 prompt 里放 JSON:默认
{}容易和 JSON 冲突,官方建议改为< >等分隔符。(Using a custom template renderer) ([Home][1]) - PromptTemplate 不止生成字符串,还能生成 Message 或 Prompt:它实现了多组接口,既能
render()出字符串,也能createMessage(model)或create(model, options)直接构造 Prompt。(PromptTemplate) ([Home][1]) - 支持把 prompt 放到资源文件里:可以用
Resource(classpath 文件)承载 system prompt,避免把长提示词塞在 Java 代码里。(Using resources instead of raw Strings) ([Home][1]) - Prompt engineering 的基本构成:指令(Instructions)+ 外部上下文(External Context)+ 用户输入(User Input)+ 输出指示(Output Indicator);并提醒“要求 JSON 也不一定严格遵守”。(Prompt Engineering / Creating effective prompts) ([Home][1])
- Tokens 是成本与能力边界:输入输出都计 token;模型有 token 上限(context window),超了就不处理;响应 metadata 会包含 token 用量,适合做成本监控。(Tokens) ([Home][1])
- 定位类比很实用:官方把 prompt 处理类比为 Spring MVC 的“View”(含占位符替换),基础类比 JDBC;
ChatModel像 JDBC 核心,ChatClient像JdbcClient并可结合 Advisor 做更高层能力。(Prompts intro) ([Home][1])
3. 动手10分钟
下面用一个最典型的落地:“带 system 规矩 + 模板化变量 + 返回文本”。你只要项目里已经有一个可用的ChatModel(通过任意模型 Starter 自动装配)即可。
3.1 依赖/配置(示意)
- Spring Boot Web
- 任意 Spring AI ChatModel 的 Starter(OpenAI/Anthropic/Ollama/…),确保容器里能注入
ChatModel
这一节不绑定某一家模型,避免你环境不一致跑不通;核心是 Prompt API 的用法。
3.2 关键代码(Controller/Service)
Service:用 PromptTemplate 组装 Prompt,然后chatModel.call(prompt)
@ServicepublicclassPromptDemoService{privatefinalChatModelchatModel;publicPromptDemoService(ChatModelchatModel){this.chatModel=chatModel;}publicStringjoke(Stringadjective,Stringtopic,Stringname,Stringvoice){// user message(文档示例:PromptTemplate + create(Map) + call) :contentReference[oaicite:12]{index=12}PromptTemplateuserTpl=newPromptTemplate("Tell me a {adjective} joke about {topic}");MessageuserMessage=userTpl.createMessage(Map.of("adjective",adjective,"topic",topic));// system message(文档示例:SystemPromptTemplate 创建 system role message) :contentReference[oaicite:13]{index=13}StringsystemText=""" You are a helpful AI assistant. Your name is {name} You should reply with your name and in the style of a {voice}. """;SystemPromptTemplatesystemTpl=newSystemPromptTemplate(systemText);MessagesystemMessage=systemTpl.createMessage(Map.of("name",name,"voice",voice));Promptprompt=newPrompt(List.of(userMessage,systemMessage));ChatResponseresp=chatModel.call(prompt);// 这里按你使用的 ChatModel 实现取结果;示例保持简洁returnresp.getResult().getOutput().getContent();}}Controller:对外暴露一个接口
@RestController@RequestMapping("/demo")publicclassPromptDemoController{privatefinalPromptDemoServiceservice;publicPromptDemoController(PromptDemoServiceservice){this.service=service;}@GetMapping("/joke")publicStringjoke(@RequestParamStringadjective,@RequestParamStringtopic,@RequestParam(defaultValue="Neo")Stringname,@RequestParam(defaultValue="professional")Stringvoice){returnservice.joke(adjective,topic,name,voice);}}3.3 运行与验证步骤
curl"http://localhost:8080/demo/joke?adjective=funny&topic=database&name=Jeff&voice=standup"预期:返回内容会包含Jeff的自称,并带“standup 风格”倾向(system role 设定起作用)。([Home][1])
3.4 常见报错 1–2 个及修复
报错:模板渲染失败 / 变量找不到
- 原因:模板里
{name}{voice}这类占位符没传值;或你把 JSON 大括号当模板变量了(见坑 1)。 - 修复:补齐 Map 参数;或改分隔符。 ([Home][1])
- 原因:模板里
现象:system 明明写了规矩,但输出仍然跑偏
- 原因:角色混用、把 system 写进 user;或 system 与 user 的拼装顺序混乱。
- 修复:严格用
SystemPromptTemplate/system role message,别“糊”在 user 文本里。 ([Home][1])
4. 生产化版本:真正能上线的改造
4.1 日志与链路追踪(traceId)+ 关键指标(token)
- Message 支持 metadata map,你可以把
traceId / bizId / userId(hash)放进去做全链路关联。 ([Home][1]) - Token 用量会出现在响应 metadata 中,建议直接采集成指标(按接口/模型/租户维度聚合),用来做成本与容量预警。 ([Home][1])
4.2 成本控制(提示词瘦身 + 上下文窗管理)
文档明确:模型有 token 上限(context window),超了不处理;而且输入输出都计费。 ([Home][1])
落地建议(实践):
- 统一做“prompt 预算”:system 固定部分、RAG 引用块、用户输入各占多少 token;超预算就截断或降级策略。
- 对重复问题做缓存(尤其是固定模板 + 固定上下文的场景)。
4.3 安全与合规(脱敏、审计、prompt 注入防护)
Prompt engineering 小节强调“外部上下文”和“输出指示”的重要性,并提醒输出格式可能不严格。 ([Home][1])
落地建议(实践):
- 进入模型前先做脱敏(身份证/手机号/地址等),输出后再二次审计。
- 对“外部上下文”(RAG 文档、系统提示词)做白名单与版本管理,避免被用户输入“覆盖规矩”。
4.4 多环境配置与密钥管理
- 把 prompt 资源文件放在
classpath:/prompts/,随版本发布;敏感 key 走 Secret 管理,避免落盘(实践)。 - 文档支持用
Resource读取提示词文件,天然适合做“按环境替换”:dev 用简版 system prompt,prod 用严格版。 ([Home][1])
4.5 回归测试与评测(eval / gold set)
Prompt 是“代码”,就该有测试:同一组输入,期望输出结构/关键词/禁用词是否满足。
做法(实践):
- 为每个关键 prompt 建一个小型 gold set(20~50 条),每次改动跑回归;
- 把失败样本留档,形成“提示词变更记录 + 影响面”。
5. 你一定会踩的坑
坑 1:你在 prompt 里写 JSON,结果{}被模板当成变量
现象:模板渲染时报“找不到变量”,或者 JSON 被替换得面目全非。
原因:默认模板变量用
{},和 JSON 语法冲突;官方明确建议换分隔符,如< >。 ([Home][1])排查顺序:
- grep 你的 prompt 资源/字符串里是否包含
{} - 定位哪些是 JSON,哪些是模板占位符
- grep 你的 prompt 资源/字符串里是否包含
修复方案:使用自定义 renderer,把分隔符改为
<>。 ([Home][1])小测试验证:写一个单测渲染模板,断言渲染后的 JSON 片段保持原样、且占位符都被正确替换。
坑 2:system 规矩写得很漂亮,但你其实放错了角色
- 现象:要求“只输出 JSON”,结果模型照样输出解释性文本。
- 原因:Roles 的意义就在于“谁在立规矩”;把规矩放在 user 里,约束力更弱且更容易被用户输入覆盖。(Roles) ([Home][1])
- 排查顺序:打印最终 Prompt 的 message 列表(type + content 前 100 字),确认 system/user 分离。
- 修复方案:system 统一用 system role message;用户输入只进 user role。
- 小测试验证:构造一个恶意输入“忽略以上规则”,看 system role 是否仍能稳定约束输出(至少在你允许的范围内)。
坑 3:Prompt 越写越长,最后不是报错就是贵到肉疼
现象:接口耗时变长、偶发失败;或成本不可控。
原因:token 上限决定 context window;超出上限不处理;输入输出都计费,响应 metadata 可拿到 token 统计。(Tokens) ([Home][1])
排查顺序:
- 记录每次请求 prompt 的长度(字符/估算 token)
- 从响应 metadata 读 token 用量,按接口聚合看趋势 ([Home][1])
修复方案:
- 把“外部上下文”变成可控的片段(只塞必要信息);
- 对用户输入做长度限制;
- system prompt 拆分为“固定规则 + 可选模块”,按场景拼装(实践)。
小测试验证:压测 50 条长输入,断言 token 不超过阈值、失败率在可控范围内。
坑 4:把 prompt 写在代码里,团队协作会越来越痛
- 现象:改一行提示词要发版;review 看不清;多人冲突频繁。
- 原因:prompt 本质是长文本资产;官方支持用
Resource直接加载文件,天然适合版本管理与分层组织。 ([Home][1]) - 修复方案:system prompt、few-shot 示例、输出格式约束全部资源化,按目录拆分。
- 小测试验证:启动时校验资源文件是否存在、占位符是否都能渲染(缺变量直接 fail fast)。
6. 参考
原文链接:https://docs.spring.io/spring-ai/reference/api/prompt.html
StringTemplate(文档提到默认模板引擎来源):https://www.stringtemplate.org(用于理解
{}占位符与分隔符配置) ([Home][1])