第2篇:《Prompt不是玄学:我把提示词放到了配置文件里》

承上:上一篇我们跑通了第一段AI对话,但看着代码里硬编码的那句"请用Java程序员的风格写一首五言绝句",我浑身难受------一个写了十年Spring Boot的人,怎么能忍受魔法字符串到处乱飞?

1. 问题场景:Prompt硬编码的"坏味道"

先回顾上一篇的代码:

java 复制代码
public String chat(String message) {
    return chatClient.prompt()
            .user(message)  // 用户的输入倒是动态的
            .call()
            .content();
}

看起来还好对吧?但真实业务场景是这样的:

java 复制代码
// 场景1:客服系统,需要给AI设定人设
.user("你是一个专业的客服人员,语气要温和有礼,如果用户投诉,优先安抚情绪。现在,请回答用户的问题:" + message)

// 场景2:代码审查,需要统一输出格式
.user("请审查以下代码,按'严重程度|问题描述|修改建议|修改后代码'的格式输出:" + code)

// 场景3:多语言翻译,需要指定源语言和目标语言
.user("将以下内容从中文翻译成英文,保持专业术语的一致性:" + content)

三个问题立刻暴露:

  1. 提示词和代码耦合:改一个字就要重新编译部署
  2. 无法复用:同一个角色设定,多个接口要复制粘贴
  3. 难以管理:提示词散落在各个Service里,产品和运营想调优?先把IDE装上

后端老鸟的直觉反应:这玩意儿应该放到配置文件里。

2. Spring AI的答案:PromptTemplate

Spring AI提供了一个 PromptTemplate,用法和 RestTemplateJdbcTemplate 一样,看名字就知道是干嘛的。

它的核心思想就一句话:用占位符代替硬编码,运行时动态填充。

先看一个最简单的例子:

java 复制代码
// 以前:硬编码
.user("你是一个专业的" + role + ",请用" + tone + "的语气回答:" + message)

// 现在:占位符模板
.user("你是一个专业的{role},请用{tone}的语气回答:{message}")

2.1 代码实现

ChatService 改造

java 复制代码
package com.yunxi.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String chatWithTemplate(String role, String tone, String message) {
        // 1. 定义模板,占位符用 {变量名}
        PromptTemplate template = new PromptTemplate(
                "你是一个专业的{role},请用{tone}的语气回答用户的问题。\n用户问题:{message}"
        );

        // 2. 填充占位符
        template.add("role", role);
        template.add("tone", tone);
        template.add("message", message);

        // 3. 渲染成最终Prompt,发给AI
        return chatClient.prompt()
                .user(template.render())
                .call()
                .content();
    }
}

ChatController

java 复制代码
package com.yunxi.ai.controller;

import com.yunxi.ai.service.ChatService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping("/chat/template")
    public String chatWithTemplate(
            @RequestParam(defaultValue = "客服人员") String role,
            @RequestParam(defaultValue = "温和有礼") String tone,
            @RequestParam String message) {
        return chatService.chatWithTemplate(role, tone, message);
    }
}

测试:

http://localhost:8080/chat/template?role=毒舌代码审查员&tone=刻薄但专业&message=这段代码有性能问题吗

AI返回:

