Message 四分天下:Spring AI 如何统一消息格式

0 阅读7分钟

Spring AI 源码解读 - 第 3 篇:Prompt 与 Message 体系

消息的构建、模板化与角色映射

📖 开篇引言

在第 2 篇中,我们看到 PromptBuilder.user()system() 方法添加消息。但这些消息是如何被构建的?不同 AI 模型对消息格式的要求不同,Spring AI 如何统一处理?

本篇将深入 PromptMessagePromptTemplate 的源码,理解消息体系的设计与实现。


一、Message 接口体系

1.1 Message 顶层接口

// org.springframework.ai.chat.messages.Message
public interface Message extends Node<String> {
    
    // 获取消息内容
    String getContent();
    
    // 获取消息类型(角色)
    MessageType getMessageType();
    
    // 获取元数据
    Map<String, Object> getMetadata();
    
    // 获取消息名称(可选)
    default String getName() {
        return null;
    }
}

// 消息类型枚举
public enum MessageType {
    USER("user"),           // 用户消息
    ASSISTANT("assistant"), // AI 助手消息
    SYSTEM("system"),       // 系统提示词
    TOOL("tool");          // 工具调用结果
}

1.2 四种消息实现类

1. UserMessage - 用户消息

public class UserMessage implements Message {
    
    private final String content;
    private final Map<String, Object> metadata;
    
    public UserMessage(String content) {
        this(content, Map.of());
    }
    
    public UserMessage(String content, Map<String, Object> metadata) {
        this.content = content;
        this.metadata = metadata;
    }
    
    @Override
    public MessageType getMessageType() {
        return MessageType.USER;
    }
    
    @Override
    public String getContent() {
        return this.content;
    }
}

2. SystemMessage - 系统提示词

public class SystemMessage implements Message {
    
    private final String content;
    private final Map<String, Object> metadata;
    
    public SystemMessage(String content) {
        this(content, Map.of());
    }
    
    @Override
    public MessageType getMessageType() {
        return MessageType.SYSTEM;
    }
}

3. AssistantMessage - AI 回复消息

public class AssistantMessage implements Message {
    
    private final String content;
    private final List<ToolCall> toolCalls;  // 工具调用信息
    private final Map<String, Object> metadata;
    
    public AssistantMessage(String content) {
        this(content, List.of(), Map.of());
    }
    
    public AssistantMessage(String content, List<ToolCall> toolCalls) {
        this(content, toolCalls, Map.of());
    }
    
    @Override
    public MessageType getMessageType() {
        return MessageType.ASSISTANT;
    }
    
    // 获取工具调用信息(用于 Tool Calling)
    public List<ToolCall> getToolCalls() {
        return this.toolCalls;
    }
}

4. ToolResponseMessage - 工具调用结果

public class ToolResponseMessage implements Message {
    
    private final List<ToolResponse> responses;
    private final Map<String, Object> metadata;
    
    public ToolResponseMessage(List<ToolResponse> responses) {
        this(responses, Map.of());
    }
    
    @Override
    public MessageType getMessageType() {
        return MessageType.TOOL;
    }
    
    public List<ToolResponse> getResponses() {
        return this.responses;
    }
}

1.3 消息在多轮对话中的角色

1 轮
┌─────────────────────────────────────────┐
│ SystemMessage("你是一个 Java 专家")      │
│ UserMessage("什么是 Spring AI?")        │
│ AssistantMessage("Spring AI 是...")     │
└─────────────────────────────────────────┘

第 2 轮(历史消息 + 当前消息)
┌─────────────────────────────────────────┐
│ SystemMessage("你是一个 Java 专家")      │
│ UserMessage("什么是 Spring AI?")        │ ← 历史
│ AssistantMessage("Spring AI 是...")     │ ← 历史
│ UserMessage("它有哪些核心模块?")        │ ← 当前
└─────────────────────────────────────────┘

二、Prompt 数据模型

2.1 Prompt 类结构

// org.springframework.ai.chat.prompt.Prompt
public class Prompt implements ModelRequest<List<Message>> {
    
    private final List<Message> messages;      // 消息列表(核心)
    private final ChatOptions chatOptions;     // 可选参数
    
