欢迎光临
我们一直在努力

第三章:Prompt 管理——别再拼字符串了

第三章:Prompt 管理——别再拼字符串了


前两章我们把 Hello World 跑通了,代码长这样:

chatClient.prompt()
.user("帮我写一段冒泡排序的代码")
.call()
.content();

如果只是写个 Demo,这足够了。但上了生产,Prompt 这样写就是在给自己埋雷。 这一章教你从"拼字符串"进化到"像管 SQL 一样管 Prompt"。


3.1 告别原始人写法

先说问题。你在项目里八成见过这种代码:

// ❌ 噩梦级 Prompt 管理
String prompt = "你是一个" + role + ",请根据以下" + contextType +
"回答问题。注意:" + rules1 + "," + rules2 + "。" +
"如果用户的问题涉及" + sensitiveTopic + ",请委婉拒绝。" +
"问题:" + userQuestion;

String answer = chatClient.prompt().user(prompt).call().content();

这段代码什么问题?

  • Prompt 和业务代码混在一起:想改一句提示词,得改 Java 代码,重新编译部署
  • 拼接逻辑复杂后极易出错:少个逗号多个空格,效果天差地别
  • 没法做版本管理:你改了一版 Prompt 效果好还是差?看不出 diff
  • 产品和运营想调 Prompt?:门都没有,他们不会改 Java 代码
  • 正确方式:PromptTemplate

    // ✅ 用模板
    String template = """
    你是一个{role},请根据以下{contextType}回答问题。
    注意:{rules1},{rules2}。
    如果用户的问题涉及{sensitiveTopic},请委婉拒绝。
    问题:{question}
    """
    ;

    PromptTemplate promptTemplate = new PromptTemplate(template);

    // 填空
    Map<String, Object> variables = Map.of(
    "role", "资深Java工程师",
    "contextType", "代码规范",
    "rules1", "只输出代码不解释",
    "rules2", "使用Java 17语法",
    "sensitiveTopic", "系统漏洞",
    "question", userQuestion
    );

    Prompt prompt = promptTemplate.create(variables);
    String answer = chatClient.prompt(prompt).call().content();

    就多了 PromptTemplate 这一层,但带来的好处立竿见影:

    • Prompt 文本和变量分离:模板是模板,数据是数据
    • 可读性飙升:一眼能看懂 Prompt 长什么样,不会被 + 号淹没
    • 变量校验:如果少传了 {question},create() 直接抛异常,而不是运行时生成一段半截子 Prompt 发给 AI

    3.2 把 Prompt 放进资源文件——像管 SQL 一样管 Prompt

    PromptTemplate 解决了拼接问题,但模板本身还是嵌在 Java 代码里。对于一个正经项目,更好的做法是把 Prompt 放到资源文件里。

    目录结构

    src/main/resources/
    └── prompts/
    ├── code-review.st # 代码审查的 Prompt
    ├── bug-analysis.st # Bug 分析的 Prompt
    └── meeting-summary.st # 会议纪要的 Prompt

    .st 是 Spring AI 默认的 Prompt 模板后缀(StringTemplate),你也可以用其他模板引擎。

    code-review.st 内容

    你是一个{role},负责审查以下代码。

    审查重点:
    1. 潜在的空指针异常
    2. 不合理的异常处理
    3. 性能问题(循环中的数据库调用、不必要的大对象创建)
    4. 并发安全问题

    请按以下格式输出:
    – 严重程度:[高/中/低]
    – 问题行号:[行号]
    – 问题描述:[描述]
    – 修改建议:[建议代码]

    要审查的代码:
    ```java
    {code}

    ### Java 代码中加载

    ```java
    @Service
    public class CodeReviewService {

    private final ChatClient chatClient;
    private final Resource promptResource;

    public CodeReviewService(ChatClient.Builder builder,
    // 直接从 classpath 加载 Prompt 文件
    @Value("classpath:/prompts/code-review.st")
    Resource promptResource) {
    this.chatClient = builder.build();
    this.promptResource = promptResource;
    }

    public ReviewResult review(String code) {
    // 读取模板文件
    PromptTemplate template = new PromptTemplate(promptResource);

    // 填参数
    Prompt prompt = template.create(Map.of(
    "role", "资深Java后端开发工程师",
    "code", code
    ));

    return chatClient.prompt(prompt)
    .call()
    .entity(ReviewResult.class); // 直接映射到对象,下章细讲
    }
    }

    这样做的好处

    维度拼字符串资源文件管理
    修改成本 改代码 → 编译 → 部署 改文件 → 重启(或热加载)
    版本控制 混在 Java diff 里看不清 Git 里独立追踪每次 Prompt 变更
    协作门槛 只有开发能改 产品、运营也能提 PR 改 Prompt
    复用性 靠复制粘贴 一个模板多处引用
    国际化 几乎不可能 加个 _zh、_en 后缀就行

    实战经验:我们团队现在的新规则——Prompt 超过 3 行就必须放资源文件,不允许写在 Java 代码里。


    3.3 System Message / User Message / Assistant Message 的区别

    前面用的 .user() 其实只是冰山一角。跟大模型对话时,消息分三种角色:

    ┌──────────────────────────────────────────────────────────┐
    │ 一次完整的对话请求 │
    ├──────────────────────────────────────────────────────────┤
    │ System Message ←"你是资深工程师,只回答Java问题" │
    │ 设定 AI 的人设、行为规则、输出格式 │
    ├──────────────────────────────────────────────────────────┤
    │ User Message ←"Java里HashMap和ConcurrentHashMap │
    │ 有什么区别?" │
    │ 用户真正想问的问题 │
    ├──────────────────────────────────────────────────────────┤
    │ Assistant Message ←"上次你说要学并发,建议先看这个…" │
    │ 多轮对话时,之前的 AI 回答 │
    ├──────────────────────────────────────────────────────────┤
    │ User Message ←"好的,那具体怎么用?" │
    │ 新一轮用户问题 │
    └──────────────────────────────────────────────────────────┘

    三句话讲清楚:

    • System Message:给 AI “定规矩"的,AI 会把它当铁律。比如"你只回答 Java 问题”、“用中文回答”、“输出 JSON 格式”。
    • User Message:用户问的问题。这是对话的主体。
    • Assistant Message:AI 之前的回答。多轮对话时,把历史回答喂回去,AI 才知道"刚才聊到哪了"。

    代码怎么写

    String answer = chatClient.prompt()
    .system(systemPrompt) // 定规矩
    .user(userPrompt) // 问问题
    .advisors(new MessageChatMemoryAdvisor(...)) // 自动管理历史消息
    .call()
    .content();

    系统消息的实战套路

    大部分人用 System Message 只写一句"你是一个XX专家"。太浪费了。我总结了一个"三段式"模板:

    你是一个{role}。

    ## 工作规则
    1. {rule1}
    2. {rule2}
    3. {rule3}

    ## 输出格式
    请严格按以下JSON格式输出,不要加任何额外内容:
    {
    "field1": "…",
    "field2": "…"
    }

    ## 禁止事项
    – 不要输出HTML标签
    – 不要反问用户
    – 遇到不确定的事情,直接说"我不确定",不要编造

    三个关键点:

  • 工作规则:比"你是一个专家"具体得多。比如"遇到代码问题要先分析原因,再给修改方案"。
  • 输出格式:如果要 JSON,就在这里写好模板,AI 的遵循度会高很多。
  • 禁止事项:告诉 AI 不要干什么,往往比告诉它要干什么更有效。AI 对否定指令的遵循度很高。
  • 实战经验:禁止事项里一定要加"不要编造"。没有这一条,AI 在不知道答案的时候会瞎编一段看起来很像那么回事的内容——这就是"幻觉"。


    3.4 实战经验:Prompt 版本化 & A/B 测试

    前面解决了怎么写 Prompt 的问题。接下来是更进阶的话题:怎么知道你改的 Prompt 比之前好?

    问题场景

    你上线了一个"客服自动回复"功能,Prompt v1 效果还行。有一天你灵光一闪觉得改一下措辞会让回答更温柔,改了之后就上线了。

    三天后投诉量翻了 3 倍。但你根本不知道——因为看不出是哪个改动导致的。

    解决方案:Prompt 版本化管理

    最简单的版本化:文件名带版本号 + Git 打 Tag。

    src/main/resources/prompts/
    └── customer-service/
    ├── v1/
    │ └── reply.st
    └── v2/
    └── reply.st

    代码中通过配置决定用哪个版本:

    app:
    prompt:
    customer-service-version: v2 # 改这里就切版本

    @Service
    public class CustomerServiceBot {

    private final String promptPath;

    public CustomerServiceBot(
    @Value("${app.prompt.customer-service-version}") String version) {
    this.promptPath = "classpath:/prompts/customer-service/" + version + "/reply.st";
    }
    // …
    }

    改 Prompt 的流程变成:

  • 新建 v3/reply.st,修改内容
  • 提 PR,团队 Review Prompt 变更(就像 Review 代码一样)
  • 合并后改配置切到 v3
  • 出问题秒回滚——改配置回 v2,不需要重新部署
  • A/B 测试思路

    如果你想验证"新 Prompt 是否真的比旧的好",可以做 A/B 测试:

    @Service
    public class AbTestBotService {

    private final ChatClient chatClient;
    private final Resource promptA; // 旧版
    private final Resource promptB; // 新版

    public String reply(String userId, String question) {
    // 根据用户 ID 哈希决定用哪个版本
    boolean useVersionB = Math.abs(userId.hashCode() % 100) < 50; // 50% 流量

    Resource prompt = useVersionB ? promptB : promptA;

    PromptTemplate template = new PromptTemplate(prompt);
    String answer = chatClient.prompt(template.create(Map.of("question", question)))
    .call()
    .content();

    // 记录用了哪个版本,方便后续分析
    log.info("User {} served by prompt version: {}",
    userId, useVersionB ? "B" : "A");

    return answer;
    }
    }

    然后对比两组的指标:

    维度如何衡量
    回答质量 用户"有帮助"/"没帮助"按钮的点击率
    用户满意度 对话后的评分(👍/👎)
    任务完成率 对话后用户是否还需要转人工
    响应效率 平均对话轮数

    实战经验:大部分团队不需要上来就搞 A/B 测试。先把 Prompt 从代码里拆出来、做到版本化管理,就已经超越了 90% 的团队。A/B 测试是锦上添花,不是必需品。


    第三章小结

  • 别拼字符串:用 PromptTemplate 把 Prompt 模板和变量分离,可读性 + 安全性双收。
  • 放资源文件:像管 SQL 一样管 Prompt,改 Prompt 不用改代码,产品也能参与。
  • 三段式 System Message:人设 + 工作规则 + 输出格式 + 禁止事项,比一句"你是专家"有效十倍。
  • 版本化管理是底线:文件名带版本 + 配置切换 + Git 追踪,出问题秒回滚。

  • 下一章讲结构化输出——教你用 BeanOutputConverter 让 AI 直接返回 Java 对象,再也不用手写正则解析 AI 的回答。这是 RAG 和 Function Calling 的前置基础,很重要的。

    赞(0)
    未经允许不得转载:171主机测评 » 第三章:Prompt 管理——别再拼字符串了
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址