Spring AI 2.0.0 Prompt 入门教程:system、user、template 和流式输出 Demo Spring AI 2.0.0 Prompt 入门教程system、user、template 和流式输出 Demo很多 Spring AI Demo 一开始都是这样写的chatClient.prompt().user(你是一个 Java 专家请帮我解释这段代码回答要简洁code).call().content();能跑。但项目一复杂Prompt 很快就会变成一团字符串Stringprompt你是一个 Java 专家。你的任务是解释代码。回答要简洁按步骤输出。如果代码有问题要指出来。不要输出无关内容。代码如下\ncode;角色、任务、变量、输出格式、约束条件全塞进user。短期能跑长期很难改。下一次你想改回答风格得去字符串里找。再下一次你想换输出格式还得继续拼接。Prompt 不是不能写长。真正麻烦的是不同层级的内容混在一起。这篇就解决一个问题Spring AI 2.0.0 里system、user、template到底怎么分工先记住一句话system 放长期规则 user 放本次问题 template 放可复用结构后面所有代码都围绕这句话展开。一、先准备 ChatClient这篇示例按Spring AI 2.0.0写。默认你已经有一个能正常调用模型的 Spring Boot 项目。如果你已经接好了ChatClient可以直接跳到第二部分。如果接着前面的 DeepSeek Demo 做关键版本是propertiesjava.version17/java.versionspring-ai.version2.0.0/spring-ai.version/properties依赖是dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-deepseek/artifactId/dependency如果你的项目是手写pom.xml别忘了通过spring-ai-bom管理 Spring AI 版本dependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagement配置类似这样spring:ai:deepseek:api-key:${DEEPSEEK_API_KEY}chat:model:deepseek-v4-flashtemperature: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 里加环境变量DEEPSEEK_API_KEY你的 API Key这篇重点不是模型配置而是 Prompt 怎么组织。二、System放长期规则system适合放稳定规则。它不是随便写一句你是一个 Java 专家。这句话有用但太粗。更好的system通常会把长期规则说清楚你是谁 主要负责什么 什么能做什么不能做 回答风格是什么 信息不足时怎么处理 输出结构是什么我们做业务系统不用写得很夸张。但有一个原则很重要每次都应该生效的规则放到system。比如做一个 Java 代码助手chatClient.prompt().system( 你是一个资深 Java 工程师负责解释和审查 Java/Spring 代码。 请用简洁中文回答先给结论再解释原因。 如果代码存在风险请指出触发条件和建议改法。 如果信息不足请说明还需要补充什么不要编造项目背景。 不要输出与代码无关的泛泛建议。 ).user(这段代码有什么问题\ncode).call().content();这里的system不关心这次传进来的代码是什么。它只规定一件事无论用户问哪段代码都按这套规则回答。如果每次请求都要重复写这一段就应该提到ChatClient默认配置里ConfigurationpublicclassChatClientConfig{BeanpublicChatClientchatClient(ChatClient.Builderbuilder){returnbuilder.defaultSystem( 你是一个资深 Java 工程师负责解释和审查 Java/Spring 代码。 请用简洁中文回答先给结论再解释原因。 如果代码存在风险请指出触发条件和建议改法。 如果信息不足请说明还需要补充什么不要编造项目背景。 不要输出与代码无关的泛泛建议。 ).build();}}这样业务代码里就不用反复写固定规则。记住system管“长期怎么回答”不是管“这次问什么”。也别把system写成产品说明书。它应该稳定但不应该臃肿。和当前场景无关的公司介绍、技术栈罗列、口号都可以删掉。三、User放本次输入user放当前这一次请求。也就是用户这次问了什么 这次要处理什么数据 这次任务有什么特殊要求比如解释代码chatClient.prompt().user(请解释这段代码\n\ncode).call().content();如果下一次要生成单元测试user就换成chatClient.prompt().user(请为这段代码生成 JUnit 5 单元测试\n\ncode).call().content();长期角色和回答风格仍然由defaultSystem管。所以不推荐这样chatClient.prompt().user( 你是一个资深 Java 工程师。 回答要简洁、准确。 输出格式先给结论再解释原因。 请解释这段代码 code).call().content();更推荐这样chatClient.prompt().user(请解释这段代码\n\ncode).call().content();前提是ChatClient已经配置了defaultSystem。这样分工就很清楚system长期规则 user本次问题四、Template放可复用结构如果 Prompt 有固定格式并且要填多个变量就适合用 Template。比如“解释代码”这个任务每次结构都差不多请解释以下 {language} 代码 文件路径{filePath} 代码内容 {code} 重点关注{focus}如果用字符串拼接会变成这样Stringprompt请解释以下 language 代码\n文件路径filePath\n\n代码内容\ncode\n\n重点关注focus;变量一多就很难读。Spring AI 的ChatClient可以直接在user里写模板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渲染user和system文本里的变量。默认变量语法就是{变量名}。所以 Template 适合这些场景固定格式 多个变量 多处复用 后续可能调整 Prompt 结构但不要为了模板而模板。如果只有一个变量直接写.user(请解释这段代码\n\ncode)就够了。五、完整例子下面写一个最小接口接收一段代码让模型解释它。packagecom.example.springaideepseekdemo.controller;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.http.MediaType;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importreactor.core.publisher.Flux;RestControllerpublicclassCodeExplainController{privatefinalChatClientchatClient;publicCodeExplainController(ChatClient.Builderbuilder){this.chatClientbuilder.defaultSystem( 你是一个资深 Java 工程师负责解释和审查 Java/Spring 代码。 请用简洁中文回答先给结论再解释原因。 如果代码存在风险请指出触发条件和建议改法。 如果信息不足请说明还需要补充什么不要编造项目背景。 不要输出与代码无关的泛泛建议。 输出格式 1. 代码功能概述 2. 关键逻辑解释 3. 潜在问题或优化建议 ).build();}PostMapping(/code/explain)publicStringexplain(RequestParam(defaultValueJava)Stringlanguage,RequestParam(defaultValueunknown)StringfilePath,RequestParam(defaultValue可读性、性能、潜在 bug)Stringfocus,RequestBodyStringcode){returnchatClient.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,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxStringexplainStream(RequestParam(defaultValueJava)Stringlanguage,RequestParam(defaultValueunknown)StringfilePath,RequestParam(defaultValue可读性、性能、潜在 bug)Stringfocus,RequestBodyStringcode){returnchatClient.prompt().user(u-u.text( 请解释以下 {language} 代码。 文件路径{filePath} 代码内容 {code} 重点关注{focus} ).param(language,language).param(filePath,filePath).param(code,code).param(focus,focus)).stream().content();}}普通输出这样测curl-XPOSThttp://localhost:8080/code/explain?filePathUserService.java\-HContent-Type: text/plain\--data-binarypublic String getName(User user) { return user.getName(); }流式输出这样测curl-N-XPOSThttp://localhost:8080/code/explain/stream?filePathUserService.java\-HContent-Type: text/plain\--data-binarypublic String getName(User user) { return user.getName(); }-N的作用是关闭 curl 的缓冲方便你看到模型一段一段返回。这里没有在 URL 里直接写中文参数。如果你要在 query string 里传中文需要先做 URL 编码否则新版本 Tomcat 可能直接返回400 Bad Request。正常情况下模型会指出这段代码的核心风险如果 user 为 null调用 user.getName() 会触发 NullPointerException。这个例子里分工很清楚defaultSystem角色、风格、输出格式 user template本次任务结构 param本次变量以后要调整“回答风格”改defaultSystem。要调整“这类任务的结构”改user模板。要处理不同代码只改参数。六、几个常见坑1. 把所有东西都塞进 user这是最常见的问题。user里既有“你是专家”又有“回答要简洁”还有“请分析这段代码”。短期能跑长期一定难维护。先问自己一句这句话是不是每次请求都一样如果是就优先考虑放到system。2. system 写得太长system不是越长越好。很多人会写成你是 Java 专家精通 Spring、MyBatis、Redis、Kafka、微服务、DDD……看起来很专业但很多身份描述并不直接约束输出还会浪费 token。更好的写法是给清晰规则回答要简洁。 先给结论再解释原因。 不确定时说明不确定不要编造。3. 变量名太随意不推荐.user(u-u.text(分析 {a} 和 {b}关注 {c}).param(a,oldCode).param(b,newCode).param(c,focus))更推荐.user(u-u.text(分析 {oldCode} 和 {newCode}关注 {focus}).param(oldCode,oldCode).param(newCode,newCode).param(focus,focus))变量名就是文档。以后回头看代码不用猜a、b、c分别是什么。4. JSON 示例和模板变量冲突Spring AI 默认用{}识别模板变量。如果 Prompt 里要放 JSON 示例就可能被误识别成模板变量。比如请返回 { summary: ..., risk: ... }这时可以给这次调用配置TemplateRenderer把变量分隔符换成变量名这类写法。比如继续用默认的StTemplateRenderer但把分隔符从{}换成和。这个点不用一开始就展开但要知道模板里的{}不是普通字符它有变量含义。5. 忘了约束输出格式你想让模型按固定格式回答就要明确告诉它。比如.system( 你是 Java 代码审查助手。 输出格式只返回 JSON不要输出 Markdown。 字段包括 summary、risks、suggestions。 )不过真正要让模型稳定返回 Java 对象后面还要用结构化输出。这块更适合单独写一篇。写在最后Spring AI 里的 Prompt 管理不用想复杂。先记住三句话system 管长期规则 user 管本次问题 template 管可复用结构Prompt 不是越长越好。真正重要的是分工清楚、上下文干净、规则稳定。这一步做好了后面的结构化输出、RAG、Tool Calling都会更容易维护。配套代码按文章编号放在对应分支方便对照运行。后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。