    // 构造方法
    public Prompt(String contents) {
        this(new UserMessage(contents));
    }
    
    public Prompt(Message message) {
        this(List.of(message));
    }
    
    public Prompt(List<Message> messages) {
        this(messages, null);
    }
    
    public Prompt(List<Message> messages, ChatOptions chatOptions) {
        this.messages = messages;
        this.chatOptions = chatOptions;
    }
    
    // 获取消息列表
    public List<Message> getInstructions() {
        return this.messages;
    }
    
    // 获取选项
    public ChatOptions getOptions() {
        return this.chatOptions;
    }
}

2.2 Prompt 的构建方式

// 方式 1:单条消息
Prompt prompt1 = new Prompt("你好");
// 等价于 new Prompt(new UserMessage("你好"))

// 方式 2:消息列表
Prompt prompt2 = new Prompt(List.of(
    new SystemMessage("你是一个助手"),
    new UserMessage("你好")
));

// 方式 3:带选项
Prompt prompt3 = new Prompt(
    List.of(new UserMessage("你好")),
    OllamaOptions.builder()
        .temperature(0.3)
        .build()
);

// 方式 4:通过 Builder(推荐)
Prompt prompt4 = Prompt.builder()
    .system("你是一个助手")
    .user("你好")
    .build();

三、PromptTemplate 模板引擎

3.1 PromptTemplate 接口

// org.springframework.ai.chat.prompt.PromptTemplate
public interface PromptTemplate {
    
    // 根据变量值渲染模板
    Prompt create(Map<String, Object> variables);
    
    // 便捷方法:单个变量
    default Prompt create(String variable) {
        return create(Map.of("input", variable));
    }
}

3.2 DefaultPromptTemplate 实现

public class DefaultPromptTemplate implements PromptTemplate {
    
    private final String template;  // 模板字符串
    private final ChatOptions options; // 可选参数
    
    public DefaultPromptTemplate(String template) {
        this(template, null);
    }
    
    public DefaultPromptTemplate(String template, ChatOptions options) {
        this.template = template;
        this.options = options;
    }
    
    @Override
    public Prompt create(Map<String, Object> variables) {
        
        // 1. 替换模板中的变量
        String renderedTemplate = renderTemplate(this.template, variables);
        
        // 2. 构建 UserMessage
        UserMessage userMessage = new UserMessage(renderedTemplate);
        
        // 3. 构建 Prompt
        return new Prompt(List.of(userMessage), this.options);
    }
    
    // 变量替换实现
    private String renderTemplate(String template, Map<String, Object> variables) {
        String result = template;
        
        // 替换 ${key} 格式的变量
        for (Map.Entry<String, Object> entry : variables.entrySet()) {
            String placeholder = "${" + entry.getKey() + "}";
            String value = String.valueOf(entry.getValue());
            result = result.replace(placeholder, value);
        }
        
        return result;
    }
}

3.3 SystemPromptTemplate - 系统提示词模板

public class SystemPromptTemplate implements PromptTemplate {
    
    private final String template;  // 系统提示词模板
    
    public SystemPromptTemplate(String template) {
        this.template = template;
    }
    
    @Override
    public Prompt create(Map<String, Object> variables) {
        
        // 1. 替换系统提示词中的变量
        String renderedSystemPrompt = renderTemplate(this.template, variables);
        
        // 2. 构建 SystemMessage
        SystemMessage systemMessage = new SystemMessage(renderedSystemPrompt);
        
        // 3. 构建 Prompt(只包含系统消息)
        return new Prompt(List.of(systemMessage));
    }
}

3.4 模板使用示例

// 示例 1:简单模板
PromptTemplate template1 = new DefaultPromptTemplate(
    "请用 ${language} 语言写一个 ${topic} 的例子"
);

Prompt prompt1 = template1.create(Map.of(
    "language", "Java",
    "topic", "快速排序"
));

// 渲染结果:
// "请用 Java 语言写一个 快速排序 的例子"

// 示例 2:系统提示词模板
SystemPromptTemplate systemTemplate = new SystemPromptTemplate(
    "你是一个 ${role} 专家,擅长 ${expertise}"
);

