尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI系列5:Spring AI的聊天模型API

AI系列5:Spring AI的聊天模型API 传送门PyCharm无法识别LangChain库的问题AI系列1Spring AIAI系列2Spring AI调用本地模型AI系列3什么是TokenAI系列4Spring AI调用智谱模型再谈聊天模型在前面几节中分别介绍了几种不同模型的接入AI系列1Spring AI演示了deepseek的模型AI系列2Spring AI调用本地模型演示了qwen模型AI系列4Spring AI调用智谱模型演示了智谱模型其中deepseek、智谱用的云端模型qwen是本地模型当然也可以用gemini、gpt这种国外模型考虑需要梯子支持就没有演示不过无论哪一种背后的基础都在于同一个词模型。关于模型在AI系列3什么是Token里面有过介绍再回顾一下定义模型ModelsAI models are algorithms designed to process and generate information, often mimicking human cognitive functions. By learning patterns and insights from large datasets, these models can make predictions, text, images, or other outputs, enhancing various applications across industries.翻译过来就是AI 模型是用于处理和生成信息的算法通常模拟人类的认知功能。通过学习海量数据集中的模式和规律这些模型能够进行预测生成文本、图像或其他输出从而赋能各行各业的应用程序。下表根据模型的输入和输出类型对多个模型进行分类图片来自 AI Concepts :: Spring AI Reference从上面可以看出Spring AI支持语言聊天、图像、音频、嵌入向量模型。而每个模型的的运行流程都是根据输入的数据类型文本、图像、音频、视频选择对应的生成式 AI 模型就能得到相应格式的输出。1.大型语言模型LLMLLM全称是Large Language Model如果想生成文本或代码就用LLM因为它的核心架构设计是为了理解和生成自然语言。这也是平时最常见、用的最多的模型比如deepseek网页版本用自然语言也就是大白话向它提问经过模型的推理之后它也通过自然语言输出答案除了deepseek之外刚才提到的其它模型qwen、glm其它的大模型厂商都提供了对应的LLM模型图片 Chat Models Comparison :: Spring AI Reference日常使用中可能会把所有看起来复杂的AI都笼统地叫“大模型”而且背后指的也都基本是LLM的模型应用比如deepseek、豆包、元宝等。就算输入的是一张图片并不是文字让它分析一下它也能给出正确的答案。而这就是LLM的多模态支持多模态刚才说了LLM的核心原理是理解和生成自然语言而这一切输入输出都是文本但是在多模型的加持下也可以输入图片或音频比如将刚才那张原理图片输入给deepseek让它分析一下它也能正确读取用户的输入并且进行思考最终给出了答案这背后的原因是deepseek内置或者说外挂了一个对应的图片解码器。工作原理当你输入一张图片或一段音频时系统会先启动一个专门的“编码器”如视觉编码器或语音识别模型将这些非文本数据转化成文本描述或文本特征向量然后再喂给背后的纯文本LLM进行处理。本质还是依赖只能根据文本进行推理的LLM但是它具备了处理图片或者音频的能力这就叫作多模态支持为什么这个说法要打个引号呢因为这种做法是现在主流的实现。现阶段LLM依靠外部的相关能力来对大模型进行增强从而实现多模型也被称为传统做法。随着前沿趋势的发展最新一代的大模型的多模态已经不再依靠这种外部手部了它本身就是原生的。工作原理模型从预训练阶段开始就是在文本、图像、音频、视频数据上“混合喂养”联合训练的模型内部不再有“文本区”和“图像区”之分所有模态的数据都被统一转化为模型内部的“通用表示”Token。模型在处理视频帧时其内部的注意力机制Attention能直接计算像素与文字之间的逻辑关系。2. 特定模型除了LLM通用模型之外还有一些针对图片、音视频、搜索等细分场景的专用模型。图像生成模型Image Generation Model输入语言 / 图像可能是文本描述或参考图像输出图像 / 视频流程说明这个模型专门负责“从文字到图像/视频”或“从图像到图像”的生成。比如输入一句描述“一只猫在草地上”它输出一张或一段对应的图片/视频。图中也将“语言/图像”作为输入输出“图像/视频”。比如Hugging Face Spaces上试用deepseek的模型因为输入的中文它不是很理解但是用智谱的就靠谱一些直接输出了结果也不用梯子GLM-Image 是智谱新旗舰图像生成模型 模型全程基于国产芯片完成训练采用独创的「自回归扩散解码器」混合架构兼顾全局指令理解与局部细节刻画克服了海报、PPT、科普图等知识密集型场景生成难题是面向以 Nano Banana Pro 为代表的新一代「认知型生成」技术范式的一次重要探索。音频转换模型Text‑to‑Voice / Voice‑to‑Text这一行其实包含两个方向相反的模型文本转语音模型Text‑to‑Voice Model输入语言文本 → 输出音频语音语音转文本模型Voice‑to‑Text Model输入音频语音 → 输出语言文本流程说明这是针对音频的专用模型负责在语音和文字之间进行互相转换。前者用于生成人声后者用于语音识别。还是使用智谱的模型来试下输出猫叫效果还不错标准主播的声音。GLM-4-Voice 是智谱推出的首个端到端语音模型。它能够直接理解和生成中英文语音实现实时语音对话并可根据用户指令灵活调整语音的情感、语调、语速和方言等特性使语音交互更加自然生动。嵌入模型Embedding Model输入文本 / 图像 / 音频 / 视频即任意多模态数据输出向量 / 嵌入Vector/Embedding流程说明嵌入模型不生成人类可读的内容而是将输入数据转化为高维数值向量即 embedding用于表示语义或特征。这些向量常用于检索、相似度计算、聚类等高级 AI 应用并不是直接给人看的。这个模型现在用的就是最近很火的RAGRetrieval-Augmented Generation检索增强生成了。这个在后面讨论RAG时再深入学习Spring AI对聊天模型的抽象Spring AI做为一个统一的框架面对不同的模型设计了一套自己的机制与API所以先来看看它对聊天模型设计的目标TheSpring AI Chat Model APIis designed to be a simple and portable interface for interacting with various AI Models, allowing developers to switch between different models with minimal code changes. This design aligns with Spring’s philosophy of modularity and interchangeability.这Spring AI Chat Model API旨在成为一个简单且可移植的界面用于与各种 AI 模型交互允许开发人员以最少的代码更改在不同模型之间切换。 这种设计与 Spring 的模块化和可互换性理念一致。所以它遵循了spring一贯的理念将具体的实现能力通过下沉到各个实现里面提供统一、稳定的接口并且将不同的能力通过抽象、隔离达到解耦。这些就通过代码来具体分析是如何设计实现的聊天模型Spring AI对聊天模型定义的接口就是ChatModel下面是它的代码package org.springframework.ai.chat.model; import java.util.Arrays; import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.Model; public interface ChatModel extends ModelPrompt, ChatResponse, StreamingChatModel { default Nullable String call(String message) { Prompt prompt new Prompt(new UserMessage(message)); Generation generation call(prompt).getResult(); return (generation ! null) ? generation.getOutput().getText() : ; } default Nullable String call(Message... messages) { Prompt prompt new Prompt(Arrays.asList(messages)); Generation generation call(prompt).getResult(); return (generation ! null) ? generation.getOutput().getText() : ; } Override ChatResponse call(Prompt prompt); /** * Gets the chat options for this model. * return the chat options * since 2.0.0 */ default ChatOptions getOptions() { return ChatOptions.builder().build(); } /** * deprecated use {link #getOptions()} instead. */ Deprecated(forRemoval true) default ChatOptions getDefaultOptions() { return getOptions(); } default FluxChatResponse stream(Prompt prompt) { throw new UnsupportedOperationException(streaming is not supported); } }很自然的就会发现前面通过Spring AI调用的例子里面都是使用第一个方法default String call(String message) { Prompt prompt new Prompt(new UserMessage(message)); Generation generation this.call(prompt).getResult(); return generation ! null ? generation.getOutput().getText() : ; }用户的所有输入都做为一个字符串参数message直接传入对应的大模型然后同步返回大模型对应的字符串结果给客户端比如之前的例子中用户输入的问题你是谁呀?仔细分析一下这个代码逻辑default Nullable String call(String message) { // 1. 将用户输入问题构建成一个用户消息提示词对象 Prompt prompt new Prompt(new UserMessage(message)); // 2. 将提示词对象传递给进去发起大模型请求 Generation generation call(prompt).getResult(); // 3. 获取大模型的返回注意是对应的文本返回 return (generation ! null) ? generation.getOutput().getText() : ; }提示词在上面第一步中出现了一个UserMessage对象从名字可以看出来应该是用户输入的消息public class UserMessage extends AbstractMessage implements MediaContent { protected final ListMedia media; public UserMessage(String textContent) { this(textContent, new ArrayList(), Map.of()); } private UserMessage(String textContent, CollectionMedia media, MapString, Object metadata) { super(MessageType.USER, textContent, metadata); Assert.notNull(media, media cannot be null); Assert.noNullElements(media, media cannot have null elements); this.media new ArrayList(media); } public UserMessage(Resource resource) { this(MessageUtils.readResource(resource)); } // 省略其它方法get }然后看到它实现了Message接口public interface Message extends Content { MessageType getMessageType(); }而这个接口里面有个MessageType枚举类public enum MessageType { /** * A {link Message} of type {literal user}, having the user role and originating * from an end-user or developer. * see UserMessage */ USER(user), /** * A {link Message} of type {literal assistant} passed in subsequent input * {link Message Messages} as the {link Message} generated in response to the user. * see AssistantMessage */ ASSISTANT(assistant), /** * A {link Message} of type {literal system} passed as input {link Message * Messages} containing high-level instructions for the conversation, such as behave * like a certain character or provide answers in a specific format. * see SystemMessage */ SYSTEM(system), /** * A {link Message} of type {literal function} passed as input {link Message * Messages} with function content in a chat application. * see ToolResponseMessage */ TOOL(tool); }显然我们可以将刚才的UserMessage与MessageType中的USER对应那它具体代表什么意思呢其它几种类型又是什么意思呢消息类型MessageType是一个枚举定义了聊天消息的四种类型枚举值字符串值含义USERuser用户或开发者发的消息ASSISTANTassistantAI助手生成的消息SYSTEMsystem系统级指令、角色设定、格式要求TOOLtool工具/函数调用结果每个枚举常量后面的字符串比如user、assistant通常就是最终发给大模型API时的role字段这点在官网上也点明了的下面是对应的类图结构图片 Chat Model API :: Spring AI Reference为什么称这个MessageType是角色呢这主要源于OpenAI API 的约定并已成为行业通用标准。例如请求 OpenAI 风格接口时消息可能长这样[ { role: system, content: 你是一个 Java 助手 }, { role: user, content: 解释一下枚举 }, { role: assistant, content: 枚举是一种特殊类... } ]OpenAI 的role约定在OpenAI的Chat Completions API中每条消息都是一个JSON对象其中role字段是必需的用于告诉模型“这条消息是谁说的”。其官方约定的取值包括role值含义system系统指令设定 AI 的角色、行为准则user终端用户的输入或提问assistantAI模型之前生成的回复tool工具/函数调用的返回结果这个约定最早由OpenAI在ChatMLChat Markup Language中提出并在Chat Completions 接口中正式落地。所有兼容OpenAI API的模型如 Azure OpenAI、DeepSeek 等都遵循这套角色规范。具体可以参见这里面MessagesSpring AI作为抽象层需要屏蔽不同AI提供商的API差异。由于OpenAI的Chat Completions接口已成为事实标准Spring AI的MessageType枚举直接映射了OpenAI的四个role取值系统角色系统角色设置系统指令设定 AI 的角色、行为准则大概意思就是指导AI的行为和响应风格为 AI如何解释和回复输入设置参数或规则。这类似于在开始对话之前向AI提供指令。比如还是用以前那个例子通过system-系统角色来设定一下AI的风格你是一个严谨的AI助手请用简洁专业的语言回答问题然后修改一下前面的例子GetMapping(/ai/generate) public Map generate(RequestParam(value message, defaultValue Tell me a joke) String message) { SystemMessage systemMessage new SystemMessage(你是一个严谨的AI助手请用简洁专业的语言回答问题); UserMessage userMessage new UserMessage(message); return Map.of(generation, this.chatModel.call(userMessage, systemMessage)); }然后问题还是原来那个问题你是谁呀可以很明显的对比发现比以前回答简洁了许多当然这个方法调用的不再是上面那个默认的call方法传递的不是String而是另外一个接收Message的动态参数的方法default String call(Message... messages) { Prompt prompt new Prompt(Arrays.asList(messages)); Generation generation this.call(prompt).getResult(); return generation ! null ? generation.getOutput().getText() : ; }像主流的聊天系统也提供了设置系统角色或者sytem_prompt的功能比如智谱用户角色用户角色表示用户的输入 – 他们对AI的问题、命令就是UserMessage对象至于UserMessage与SystemMessage的区别最大的就是SystemMessage全局的不需要用户每次都在提示词中编写并发给大模型了这里就不过多赘述了助理角色三个角色里面可能最难理解的就是助理角色Assistant Role了。先来看看Spring AI对它的描述Assistant Role: The AI’s response to the user’s input. More than just an answer or reaction, it’s crucial for maintaining the flow of the conversation. By tracking the AI’s previous responses (its Assistant Role messages), the system ensures coherent and contextually relevant interactions. The Assistant message may contain Function Tool Call request information as well. It’s like a special feature in the AI, used when needed to perform specific functions such as calculations, fetching data, or other tasks beyond just talking.AI 对用户输入的响应。 它不仅仅是一个答案或反应它对于保持对话的流畅性也至关重要。 通过跟踪 AI 之前的响应其“助理角色”消息系统可确保连贯且与上下文相关的交互。 Assistant 消息也可能包含函数工具调用请求信息。 它就像 AI 中的一项特殊功能在需要时用于执行特定功能例如计算、获取数据或除交谈之外的其他任务。Assistant Role的核心作用一句话就能概括它代表“AI 之前说过的话”。在多轮对话或工具调用中必须把 AI 之前的回复也作为历史消息传给模型否则模型根本不知道它自己上一轮说了什么。比如下面这个简单的例子我们先告诉大模型用户的名字叫小明再反问大模型用户的名字是什么在第一次提问的时候大模型回复了收到小明但是再提问的时候它竟然说不知道用户的名字了打脸来的如此之快难道是智谱模型不够智能这就需要Assistant Role的作用了。现在改写一下上面的例子代码如下GetMapping(/ai/generate) public Map generate(RequestParam(value message, defaultValue Tell me a joke) String message) { SystemMessage systemMessage new SystemMessage(你是一个严谨的AI助手请用简洁专业的语言回答问题); 用户第一次的提问模拟 UserMessage userMessage1 new UserMessage(我的名字叫小明); // 大模型第一次的回复进行模拟用再次传递给大模型 AssistantMessage assistantMessage new AssistantMessage(收到小明。请问有什么可以帮助您); // 用户第二次的提问 UserMessage userMessage new UserMessage(message); return Map.of(generation, this.chatModel.call(userMessage1, userMessage, systemMessage, assistantMessage)); }在刚才的代码中手动用户第一次提问、大模型第一次的回复进行模拟用再次传递给大模型。然后再次进行刚才的提问我叫什么名字大模型的记忆通过上面的例子发现大模型好像没有记忆一样会忘记自己说过的话。更准确地说这不是“大模型的忘记”而是“大模型没有内置记忆需要用户提供上下文”。专业上主要涉及这几个术语大模型是“无状态”的StatelessOpenAI、DeepSeek这类Chat API本身不保存会话状态。每次调用chatModel.call(prompt)模型都是独立推理用户传给它什么它就基于什么生成回复。它不会自动记得用户上一轮说了什么因为它根本没有任何“上一轮”的内部存储。所以如果用户第二次只传UserMessage我叫什么名字模型没有见过“我叫小明”这句话自然不知道。这不是它“忘了”而是用户根本没把那条信息放进这次输入里。传给模型的完整消息列表叫“上下文”Context每次请求时把SystemMessage、UserMessage、AssistantMessage按顺序拼成一个列表这个列表就是本次推理的上下文也叫对话历史Conversation History。其中UserMessage用户说过的话AssistantMessageAI之前说过的话SystemMessage系统指令AssistantMessage的核心作用就是把AI之前的输出重新放回上下文里让模型这次能“看到自己上一轮说了什么”。“记忆”是应用层实现的不是模型自带的所谓“大模型记住了”其实是你的程序把历史消息存了下来内存、数据库、Redis 等下次请求时再拼进上下文。Spring AI里的ChatMemory、MessageChatMemoryAdvisor就是干这个的。所以模型本身没有记忆→ 专业叫“无状态”你每次传进去的历史→ 专业叫“上下文”或“对话历史”让模型看起来有记忆→ 专业叫“上下文管理”或“记忆机制”总结大模型是无状态的依赖上下文Context进行推理AssistantMessage 是对话历史的一部分。ChatMemory上面提到的大模型是“无状态”的Stateless可以在Spring AI的官网上的描述Large language models (LLMs) are stateless, meaning they do not retain information about previous interactions. This can be a limitation when you want to maintain context or state across multiple interactions. To address this, Spring AI provides chat memory features that allow you to store and retrieve information across multiple interactions with the LLM.大型语言模型 LLM 是无状态的这意味着它们不会保留有关以前交互的信息。当您希望在多个交互中维护上下文或状态时这可能是一个限制。为了解决这个问题Spring AI 提供了聊天内存功能允许您在与 LLM 的多次交互中存储和检索信息。而ChatMemory就是用来解决这种情况的具体可以参考Chat Memory先来看一下它的用法介绍。快速开始Spring AI会自动配置ChatMemory可以直接在应用程序中使用的beanAutowired ChatMemory chatMemory;因为前面的例子中使用的ChatModel所以改写代码如下RestController public class ChatMemoryController { Autowired private ZhiPuAiChatModel chatModel; Autowired private ChatMemory chatMemory; GetMapping(/ai/chatmemory/generate) public String chat(String conversationId, String message) { // 1. 用户新消息加入记忆 UserMessage userMessage new UserMessage(message); chatMemory.add(conversationId, userMessage); // 2. 取出该会话的全部历史包含刚加入的用户消息 ListMessage history chatMemory.get(conversationId); // 3. 调用模型 ChatResponse response chatModel.call(new Prompt(history)); // 4. 取出 AI 回复 AssistantMessage assistantMessage response.getResult().getOutput(); // 5. 把 AI 回复也加入记忆 chatMemory.add(conversationId, assistantMessage); // 6. 返回文本 return assistantMessage.getText(); } }运行一下然后连问它三个问题第一次问它请输出我叫什么名字因为没有告诉过它所以它也回答不来。所以接着告诉它我们的名字我的名字叫小明现在看起来大模型明确回复知道我们是谁了现在可以喊出那几个字了我叫什么名字至此就通过ChatMemory实现了一个最简单的会话历史会话存储内存存储Spring AI提供了ChatMemoryRepository用于存储聊天内存的抽象。直接看看刚才的代码里面的chatMemory.add方法源码public interface ChatMemory { String CONVERSATION_ID chat_memory_conversation_id; default void add(String conversationId, Message message) { Assert.hasText(conversationId, conversationId cannot be null or empty); Assert.notNull(message, message cannot be null); this.add(conversationId, List.of(message)); } void add(String conversationId, ListMessage messages); }ChatMemory是一个接口它的实现就是MessageWindowChatMemorypackage org.springframework.ai.chat.memory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Stream; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.util.Assert; public final class MessageWindowChatMemory implements ChatMemory { private static final int DEFAULT_MAX_MESSAGES 20; private final ChatMemoryRepository chatMemoryRepository; private final int maxMessages; private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) { Assert.notNull(chatMemoryRepository, chatMemoryRepository cannot be null); Assert.isTrue(maxMessages 0, maxMessages must be greater than 0); this.chatMemoryRepository chatMemoryRepository; this.maxMessages maxMessages; } public void add(String conversationId, ListMessage messages) { Assert.hasText(conversationId, conversationId cannot be null or empty); Assert.notNull(messages, messages cannot be null); Assert.noNullElements(messages, messages cannot contain null elements); ListMessage memoryMessages this.chatMemoryRepository.findByConversationId(conversationId); ListMessage processedMessages this.process(memoryMessages, messages); this.chatMemoryRepository.saveAll(conversationId, processedMessages); } }最终的存储就是通过ChatMemoryRepository这个接口来实现的package org.springframework.ai.chat.memory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.ai.chat.messages.Message; import org.springframework.util.Assert; public final class InMemoryChatMemoryRepository implements ChatMemoryRepository { MapString, ListMessage chatMemoryStore new ConcurrentHashMap(); public ListString findConversationIds() { return new ArrayList(this.chatMemoryStore.keySet()); } public void saveAll(String conversationId, ListMessage messages) { Assert.hasText(conversationId, conversationId cannot be null or empty); Assert.notNull(messages, messages cannot be null); Assert.noNullElements(messages, messages cannot contain null elements); this.chatMemoryStore.put(conversationId, messages); } }而这个就可以看出默认存储其实就是内存存储InMemoryChatMemoryRepositorystores messages in memory using aConcurrentHashMap.By default, if no other repository is already configured, Spring AI auto-configures aChatMemoryRepositorybean of typeInMemoryChatMemoryRepositorythat you can use directly in your application.InMemoryChatMemoryRepository使用ConcurrentHashMap.默认情况下如果尚未配置其他存储库则 Spring AI 会自动配置ChatMemoryRepository类型的 beanInMemoryChatMemoryRepository可以直接在应用程序中使用。当然Spring AI也支持其它的存储进行扩展比如数据库、nosql之类数据库存储JdbcChatMemoryRepository是使用 JDBC 在关系数据库中存储消息的内置实现。它支持多个开箱即用的数据库适用于需要持久存储聊天内存的应用程序。这里就不做演示了具体可以看JdbcChatMemoryRepositoryNosqlCassandraCassandraChatMemoryRepository使用 Apache Cassandra 存储消息。它适用于需要持久存储聊天内存的应用程序尤其是可用性、持久性、规模以及利用生存时间 TTL 功能时。这里就不做演示了具体可以看CassandraChatMemoryRepositoryRedis也支持redis这里也不展开了具体可以看RedisChatMemoryRepository文档数据库Spring AI甚至支持mongo这里也不展开了具体可以看MongoChatMemoryRepository会话保持这个地方的会话指的是跟大模型多轮对话时为了保持用户与大模型之间的会话联系而做的系统会话设置可以类比为用户登录的session。比如deepseek里面的开启新对话、对话历史等每一条记录都对应一个会话这就可以类比登录session的sessionId。而在聊天模型的场景就是conversationId上面代码中的在007就是conversationId只不过前面方便测试写死了一个值在真实的场景下也需要应用层面来维护会话的生成、注销、存储等生命周期。ChatClient前面直接使用ChatModel而不是ChatClient中所以都是显式管理内存。还有一个简单的办法是直接使用ChatClient APIChatClient Advisor 自动记忆。实际开发中一般不用手动chatMemory.add()而是用ChatClientMessageChatMemoryAdvisor它会自动帮你存取。这个在下一节在仔细来介绍并实现一个完整的例子
返回列表