SpringAIのきほんのき

## Hello SpringAI
[SpringAI](https://spring.pleiades.io/spring-ai/reference/)はSpringの**AIアプリケーションフレームワーク**です。テキスト生成に限らず画像生成AIや音声生成AIにもサポートを広げています。
テキスト生成においては他のLangChain4jと同じように、さまざまな開発補助をしてくれます。

![スクリーンショット 2024-07-19 11.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3801871/98a8dac2-731a-8352-d037-9850b1987389.png)
> [人工知能](https://icons8.com/icon/101164/artificial-intelligence) アイコン by [Icons8](https://icons8.com)


GPTなどの主要なLLMサービスには対応しており、簡単なコードの実装でLLMが動作するアプリを開発することができます。
ローカルLLMに対しては[Ollama](https://ollama.com/)経由で対応しています。

Ollamaやllama.cppについては、以下のとおりになります。

|名前|説明|
|----|----|
|llama.cpp|「量子化」という技術をもって非力なコンピュータにおいても、それなりなサイズのLLMが動作できるようにしてくれる実行環境|
|Ollama|実行可能になった言語モデルの管理、APIサーバーの自動起動などを行うツール|

[Llama3](https://llama.meta.com/)、llama.cpp、Ollamaは名前が似ていますが別物なので注意が必要です。

## 基本的な扱い方
**執筆時点での最新バージョンは「1.0.0-SNAPSHOT」です。
直近でも仕様が大きく変更になるなど、安定版ではないことをご留意願います。**
APIドキュメントが古いままだったり、リファレンスが日本語として翻訳しきれていなかったりと不完全な状態です。場合によっては[GitHub](https://github.com/spring-projects/spring-ai)で最新の情報を仕入れた方が良いかもしれません。

基本的な扱い方は、
1. コントローラークラスに、扱いたい言語モデルに対応した**ChatModel実装クラス**を注入
1. **Promptオブジェクト**を生成し、チャット内容を詰め込む
1. 注入した**ChatModel型オブジェクト**の`call(Prompt)`もしくは`stream(Prompt)`を呼び出す
1. 戻り値として**ChatResponseオブジェクト**を受け取り、クライアント側へ渡す

です。

`call(Prompt)`もしくは`stream(Prompt)`については`call(String)`、`stream(String)`とすることもでき、その場合は上記の「2.」の工程は省略できます。（Promptオブジェクトは自動生成される）

コードにしてみると以下のとおりになります。

> [公式リファレンス](https://spring.pleiades.io/spring-ai/reference/api/chat/ollama-chat.html)より引用（OllamaChatModelの場合のサンプルコード）
>```java:
>    @RestController
>    public class ChatController {
>        private final OllamaChatModel chatModel;
>        @Autowired
>        public ChatController(OllamaChatModel chatModel) {
>            this.chatModel = chatModel;
>        }
>        @GetMapping("/ai/generate")
>        public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
>            return Map.of("generation", chatModel.call(message));
>        }
>        @GetMapping("/ai/generateStream")
>	    public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
>            Prompt prompt = new Prompt(new UserMessage(message));
>            return chatModel.stream(prompt);
>        }
>    }
>```

## クラス構造
### 1.ChatModel
さまざまな言語モデルと対話するためのクラスです。
抜粋したクラス図は以下のとおりです。

```mermaid
classDiagram
direction

class ChatModel {
    <<interface>>
    + call(Prompt) ChatResponse
}

class StreamingChatModel {
    <<interface>>
    + stream(Prompt) Flux~ChatResponse~
}

class OpenAiChatModel {
    + call(Prompt) ChatResponse
    + stream(Prompt) Flux~ChatResponse~
}

class VertexAiGeminiChatModel {
    + call(Prompt) ChatResponse
    + stream(Prompt) Flux~ChatResponse~
}

class OllamaChatModel {
    + call(Prompt) ChatResponse
    + stream(Prompt) Flux~ChatResponse~
}

ChatModel <|.. OllamaChatModel
StreamingChatModel <|.. OllamaChatModel
ChatModel <|.. OpenAiChatModel
StreamingChatModel <|.. OpenAiChatModel
ChatModel <|.. VertexAiGeminiChatModel
StreamingChatModel <|.. VertexAiGeminiChatModel

```

返答の仕方として2種類、**ChatModelインターフェース**と**StreamingChatModelインターフェース**が用意されています。

公式に対応している言語モデルは実装クラスが用意されており、基本的には**使いたい言語モデルのクラスを選択してコントローラークラスに注入**します。
なにかしらの理由でカスタムしたい場合は、実装クラスの継承、もしくはインターフェースの実装を独自で行います。

代表的なメソッドは以下の通りです。

|メソッド|戻り値|説明|
|----|----|----|
|`call(Prompt prompt)`|ChatResponse|通常のレスポンスを返す（まとめて1回）|
|`stream(Prompt prompt)`|Flux\<ChatResponse>|レスポンスをストリーミングする|

### 2.Prompt
言語モデルに対してリクエストする際の情報をまとめたクラスです。
抜粋したクラス図は以下のとおりです。

```mermaid
classDiagram
direction RL

class ModelRequest~T~ {
    <<interface>>
    + getInstructions() T
    + getOptions() ModelOptions
}
class ModelOptions {
    <<interface>>
}

class Prompt {
    + getInstructions() List~Message~
    + getOptions() ChatOptions
}
class ChatOptions {
     
}
class Message {
    <<interface>>
    + getContent() 
    + getMessageType()
}
class AbstractMessage {
     
}
class UserMessage {
     
}

Message <-- Prompt
AbstractMessage ..|> Message
ModelOptions <|.. ChatOptions
ModelRequest <|.. Prompt
ModelRequest --> ModelOptions
ChatOptions <-- Prompt
UserMessage --|> AbstractMessage

```

Promptは**Message（AbstractMessage）**と**ChatOptions**をカプセル化するModelRequestの実装クラスです。

Message（AbstractMessage）には「言語モデルに渡すテキストメッセージ」と「そのメッセージの種別（enum）」が格納されます。

ChatOptionsは言語モデルを使用する際の**既定設定を上書き**したいときにオブジェクト生成をします。シンプルに使用したいときに意識する必要はありません。

UserMessageは、Message（AbstractMessage）オブジェクトの新規生成処理を**ラップします**。メッセージ種別が「USER」であるオブジェクトを、`new`するだけで新規生成してくれます。後述するAssistantMessageも同じようにメッセージ種別が「ASSISTANT」であるオブジェクトを生成してくれます。
通常においては直接Message（AbstractMessage）オブジェクトを生成するのではなく、UserMessageなどを用いて間接的に生成するのが良さそうです。

|クラス|説明|
|----|----|
|Prompt|MessageとChatOptionsをカプセル化する|
|Message（AbstractMessage）|「テキストメッセージ情報」と「メッセージ種別（USERやASSISTANTなど）情報」を所有する|
|ChatOptions|上書きしたい既定設定の情報を所有する|
|UserMessage|メッセージ種別が「USER」であるMessage型オブジェクトを新規に生成する|

### 3.ChatResponse
言語モデルからの返答はChatResponseオブジェクトで出力されます。
抜粋したクラス図は以下のとおりです。

```mermaid
classDiagram
direction RL

 class ChatResponse {
     + getResults() List~Generation~
 }

 class Generation {
     + getOutput() AssistantMessage
 }

 class AssistantMessage {
      
 }

 class ModelResponse {
     <<interface>>
     + getResults() List~R extends ModelResult~?~~
 }

 class ModelResult~T~ {
     <<interface>>
     + getOutput() T
 }

 class Message {
     <<interface>>
     + getContent() 
     + getMessageType()
 }
 class AbstractMessage {
    
 }

 Generation --o ChatResponse
 AssistantMessage <-- Generation

 AbstractMessage <|-- AssistantMessage
 Message <|.. AbstractMessage

 ModelResponse <|.. ChatResponse
 ModelResult <|.. Generation
 ModelResponse o-- ModelResult
 ```

本来は**メタデータ用のクラスもあります**が、図では省略しています。

ChatResponseにはGenerationオブジェクトのListが含まれています。
GenerationはAssistantMessage型のフィールドを所有しているため、AssistantMessageを経由してMessage（AbstractMessage）がGenerationに内包されていることになります。（Generationの中身はメッセージ種別が「ASSISTANT」のMessage）

生成されたレスポンスにアクセスしたい場合、構造上Listになっているので、`ChatResponse#getResults()`に対して`get(0)`を使います。（もしくは`ChatResponse#getResult()`を呼ぶことで自動的に`get(0)`し、単一のGenerationを取得します）

よって、テキストのみを取得したい場合は、
```java
    @GetMapping("/generate")
    public String generate() {
        Prompt prompt = new Prompt(new UserMessage("Tell me a joke"));
        ChatResponse response = CHAT_MODEL.call(prompt);

        // 生成されたテキスト部分のみを抽出
        return response.getResult().getOutput().getContent();
    }
```
となります。