まず、多モーダルとは何かを説明します:人間の学習方法を想像してみてください。視覚、聴覚、触覚があります。その中で最も重要なのは視覚です。機械は見ることができるのでしょうか?もちろんです。簡単に言えば、AI に見せ、聞かせ、触れさせることが多モーダルです。
人間は複数のデータ入力モードを通じて同時に知識を処理します。私たちの学び方や経験はすべて多モーダルです。私たちは視覚だけでなく、音声やテキストも持っています。
人間は同時に複数のデータ入力モードを通じて知識を処理します。私たちの学び方や経験はすべて多モーダルです。私たちは視覚だけでなく、音声やテキストも持っています。
これらの学習の基本原則は、現代教育の父であるジョン・アモス・コメニウスによって、1658 年の著作『Orbis Sensualium Pictus』で明らかにされました。
現代教育の父であるジョン・アモス・コメニウスは、1658 年の著作『Orbis Sensualium Pictus』でこれらの学習の基本原則を明らかにしました。
「自然に関連するすべてのものは、組み合わせて教えられるべきである」
「自然に関連するすべてのものは、組み合わせて教えられるべきである」
マルチモーダリティ API#
OpenAI を例にとると、現在多モーダルをサポートしているモデルはまだ少なく、基本的には最新の gpt-4-visual-preview
と gpt-4o
だけです。詳細な説明は公式文書にあります。
以下は公式の 2 つの例です:
byte[] imageData = new ClassPathResource("/multimodal.test.png").getContentAsByteArray();
var userMessage = new UserMessage("この画像に何が見えるか説明してください?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));
var userMessage = new UserMessage("この画像に何が見えるか説明してください?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG,
"https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png")));
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()).build()));
メディア情報が UserMessage
に設定されていることがわかります。これはユーザーの入力を表しており、以前はテキストの入力のみを使用していましたが、ファイルの入力もサポートされていることがわかりました。
ここには new Media()
メソッドもあり、上記の 2 つの方法で異なる 2 種類のパラメータが渡されています。最初のものは byte[]
配列を渡し、2 番目のものは URL
リンクを渡します。
実際、このメソッドは 3 種類のパラメータをサポートしています。
ただし、最初の byte[]
配列の方法はすでに非推奨とされています。
原理がわかったので、以前のストリーミング出力のコードを少し変更すれば良いです:
/**
* @author LiZhiAo
* @date 2024/6/24 16:09
*/
@RestController
@RequestMapping("/multi")
@RequiredArgsConstructor
@CrossOrigin
public class MultiController {
private final OpenAiConfig openAiConfig;
private static final Integer MAX_MESSAGE = 10;
private static Map<String, List<Message>> chatMessage = new ConcurrentHashMap<>();
/**
* 提示語を返す
* @param message ユーザーが入力したメッセージ
* @return Prompt
*/
@SneakyThrows
private List<Message> getMessages(String id, String message, MultipartFile file) {
String systemPrompt = "{prompt}";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);
Message userMessage = null;
if (file == null){
userMessage = new UserMessage(message);
}else if (!file.isEmpty()){
userMessage = new UserMessage(message, List.of(new Media(MimeType.valueOf(file.getContentType()), file.getResource())));
}
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());
}
@PostMapping("/chat/{id}/{message}")
@SneakyThrows
public SseEmitter chat(@PathVariable String id, @PathVariable String message,
HttpServletResponse response, @RequestParam(value = "file", required = false) MultipartFile file ){
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
SseEmitter emitter = SseEmitterUtils.connect(id);
List<Message> messages = getMessages(id, message, file);
System.err.println("chatMessageのサイズ: " + messages.size());
System.err.println("chatMessage: " + chatMessage);
if (messages.size() > MAX_MESSAGE){
SseEmitterUtils.sendMessage(id, "対話回数が多すぎます。しばらくしてから再試行してください🤔");
} else {
// モデルの出力ストリームを取得
Flux<ChatResponse> stream = openAiConfig.openAiChatClient().prompt(new Prompt(messages)).stream().chatResponse();
// ストリーム内のメッセージをSSEで送信
Mono<String> result = stream
.flatMap(it -> {
StringBuilder sb = new StringBuilder();
System.err.println(it.getResult().getOutput().getContent());
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;
}
}
最後の結果は以下の通りです:
フロントエンドはもう変更する気がないので、このままでいいです🥱