欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
Spring AI实战全系列链接
- Spring AI实战之一:快速体验(OpenAI)
- Spring AI实战之二:Chat API基础知识大串讲(重要)
- SpringAI+Ollama三部曲之一:极速体验
- SpringAI+Ollama三部曲之二:细说开发
本篇概览
- 如果说前文是最简单的介绍Spring AI,满足Java程序员的好奇心,那么本篇就是正儿八经的基础课了:梳理Spring AI框架中的Chat核心API、类、接口,SpringAI的能力就是依靠它们释放出来的
- 本篇的目标:学习SpringAI库的最基本的类、接口、API,并了解它们的具体用途
用一个问题开篇
- 首先回答一个问题,为什么标题是Chat API基础知识,而不是Spring AI基础知识?
- 因为Spring AI内部由很多部分组成,Chat只是其中之一,或者说大模型提供的能力有很多,聊天只是其中一部分,SpringAI提供的能力如下所示
- 所以,Spring AI的内容很多,Chat只是其中一部分,但是这部分非常重要且基础,适合用来入门
- 接下来开始正式学习吧,东西不多,总结下来就是:六个概念+三个层次,掌握了它们,各种大模型都能轻松驾驭了
关于Chat API的六个核心概念和三个层次
- 从业务逻辑上看,Chat API涉及到六个概念,分为三类,如下图所示
- client:这个好理解,代表各模型的客户端,负责请求和响应的
- prompt:理解成请求的最外层封装,里面有message和option
- message:这个好理解了,发送到大模型的内容,另外还包含了一些属性在里面,以及消息类型
- option:相当于参数、控制项,例如本次对话的temperature(值越小,大模型回答越严谨,值越大,大模型回答越有创造性)
- response:响应对象,里面封装了大模型返回的信息,主要是generation
- geenration:这里面是具体的返回内容
- 再来看三个层次,前面我们知道SpringAI支持大模型的多种能力,聊天只是其中一种,因此就有一个代表最顶层的抽象层,与大模型有关的各种能力,都在此有个定义,然后是代表各种能力的抽象层,如聊天、图片、嵌入式处理等,最后是每一种能力在各类具体大模型上的实现,如下图所示
- 到现在为止咱们还没有看一行代码一个API,但是从理论上对Chat API的定位、关系已经基本了解了,是时候结合代码来看了
官方图
- 下图来自官方文档,结合前面的分析来看一下,后面有导读
- 先看最下面橙色这层,中间是client,这里有两种,ModelClient代表了常规的请求响应,StreamingModelClient代表了流式响应(数据并非一次性传输,而是建立链接后源源不断的输出)
- client的左侧是request,里面包含了option,至于prompt,那是Chat的概念,所以不会出现在橙色这一层
- client右侧是response,同样只有抽象的ResponseMeta和ResultMetaData,generation是Chat的概念,不会在橙色这一层出现
- 再往上看,绿色的就是功能抽象层了,ChatClient继承了ModelClient,Prompt继承了ModelRequest,代表Chat领域的请求,同理CharResponse继承了ModelResponse
- 有了理论基础,一张官方图就让我们看清了Chat API的大概,现在还缺点东西,就是具体的实现层,毕竟有很多种大模型能,最终编码时还是要用到实现层的类,有没有什么方式将实现层完美的展现出来?
- 感谢SpringAI官方,实现层和功能抽象层的关系,被下面的官方图梳理得清清楚楚
- 此刻再来回顾Spring AI实战之一:快速体验(OpenAI)一文的代码,如下所示,尽管调用了OpenAI的接口,但是并未看到OpenAI相关的类,这是因为Spring已经做好了封装,咱们直接用依赖注入的ChatClient即可,这是抽象层接口,具体实现是SpringAI根据propeties的配置实例化的OpeAIChatClient对象
package com.bolingcavalry.helloopenai.controller;
import org.springframework.ai.chat.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class SimpleAiController {
// 负责处理OpenAI的bean,所需参数来自properties文件
private final ChatClient chatClient;
public SimpleAiController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping("/ai/simple")
public Map<String, String> completion(@RequestBody Map<String,String> map) {
return Map.of("generation", chatClient.call(map.get("message")));
}
}
- 其实说到这里,您已经对Chat API有了比较清晰的理解了,来看看那六大概念的具体代码吧,接下来是一段自然的、水到渠成的体验,毕竟已经领会了其神,现在是观其形的时候
- 接下来要看的代码如下图所示
ChatClient
- 大模型聊天功能的客户端接口,在进程中,其实现就是各大模型对应的客户端类
public interface ChatClient extends ModelClient<Prompt, ChatResponse> {
default String call(String message) {// implementation omitted
}
@Override
ChatResponse call(Prompt prompt);
}
- 可见主要是call方法,这就是最常规的聊天功能,调用call发送请求,返回值就是大模型的响应
StreamingChatClient
- 这也是客户端类,用于调用大模型的功能,与ChatClient不同的是,ChatClient是请求响应,返回对象ChatResponse就是大模型返回的全部内容,而StreamingChatClient返回的是Flux,这是流式返回,可以讲大模型的响应进行流式输出,如果您使用过各种大模型聊天工具,会发现响应的内容并非一次性展现,而是一段一段的内容,持续不断的展现出来,这就是流式响应的效果
@FunctionalInterface
public interface StreamingChatClient extends StreamingModelClient<Prompt, ChatResponse> {
default Flux<String> stream(String message) {
Prompt prompt = new Prompt(message);
return stream(prompt).map(response -> (response.getResult() == null || response.getResult().getOutput() == null
|| response.getResult().getOutput().getContent() == null) ? ""
: response.getResult().getOutput().getContent());
}
@Override
Flux<ChatResponse> stream(Prompt prompt);
}
- 注意注解FunctionalInterface,表明这是个函数式接口
Prompt
- 前面看过了ChatClient和StreamingChatClient,会发现入参都是Prompt,可见这就是和大模型一次聊天的入参
- 下面是Prompt的源码,去掉了构造函数、toString这些之后就会发现,最重要的是Message和ChatOption,所以Prompt只是个打包,真正要提交到大模型的其实是Message和ChatOption
public class Prompt implements ModelRequest<List<Message>> {
private final List<Message> messages;
private ChatOptions modelOptions;
@Override
public ChatOptions getOptions() {..}
@Override
public List<Message> getInstructions() {...}
public String getContents() {
StringBuilder sb = new StringBuilder();
for (Message message : getInstructions()) {
sb.append(message.getContent());
}
return sb.toString();
}
// constructors and utility methods omitted
}
- 如果您对OpenAI有所了解,就知道prompt(提示词)并非只有用户输入的聊天内容那么简单,而是system、user 、assistant等多种类型 ,所以这里的Prompt并非只是一个外壳那么简单,它与不同类型的message、不同的辅助类等一起提供了完善的提示词功能,这个会有单独的文章来说明和实战,本篇只要记得它的最终形态就是打好的包用于提交给大模型
- 如果只是最基本的聊天,下面这个构造方法来创建对象就行了
public Prompt(String contents) {
this(new UserMessage(contents));
}
Message
- Message很好理解:在聊天过程中,聊天内容对应的对象,请求和响应用的都是Message,不过由于消息类型的多样性,Message被设计成了接口,根据不同类型都有对应的实现,如下图所示
- Message自身非常简单,能保证使用方取到消息内容、类型即可
public interface Message {
String getContent();
List<Media> getMedia();
Map<String, Object> getProperties();
MessageType getMessageType();
}
- 另外要注意的是消息类型,一共四种
public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
FUNCTION("function");
ChatOptions
- ChatOptions代表可以传递给大模型的控制参数,具体有哪些参数和大模型自身开放的特性有关,举个例子,下面是OpenAI开放的参数
- presencePenalty : 影响模型在生成文本时重复词语或概念的倾向
- frequencyPenalty:影响模型在生成文本时对已出现过词语的偏好程度
- 按照上面的解释,既然各种大模型都有自己的参数,那么设计ChatOptions能干啥?应该能放一些通用的控制参数吧,打开代码一看果然如此,共有三个通用参数,我都加了中文注释,另外请关注类的注释,也说明了这些参数是通用的、可移植、夸模型
/**
* The ChatOptions represent the common options, portable across different chat models.
*/
public interface ChatOptions extends ModelOptions {
// 大模型生成的内容应该更严谨还是更有创造性
Float getTemperature();
// 返回概率超过P的所有内容
Float getTopP();
// 返回概率最高的前K个内容
Integer getTopK();
}
- ChatOptions只是接口,对应的实现是ChatOptionsImpl,源码没啥好看的,就是temperature、topP、topK的get和set而已,为了实例化ChatOptionsImpl,还有配套工具ChatOptionsBuilder,用法如下
ChatOptions portablePromptOptions = ChatOptionsBuilder.builder()
.withTemperature(0.9f)
.withTopK(100)
.withTopP(0.6f)
.build();
- 代码看到这里,长期CRUD的我不禁产生一个想法:ChatOptions接口应该很不实用,而且用起来也很别扭,因为各大模型特有的参数和这个接口都没有关系,去看了下OpenAiChatClient.java(Ollama的客户端实现类,里面有段代码是用来封装请求的),果然,这代码真是不够优雅(个人感觉)
ChatResponse
- 看完请求该看响应了,既然Generation才是真正的响应内容,那么ChatResponse也就是个壳,里面包了Generation,打开源码一看,只有Generation和ChatResponseMetadata,这个ChatResponseMetadata可以理解为元信息,主要返回了大模型的API的使用情况说明,以及限速的详细信息
public class ChatResponse implements ModelResponse<Generation> {
private final ChatResponseMetadata chatResponseMetadata;
private final List<Generation> generations;
@Override
public ChatResponseMetadata getMetadata() {...}
@Override
public List<Generation> getResults() {...}
// other methods omitted
}
Generation
- Generation中有响应的具体信息,由ChatGenerationMetadata和AssistantMessage组成
public class Generation implements ModelResult<AssistantMessage> {
private AssistantMessage assistantMessage;
private ChatGenerationMetadata chatGenerationMetadata;
@Override
public AssistantMessage getOutput() {...}
@Override
public ChatGenerationMetadata getMetadata() {...}
// other methods omitted
}
- ChatGenerationMetadata代表返回内容的元信息,包含了结束原因、生成内容的过滤规则
- AssistantMessage更容易理解了:类型是ASSISTANT的消息,这个assistant就是助理角色,assistant消息就是大模型返回的聊天响应,源码如下
public class AssistantMessage extends AbstractMessage {
public AssistantMessage(String content) {
super(MessageType.ASSISTANT, content);
}
public AssistantMessage(String content, Map<String, Object> properties) {
super(MessageType.ASSISTANT, content, properties);
}
@Override
public String toString() {
return "AssistantMessage{" + "content='" + getContent() + '\'' + ", properties=" + properties + ", messageType="
+ messageType + '}';
}
}
- 至此,基础理论知识已经过了一遍,相信大家和我一样,进入了一看就会,一用就废的微妙阶段,不急,下一篇就是精彩的实战篇,这些知识点终究会在实战中用到,随着一行行代码一次次请求被理解,最终融汇贯通
你不孤单,欣宸原创一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列