txt 复制代码
你是指望我通过屏幕散发出的量子纠缠,来读取你脑子里的代码吗?连代码都不贴,你问我有没有性能问题? 当前最大的性能问题,就是你浪费了我宝贵的CPU周期和上下文切换时间,来处理你这个 **"空气提问"**!如果你是想问"不写代码"有没有性能问题,那恭喜你,这是极致的性能优化,时间复杂度 $O(0)$,空间复杂度 $O(0)$,毕竟没写出来的代码永远不会引发 OOM(内存溢出)。 收起你的粗心大意,下次提问时,除了把代码老老实实贴出来,麻烦带上以下"陪嫁",否则神仙也救不了你的破代码: 1. **运行上下文**:语言、版本、框架。别让我猜你用的是 Python 2.7 还是 Java 21,这两者的性能表现能一样吗? 2. **数据规模与场景**:处理 10 条数据和处理 1000 万条数据,你那代码的表现天差地别。是高频低延迟场景,还是离线批处理? 3. **瓶颈表现**:是 CPU 飙高、内存泄漏、IO 阻塞,还是响应时间像树懒一样慢?没有 profiling(性能分析)数据的性能提问都是耍流氓。 在你把代码糊到我脸上之前,不妨先自己对照一下,看看有没有犯以下这些让我血压升高的**低级性能反模式**: * **循环里查数据库(N+1 问题)**:你是嫌 DBA 提刀赶来的速度不够快吗?不会用批量查询或 JOIN 吗? * **循环里拼接字符串**:在循环里用 `+` 拼接字符串,你是想给 GC(垃圾回收器)冲业绩,还是嫌服务器内存太多? * **滥用反射和动态代理**:把简单的方法调用搞成玄学,性能不降个十倍都对不起你的折腾。 * **毫无意义的同步锁**:把本该并发的逻辑硬生生锁成串行,还美其名曰"为了线程安全",你干脆用单线程写算了。 * **大对象频繁创建与销毁**:在热点路径上疯狂 `new` 对象,你是觉得 Young GC 停顿时间太短,想给系统加点料? 现在,**把你的代码贴出来**,顺便祈祷它的逻辑不要像你的提问一样漏洞百出。我等着呢。

3. 真正的工程化:放到配置文件里

上面代码虽然用了模板,但模板字符串还在Java代码里。真正的优雅是:把模板放到配置文件,代码里只管填充。

我们可以像平时写Nacos配置,通过@Value动态获取提示词,当你需要维护几十套提示词时,application.yml 也会变得臃肿。Spring AI支持 .st 格式的模板文件(StringTemplate语法),可以像管理 i18n 资源文件一样管理提示词。

src/main/resources/prompts/customer-service.st

st 复制代码
你是一个资深Java代码审查员。

审查要点:
1. 命名规范:类名大驼峰,方法名小驼峰,变量名有意义
2. 性能问题:循环内查库、N+1问题、大对象创建
3. 安全漏洞:SQL注入、XSS、敏感信息泄露

请对以下代码进行审查,按"严重程度|问题描述|修改建议"的格式输出:

{code}

src/main/resources/prompts/code-review.st

st 复制代码
你是一个专业的客服人员,你的名字叫小慧。

工作要求:
1. 语气温和有礼,多用"您"、"请"等敬语
2. 如果用户投诉,优先安抚情绪,再解决问题
3. 如果遇到你不知道的问题,诚实告知,并建议联系人工客服
4. 回答简洁,不超过200字

用户问题:{message}

请回答:

StPromptService:

java 复制代码
package com.yunxi.ai.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Service
public class StPromptService {

    private final ChatClient chatClient;

    @Value("classpath:prompts/customer-service.st")
    private Resource customerServicePrompt;

    @Value("classpath:prompts/code-review.st")
    private Resource codeReviewPrompt;

    public StPromptService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String chat(String promptType, String input) throws IOException {
        Resource resource = switch (promptType) {
            case "customer-service" -> customerServicePrompt;
            case "code-review" -> codeReviewPrompt;
            default -> throw new IllegalArgumentException("未知提示词类型:" + promptType);
        };

        String templateContent = resource.getContentAsString(StandardCharsets.UTF_8);
        PromptTemplate template = new PromptTemplate(templateContent);
        template.add("message", input);
        template.add("code", input);

        return chatClient.prompt()
                .user(template.render())
                .call()
                .content();
    }
}

PromptConfigController:

java 复制代码
package com.yunxi.ai.controller;

