iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
生成式 AI

使用 Spring AI 打造企業 RAG 知識庫系列 第 15

Day15 - ChatClient vs ChatModel

  • 分享至 

  • xImage
  •  

既生瑜,何生亮

https://ithelp.ithome.com.tw/upload/images/20240815/20161290XZLxuW3Mlf.png
記得我們 Day3 提到要自動綁定 ChatClient 卻失敗吧,今天來看看如何解決

程式碼實作

要初始化 ChatClient 只能使用 Builder 建立

@RestController
class MyController {
    private final ChatClient chatClient;
    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }
}

若有其他 Component 會使用 AI,可以在 Config 類別建立 Bean,要使用的 Component 在自動綁定即可

@Configuration
class Config {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.build();
    }
}

其實凱文大叔一直覺得之後的改版一定會讓 ChatClient 可以自動綁定

Client在Spring框架的意義

前面我們 ChatModel 也用得好好的,為何在講記憶之前要先說 ChatClient ?

在 Spring 框架中,結尾冠上 Client 的類別,就是更高級的封裝,例如:RestClient、WebClient、JdbcClient等,而且這一系列的類別都提供 Fluent 風格的 API,雖然 ChatModel 也提供一部分的 Fluent API,不過僅限於類別本身的參數,許多外部設定還是得先建立其他物件在引入

ChatClient 最大的差別就是將這些外部類別設定的資料也一起整合進來,下面就來比較 ChatModel 與 ChatClient 寫法的差異吧

ChatClient與ChatModel用法差異

1. Prompt

ChatModel:

ChatResponse response = chatModel
				.call(new Prompt(
			    	new UserMessage("Tell me a joke"),
			    ));

ChatClient:

ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();

這個範例看起來雖然程式碼差不多,不過可以看到 ChatClient 在設定 Prompt 時直接使用 .prompt(),ChatModel 卻要自己 new 一個,這就是兩者最大的差別

2. PromptTemplate

ChatModel:

@GetMapping("/template")
	public String template1(@RequestParam String llm) {	
		String template = "請問{llm}目前有哪些模型,各有甚麼特殊能力";
		PromptTemplate promptTemplate = new PromptTemplate(template);
		Prompt prompt = promptTemplate.create(Map.of("llm", llm));
		ChatResponse response = chatModel.call(prompt);
		return response.getResult().getOutput().getContent();
	}

ChatClient:

	@GetMapping("/template")
	public String template(@RequestParam String llm) {
		String template = "請問{llm}目前有哪些模型,各有甚麼特殊能力";
		ChatResponse response = chatClient.prompt()
				.user(u -> u.text(template)
						.param("llm", llm)
						)
				.call()
				.chatResponse();
		return response.getResult().getOutput().getContent();
	}

這個例子可以看出 ChatClient 在設定 Message 時直接使用 Lambda 語法建立 PromptTemplate 並使用 param 帶入參數,一氣呵成

3. Structured Output Converter

ChatModel:

record ActorsFilms(String actor, List<String> movies) {};
@GetMapping("/films")
public ActorsFilms films(String actor) {
	String template = """
	        列出演員{actor}最有名的五部電影,需用繁體中文回答
	        {format}
	        """;
	BeanOutputConverter<ActorsFilms> beanOutputConverter =
		    new BeanOutputConverter<>(ActorsFilms.class);
	String format = beanOutputConverter.getFormat();
	Generation generation = chatModel.call(
		    new Prompt(new PromptTemplate(template, Map.of("actor", actor, "format", format)).createMessage())).getResult();
	ActorsFilms actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
	return actorsFilms;
}

ChatClient:

record ActorsFilms(String actor, List<String> movies) {};
@GetMapping("/films")
public ActorsFilms films(String actor) {
String template = """
	        列出演員{actor}最有名的五部電影,需用繁體中文回答
	        """;
	ActorFilms actorFilms = chatClient.prompt()
    .user(u -> u.text(template).param("actor", actor))
    .call()
    .entity(ActorFilms.class);
  return actorFilms;
}

