12
7

Amazon BedrockとLangChainをサーバレスで動かす!(ついでにフロントも作る!)

Last updated at Posted at 2024-09-04

この記事について

Amazon Bedrockはサーバレスの良い感じのサービスですが、それをLangChainから使う場合、お手軽な方法としては、コンテナランナーで稼働させることになります。
しかし、AWSの場合にはお手軽なコンテナランナーがなく、Fargate一択(言い過ぎ)になります。

この記事では、バックエンド(LangChainからのBedrock呼び出し)にLambdaを使うお手軽な方法を試すことを主眼としています。

(しかし、バックエンドだけあっても微妙なので、ついでにフロントも作っています。こちらは、生成AIをフル活用してほとんどコードを書かない。というか何も考えないということを重視しています!)

結果的にあまりお手軽ではなくなっています。

全体の構成

図にしようかと思いましたが面倒なので文字で。

  • フロントエンド
    • React
    • ビルドツール:vite
  • バックエンド
    • AWS Lambda
    • FastAPI
    • LangChain
    • Amazon Bedrock (Claude3.5 Sonnet)

ソースコードリポジトリ

以下に全体のソースコードがあります。
https://github.com/tis-abe-akira/langchain-react-lambda

どうしたことか、TypeScriptのプロジェクトとして認識されてしまいました。
Screenshot 2024-09-04 at 19.18.31.png

バックエンドを開発する!

参考にしたサイト

以下のサイトを思い切り参考にしています。このサイトがなければこの記事は存在しなかったと思います。
【生成AI】AWS Lambda(Python) と LangChain(LCEL) を使ってストリーミング出力したい

FastAPI Response Streaming

デプロイの手順

ここではSAMを使います。
API_KEYの検証は、Lambda上で動くPythonスクリプトで実施しています。したがって、適当な文字列を指定してビルド&デプロイしてください。

sam build
export OUR_API_KEY=ランダムな文字列を指定

sam deploy --parameter-overrides ApiKey=${OUR_API_KEY} --guided

# 以下は入力例です。
Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: 
        AWS Region [ap-northeast-1]: us-east-1
        Parameter ApiKey: #APIキーを入力
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: N
        FastAPIFunction Function Url has no authentication. Is this okay? [y/N]: y  ← APIキーをLambda内で検証
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

~ しばらく待つと以下のメッセージが表示される ~

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

~ 最終的にデプロイが成功すると以下のように、LambdaのARNおよびFunction URLが表示される ~

CloudFormation outputs from deployed stack
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                                                                                      
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 FastAPIFunction                                                                                                                                                                                                          
Description         FastAPI Lambda Function ARN                                                                                                                                                                                              
Value               arn:aws:lambda:us-east-1:999999999999:function:sam-app-FastAPIFunction-XXXXXXXXXX                                                                                                                                      

Key                 FastAPIFunctionUrl                                                                                                                                                                                                       
Description         Function URL for FastAPI function                                                                                                                                                                                        
Value               https://XXXXXXXXXX.lambda-url.us-east-1.on.aws/                                                                                                                                                    
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

curlによるテスト

curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: ${OUR_API_KEY}" \
-d '{"question": "カナダの首都は?"}' \
https://XXXXXXXXXX.lambda-url.us-east-1.on.aws/api/qa

# 以下のようなレスポンスが返ってくる
カナダの首都はオタワ(Ottawa)です。

オタワに関する主な情報:

1. 場所:オンタリオ州東部に位置し、ケベック州との州境に近い。

2. 人口:約100万人(都市圏を含む)

3. 公用語:英語とフランス語(カナダは公式に二言語国家)

4. 主要な政府機関:連邦議会議事堂、首相官邸、総督官邸など

5. 観光名所:リドー運河、カナダ国立美術館、平和の塔など

6. 気候:大陸性気候で、夏は暖かく冬は寒い

7. 経済:政府機関や技術産業が主要な雇用源

オタワは1857年にビクトリア女王によってカナダの首都に選ばれました。トロントやモントリオールなどの大都市と比べると小さめですが、政治の中心地として重要な役割を果たしています。

SAMのテンプレート

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  fastapi response streaming

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 60

Parameters:
  ApiKey:
    Type: String
    NoEcho: true
    Description: API Key for authentication

Resources:
  FastAPIFunction:
    Type: AWS::Serverless::Function
    Properties:
      Policies:
        - Statement:
            - Sid: BedrockInvokePolicy
              Effect: Allow
              Action:
                - bedrock:InvokeModelWithResponseStream
              Resource: "*"
      CodeUri: app/
      Handler: run.sh
      Runtime: python3.11
      MemorySize: 256
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
          AWS_LWA_INVOKE_MODE: response_stream
          PORT: 8000
          API_KEY: !Ref ApiKey
      Layers:
        - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:22
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: RESPONSE_STREAM
        Cors:
          AllowOrigins:
            - "*"
          AllowMethods:
            - "*"
          AllowHeaders:
            - X-API-Key
            - content-type
          MaxAge: 300

Outputs:
  FastAPIFunctionUrl:
    Description: "Function URL for FastAPI function"
    Value: !GetAtt FastAPIFunctionUrl.FunctionUrl
  FastAPIFunction:
    Description: "FastAPI Lambda Function ARN"
    Value: !GetAtt FastAPIFunction.Arn

