AWS Lambda Function URLs で作るチャットボット

AWS Lambda Function URLs で作るチャットボット

Claude 3 を活用したチャットボットを、AWS のサーバーレスなアーキテクチャで構築してみましょう。Lambda Function URLs を使用して、シンプルなシステムを作ります。
Clock Icon2025.02.10

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" というシンプルなものですが、以下の点に注意が必要です:

実装時の注意点

  1. ストリーミングを利用する場合は bedrock:InvokeModelWithResponseStream の権限が必要
  2. Lambda Function URLs の CORS 設定を適切に行う
  3. デフォルトのタイムアウト値では短いので適切に調整

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 のオプションを指定するところが大事で、今回の場合は streamProtocoldata から 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 での実装があまりなかったので書いてみました。便利なので実装の参考にしていただけたら幸いです。

参照

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.