しばらくの間、spring ai は 1.0 に到達しました。0.8.1 と比較すると、かなりの違いがあります。最近は時間がたっぷりあるので、いじってみることにしました。
プロジェクト設定#
プロジェクトの設定を初期化する方法は 2 つあります。一つは作成時に直接対応する依存関係を選択する方法です。
もう一つは手動で設定する方法です。
maven
に以下を追加します。
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
次に、Dependency Management を追加します。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
最後に、対応する大規模言語モデルの依存関係を追加します。
<!-- OpenAI依存 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Ollama依存 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
次に、設定ファイルを作成します。
spring:
ai:
ollama:
base-url: http://127.0.0.1:11434/
chat:
model: qwen2:7b
openai:
base-url: https://xxx
api-key: sk-xxx
chat:
options:
model: gpt-3.5-turbo
server:
port: 8868
設定ファイルには 2 つのモデルを設定しました。一つは ollama
のもので、もう一つは openai
のものです。他のモデルについては、自分でドキュメントを参照して設定できます。
呼び出し#
1.0 バージョンでは呼び出し方法が変更され、主にインスタンス化されるオブジェクトが変わりました。
最新バージョンでは Chat Client API
が新たに追加され、もちろん前のバージョンの Chat Model API
もまだ存在しています。
彼らの違いは以下の通りです。
api | 範囲 | 作用 |
---|---|---|
Chat Client API | 単一モデルに適しており、グローバルにユニークです。複数モデルの設定は衝突を引き起こします | 最上位の抽象化で、この API はすべてのモデルを呼び出すことができ、迅速な切り替えが可能です |
Chat Model API | シングルトンパターンで、各モデルはユニークです | 各モデルには具体的な実装があります |
Chat Client#
Chat Client はデフォルトでグローバルにユニークであるため、設定ファイルには単一のモデルしか設定できません。そうでないと、Bean の初期化時に衝突が発生します。
以下は公式のサンプルコードです。
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/ai")
String generation(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.call()
.content();
}
}
同時に、作成時にいくつかのモデルのデフォルトパラメータを指定することもできます。
設定クラスを作成します。
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("あなたは友好的なチャットボットで、海賊の声で質問に答えます")
.build();
}
}
使用する際は @Autowired
で注入します。
複数モデルの設定を使用するには、ChatClient.Builder の自動設定を無効にする必要があります。
spring:
ai:
chat:
client:
enabled: false
次に、対応する設定ファイルを作成します。openai を例にします。
/**
* @author LiZhiAo
* @date 2024/6/19 20:47
*/
@Component
@RequiredArgsConstructor
public class OpenAiConfig {
private final OpenAiChatModel openAiChatModel;
public ChatClient openAiChatClient() {
ChatClient.Builder builder = ChatClient.builder(openAiChatModel);
builder.defaultSystem("あなたは友好的な人工知能で、ユーザーの質問に基づいて回答します");
return ChatClient.create(openAiChatModel);
}
}
これで呼び出すモデルを指定できます。
// 注入
private final OpenAiConfig openAiConfig;
// 呼び出し
Flux<ChatResponse> stream = openAiConfig.openAiChatClient().prompt(new Prompt(messages)).stream().chatResponse();
Chat Model#
各モデルにはそれぞれの Chat Model があり、同様に設定ファイルに基づいて自動装配されます。
OpenAiChatModel
を例にすると、ソースコードから装配プロセスを見ることができます。
したがって、呼び出しも非常に簡単です。
// 注入
private final OpenAiChatModel openAiChatModel;
// 呼び出し
Flux<ChatResponse> stream = openAiChatModel.stream(new Prompt(messages));
qwen2#
以前のある時期、私は LM Studio
を使用して llama3
をインストールし、 Local Inference Server
を起動してデバッグを試みました。
残念ながら、単純な呼び出しは確かに成功しましたが、ストリーミング出力に関しては常にエラーが発生しました。
仕方がないので、最終的には ollama
+ Open WebUI
の方法でローカルモデル API を起動しました。
インストール手順#
インストール環境は Windows コンピュータを例にし、NVIDIA グラフィックカードを備えています。他の方法については、Open WebUI のインストール方法を参照してください。
- ollamaをインストールします(オプション)。
- Docker Desktop をインストールします。
- イメージを実行します。
もしステップ 1 を実行している場合は、コンピュータに ollama をインストールします。ステップ 1 をスキップした場合は、以下の ollama を含むイメージを選択できます。docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
- モデルをダウンロードします。
コンテナが実行された後、Web 管理ページにアクセスしてモデルをダウンロードします。
qwen2
を例にすると、モデルを引き出す際にqwen2:7b
と入力して qwen2 の 7B バージョンをダウンロードします。
ステップ 2、3 の実行中に CUDA の問題が発生する可能性があります。
Unexpected error from cudaGetDeviceCount(). Did you run some cuda functions before calling NumCudaDevices() that might have already set an error? Error 500: named symbol not found
検索したところ、N カードのドライババージョンが 555.85 の場合に発生する可能性があることがわかりました。
解決方法は非常に簡単で、Docker Desktop を最新バージョンに更新するだけです。
実際に試したところ、 qwen2:7b
の中国語の返信は llama3:8b
よりもはるかに良く、残る欠点はマルチモーダルをサポートしていないことですが、どうやら開発チームはすでに取り組んでいるようです🎉
まとめ#
バックエンドコード#
完全な Controller は以下の通りです。
@RestController
@RequestMapping("/llama3")
@CrossOrigin
@RequiredArgsConstructor
public class llama3Controller {
private final OpenAiConfig openAiConfig;
private final OllamaConfig ollamaConfig;
private static final Integer MAX_MESSAGE = 10;
private static Map<String, List<Message>> chatMessage = new ConcurrentHashMap<>();
/**
* 提示語を返す
* @param message ユーザーが入力したメッセージ
* @return Prompt
*/
private List<Message> getMessages(String id, String message) {
String systemPrompt = "{prompt}";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);
Message userMessage = new UserMessage(message);
Message systemMessage = systemPromptTemplate.createMessage(MapUtil.of("prompt", "あなたは役に立つAIアシスタントです"));
List<Message> messages = chatMessage.get(id);
// メッセージが取得できなかった場合、新しいメッセージを作成し、システムの提示とユーザーの入力メッセージをメッセージリストに追加します
if (messages == null){
messages = new ArrayList<>();
messages.add(systemMessage);
messages.add(userMessage);
} else {
messages.add(userMessage);
}
return messages;
}
/**
* 接続を作成します
*/
@SneakyThrows
@GetMapping("/init/{message}")
public String init() {
return String.valueOf(UUID.randomUUID());
}
@GetMapping("chat/{id}/{message}")
public SseEmitter chat(@PathVariable String id, @PathVariable String message, HttpServletResponse response) {
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
SseEmitter emitter = SseEmitterUtils.connect(id);
List<Message> messages = getMessages(id, message);
System.err.println("chatMessageのサイズ: " + messages.size());
System.err.println("chatMessage: " + chatMessage);
if (messages.size() > MAX_MESSAGE){
SseEmitterUtils.sendMessage(id, "対話回数が多すぎます。しばらくしてから再試行してください🤔");
}else {
// モデルの出力ストリームを取得します
Flux<ChatResponse> stream = ollamaConfig.ollamaChatClient().prompt(new Prompt(messages)).stream().chatResponse();
// ストリーム内のメッセージをSSEで送信します
Mono<String> result = stream
.flatMap(it -> {
StringBuilder sb = new StringBuilder();
Optional.ofNullable(it.getResult().getOutput().getContent()).ifPresent(content -> {
SseEmitterUtils.sendMessage(id, content);
sb.append(content);
});
return Mono.just(sb.toString());
})
// メッセージを文字列に結合します
.reduce((a, b) -> a + b)
.defaultIfEmpty("");
// メッセージをchatMessage内のAssistantMessageに保存します
result.subscribe(finalContent -> messages.add(new AssistantMessage(finalContent)));
// メッセージをchatMessageに保存します
chatMessage.put(id, messages);
}
return emitter;
}
}
フロントエンドコード#
gpt にフロントエンドページを少し変更してもらい、MD のレンダリングとコードハイライトをサポートしました。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script>
</head>
<body class="bg-zinc-100 dark:bg-zinc-800 min-h-screen p-4">
<div class="flex flex-col h-full">
<div id="messages" class="flex-1 overflow-y-auto p-4 space-y-4">
<div class="flex items-end">
<img src="https://placehold.co/40x40" alt="avatar" class="rounded-full">
<div class="ml-2 p-2 bg-white dark:bg-zinc-700 rounded-lg w-auto max-w-full">こんにちは~(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄</div>
</div>
</div>
<div class="p-2">
<input type="text" id="messageInput" placeholder="メッセージを入力してください..."
class="w-full p-2 rounded-lg border-2 border-zinc-300 dark:border-zinc-600 focus:outline-none focus:border-blue-500 dark:focus:border-blue-400">
<button onclick="sendMessage()"
class="mt-2 w-full bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white p-2 rounded-lg">送信</button>
</div>
</div>
<script>
let sessionId; // セッションIDを保存するための変数
let markdownBuffer = ''; // バッファ
// markedとhighlight.jsを初期化します
marked.setOptions({
highlight: function (code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
});
// HTTPリクエストを送信し、レスポンスを処理します
function sendHTTPRequest(url, method = 'GET', body = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject(xhr.statusText);
if (body) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(body));
} else {
xhr.send();
}
});
}
// サーバーから返されたSSEストリームを処理します
function handleSSEStream(stream) {
console.log('ストリームが開始されました');
const messagesContainer = document.getElementById('messages');
const responseDiv = document.createElement('div');
responseDiv.className = 'flex items-end';
responseDiv.innerHTML = `
<img src="https://placehold.co/40x40" alt="avatar" class="rounded-full">
<div class="ml-2 p-2 bg-white dark:bg-zinc-700 rounded-lg w-auto max-w-full"></div>
`;
messagesContainer.appendChild(responseDiv);
const messageContentDiv = responseDiv.querySelector('div');
// 'message'イベントをリッスンし、バックエンドが新しいデータを送信したときにトリガーします
stream.onmessage = function (event) {
const data = event.data;
console.log('受信したデータ:', data);
// 受信したデータをバッファに追加します
markdownBuffer += data;
// バッファをMarkdownとして解析し、表示を試みます
messageContentDiv.innerHTML = marked.parse(markdownBuffer);
// highlight.jsを使用してコードをハイライトします
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// スクロールバーを下部に保ちます
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
}
// メッセージを送信します
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
const messagesContainer = document.getElementById('messages');
const newMessageDiv = document.createElement('div');
newMessageDiv.className = 'flex items-end justify-end';
newMessageDiv.innerHTML = `
<div class="mr-2 p-2 bg-green-200 dark:bg-green-700 rounded-lg max-w-xs">
${message}
</div>
<img src="https://placehold.co/40x40" alt="avatar" class="rounded-full">
`;
messagesContainer.appendChild(newMessageDiv);
input.value = '';
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// 最初のメッセージを送信する際、initリクエストを送信してセッションIDを取得します
if (!this.sessionId) {
console.log('init');
sendHTTPRequest(`http://127.0.0.1:8868/llama3/init/${message}`, 'GET')
.then(response => {
this.sessionId = response; // セッションIDを保存します
return handleSSEStream(new EventSource(`http://127.0.0.1:8868/llama3/chat/${this.sessionId}/${message}`))
});
} else {
// その後のリクエストは直接chatインターフェースに送信します
handleSSEStream(new EventSource(`http://127.0.0.1:8868/llama3/chat/${this.sessionId}/${message}`))
}
}
}
</script>
</body>
</html>
Spring AI
Open WebUI
2024 最新 Spring AI 零基础入门到精通教程(一套轻松搞定 AI 大模型应用开发)
上面视频对应文档(密码:wrp6)