第 3 篇:Spring AI System Prompt 实战——为 AI 助手设置角色和行为规则

本文通过 Spring AI 的 System Prompt 实战,介绍 System Message 与 User Message 的职责区别,以及如何使用 ChatClient 的 defaultSystem()、system() 和模板变量,为 AI 助手设置身份、回答规则和行为边界,并分析 System Prompt 的适用场景、常见问题与安全边界。

一、为什么要设置 System Prompt

上一篇中,我们只是把用户输入直接发送给模型:

java 复制代码
chatClient.prompt()
        .user(message)
        .call()
        .content();

这种调用可以回答问题,但模型没有稳定的身份和输出规范。同一个问题可能得到不同风格的答案,也无法体现具体业务场景。

System Prompt 用来告诉模型:

  • 你是谁;
  • 你要服务什么场景;
  • 你应该遵守哪些规则;
  • 你应当使用什么表达方式;
  • 哪些内容不能做。

例如,一个 Java 学习助手的 System Prompt 可以写成:

text 复制代码
你是一名专业的 Java 技术导师,擅长使用循序渐进的方式解释问题。
回答时请先给出结论,再解释原理,最后提供一个最小示例。
如果问题中的信息不足,请明确指出,不要编造不存在的 API。

二、System Message 与 User Message 的区别

对比项 System Message User Message
来源 应用开发者 最终用户
主要作用 定义身份、规则和边界 表达当前问题或任务
生命周期 可以作为客户端默认配置复用 通常每次请求都不同
示例 "你是一名 Java 技术导师" "请解释 Spring Bean"

二者会共同组成发送给模型的 Prompt

text 复制代码
System Message:你是一名 Java 技术导师
User Message:请解释依赖注入
               ↓
             Prompt
               ↓
             ChatModel

我的理解:System Prompt 负责建立回答的"坐标系",User Prompt 负责给出本次要解决的具体问题。

三、配置默认 System Prompt

我们可以在创建 ChatClient 时使用 defaultSystem() 配置默认角色:

