メインコンテンツまでスキップ

LangChainをAPI化するLangServeをLambda上で動作させるのはめちゃ簡単デス

· 約12分
moritalous
お知らせ

過去にQiitaに投稿した内容のアーカイブです。

少し前にLangChain開発元から新しいツールとしてLangServeというものがリリースされています。

https://blog.langchain.dev/introducing-langserve/

LangServe is the easiest and best way to deploy any any LangChain chain/agent/runnable.

生成系AIを使ったAPIを簡単に作成し利用できる仕組みで、プロダクション環境でどんどんLangChainを使ってねというメッセージと捉えました。

イメージとしてはこんな仕組みです。

FastAPI上で動作し、API仕様が定められている感じです。

  • Invoke API 単一の入力で処理を実行する
  • Batch API 複数の入力で処理を実行する
  • Stream API 単一の入力で処理を行い、結果をストリームで返却する
  • Stream_log API 単一の入力で処理を行い、結果だけでなく途中の経過もストリームで返却する

ローカルでお試し実行

まずはローカル環境で動作させてみます。

  1. LangServeのインストール

    pydanticはv2ではなくv1を使うため、バージョン指定でインストールしています。 生成系AIにAmazon Bedrockを使用するため、Boto3もインストールしています。

    pip install langserve[server] pydantic==1.10.13 boto3
  2. サーバー側ロジックを記述

    こちらを参考にしました。

    add_routesを使って、BedrockChatモデルを使用する/bedrockのパスを定義しています。

    server.py
    #!/usr/bin/env python

    from fastapi import FastAPI
    from langchain_community.chat_models import BedrockChat

    from langserve import add_routes

    app = FastAPI(
    title="LangServe",
    version="1.0",
    description="LangChain Server",
    )

    add_routes(
    app,
    BedrockChat(model_id="anthropic.claude-instant-v1"),
    path="/bedrock",
    )

    if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)

    なんと、これだけw

  3. 起動

    python server.pyでLangServeを起動します。素敵なログが出力されます。

    INFO:     Started server process [7582]
    INFO: Waiting for application startup.

    __ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
    | | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
    | | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
    | | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
    | `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
    |_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|

    LANGSERVE: Playground for chain "/bedrock/" is live at:
    LANGSERVE: │
    LANGSERVE: └──> /bedrock/playground/
    LANGSERVE:
    LANGSERVE: See all available routes at /docs/

    INFO: Application startup complete.
    INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
  4. ブラウザでhttp://127.0.0.1:8000/docsにアクセスすると、Swagger UIでOpenAPIドキュメントが表示されます。

    127.0.0.1_8000_docs.png

    先程紹介したものよりもいくつか追加でAPIが定義されました。

    この画面上からAPIのテスト実行も可能です。

    もちろんREST APIなので、表示されるcurlリクエストで呼び出すことも可能です。

    curl -X 'POST' \
    'http://127.0.0.1:8000/bedrock/invoke' \
    -H 'accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
    "input": "ポエムを作ってください。",
    "config": {},
    "kwargs": {}
    }'
    {
    "output": {
    "content": " はい、以下は簡単なポエムです。\n\nひとゆびの距離 \n\n夜の静けさにさびしく\n窓の外は星空を照らす\n目の前はひとつの指\n地球の行方はわからない\n\nたったひとつの指の先\n無限な宇宙が広がる\n星々はとても遠いのに\nひとゆびのずれにかかる\n\n指を動かせば景色が変わる\n新しい場所が見えてくる\nでも今はここにいる\nひとゆび先で世界は広がる\n\n簡単なポエムですが、ひとゆびの距離に宇宙の広さを重ね合わせました。内容や表現が不十分な部分があると思いますが、ご要",
    "additional_kwargs": {},
    "type": "ai",
    "example": false
    },
    "callback_events": [],
    "metadata": {
    "run_id": "117fae40-1ed6-4858-bca0-c44bc2707c71"
    }
    }
注記
  • プレイグラウンド

http://127.0.0.1:8000/bedrock/playground/にアクセスすると、プレイグラウンド画面が表示されます。(すごい!)

ここでもAPIの動作確認ができます。

ドキュメントによるとプレイグラウンドの画面をカスタマイズすることもできるようです。

Playground

クライアントから呼び出す

APIにできたので、ブラウザから呼び出してみようと思います。

とても単純なHTMLを作成しました。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
async function call() {
const response = await fetch("http://127.0.0.1:8000/bedrock/invoke", {
method: "POST",
body: JSON.stringify({
"input": "ポエムを作ってください。"
})
});
console.log((await response.json()).output.content);
}
call();
</script>
</body>
</html>

index.htmlのディレクトリで簡易HTTPサーバーを起動します。

python -m http.server 8080

サーバー側とクライアント側でポート番号が異なるため、CORSリクエストになります。 サーバー側でCORSを有効化してサーバーを再起動します。

  #!/usr/bin/env python

from fastapi import FastAPI
+ from fastapi.middleware.cors import CORSMiddleware
from langchain_community.chat_models import BedrockChat

from langserve import add_routes

app = FastAPI(
title="LangServe",
version="1.0",
description="LangChain Server",
)

+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://127.0.0.1:8080"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )

add_routes(
app,
BedrockChat(model_id="anthropic.claude-instant-v1"),
path="/bedrock",
)

if __name__ == "__main__":
import uvicorn

uvicorn.run(app)

ブラウザでhttp://127.0.0.1:8080/にアクセスすると、consoleに出力されました。

はい、簡単なポエムを作ってみます。

白い雪は降り注ぎに
木々はゆっくりと眠りにつく
星が光り夜は更けて
静かに闇が訪れる

いかがでしょうか。内容はシンプルでテーマは自然の季節の移り変わりを詠んでいます。ポエムの作成は人工知能の限界がある部分なので、改善点があれば教えていただければ幸いです。自由な表現は難しい技術なのです。

Streming呼び出しもあるのですが、ちょっとうまくいきませんでした。。

注記

ドキュメントによるとLangChain.jsのversion 0.0.166以降で、以下のような呼び出しが可能なようです。(未確認)

import {RemoteRunnable} from "langchain/runnables/remote";

const chain = new RemoteRunnable({
url: `http://localhost:8000/joke/`,
});
const result = await chain.invoke({
topic: "cats",
});

