
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 ChatOptionsPrompt作为容器承载ListMessage与请求选项每条消息有角色MessageType和内容组合成更细腻的对话上下文。API Overview / Prompt ([Home][1])Message 自带 metadataMessage除了 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 放到资源文件里可以用Resourceclasspath 文件承载 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”含占位符替换基础类比 JDBCChatModel像 JDBC 核心ChatClient像JdbcClient并可结合 Advisor 做更高层能力。Prompts intro ([Home][1])3. 动手10分钟下面用一个最典型的落地“带 system 规矩 模板化变量 返回文本”。你只要项目里已经有一个可用的ChatModel通过任意模型 Starter 自动装配即可。3.1 依赖/配置示意Spring Boot Web任意 Spring AI ChatModel 的 StarterOpenAI/Anthropic/Ollama/…确保容器里能注入ChatModel这一节不绑定某一家模型避免你环境不一致跑不通核心是 Prompt API 的用法。3.2 关键代码Controller/ServiceService用 PromptTemplate 组装 Prompt然后chatModel.call(prompt)ServicepublicclassPromptDemoService{privatefinalChatModelchatModel;publicPromptDemoService(ChatModelchatModel){this.chatModelchatModel;}publicStringjoke(Stringadjective,Stringtopic,Stringname,Stringvoice){// user message文档示例PromptTemplate create(Map) call :contentReference[oaicite:12]{index12}PromptTemplateuserTplnewPromptTemplate(Tell me a {adjective} joke about {topic});MessageuserMessageuserTpl.createMessage(Map.of(adjective,adjective,topic,topic));// system message文档示例SystemPromptTemplate 创建 system role message :contentReference[oaicite:13]{index13}StringsystemText You are a helpful AI assistant. Your name is {name} You should reply with your name and in the style of a {voice}. ;SystemPromptTemplatesystemTplnewSystemPromptTemplate(systemText);MessagesystemMessagesystemTpl.createMessage(Map.of(name,name,voice,voice));PromptpromptnewPrompt(List.of(userMessage,systemMessage));ChatResponserespchatModel.call(prompt);// 这里按你使用的 ChatModel 实现取结果示例保持简洁returnresp.getResult().getOutput().getContent();}}Controller对外暴露一个接口RestControllerRequestMapping(/demo)publicclassPromptDemoController{privatefinalPromptDemoServiceservice;publicPromptDemoController(PromptDemoServiceservice){this.serviceservice;}GetMapping(/joke)publicStringjoke(RequestParamStringadjective,RequestParamStringtopic,RequestParam(defaultValueNeo)Stringname,RequestParam(defaultValueprofessional)Stringvoice){returnservice.joke(adjective,topic,name,voice);}}3.3 运行与验证步骤curlhttp://localhost:8080/demo/joke?adjectivefunnytopicdatabasenameJeffvoicestandup预期返回内容会包含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 关键指标tokenMessage 支持 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 promptprod 用严格版。 ([Home][1])4.5 回归测试与评测eval / gold setPrompt 是“代码”就该有测试同一组输入期望输出结构/关键词/禁用词是否满足。做法实践为每个关键 prompt 建一个小型 gold set2050 条每次改动跑回归把失败样本留档形成“提示词变更记录 影响面”。5. 你一定会踩的坑坑 1你在 prompt 里写 JSON结果{}被模板当成变量现象模板渲染时报“找不到变量”或者 JSON 被替换得面目全非。原因默认模板变量用{}和 JSON 语法冲突官方明确建议换分隔符如 。 ([Home][1])排查顺序grep 你的 prompt 资源/字符串里是否包含{}定位哪些是 JSON哪些是模板占位符修复方案使用自定义 renderer把分隔符改为。 ([Home][1])小测试验证写一个单测渲染模板断言渲染后的 JSON 片段保持原样、且占位符都被正确替换。坑 2system 规矩写得很漂亮但你其实放错了角色现象要求“只输出 JSON”结果模型照样输出解释性文本。原因Roles 的意义就在于“谁在立规矩”把规矩放在 user 里约束力更弱且更容易被用户输入覆盖。Roles ([Home][1])排查顺序打印最终 Prompt 的 message 列表type content 前 100 字确认 system/user 分离。修复方案system 统一用 system role message用户输入只进 user role。小测试验证构造一个恶意输入“忽略以上规则”看 system role 是否仍能稳定约束输出至少在你允许的范围内。坑 3Prompt 越写越长最后不是报错就是贵到肉疼现象接口耗时变长、偶发失败或成本不可控。原因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.htmlStringTemplate文档提到默认模板引擎来源https://www.stringtemplate.org用于理解{}占位符与分隔符配置 ([Home][1])