Prompt prompt2 = systemTemplate.create(Map.of(
    "role", "Java",
    "expertise", "Spring Framework"
));

// 渲染结果:
// SystemMessage("你是一个 Java 专家,擅长 Spring Framework")

四、消息角色映射(Role Mapping)

4.1 为什么需要角色映射?

不同 AI 模型对消息角色的定义略有不同:

Spring AI 标准          Ollama API          OpenAI API
─────────────────      ─────────────      ─────────────
USER                   "user"             "user"
ASSISTANT              "assistant"        "assistant"
SYSTEM                 "system"           "system"
TOOL                   "tool"             "tool"

虽然大多数模型一致,但有些模型可能有特殊要求。

4.2 角色映射的实现

// org.springframework.ai.chat.messages.MessageType
public enum MessageType {
    USER("user"),
    ASSISTANT("assistant"),
    SYSTEM("system"),
    TOOL("tool");
    
    private final String value;
    
    MessageType(String value) {
        this.value = value;
    }
    
    public String getValue() {
        return this.value;
    }
}

4.3 OllamaChatModel 中的角色映射

// OllamaChatModel 将 Spring AI Message 转换为 Ollama Message
private ChatRequest.Message toOllamaMessage(Message message) {
    
    // 获取消息角色
    String role = switch (message.getMessageType()) {
        case USER      -> "user";
        case ASSISTANT -> "assistant";
        case SYSTEM    -> "system";
        case TOOL      -> "tool";
    };
    
    // 构建 Ollama Message
    return new ChatRequest.Message(
        role,
        message.getContent()
    );
}

4.4 OpenAIChatModel 中的角色映射

// OpenAIChatModel 的角色映射(基本相同)
private OpenAiApi.ChatCompletionRequest.ChatCompletionMessage 
    toOpenAiMessage(Message message) {
    
    String role = switch (message.getMessageType()) {
        case USER      -> "user";
        case ASSISTANT -> "assistant";
        case SYSTEM    -> "system";
        case TOOL      -> "tool";
    };
    
    return new OpenAiApi.ChatCompletionRequest.ChatCompletionMessage(
        role,
        message.getContent()
    );
}

五、Prompt 构建的完整流程

5.1 从 PromptBuilder 到 Prompt

// 用户代码
chatClient.prompt()
    .system("你是一个助手")
    .user("你好")
    .call();

// 内部流程
// 1. prompt() 返回 PromptUserMessageBuilder
PromptUserMessageBuilder builder = chatClient.prompt();

// 2. system() 设置系统提示词
builder.system("你是一个助手");
// builder.systemPromptTemplate = new SystemPromptTemplate("你是一个助手")

// 3. user() 添加用户消息
builder.user("你好");
// builder.messages = [UserMessage("你好")]

// 4. call() 触发 buildPrompt()
Prompt prompt = builder.buildPrompt();

// buildPrompt() 的实现
private Prompt buildPrompt() {
    List<Message> allMessages = new ArrayList<>();
    
    // 1. 如果有系统提示词,先添加 SystemMessage
    if (this.systemPromptTemplate != null) {
        Prompt systemPrompt = this.systemPromptTemplate.create(Map.of());
        allMessages.addAll(systemPrompt.getInstructions());
    }
    
    // 2. 添加用户消息
    allMessages.addAll(this.messages);
    
    // 3. 构建最终 Prompt
    return new Prompt(allMessages, this.options);
}

// 最终 Prompt 的消息列表
// [
//     SystemMessage("你是一个助手"),
//     UserMessage("你好")
// ]

5.2 Prompt 的完整数据结构

Prompt
├── messages: List<Message>
   ├── [0] SystemMessage
      ├── content: "你是一个助手"
      ├── messageType: SYSTEM
      └── metadata: {}
   
   ├── [1] UserMessage
      ├── content: "你好"
      ├── messageType: USER
      └── metadata: {}
   
   └── [2] AssistantMessage (历史消息)
       ├── content: "你好,有什么可以帮助你的吗?"
       ├── messageType: ASSISTANT
       └── metadata: {}

└── chatOptions: ChatOptions
    ├── model: "qwen2.5:14b"
    ├── temperature: 0.3
    └── maxTokens: 2048

六、消息内容的多种形式