Lambda化

ローカルでの動作が確認できたので、Lambda化します。

AWS製のAWS Lambda Web AdapterというFastAPIのLambda化を行う便利なライブラリーがあります。これを使います。

https://github.com/awslabs/aws-lambda-web-adapter

こんなイメージです。

examples/fastapi-response-streaming-zipにサンプルが用意されていますので、これを参考に作成します。

tree fastapi-response-streaming-zip
fastapi-response-streaming-zip
├── README.md
├── __init__.py
├── app
│ ├── __init__.py
│ ├── main.py
│ ├── requirements.txt
│ └── run.sh
├── samconfig.toml
└── template.yaml

1 directory, 8 files

AWS Lambda Web Adapterは、レイヤーで指定されています。

注記

レイヤーを指定する だけ です。すごいですね。

  FastAPIFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: app/
Handler: run.sh
Runtime: python3.12
MemorySize: 256
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
AWS_LWA_INVOKE_MODE: response_stream
PORT: 8000
+ Layers:
+ - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:18
FunctionUrlConfig:
AuthType: NONE
InvokeMode: RESPONSE_STREAM

LangServeをレイヤーにして追加しましょう。

fastapi-response-streaming-zip/langserve-layer/requirements.txt
langserve[server]
pydantic==1.10.13

Pythonのバージョンを3.12にし、Bedrockの権限も付与します。

fastapi-response-streaming-zip/template.yaml
  FastAPIFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: app/
Handler: run.sh
- Runtime: python3.11
+ Runtime: python3.12
MemorySize: 256
+ Policies:
+ - arn:aws:iam::aws:policy/AmazonBedrockFullAccess
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
AWS_LWA_INVOKE_MODE: response_stream
PORT: 8000
Layers:
- !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:18
+ - !Ref LangServeLayer
FunctionUrlConfig:
AuthType: NONE
InvokeMode: RESPONSE_STREAM
+ LangServeLayer:
+ Type: AWS::Serverless::LayerVersion
+ Properties:
+ ContentUri: langserve-layer/
+ CompatibleRuntimes:
+ - python3.12
+ Metadata:
+ BuildMethod: python3.12
+ BuildArchitecture: x86_64

注記

LambdaのPython 3.12ランタイムはBoto3のバージョンが1.28.72のため、Bedrockに対応しています。そのため、レイヤーなどでboto3を追加インストールする必要はありません。

main.pyに先程のserver.pyの内容を反映します。

fastapi-response-streaming-zip/app/main.py
#!/usr/bin/env python

from fastapi import FastAPI
from langchain_community.chat_models import BedrockChat

from langserve import add_routes

app = FastAPI(
title="LangServe",
version="1.0",
description="LangChain Server",
)

add_routes(
app,
BedrockChat(model_id="anthropic.claude-instant-v1"),
path="/bedrock",
)

if __name__ == "__main__":
import uvicorn

uvicorn.run(app)

ビルドしてデプロイします。

sam build

sam deploy --guided

デプロイできたらFunction URLにアクセスします。

curl -X 'POST' \
'https://xxxxx.lambda-url.ap-northeast-1.on.aws/bedrock/invoke' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"input": "ポエムを作ってください。",
"config": {},
"kwargs": {}
}'
{
"output": {
"content": " はい、思いつきました。\n\n雨のポエム\n\n空は暗い雲に覆われて\n静かに降り注ぐ雨\n一筋の光も見えません\n\n道の上は水まみれ\n傘をさして歩く人\n靴がぬかるみに沈み込んで\n\n窓から外を見下ろす\n心地よいひまわりの時間\n本をめくりながら過ごす\n\n雨はじわじわと弱まって\n虹が現れるかもしれない\n希望を持とうではないか\n\n人生にも雨期があるというのに\nその先には必ず晴れが開いていく\n信じて待つ時が来る",
"additional_kwargs": {},
"type": "ai",
"example": false
},
"callback_events": [],
"metadata": {
"run_id": "8e17ccdf-bc21-4867-a20f-dd1b1f841103"
}
}

ローカルと同じように動作しました。

また、すごいことに{Function URLエンドポイント}/docsOpenAPIのドキュメントページも表示できます!!

すごいっす!AWS Lambda Web Adapter!!

注記

LangServeで作成されるAPIを限定するにはこのような指定を、

add_routes(
app,
BedrockChat(model_id="anthropic.claude-instant-v1"),
path="/bedrock",
enabled_endpoints=["invoke", "batch"]
)

OpenAPIのドキュメントを無効化したい場合はこのような指定をすると良さそうです。

app = FastAPI(
title="LangServe",
version="1.0",
description="LangChain Server",
docs_url=None,
redoc_url=None,
openapi_url=None
)