Lambdaのコード

main.python
from fastapi import FastAPI, Security, HTTPException
from fastapi.security import APIKeyHeader
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from langchain_aws import BedrockChat
from langchain_core.output_parsers import StrOutputParser
import os

app = FastAPI()

API_KEY = os.environ.get("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

async def get_api_key(api_key_header: str = Security(api_key_header)):
    if api_key_header == API_KEY:
        return api_key_header
    raise HTTPException(status_code=403, detail="Could not validate API KEY")


class RequestBody(BaseModel):
    question: str

def bedrock_stream(question: str):

    model = BedrockChat(
        model_id="anthropic.claude-3-5-sonnet-20240620-v1:0",
        model_kwargs={"max_tokens": 1000},
    )

    chain = model | StrOutputParser()

    for chunk in chain.stream(question):
        yield chunk

@app.post("/api/qa", dependencies=[Security(get_api_key)])
async def api_qa(body: RequestBody):

    return StreamingResponse(
        bedrock_stream(body.question),
        media_type="text/event-stream",
    )

@app.get("/api/health-check", dependencies=[Security(get_api_key)])
async def health_check():
    return {"status": "ok"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

フロントエンドを開発する

生成AI(v0)にアウトラインを相談しスケルトンコードを作ってもらう

生成AIにコードを作ってもらっている様子です。
v0-create-front.gif

プロンプトは以下のような内容を投げています。(画像で参考になりそうな画面スクショを渡しています)

添付ファイルのようなシンプルなUIのAIチャットを作ってください。
プロトタイプなので、会話は一連の流れではなく、リクエスト単位に一往復で完結します。
現在はリセットやクリアのボタンがないですが、それを追加してください。
AIの回答内容はMarkdownで返却されることを想定しReactMarkdownを使ってください。

実行の様子(一応の完成形)

react-front.gif

プロジェクトのセットアップ手順

プロジェクトの初期化

npm create vite@latest frontend -- --template react-ts
cd frontend

依存関係のインストール

npm install
npm install react-markdown @radix-ui/react-slot class-variance-authority clsx tailwind-merge lucide-react

Tailwind CSSの設定

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.jsを以下の内容で置き換えます。

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

src/index.cssを以下の内容で置き換えます。

@tailwind base;
@tailwind components;
@tailwind utilities;

テスト実行

npm run dev

# ブラウザで http://localhost:5173 を開く

ソースコード(メインのコンポーネントのみ)

AIChat.tsx

import { useState } from "react";
import { Button } from "../components/ui/button";
import { Textarea } from "../components/ui/textarea";
import {
  Card,
  CardHeader,
  CardTitle,
  CardContent,
  CardFooter,
} from "../components/ui/card";
import ReactMarkdown from "react-markdown";
import API_CONFIG from "../config";

export default function AIChat() {
  const [input, setInput] = useState("");
  const [answer, setAnswer] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);

    try {
      const response = await fetch(API_CONFIG.ENDPOINT, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-Key": API_CONFIG.API_KEY,
        },
        body: JSON.stringify({ question: input }),
      });

      if (!response.body) {
        throw new Error("Response body is null");
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        if (value && value.length > 0 && isLoading) {
          setIsLoading(false); // valueが空でない場合にisLoadingをfalseに設定
        }
        const chunk = decoder.decode(value);
        setAnswer((prevAnswer) => prevAnswer + chunk);
      }
    } catch (error) {
      console.error("Error:", error);
      setAnswer("An error occurred while fetching the answer.");
    } finally {
      setIsLoading(false);
    }
  };

  const handleReset = () => {
    setInput("");
    setAnswer("");
  };

  const handleClear = () => {
    setAnswer("");
  };

  return (
    <Card className="w-full max-w-2xl mx-auto">
      <CardHeader>
        <CardTitle>質問応答アプリ</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={handleSubmit} className="space-y-4">
          <Textarea
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="質問を入力してください。"
            className="w-full h-32"
          />
          <Button type="submit" disabled={isLoading || !input.trim()} className="bg-blue-700 text-white">
            {isLoading ? "送信中..." : "送信"}
          </Button>
        </form>
        {answer && (
          <div className="mt-6">
            <h3 className="text-lg font-semibold mb-2">回答:</h3>
            <Card className="p-4 bg-muted">
              <ReactMarkdown>{answer}</ReactMarkdown>
            </Card>
          </div>
        )}
      </CardContent>
      <CardFooter className="flex justify-end space-x-2">
        <Button onClick={handleClear} variant="outline">
          クリア
        </Button>
        <Button onClick={handleReset} variant="outline">
          リセット
        </Button>
      </CardFooter>
    </Card>
  );
}

まとめ

ここまでお読みいただき誠にありがとうございました。

このレベルの機能であれば、Streamlitのスクリプト中にLangChainのコードを埋め込んだ方が、お手軽だと思います。
しかしながら、いろいろな機能を付加するとか、一連の処理の中で生成AIの呼び出しをするといったユースケースにおいては、サーバレス(Lambda)での実行というのは有力な選択肢になると思います。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7