結構化輸出時 ChatClient 竟然只用 .entity() 就取代 BeanOutputConverter 越複雜的案例使用 ChatClient 就會讓程式碼更簡潔,ChatClient 其實是將 BeanOutputConverter 包進類別裡處理結構化輸出

4. Function Calling

因為 ChatClient 把 .prompt() 加入 Fluent API 中,原本要使用 Options 設定的 Function 的部分也跟著一起簡化了

ChatModel

@GetMapping("/func")
    public String func(String prompt) {
        return chatModel.call(
            new Prompt(prompt, 
               OpenAiChatOptions.builder()
               // Funciton可以放多筆,也能依據 API 接口放上合適的 Function
               .withFunction("ProductSalesInfo")
               .withFunction("ProductDetailsInfo")
               .build())
        		).getResult().getOutput().getContent();
    }

ChatClient

@GetMapping("/func")
    public String func(String prompt) {
        return chatClient.prompt().
            .user(prompt)
            .functions("ProductSalesInfo","ProductDetailsInfo")
            .call().content();
    }

Function 數量若不多,甚至能省略在 Config 中建立 Bean 的部分( 設定 Bean 的方式請參考 Day11),直接將 name、description 和 function 的實體一起放在 chatClient 中,如下面的程式碼

@GetMapping("/func")
    public String func(String prompt) {
        return chatClient.prompt().
            .user(prompt)
            .function("CurrectDateTime","Get the Date Time",new CurrectDateTimeFunction())
            .call().content();
    }

5. Using Defaults

在 ChatClient 中還能設定 Default 參數,這部分需要在 builder 時設定,之後執行時沒帶入參數就會使用預設的內容,舉個實際例子

@Configuration
class Config {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是個友善的聊天機器人,不管問甚麼問題都盡可能提供答案")
                .build();
    }
}

之後呼叫 chatClient 時若沒使用 .system() 覆蓋 SystemMessage,Spring AI 就會使用 defaultSystem 的內容來作為SystemMessage

下面是預設的方法以及正常呼叫的方法

Default Standard
defaultSystem system
defaultUser user
defaultFunction function
defaultFunctions functions
defaultOptions options
defaultAdvisors advisors

有注意到 advisors 嗎?這是前面沒提過的內容,也會是後面記憶跟 RAG 最重要的調用方法,就留在後面慢慢說明囉

回顧

今天學到的內容:

  1. 了解 Client 結尾的 Class 在 Spring 框架的含意
  2. ChatClient 與 ChatModel 用法比較
    • Prompt
    • PromptTemplate
    • Structured Output Converter
    • Function Calling
    • Using Defaults

今天以後程式碼的部分會盡量使用 ChatClient 來撰寫,有了前面幾天的基礎在看到簡化後的程式碼應該會更容易上手

Source Code

今日程式碼: https://github.com/kevintsai1202/SpringBoot-AI-Day15.git


認識凱文大叔

凱文大叔使用 Java 開發程式超過 20 年,對於 Java 生態非常熟悉,曾使用反射機制開發 ETL 框架,對 Spring 背後的原理非常清楚,目前以 Spring Boot 作為後端開發框架,前端使用 React 搭配 Ant Design
下班之餘在 Amazing Talker 擔任程式語言講師,並獲得學員的一致好評

最近剛成立一個粉絲專頁-凱文大叔教你寫程式 歡迎大家多追蹤,我會不定期分享實用的知識以及程式開發技巧

想討論 Spring 的 Java 開發人員可以加入 FB 討論區 Spring Boot Developer Taiwan

我是凱文大叔,歡迎一起加入學習程式的行列


上一篇
Day14 - 結構化資料轉換器
下一篇
Day16 - 魔鏡~誰是Spring AI的專家?
系列文
使用 Spring AI 打造企業 RAG 知識庫21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言