AWS Lambda Function URLs で作るチャットボット
Lambda Function URLs と Bedrock (Claude 3) を組み合わせて、サーバーレスなチャットボットを構築する一例を紹介します。Hono、AI SDK、CDKを活用した実装例と、つまずきやすいポイントを詳しく解説します。
使用技術スタック
- 言語
- TypeScript 5.6.3
- フロントエンド
- React: 19.0.0
- Next.js: 15.1.6
- Tailwind: 4.0.0
- バックエンド
- Hono: 4.6.7
- AI SDK: 4.1.24
- @ai-sdk/amazon-bedrock: "^1.1.6"
- インフラ
- AWS CDK: 2.164.1
インフラの構築
CDK を利用してインフラを組み立てていきます。基本的な構成は "User → Lambda → Bedrock" というシンプルなものですが、以下の点に注意が必要です:
実装時の注意点
- ストリーミングを利用する場合は
bedrock:InvokeModelWithResponseStream
の権限が必要 - Lambda Function URLs の CORS 設定を適切に行う
- デフォルトのタイムアウト値では短いので適切に調整
CDK の実装
npx cdk init
で初期化したコードのスタック部分を書き換えます。
import { Duration, Stack, type StackProps } from "aws-cdk-lib";
import { Cors } from "aws-cdk-lib/aws-apigateway";
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { FunctionUrlAuthType, HttpMethod, InvokeMode, Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import type { Construct } from "constructs";
export class ApiStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
const fn = new NodejsFunction(this, "lambda", {
entry: "src/index.ts",
handler: "handler",
runtime: Runtime.NODEJS_20_X,
timeout: Duration.seconds(30), // タイムアウトを30秒に設定
});
fn.addFunctionUrl({
invokeMode: InvokeMode.RESPONSE_STREAM,
authType: FunctionUrlAuthType.NONE,
cors: {
allowedOrigins: Cors.ALL_ORIGINS,
allowedMethods: [HttpMethod.ALL],
allowedHeaders: Cors.DEFAULT_HEADERS,
},
});
// Bedrock用のIAMポリシー設定
const policy = new PolicyStatement({
effect: Effect.ALLOW,
actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
resources: ["*"],
});
fn.role?.addToPrincipalPolicy(policy);
}
}
Lambda 関数の実装
まずは必要なパッケージをインストールします。
npm i @ai-sdk/amazon-bedrock ai hono
そして、CDK が読み込む src/index.ts
にて、Bedrock を呼び出してレスポンスを返すコードを実装します。
AI SDK のおかげで実装がかなりシンプルにできます。
import { bedrock } from "@ai-sdk/amazon-bedrock";
import { streamText as AiStreamText } from "ai";
import { Hono } from "hono";
import { streamHandle } from "hono/aws-lambda";
import { streamText } from "hono/streaming";
const MODEL = "anthropic.claude-3-sonnet-20240229-v1:0";
const app = new Hono();
app.post("/chat", async (context) => {
return streamText(context, async (stream) => {
const { messages } = await context.req.json();
console.log(`request: ${JSON.stringify(messages)}`);
const result = AiStreamText({
model: bedrock(MODEL),
messages,
onError: async (error) => {
console.log(`error: ${JSON.stringify(error)}`);
},
onFinish: async ({ text, finishReason }) => {
console.log(`finished: ${finishReason}, ${text}`);
},
});
for await (const text of result.textStream) {
await stream.write(text);
}
});
});
export const handler = streamHandle(app);
上記のコードができたら、 npx cdk deploy
でデプロイして、Lambda Function URL を取得しましょう。
フロントエンドの実装
npx create-next-app@latest
で作成したデフォルトルートのページを書き換えましょう。
useChat のオプションを指定するところが大事で、今回の場合は streamProtocol
を data
から text
に切り替える必要があります。あとの実装はテキストの表示とAPI リクエストを投げるようにしているだけです。
"use client";
import { useChat } from "ai/react";
export default function Home() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "YOUR_LAMBDA_FUNCTION_URL", // Lambda Function URLを設定
streamProtocol: "text", // 重要: プロトコルをtextに設定
});
return (
<div className="mx-6 flex h-lvh max-w-5xl flex-1 flex-col lg:mx-auto">
<div className="mt-10 grow">
<p className="text-3xl">Ask anything to chatbot</p>
<div className="mt-10 flex flex-col gap-6">
{messages.map((m) => (
<div key={m.id} className="whitespace-pre-wrap">
<div className="text-xl">{m.role === "user" ? "You" : "AI"}</div>
<section className="mt-2 text-lg">{m.content}</section>
</div>
))}
</div>
</div>
<footer className="mb-6 flex gap-4">
<textarea
placeholder="メッセージを入力"
value={input}
onChange={handleInputChange}
className="field-sizing-content w-full resize-none rounded-md bg-stone-600 p-4"
/>
<button type="button" onClick={handleSubmit}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-10"
>
<title>enter the input</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</button>
</footer>
</div>
);
}
あとは npm run dev
でチャットボットが動いてくれます。
さいごに
Bedrock との繋ぎ込みに AI SDK を利用すると非常に簡単になるのですが、Lambda での実装があまりなかったので書いてみました。便利なので実装の参考にしていただけたら幸いです。