Amazon BedrockでSpring AIを試す

はじめに

先日、Spring AIが1.0となりGAしたというニュースを目にしました。

spring.io

早速、Amazon Bedrockを対象に実装してみることにしました。

前提

今回は以下の環境で試しました。

はじめかた

リファレンスのGetting Startedからはじめてみようかなと思いましたが、まだドキュメントに古い記載が残っている感じがします。

docs.spring.io

spring initializrから雛形を作った方が手っ取り早いかなと思います。

https://start.spring.io/

DependenciesはAmazon Bedrock ConverseSpring Webを選びました。

また、settings.gradleにtoolchainを動かすために必要なplugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' }の記述も抜けていました。ビルド環境にJavaコンパイル環境がない場合はちょっとしたはまりどころかと思います(はまりました)。

あとは実装に移ります。

チャットアプリを作る

チャットアプリを作るのはまずはChatClientを作る必要があるようです。

docs.spring.io

そのあとはモデルごと(使用する生成AIの対象ごと)の実装となります。Bedrockの場合は以下のドキュメントを見ると良いと思います。ただ、ところどころサンプルアプリがそのままだとコンパイルエラーが出たりしますのでちょっと注意です。

docs.spring.io

設定ファイル

設定ファイルのapplication.propertiesは以下のように設定します。

spring.application.name=demo

spring.ai.bedrock.aws.region=us-east-1
spring.ai.bedrock.converse.chat.options.model=amazon.nova-lite-v1:0

リージョンやモデルIDはそれぞれ利用するものを設定してください。アクセスキーやシークレットキーなどは設定せず、EC2に割り当てたIAMロールにて権限を付与することで実現します。

また、spring.ai.bedrock.converse.chat.options.max-tokensのデフォルト値が500となっています。少し長い回答が欲しい場合、これだと途中で回答が切れてしまうことが多いので少し数を増やした方が良いかと思います。

IAMロール

IAMロールは以下のポリシーを追加することで対応しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0"
        }
    ]
}

今回の実装では、最低限bedrock:InvokeModelbedrock:InvokeModelWithResponseStreamへの権限が必要です。Resourceは使用するモデルに限定するためにつけました。

Controller

Controllerは以下のような実装としました。

package com.github.miyohide.demo;

import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
public class MyController {
  private final ChatClient chatClient;

  public MyController(ChatClient.Builder builder) {
    this.chatClient = builder.build();
  }

  @GetMapping("/ai/gen")
  public Map<String, String> chat(
    @RequestParam(value = "message", defaultValue = "Tell me a joke") String message
  ) {
    String response = this.chatClient.prompt(message).call().content();
    return Map.of("gen", response);
  }

  @GetMapping(value = "/ai/genstream", produces = MediaType.APPLICATION_JSON_VALUE)
  public Flux<ChatResponse> chatStream(
    @RequestParam(value = "message", defaultValue = "Tell me a joke") String message
  ) {
    return this.chatClient.prompt(message).stream().chatResponse();
  }
}

ここまで実装したら./gradlew bootRunとして実行し、/ai/gen?message=プロンプトにアクセスすると簡単なチャットアプリが動きます。以下は東京の天気を教えてと質問した例。

/ai/genstreamは以下のようなJSONが返ってきます。

これを読みやすくするには少しだけJavaScriptなどで頑張る必要がありそうです。

ここまでで簡単なチャットアプリは実装できました。

なお、アプリのログには以下のものが出力されていました。

The topK option is not supported by BedrockProxyChatModel. Ignoring.

注意点

現時点ではcross-region inferenceには未対応のようです。

github.com

実際、東京リージョンでアプリを起動しモデルIDとしてapac.amazon.nova-lite-v1:0を指定したのですが、software.amazon.awssdk.services.bedrockruntime.model.ValidationExceptionが吐かれうまく動きませんでした。

考察

Springに慣れた人であれば、生成AIアプリを作るためにSpring AIを活用するのは良い選択肢の一つかと思います。まだGAしたばかりでドキュメントが追いついていないところもあるのですが、徐々に整備されていくかと思います。

今日書いた部分ぐらいはわざわざフレームワークを使う必要はないこともあるのですが、徐々にフレームワークのありがたさを感じるシーンは出てくるかと思います。そのうちの一つがChat Memoryかと思います。

docs.spring.io

上記のドキュメントにも書かれていますが、コンテキストを保持しないので「より詳しく」と追加の質問としてプロンプトを投げても前回の回答を踏まえない回答しか返ってことないです。

これらについても検証してみたいと思います。