Amazon Bedrock × LangChainでChatGPT詩人を差し替えてみた【Claude 3 Haiku対応】

# はじめに

以前、LangChainとChatGPT APIを使って環境データから俳句を生成するアプリを作成した。今回はそのアーキテクチャを活かし、インターンで必要となった**Amazon Bedrock**と**LangChain**を使った詩人（LLM）へ差し替えてみたので、その手順と実装内容をまとめておく。

**前回の記事**

https://qiita.com/koichi2426/items/4b4eb65fd85f4c6edaea

**プロジェクトファイル**

https://github.com/koichi2426/koko

**アプリケーション**

https://koko-opal-ten.vercel.app/


# きっかけ

インターンの要件でAmazon BedrockとLangChainの理解が必要になった。せっかくなら自分のアプリケーションに組み込んで、Claude 3の出力も試してみたいと思い、ChatGPTで実装していた部分をBedrock対応に差し替えることにした。

# アーキテクチャ

このプロジェクトではクリーンアーキテクチャを採用しており、詩を生成する責務は`Poet`というインターフェースを通じて抽象化している。もともとは`ChatGptPoet`というクラスでChatGPT APIを使っていたが、今回これを`ChatBedrockPoet`に置き換えるだけで済む。

# 実装

## ChatBedrockPoet.ts

```ts
// app/backend/infrastructure/poet/ChatBedrockPoet.ts

// LangChainでAmazon Bedrock（Claude 3）を使うためのモデル
import { ChatBedrock } from "@langchain/community/chat_models/bedrock";
// プロンプトテンプレートを作成するためのユーティリティ
import { PromptTemplate } from "@langchain/core/prompts";

// ドメイン層の詩人インターフェース
import { Poet } from "../../domain/poet";
// 生成された詩（俳句）を表すドメインオブジェクト
import { Poem } from "../../domain/poem";
// 環境データ（天気・気温など）を表すドメインオブジェクト
import { Environment } from "../../domain/environment";

// Claude 3を使って俳句を生成する詩人クラスの実装
export default class ChatBedrockPoet implements Poet {
  // 詩人の名前（UI表示などに利用可能）
  name = "Bedrock詩人（Claude 3対応）";

  // LangChainのChatBedrockモデルインスタンス（privateで内部利用）
  private llm: InstanceType<typeof ChatBedrock>;

  // コンストラクタでBedrockモデルの初期化
  constructor() {
    // 環境変数からAWS認証情報とリージョン・モデルIDを取得
    const accessKeyId = process.env.BEDROCK_AWS_ACCESS_KEY_ID;
    const secretAccessKey = process.env.BEDROCK_AWS_SECRET_ACCESS_KEY;
    const region = process.env.BEDROCK_AWS_REGION ?? "us-east-1";
    const model =
      process.env.BEDROCK_MODEL_ID ??
      "anthropic.claude-3-haiku-20240307-v1:0"; // Claude 3 Haiku モデルのデフォルト

    // 認証情報がなければエラー
    if (!accessKeyId || !secretAccessKey) {
      throw new Error("AWS credentials for Bedrock are not set in .env");
    }

    // LangChainのChatBedrockクライアントを初期化
    this.llm = new ChatBedrock({
      model,
      region,
      credentials: {
        accessKeyId,
        secretAccessKey,
      },
      temperature: 0.7, // 出力の多様性
      maxRetries: 2,    // API呼び出し失敗時の再試行回数
    });
  }

  // 環境情報から俳句を生成するメイン処理
  async composePoem(env: Environment): Promise<Poem> {
    // 俳句生成用のプロンプトテンプレートを定義
    const promptTemplate = PromptTemplate.fromTemplate(
      `以下の情報をもとに、とても可愛らしい俳句（17音以内、日本語）を1つ作ってください。\n\n場所: {location}\n気温: {temperature}℃\n湿度: {humidity}%\n天気: {weather}\n時間帯: {time}`
    );

    // 環境情報をテンプレートに埋め込んでプロンプトを生成
    const prompt = await promptTemplate.format({
      location: env.location,
      temperature: env.temperature.toString(),
      humidity: env.humidity.toString(),
      weather: env.weather,
      time: env.time.toLocaleTimeString("ja-JP"), // 日本語の時間表記
    });

    // デバッグ用にプロンプトを出力
    console.log("🔎 Claude 3 Prompt:\n", prompt);

    try {
      // Claude 3にプロンプトを送信し、応答を受け取る
      const response = await this.llm.invoke([
        { role: "user", content: prompt },
      ]);

      let resultText = "";

      // 応答が単一の文字列だった場合
      if (typeof response.content === "string") {
        resultText = response.content.trim();
      }
      // 応答が複数のテキストパーツだった場合（Claudeの形式に対応）
      else if (Array.isArray(response.content)) {
        const textParts = response.content
          .filter((part): part is { type: "text"; text: string } => part.type === "text")
          .map((part) => part.text)
          .join(" ");
        resultText = textParts.trim();
      }

      // 結果が空ならエラー
      if (!resultText) {
        throw new Error("Empty or invalid response from Claude 3 model");
      }

      // 生成されたテキストからPoemオブジェクトを生成して返す
      return new Poem(resultText);
    } catch (error) {
      // Claude 3呼び出しに失敗した場合はエラーをログ出力して再スロー
      console.error("❌ Claude 3 API Error:", error);
      throw new Error("Failed to generate poem via Claude 3 (Messages API)");
    }
  }
}

```

## APIルートの変更

```ts
// app/api/GeneratePoem/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { GeneratePoemAction } from '@/app/backend/adapter/api/action/generate-poem';
import { NewGeneratePoemInteractor } from '@/app/backend/usecase/GeneratePoem';
import ChatBedrockPoet from '@/app/backend/infrastructure/poet/ChatBedrockPoet';
import { DefaultPoemPresenter } from '@/app/backend/adapter/presenter/GeneratePoemPresenter';
import { Environment } from '@/app/backend/domain/environment';

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();

    const environment: Environment = {
      location: body.location,
      temperature: parseFloat(body.temperature),
      humidity: parseFloat(body.humidity),
      weather: body.weather,
      time: new Date(body.time),
    };

    const usecase = NewGeneratePoemInteractor(
      new ChatBedrockPoet(),// (ここを差し替えるだけ！！！)
      new DefaultPoemPresenter()
    );

    const action = new GeneratePoemAction(usecase);
    const result = await action.execute({ environment });

    return NextResponse.json(result.data, { status: result.status });
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid input or server error' },
      { status: 500 }
    );
  }
}
```

# 試してみた結果

Claude 3 Haikuは名前の通り、日本語の俳句生成に非常に適していた。ChatGPTと比較してもより自然で情緒的な表現が多く、場合によってはこちらのほうが好まれる可能性があると感じた。

# まとめ

クリーンアーキテクチャの利点を活かすことで、ChatGPTからAmazon Bedrock（Claude 3）への差し替えを**最小限の変更で**実現できた。LangChainがBedrock対応していたおかげで、インフラ層の実装もシンプルに保てた。

今後は用途に応じて、生成タスクごとにモデルを使い分けたり、複数詩人を切り替える機能も実装していきたい。