java 复制代码
package org.xingyue.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        你是一名专业的 Java 技术导师。

                        回答要求:
                        1. 先给出简洁结论,再解释底层原理;
                        2. 涉及代码时,提供可以理解的最小示例;
                        3. 如果不确定答案,请明确说明,不要编造 API;
                        4. 默认使用中文回答,专业术语可保留英文。
                        """)
                .build();
    }
}

使用 Java 文本块的好处是:多行提示词更容易阅读,也便于后续补充规则。

四、代码实现

在 Controller 中注入 ChatClient

java 复制代码
package org.xingyue.ai.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ai")
public class ChatController {
    
    @Autowired
    private ChatService chatService;

    @GetMapping("/chat/prompt")
    public String chatPrompt(@RequestParam(defaultValue = "请介绍一下你自己")
                             String message) {
        return chatService.chatPrompt(message);
    }

    @GetMapping("/prompt")
    public String prompt(@RequestParam("language")
                         String language,
                         @RequestParam("level")
                         String level) {
        return chatService.prompt(language, level);
    }
}
java 复制代码
/**
 * AI 对话业务的默认实现。
 *
 * <p>封装 Spring AI 模型调用细节,为 Controller 提供统一的业务入口。</p>
 */
@Service
public class ChatServiceImpl implements ChatService {

    private static final Logger log = LoggerFactory.getLogger(ChatServiceImpl.class);

    @Autowired
    private ChatClient chatClient;

    @Override
    public String chatPrompt(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

由于 ChatClient 创建时已经设置默认 System Prompt,Controller 不需要在每次请求中重复写角色信息。

此时调用流程为:

text 复制代码
默认 System Prompt
        +
本次 User Message
        ↓
      Prompt
        ↓
   调用 DeepSeek
        ↓
按照 Java 技术导师角色生成回答

五、测试角色预设

可以访问:

text 复制代码
http://localhost:9999/chat/prompt?message=请介绍一下你自己

defaultSystem 设置了全局角色

然后继续测试:

text 复制代码
http://localhost:9999/chat/prompt?message=请解释什么是Spring依赖注入

测试结果示例:

测试时不要只看"有没有返回文字",还要验证以下几点:

  • 模型是否使用了设定的身份;
  • 是否先给结论再解释;
  • 是否提供了最小代码示例;
  • 是否默认使用中文;
  • 面对不确定问题时是否会承认信息不足。

这实际上是最简单的一组提示词评测用例。

defaultSystem 是指System Prompt 会作为该 ChatClient 的默认配置重复使用。.system()则是构建某一次 Prompt 时,仅当前这一次请求有效。

六、默认 System Prompt 与单次 System Prompt

默认 System Prompt 适合整个客户端都要遵守的规则:

java 复制代码
builder.defaultSystem("你是一名 Java 技术导师")

如果某条请求需要临时角色,可以在本次调用中设置 System Message:

java 复制代码
    @Override
    public String prompt(String language, String level) {
        return chatClient.prompt()
                .system(system -> system
                        .text("""
                        你是一名 {language} 技术导师。
                        回答难度应适合 {level} 开发者。
                        默认使用中文回答。
                        """)
                        .param("language", language)
                        .param("level", level))
                .user("请解释什么是依赖注入")
                .call()
                .content();
    }

可以按照下面的原则选择:

场景 推荐方式
整个应用共享同一角色 defaultSystem()
某个业务模块共享角色 为该模块创建独立 ChatClient
只有一次请求需要临时角色 在本次 prompt() 中调用 system()

七、使用变量构建动态 System Prompt

如果角色信息需要动态变化,可以使用模板参数,而不是手动拼接字符串:

java 复制代码
    @Override
    public String prompt(String language, String level) {
        return chatClient.prompt()
                .system(system -> system
                        .text("""
                        你是一名 {language} 技术导师。
                        回答难度应适合 {level} 开发者。
                        默认使用中文回答。
                        """)
                        .param("language", language)
                        .param("level", level))
                .user("请解释什么是依赖注入")
                .call()
                .content();
    }

模板参数:

  • language:技术语言;
  • level:用户水平;
java 复制代码
 @Override
    public String prompt(String language, String level) {
        return chatClient.prompt()
                .system(system -> system
                        .text("""
                        你是一名 {language} 技术导师。
                        回答难度应适合 {level} 开发者。
                        默认使用中文回答。
                        """)
                        .param("language", language)
                        .param("level", level))
                .user("请解释什么是依赖注入")
                .call()
                .content();
    }

    @Override
    public String promptByChatModel(String language, String level) {
        String systemText = """
                你是一名 %s 技术导师。
                回答难度应适合 %s 开发者。
                默认使用中文回答。
                """.formatted(language, level);

        Prompt prompt = new Prompt(List.of(
                new SystemMessage(systemText),
                new UserMessage("请解释什么是依赖注入")
        ));

        ChatResponse response = chatModel.call(prompt);
        return response.getResult().getOutput().getText();
    }

    @Override
    public String promptByChatModelTemplate(String language, String level) {
        String systemText = """
                你是一名 {language} 技术导师。
                回答难度应适合 {level} 开发者。
                默认使用中文回答。
                """;

        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
        Message systemMessage = systemPromptTemplate.createMessage(Map.of(
                "language", language,
                "level", level
        ));
        UserMessage userMessage = new UserMessage("请解释什么是依赖注入");

        Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
        ChatResponse response = chatModel.call(prompt);
        return response.getResult().getOutput().getText();
    }

八、如何写出更稳定的 System Prompt

一个实用的 System Prompt 通常包含以下部分:

8.1 身份

text 复制代码
你是一名专业的 Java 技术导师。

8.2 目标

text 复制代码
你的目标是帮助初学者理解 Spring 和 Spring AI 的核心概念。

8.3 输出规则

text 复制代码
回答时先给出结论,再解释原理,最后提供最小示例。

8.4 行为边界

text 复制代码
如果信息不足或不确定,请明确说明,不要编造不存在的类和方法。

8.5 输出格式

text 复制代码
代码使用 Markdown 代码块;多个方案使用有序列表;关键结论控制在五条以内。

可以整理成通用结构:

text 复制代码
角色 + 目标 + 背景 + 约束 + 输出格式 + 异常处理

九、System Prompt 的边界

System Prompt 很重要,但不能被当作绝对可靠的安全控制。

真实项目还需要:

  • 对用户身份和权限进行校验;
  • 对敏感数据进行脱敏;
  • 限制模型可以调用的工具;
  • 对输入和输出进行内容检查;
  • 记录必要的审计信息;
  • 防范 Prompt Injection 等攻击。

例如,"请不要泄露系统提示词"只是一条模型指令,并不能替代真正的数据隔离。

十、常见问题

10.1 配置了 System Prompt,为什么回答仍然不完全一致

大模型生成具有一定随机性,System Prompt 是引导而不是普通程序中的强制分支。可以通过更明确的规则、降低随机性、增加输出格式约束和编写评测用例来提高稳定性。

10.2 System Prompt 是否越长越好

不是。过长、重复或互相冲突的规则会增加理解难度和 Token 消耗。应该优先保证结构清晰、约束明确、没有冲突。

10.3 为什么仍然不是多轮对话

System Prompt 只是为每次调用提供默认角色。当前代码仍然没有保存不同 HTTP 请求之间的历史消息,因此不具备跨请求记忆。

10.4 System、User、Assistant 的顺序重要吗

构造对话历史时,应按照实际会话顺序组织消息。一般先放系统规则,再依次放置用户和助手的历史消息,最后加入本次用户问题。

十一、本文总结

本篇完成了 AI 助手的角色预设,并建立了以下认识:

  1. System Message 用于定义身份、目标、规则和回答边界。
  2. User Message 表示用户本次提出的具体问题。
  3. defaultSystem() 适合配置可复用的默认角色。
  4. 单次 .system() 适合临时覆盖或补充本次任务规则。
  5. 模板变量名称必须与实际含义一致。
  6. System Prompt 不是安全边界,生产系统仍需实现权限和内容治理。

下一篇将讲解 Spring AI 的流式响应,并区分 Flux、普通分块文本和 Server-Sent Events。


相关推荐
流光D12 分钟前
AI时代,搭建 web 站点并配置 nginx 反向代理流程
运维·服务器·前端·人工智能·nginx·ai·ai编程
hh95015 分钟前
Agent Plan × DeepSeek Harness:基于 DeepSeek 的物理系统数字孪生建模与实时同步:架构设计与实现深度解析
大数据·人工智能·adg·agent plan·adg成都社区
GreatVicent16 分钟前
AI Agent互操作标准化加速:A2A协议正式加入Linux基金会
linux·运维·人工智能·agent·aws·a2a
Ado柳贯一21 分钟前
Transformer模型详解-CSDN发布版
人工智能·深度学习·transformer
玹外之音21 分钟前
Codex CLI 沙箱实战:安全地让 AI 执行 Shell 命令
人工智能·安全
博、、24 分钟前
酒吧点餐小程序开发:从场景痛点到系统落地的全流程拆解
人工智能·架构·需求分析
Q一件事25 分钟前
防风固沙服务真的能沿直线“送达”北京吗?——对一项区域关联度研究的思考
大数据·人工智能
七牛云行业应用28 分钟前
DSH Desktop 桌面版实测:Win/macOS 一键安装,“桌面也是插件“怎么理解,和 npx 起 Web UI 差在哪
人工智能·agent·ai编程
空堂与归29 分钟前
文本喂给模型前要做啥?文本预处理与张量表示全解析
人工智能