Spring AI 2.0.0 Prompt 最小 Demo:system、user、template 到底怎么分工

Spring AI 2.0.0 Prompt 最小 Demo:system、user、template 到底怎么分工

很多 Spring AI Demo 一开始都是这样写的:

java 复制代码
chatClient.prompt()
        .user("你是一个 Java 专家,请帮我解释这段代码,回答要简洁:" + code)
        .call()
        .content();

能跑。

但项目一复杂,Prompt 很快就会变成一团字符串:

java 复制代码
String prompt = "你是一个 Java 专家。" +
        "你的任务是解释代码。" +
        "回答要简洁,按步骤输出。" +
        "如果代码有问题,要指出来。" +
        "不要输出无关内容。" +
        "代码如下:\n" + code;

角色、任务、变量、输出格式、约束条件,全塞进 user

短期能跑,长期很难改。

下一次你想改回答风格,得去字符串里找。

再下一次你想换输出格式,还得继续拼接。

Prompt 不是不能写长。

真正麻烦的是:

不同层级的内容混在一起。

这篇就解决一个问题:

Spring AI 2.0.0 里,systemusertemplate 到底怎么分工?

先记住一句话:

text 复制代码
system 放长期规则
user 放本次问题
template 放可复用结构

后面所有代码都围绕这句话展开。


一、先准备 ChatClient

这篇示例按 Spring AI 2.0.0 写。

默认你已经有一个能正常调用模型的 Spring Boot 项目。

如果你已经接好了 ChatClient,可以直接跳到第二部分。

如果接着前面的 DeepSeek Demo 做,关键版本是:

xml 复制代码
<properties>
    <java.version>17</java.version>
    <spring-ai.version>2.0.0</spring-ai.version>
</properties>

依赖是:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

如果你的项目是手写 pom.xml,别忘了通过 spring-ai-bom 管理 Spring AI 版本:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置类似这样:

yaml 复制代码
spring:
  ai:
    deepseek:
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        model: deepseek-v4-flash
        temperature: 0.3

注意,Spring AI 2.0.0 里,DeepSeek 的模型和温度配置直接放在 spring.ai.deepseek.chat 下面。

如果你从 1.x 示例迁移过来,不要继续写成 spring.ai.deepseek.chat.options.model

这个系列里的示例模型都统一用 deepseek-v4-flash

如果用 IDEA 运行,在 Run/Debug Configurations 里加环境变量:

text 复制代码
DEEPSEEK_API_KEY=你的 API Key

这篇重点不是模型配置,而是 Prompt 怎么组织。


二、System:放长期规则

system 适合放稳定规则。

它不是随便写一句:

text 复制代码
你是一个 Java 专家。

这句话有用,但太粗。

更好的 system,通常会把长期规则说清楚:

text 复制代码
你是谁
主要负责什么
什么能做,什么不能做
回答风格是什么
信息不足时怎么处理
输出结构是什么

我们做业务系统不用写得很夸张。

但有一个原则很重要:

每次都应该生效的规则,放到 system

比如做一个 Java 代码助手:

java 复制代码
chatClient.prompt()
        .system("""
                你是一个资深 Java 工程师,负责解释和审查 Java/Spring 代码。
                请用简洁中文回答,先给结论,再解释原因。
                如果代码存在风险,请指出触发条件和建议改法。
                如果信息不足,请说明还需要补充什么,不要编造项目背景。
                不要输出与代码无关的泛泛建议。
                """)
        .user("这段代码有什么问题:\n" + code)
        .call()
        .content();

这里的 system 不关心这次传进来的代码是什么。

它只规定一件事:

无论用户问哪段代码,都按这套规则回答。

如果每次请求都要重复写这一段,就应该提到 ChatClient 默认配置里:

