首页 文章 精选 留言 我的

精选列表

搜索[HTTP/3],共10000篇文章
优秀的个人博客,低调大师

《Spring AI 2.0实战》第3招 Prompt不只是字符串

## 01、痛点 凌晨两点,运营截图过来:技术客服助手把“退款多久到账”答成了“我们支持 7×24 小时工单服务”。用户连问三次,它始终没有给出时间。我是老李,被拉进群时第一反应是看模型是不是抽风。结果顺着日志一层层追,问题不在模型,而在 Controller 里一段 23 行的字符串。 那段代码大致长这样: ```java String prompt = "你是技术客服助手,请友好回答用户问题。注意:不要回答价格、退款、合同。如果用户问退款,请转人工。回答请引用文档编号。当前用户问题是:" + userQuestion + ",请开始回答。"; ``` 问题非常典型:角色设定、回答边界、引用要求、用户输入全被拼在一个 Java 表达式里。为了支持“退款问题转人工”,一个 if 又往里塞了几个三元表达式。后来为了控制回答长度,又追加了一句条件拼接。最后谁都不敢动这段代码,因为不知道哪里藏着一个空格会影响输出。 这就是硬编码提示词的典型失控路径。它看起来只是一段字符串,实际上已经承担了业务规则、产品策略和交互边界。字符串没有结构、没有版本、没有测试,改一次就要发一次版,多人协作时冲突几乎必然。更麻烦的是,当模型效果变差时,你根本无法快速判断是模板变了、参数错了,还是模型本身行为漂移。 现在 Spring AI 在 Java 项目里越来越常见,很多团队还停留在“把提示词当普通字符串拼”的阶段。我觉得有必要把这个问题讲透,并且给出一个可落地的治理方式。 ## 02、是什么 Spring AI 对 Prompt 的抽象,和很多人过去的理解不太一样。它把 Prompt 看成应用输入模型,而不是“请回答问题”这么一句话。一个完整的 Prompt 可以由多条消息组成,每条消息有明确角色: - System:系统消息,设定角色、能力边界、回答规范; - User:用户消息,承载具体问题或上下文; - Assistant:助手消息,承载历史回答,用来做多轮上下文。 这意味着,真正进入模型的不是一段不可拆分的文本,而是一个消息列表。System 部分可以独立维护,User 部分可以只放动态业务上下文。这个区分非常关键。 Spring AI 还提供了 PromptTemplate。它和 MVC 的 View 非常像:模板负责稳定结构,变量负责动态填充。比如一个提示词模板可以写成“你是{role},请根据{context}回答{question}”,模板本身不会因为具体用户问题而变。 再类比 SQL。早期的 Java 项目常常用字符串拼接 SQL,后来发现占位符和预编译语句才能同时解决可维护性与安全性。PromptTemplate 的角色就类似 SQL 中的占位符:固定语义的部分提前定义,动态参数由程序注入。模板负责稳定规则,变量负责动态业务上下文。 还有一点容易被忽略:Spring AI 支持默认 System Prompt。项目可以配置一个全局默认系统提示词,比如“你是企业内部的正式助手,回答需简洁、准确”。这样即使某个具体场景没有提供 System 消息,也能保持基础人设一致。 ## 03、为什么 为什么不能把所有业务规则拼到 Controller 字符串里?因为 Controller 的职责是协调请求,不是维护内容策略。把提示词拼在这里,实际上是把“内容资产”塞进了“流程代码”。 第一个问题是变更成本。产品要调整客服助手的回答边界,比如从“不回答退款”改成“可以回答退款时间,但不回答具体金额”。如果规则在 Java 字符串里,开发必须改代码、提交、评审、发布。如果规则在模板资源里,产品配置变更可以走配置中心或独立发布,甚至可以做灰度。 第二个问题是可测试性。硬编码字符串无法单独验证。你想断言“系统提示词中必须包含引用要求”,只能启动整个应用去抓请求日志。模板化之后,一个单元测试就可以渲染模板并断言关键规则存在。 第三个问题是版本追溯。字符串常量没有版本号,在 Git 里只能看到某一行被修改,却无法知道这次修改对应哪个策略版本。模板文件可以带版本号、变更记录、负责人信息,像接口契约一样管理。 第四个问题是协作冲突。同一个 Controller 里,客服助手、摘要生成、分类器三套提示词挤在一起,改了 A 影响 B,合并时冲突不断。拆成独立模板文件后,负责不同场景的人可以并行修改。 最后,Prompt 也有类似注入风险。如果用户输入直接拼进系统指令,用户可以通过特殊内容诱导模型忽略原规则。模板和参数分离后,用户输入只会被当作变量值填充,不再直接参与规则拼接。 ## 04、架构 我建议的目录结构很简单: ``` src/main/resources/prompts/ tech-support/ system.st user.st metadata.properties ``` 其中 system.st 是系统提示词模板,user.st 是用户提示词模板,metadata.properties 记录版本号和更新说明。 加载层可以定义一个 PromptCatalog 组件,负责读取资源文件并缓存解析结果。渲染层使用 Spring AI 的 PromptTemplate,把 Map 参数传给模板,生成 Message 对象。调用层只关心组装消息列表,不感知模板内容。 架构流程如下: ``` Controller 接收用户问题 → PromptService 加载模板 → 渲染 System 与 User 消息 → 组装 List → 调用模型 → 返回回答 ``` 这个链路里,模板是静态资源,参数是动态上下文。默认 System Prompt 可以在 PromptService 初始化时统一注入。比如先读取全局默认配置,再根据场景覆盖。 版本管理可以放在 metadata.properties 里: ``` prompt.name=tech-support prompt.version=1.2.0 prompt.updated=2025-01-15 prompt.owner=platform-team ``` 这样每次模板变更都有一个可识别的基线,日志里也能打印当前使用版本,方便线上问题回溯。 ## 05、实战 下面用一个“技术客服助手”来演示。需求很明确:System 固定角色、回答边界和引用要求;User 只传入具体问题。 system.st 模板可以这样写: ``` 你是技术客服助手。 边界:只回答产品使用、故障排查、账号问题;不回答价格、退款时限、合同条款。 引用要求:涉及操作步骤必须给出对应文档编号或链接。 如果用户问题超出范围,回复:该问题需要人工客服,并引导提交工单。 ``` user.st 模板只保留一个占位符: ``` 当前用户问题:{question} ``` Java 侧加载逻辑如下: ```java @Component public class PromptCatalog { private final ResourceLoader resourceLoader; private final Map cache = new ConcurrentHashMap<>(); public String load(String path) { return cache.computeIfAbsent(path, p -> { try { Resource resource = resourceLoader.getResource(p); return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("failed to load prompt: " + p, e); } }); } } ``` 调用时使用 PromptTemplate: ```java String systemText = catalog.load("classpath:/prompts/tech-support/system.st"); String userText = catalog.load("classpath:/prompts/tech-support/user.st"); Message systemMessage = new PromptTemplate(systemText).createMessage(Map.of()); Message userMessage = new PromptTemplate(userText).createMessage(Map.of("question", question)); List messages = List.of(systemMessage, userMessage); ``` 版本号从 metadata.properties 读取,并打印到日志: ```java String version = catalog.loadVersion("tech-support"); log.info("using prompt version: {}", version); ``` 测试可以这样写: ```java @Test void systemPromptShouldContainBoundaryRules() { String systemText = catalog.load("classpath:/prompts/tech-support/system.st"); assertThat(systemText).contains("不回答价格"); assertThat(systemText).contains("引用要求"); assertThat(systemText).contains("人工客服"); } ``` 这样,规则是否完整不再靠人眼,而是进入 CI。 ## 06、洞见 这个改造做完后,最大的感受不是代码变干净了,而是我们对 Prompt 的治理思路变了。过去我们总把 Prompt 当成临时文本,今天改一句,明天加一句,改完就忘。现在它像数据库 Schema 一样,有版本、有评审、有测试。 为什么在 Java 项目里尤其要这样做?因为 Java 项目通常有成熟的配置管理体系、发布流程和代码评审文化。如果 Prompt 继续散落在代码常量里,这些体系完全用不上。反过来,把 Prompt 迁到模板资源里,它就能复用配置中心、CI、审计和回滚能力。 这和 MVC 的历史非常相似。早期 JSP 里 Java 代码和 HTML 混在一起,后来才拆出 View 层和模板引擎。现在很多项目里 Java 代码和 Prompt 混在一起,迟早也要拆。SQL 也走过同样的路,从字符串拼接走到预编译语句和 ORM。不是技术退步,而是工程化成熟。 模板并不是限制灵活性。真正稳定的规则本来就不该每天变,真正变化的是用户问题、上下文和业务参数。把它们分开后,变化点收敛到变量,稳定点固化到模板。这样每次调整都可以被 diff、被评审、被统计。 同时,测试不能只测代码逻辑,也要测提示词资产。比如每次模板变更,CI 必须断言渲染结果包含必要边界词。线上模型效果变差时,版本号能告诉我们是不是昨天那次提示词调整导致的。 ## 07、行动 如果今天就想动手,可以按下面几步来。 第一,盘点现有 Java 代码里的 Prompt 字符串。至少找出三个使用频率高、规则比较固定的场景,比如客服助手、摘要生成、分类判断。 第二,为每个场景建立模板资源文件。System 放角色、边界、引用要求;User 只放动态参数。不要把业务规则继续写在 Controller 里。 第三,给每个模板加版本号。可以在文件头部加注释,或者用 metadata.properties。版本号必须可读、可打印。 第四,补测试。把“渲染结果必须包含某条规则”变成断言,纳入 CI。这样模板变更至少能守住下限。 第五,建立变更记录。模板的修改和代码一样走评审,说明为什么改、影响哪个场景。线上日志打印版本号,问题回溯时先看版本。 这个清单不需要一步到位,但至少可以先做到“模板化 + 版本号 + 一条测试”。这三件事做完,失控的概率会大幅下降。 ## 08、小结 回头再看那次凌晨故障,本质不是模型笨,而是我们把一个生产级助手的内容策略塞进了 Controller 字符串里。Spring AI 给了我们消息模型和模板能力,剩下的就是工程纪律。 模板负责稳定规则,变量负责动态业务上下文。这句话可以作为判断标准:如果一段 Prompt 里既有规则又有具体问题,就说明该拆了。拆完之后,Prompt 从字符串变成资产,团队才可能真正管好它。 ## 09、链接与概念说明 - Spring AI:面向 Java 生态的 AI 应用开发框架,提供消息模型、模板和数据接入能力。 - Message:Prompt 的最小组成单位,包含角色和文本内容。常见角色有 System、User、Assistant。 - System Message:系统消息,设定助手角色、回答边界、输出规范。 - User Message:用户消息,承载具体问题或业务上下文。 - Assistant Message:助手消息,可作为历史对话上下文传给模型。 - PromptTemplate:提示词模板,支持占位符和参数渲染,和 MVC View、SQL 占位符思路一致。 - 默认 System Prompt:项目级别的统一系统提示词,可为所有场景兜底。 - 版本号规范:建议至少包含场景名、版本号、更新时间、负责人。版本号可打印到日志,方便线上回溯。 延伸阅读建议从 Spring AI 官方文档的提示词与消息章节开始,再对比 MVC 模板引擎和 PreparedStatement 的演进思路,会更容易理解为什么会走到模板化这一步。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册