はじめに
以前、LangChainとChatGPT APIを使って環境データから俳句を生成するアプリを作成した。今回はそのアーキテクチャを活かし、インターンで必要となったAmazon BedrockとLangChainを使った詩人(LLM)へ差し替えてみたので、その手順と実装内容をまとめておく。
前回の記事
プロジェクトファイル
アプリケーション
きっかけ
インターンの要件でAmazon BedrockとLangChainの理解が必要になった。せっかくなら自分のアプリケーションに組み込んで、Claude 3の出力も試してみたいと思い、ChatGPTで実装していた部分をBedrock対応に差し替えることにした。
アーキテクチャ
このプロジェクトではクリーンアーキテクチャを採用しており、詩を生成する責務はPoet
というインターフェースを通じて抽象化している。もともとはChatGptPoet
というクラスでChatGPT APIを使っていたが、今回これをChatBedrockPoet
に置き換えるだけで済む。
実装
ChatBedrockPoet.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ルートの変更
// 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対応していたおかげで、インフラ層の実装もシンプルに保てた。
今後は用途に応じて、生成タスクごとにモデルを使い分けたり、複数詩人を切り替える機能も実装していきたい。