java 复制代码
@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        你是一个资深 Java 工程师,负责解释和审查 Java/Spring 代码。
                        请用简洁中文回答,先给结论,再解释原因。
                        如果代码存在风险,请指出触发条件和建议改法。
                        如果信息不足,请说明还需要补充什么,不要编造项目背景。
                        不要输出与代码无关的泛泛建议。
                        """)
                .build();
    }
}

这样业务代码里就不用反复写固定规则。

记住:

system 管"长期怎么回答",不是管"这次问什么"。

也别把 system 写成产品说明书。

它应该稳定,但不应该臃肿。和当前场景无关的公司介绍、技术栈罗列、口号,都可以删掉。


三、User:放本次输入

user 放当前这一次请求。

也就是:

text 复制代码
用户这次问了什么
这次要处理什么数据
这次任务有什么特殊要求

比如解释代码:

java 复制代码
chatClient.prompt()
        .user("请解释这段代码:\n\n" + code)
        .call()
        .content();

如果下一次要生成单元测试,user 就换成:

java 复制代码
chatClient.prompt()
        .user("请为这段代码生成 JUnit 5 单元测试:\n\n" + code)
        .call()
        .content();

长期角色和回答风格,仍然由 defaultSystem 管。

所以不推荐这样:

java 复制代码
chatClient.prompt()
        .user("""
                你是一个资深 Java 工程师。
                回答要简洁、准确。
                输出格式:先给结论,再解释原因。

                请解释这段代码:
                """ + code)
        .call()
        .content();

更推荐这样:

java 复制代码
chatClient.prompt()
        .user("请解释这段代码:\n\n" + code)
        .call()
        .content();

前提是 ChatClient 已经配置了 defaultSystem

这样分工就很清楚:

text 复制代码
system:长期规则
user:本次问题

四、Template:放可复用结构

如果 Prompt 有固定格式,并且要填多个变量,就适合用 Template。

比如"解释代码"这个任务,每次结构都差不多:

text 复制代码
请解释以下 {language} 代码:

文件路径:{filePath}

代码内容:
{code}

重点关注:{focus}

如果用字符串拼接,会变成这样:

java 复制代码
String prompt = "请解释以下 " + language + " 代码:\n" +
        "文件路径:" + filePath + "\n\n" +
        "代码内容:\n" + code + "\n\n" +
        "重点关注:" + focus;

变量一多,就很难读。

Spring AI 的 ChatClient 可以直接在 user 里写模板:

java 复制代码
chatClient.prompt()
        .user(u -> u.text("""
                请解释以下 {language} 代码:

                文件路径:{filePath}

                代码内容:
                {code}

                重点关注:{focus}
                """)
                .param("language", language)
                .param("filePath", filePath)
                .param("code", code)
                .param("focus", focus))
        .call()
        .content();

这里的 {language}{filePath}{code}{focus} 都是模板变量。

Spring AI 会在调用前替换成真实值。

在 Spring AI 2.0.0 里,ChatClient 默认使用 StTemplateRenderer 渲染 usersystem 文本里的变量。默认变量语法就是 {变量名}

所以 Template 适合这些场景:

text 复制代码
固定格式
多个变量
多处复用
后续可能调整 Prompt 结构

但不要为了模板而模板。

如果只有一个变量,直接写:

java 复制代码
.user("请解释这段代码:\n\n" + code)

就够了。


五、完整例子

下面写一个最小接口:接收一段代码,让模型解释它。

java 复制代码
package com.example.springaideepseekdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class CodeExplainController {

    private final ChatClient chatClient;

    public CodeExplainController(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("""
                        你是一个资深 Java 工程师,负责解释和审查 Java/Spring 代码。
                        请用简洁中文回答,先给结论,再解释原因。
                        如果代码存在风险,请指出触发条件和建议改法。
                        如果信息不足,请说明还需要补充什么,不要编造项目背景。
                        不要输出与代码无关的泛泛建议。
                        输出格式:
                        1. 代码功能概述
                        2. 关键逻辑解释
                        3. 潜在问题或优化建议
                        """)
                .build();
    }

    @PostMapping("/code/explain")
    public String explain(
            @RequestParam(defaultValue = "Java") String language,
            @RequestParam(defaultValue = "unknown") String filePath,
            @RequestParam(defaultValue = "可读性、性能、潜在 bug") String focus,
            @RequestBody String code) {

        return chatClient.prompt()
                .user(u -> u.text("""
                        请解释以下 {language} 代码。

                        文件路径:{filePath}

                        代码内容:
                        {code}

                        重点关注:{focus}
                        """)
                        .param("language", language)
                        .param("filePath", filePath)
                        .param("code", code)
                        .param("focus", focus))
                .call()
                .content();
    }

    @PostMapping(value = "/code/explain/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> explainStream(
            @RequestParam(defaultValue = "Java") String language,
            @RequestParam(defaultValue = "unknown") String filePath,
            @RequestParam(defaultValue = "可读性、性能、潜在 bug") String focus,
            @RequestBody String code) {

        return chatClient.prompt()
                .user(u -> u.text("""
                        请解释以下 {language} 代码。

                        文件路径:{filePath}

                        代码内容:
                        {code}

                        重点关注:{focus}
                        """)
                        .param("language", language)
                        .param("filePath", filePath)
                        .param("code", code)
                        .param("focus", focus))
                .stream()
                .content();
    }
}

普通输出这样测:

bash 复制代码
curl -X POST "http://localhost:8080/code/explain?filePath=UserService.java" \
  -H "Content-Type: text/plain" \
  --data-binary 'public String getName(User user) { return user.getName(); }'

流式输出这样测:

bash 复制代码
curl -N -X POST "http://localhost:8080/code/explain/stream?filePath=UserService.java" \
  -H "Content-Type: text/plain" \
  --data-binary 'public String getName(User user) { return user.getName(); }'

-N 的作用是关闭 curl 的缓冲,方便你看到模型一段一段返回。

这里没有在 URL 里直接写中文参数。

如果你要在 query string 里传中文,需要先做 URL 编码,否则新版本 Tomcat 可能直接返回 400 Bad Request

正常情况下,模型会指出这段代码的核心风险:

text 复制代码
如果 user 为 null,调用 user.getName() 会触发 NullPointerException。

这个例子里,分工很清楚:

text 复制代码
defaultSystem:角色、风格、输出格式
user template:本次任务结构
param:本次变量

以后要调整"回答风格",改 defaultSystem

要调整"这类任务的结构",改 user 模板。

要处理不同代码,只改参数。


六、几个常见坑

1. 把所有东西都塞进 user

这是最常见的问题。

user 里既有"你是专家",又有"回答要简洁",还有"请分析这段代码"。

短期能跑,长期一定难维护。

先问自己一句:

这句话是不是每次请求都一样?

如果是,就优先考虑放到 system

2. system 写得太长

system 不是越长越好。

很多人会写成:

text 复制代码
你是 Java 专家,精通 Spring、MyBatis、Redis、Kafka、微服务、DDD......

看起来很专业,但很多身份描述并不直接约束输出,还会浪费 token。

更好的写法是给清晰规则:

text 复制代码
回答要简洁。
先给结论,再解释原因。
不确定时说明不确定,不要编造。

3. 变量名太随意

不推荐:

java 复制代码
.user(u -> u.text("分析 {a} 和 {b},关注 {c}")
        .param("a", oldCode)
        .param("b", newCode)
        .param("c", focus))

更推荐:

java 复制代码
.user(u -> u.text("分析 {oldCode} 和 {newCode},关注 {focus}")
        .param("oldCode", oldCode)
        .param("newCode", newCode)
        .param("focus", focus))

变量名就是文档。

以后回头看代码,不用猜 abc 分别是什么。

4. JSON 示例和模板变量冲突

Spring AI 默认用 {} 识别模板变量。

如果 Prompt 里要放 JSON 示例,就可能被误识别成模板变量。

比如:

text 复制代码
请返回:
{
  "summary": "...",
  "risk": "..."
}

这时可以给这次调用配置 TemplateRenderer,把变量分隔符换成 <变量名> 这类写法。

比如继续用默认的 StTemplateRenderer,但把分隔符从 {} 换成 <>

这个点不用一开始就展开,但要知道:模板里的 {} 不是普通字符,它有变量含义。

5. 忘了约束输出格式

你想让模型按固定格式回答,就要明确告诉它。

比如:

java 复制代码
.system("""
        你是 Java 代码审查助手。
        输出格式:只返回 JSON,不要输出 Markdown。
        字段包括 summary、risks、suggestions。
        """)

不过,真正要让模型稳定返回 Java 对象,后面还要用结构化输出。

这块更适合单独写一篇。


写在最后

Spring AI 里的 Prompt 管理,不用想复杂。

先记住三句话:

text 复制代码
system 管长期规则
user 管本次问题
template 管可复用结构

Prompt 不是越长越好。

真正重要的是分工清楚、上下文干净、规则稳定。

这一步做好了,后面的结构化输出、RAG、Tool Calling,都会更容易维护。

配套代码按文章编号放在对应分支,方便对照运行。完整系列也会同步整理在公众号「AI Agent 实战有术」。

相关推荐
未秃头的程序猿2 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
小旭Coding2 小时前
卧靠!Go 传给前端的 int64 竟然变成了这个?
后端
用户298698530142 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端
kunge20132 小时前
深度剖析Claude Code 的CLAUDE.md加载逻辑
后端·vibecoding
米沙AI2 小时前
MSYS2 快速使用版本
后端
Csvn2 小时前
Docker 进阶 — 网络模型、数据持久化与多阶段构建
后端
用户4279254051712 小时前
《微博开放平台官方CLI开源了:70+API一行搞定,AI Agent原生支持》
后端
Csvn2 小时前
文本处理三剑客 — grep、sed、awk 实战精讲
后端