6.1 文本消息

// 最常见的形式
UserMessage message = new UserMessage("你好");

6.2 带元数据的消息

// 消息可以携带额外的元数据
UserMessage message = new UserMessage(
    "你好",
    Map.of(
        "userId", "user-123",
        "timestamp", System.currentTimeMillis(),
        "source", "web"
    )
);

// 获取元数据
Map<String, Object> metadata = message.getMetadata();
String userId = (String) metadata.get("userId");

6.3 带工具调用的 AssistantMessage

// AI 可能决定调用工具
List<ToolCall> toolCalls = List.of(
    new ToolCall(
        "get_weather",  // 工具名称
        Map.of("city", "北京")  // 工具参数
    )
);

AssistantMessage message = new AssistantMessage(
    "我来帮你查询天气",
    toolCalls
);

七、消息序列化与反序列化

7.1 为什么需要序列化?

当消息需要存储到数据库或通过网络传输时,需要序列化。

7.2 JSON 序列化示例

// 消息对象
UserMessage message = new UserMessage("你好");

// 序列化为 JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(message);
// {"content":"你好","messageType":"USER","metadata":{}}

// 反序列化
UserMessage deserialized = mapper.readValue(json, UserMessage.class);

7.3 ChatMemory 中的序列化

// MessageWindowChatMemory 将消息存储到内存
public class MessageWindowChatMemory implements ChatMemory {
    
    private final Map<ConversationId, List<Message>> conversationHistory;
    
    @Override
    public void add(ConversationId conversationId, Message message) {
        // 直接存储 Message 对象
        conversationHistory
            .computeIfAbsent(conversationId, k -> new ArrayList<>())
            .add(message);
    }
    
    @Override
    public List<Message> get(ConversationId conversationId) {
        // 直接返回 Message 对象列表
        return conversationHistory.getOrDefault(conversationId, List.of());
    }
}

八、Prompt 与 ChatOptions 的关系

8.1 Prompt 包含 ChatOptions

// Prompt 可以携带 ChatOptions
Prompt prompt = new Prompt(
    List.of(new UserMessage("你好")),
    OllamaOptions.builder()
        .model("qwen2.5:14b")
        .temperature(0.3)
        .build()
);

// 获取选项
ChatOptions options = prompt.getOptions();

8.2 Options 的优先级

Prompt 中的 Options(请求级)
        ↓ 覆盖
ChatClient 默认 Options
        ↓ 覆盖
application.properties 配置

8.3 Options 合并示例

// application.properties
spring.ai.ollama.chat.options.temperature=0.8

// ChatClient 构建时
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultOptions(OllamaOptions.builder()
        .temperature(0.5)
        .maxTokens(1000)
        .build())
    .build();

// 请求时
Prompt prompt = new Prompt(
    List.of(new UserMessage("你好")),
    OllamaOptions.builder()
        .temperature(0.3)  // 请求级
        .build()
);

// 最终合并结果
// temperature = 0.3(请求级覆盖)
// maxTokens = 1000(使用 ChatClient 默认)

九、小结

9.1 本篇要点

主题核心要点
Message 体系四种消息类型:USER、ASSISTANT、SYSTEM、TOOL
Prompt 数据模型消息列表 + ChatOptions 的容器
PromptTemplate模板引擎,支持 ${variable} 变量替换
SystemPromptTemplate系统提示词模板,用于动态生成系统消息
角色映射不同模型的消息角色转换
Prompt 构建流程system() → user() → buildPrompt()
消息元数据消息可携带额外的上下文信息

9.2 关键类清单

类 / 接口职责
Message消息接口
UserMessage用户消息
SystemMessage系统提示词
AssistantMessageAI 回复消息
ToolResponseMessage工具调用结果
Prompt请求封装(消息 + 选项)
PromptTemplate模板接口
DefaultPromptTemplate默认模板实现
SystemPromptTemplate系统提示词模板
MessageType消息类型枚举

系列目录

  • 第 1 篇:整体架构与核心抽象
  • 第 2 篇:ChatClient 调用链路
  • 第 3 篇:Prompt 与 Message 体系(本篇)

需要Spring AI系列学习代码的同学 欢迎关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。