歡迎訪問我的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}@OverrideChatResponse 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());}@OverrideFlux<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;@Overridepublic ChatOptions getOptions() {..}@Overridepublic 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;@Overridepublic ChatResponseMetadata getMetadata() {...}@Overridepublic List<Generation> getResults() {...}// other methods omitted
}
Generation
- Generation中有響應的具體信息,由ChatGenerationMetadata和AssistantMessage組成
public class Generation implements ModelResult<AssistantMessage> {private AssistantMessage assistantMessage;private ChatGenerationMetadata chatGenerationMetadata;@Overridepublic AssistantMessage getOutput() {...}@Overridepublic 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);}@Overridepublic String toString() {return "AssistantMessage{" + "content='" + getContent() + '\'' + ", properties=" + properties + ", messageType="+ messageType + '}';}}
- 至此,基礎理論知識已經過了一遍,相信大家和我一樣,進入了一看就會,一用就廢的微妙階段,不急,下一篇就是精彩的實戰篇,這些知識點終究會在實戰中用到,隨著一行行代碼一次次請求被理解,最終融匯貫通
你不孤單,欣宸原創一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 數據庫+中間件系列
- DevOps系列