import com.yunxi.ai.service.StPromptService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class PromptConfigController {

    private final StPromptService stPromptService;

    public PromptConfigController( StPromptService stPromptService) {
        this.stPromptService = stPromptService;
    }

    @GetMapping("/chat/config")
    public String chatWithConfig(@RequestParam String type, @RequestParam String input) throws IOException {
        return stPromptService.chat(type, input);
    }
}

此时的目录结构:

txt 复制代码
src/main/resources/
├── application.yml
└── prompts/
    ├── customer-service.st
    └── code-review.st

有没有觉得这套东西很眼熟?@Value 注入资源文件、外部化配置、模板占位符------全是Spring生态的标准玩法。Spring AI没有发明新东西,它只是把Spring已有的基础设施搬到了AI领域。

4. 系统提示词 vs 用户提示词

上一篇文章我们只用了 .user(),但其实还有一个更重要的角色:SystemMessage

Spring AI的消息模型:

消息类型 角色 用途
SystemMessage 系统 设定AI的行为规则、角色、输出格式,优先级最高
UserMessage 用户 用户的具体问题或指令
AssistantMessage AI AI的历史回答(多轮对话时用)

上面的"人设提示词"其实更应该放在 SystemMessage 里:

java 复制代码
public String chatWithSystemPrompt(String systemPrompt, String userMessage) {
    return chatClient.prompt()
            .system(systemPrompt)    // 角色设定
            .user(userMessage)       // 具体问题
            .call()
            .content();
}

5. 本篇避坑指南

5.1. 坑1:占位符没填充,AI直接输出 {message}

PromptTemplate 如果漏填占位符,不会报错,但AI会把 {message} 当成普通文本输出。

解决:封装一个工具方法,创建模板后打印渲染结果做验证。

5.2. 坑2:.st 文件中文乱码

.st 文件默认编码可能不是UTF-8,读取时指定编码:

java 复制代码
resource.getContentAsString(StandardCharsets.UTF_8)

5.3. 坑3:提示词太长,超过模型Token限制

每个模型有上下文窗口限制(比如 qwen-plus 是128K),提示词超长会被截断。

解决 :控制提示词长度,或使用 spring.ai.openai.chat.options.max-tokens 限制输出长度。

5.4. 坑4:SystemMessage 和 UserMessage 搞混

SystemMessage 是规则,UserMessage 是任务。把大段任务放 SystemMessage 里,AI可能会"忘记"规则。

原则:规则放System,任务放User,界限分明。

6. 本篇小结

这一篇我们解决了Prompt管理的工程化问题:

  1. PromptTemplate:占位符替代魔法字符串
  2. .st ****文件:提示词太多时的进阶管理方案
  3. SystemMessage vs UserMessage:规则和任务各归其位

现在我们的提示词管理已经像个正经的Spring Boot项目了。但还有一个问题没解决:AI的回答总是一大段自由文本,我想要结构化的数据怎么办? 比如让AI返回JSON,直接反序列化成我的DTO对象。

下一篇,我们聊聊 结构化输出------如何让AI的回答直接映射成Java对象


本文与DeepSeek协作完成

相关推荐
「QT(C++)开发工程师」2 小时前
AI Agent 术语
人工智能·ai作画·aigc·ai编程·ai写作
愚农搬码3 小时前
Agentic AI、AI Agent、AI 工作流有什么区别?
agent·ai编程·工作流引擎
乐之者v3 小时前
AI编程 -- Agents.md 的简单示例
java·ai编程
东小西4 小时前
第1篇:《Java老鸟的第一行AI代码:我让Spring AI帮我写了首诗》
openai·ai编程
笑小枫5 小时前
用 Claude Code 推翻重写笑小枫网站
java·人工智能·spring boot·ai编程
洞窝技术7 小时前
告别 AI 过度工程:一文吃透 Ponytail 七层精简阶梯与落地实践
ai编程
w3296362717 小时前
配置 Zenoh-Bridge-ROS2DDS 实现 HTTP/WS 推送及 JSON 转换
ai编程