LangChainで作ったChain/AgentをLangServeでAPI化する
- LangServeを使うと、LangChainのrunnablesとchainsをREST APIとしてデプロイできる
- FastAPIと統合されており、データ検証にpydanticを使用している
hostedバージョンもそのうち提供予定らしいが、とりあえずローカルで試してみる。
CLIが必要になりそう&普段使わないpoetryを使うようなので、今回はdevcontainerを使う。
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm",
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
}
}
コンテナで開いて以降はコンテナ内の作業。
パッケージのインストール。extraは今回は全てを指定。
$ pip install "langserve[all]"
LangChainのCLIを使うと、LangServeに対応したプロジェクトディレクトリを作成してくれるらしいので、あわせてインストール。
$ pip install langchain-cli
$ pip freeze | egrep "lang(chain|serve)"
langchain-cli==0.0.24
langchain-core==0.2.5
langserve==0.2.2
LangChain CLIでプロジェクトを作成。
$ langchain app new my-app
対話モードになる。パッケージをここで追加できるようだけども、ENTERで一旦スキップ。
What package would you like to add? (leave blank to skip):
プロジェクトディレクトリが作成された
Success! Created a new LangChain app under "./my-app"!
Next, enter your new app directory by running:
cd ./my-app
Then add templates with commands like:
langchain app add extraction-openai-functions
langchain app add git+ssh://git@github.com/efriis/simple-pirate.git
プロジェクトディレクトリの中身はこんな感じ。
$ tree my-app/
my-app/
├── Dockerfile
├── README.md
├── app
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-312.pyc
│ │ └── server.cpython-312.pyc
│ └── server.py
├── packages
│ └── README.md
└── pyproject.toml
4 directories, 8 files
my-app/app/server.py
がAPIサーバのスクリプト。ここのadd_routes
にrunnablesを書けばいいらしい。そこはとりあえず後回し。
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from langserve import add_routes
app = FastAPI()
@app.get("/")
async def redirect_root_to_docs():
return RedirectResponse("/docs")
# Edit this to add the chain you want to add
add_routes(app, NotImplemented)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
パッケージを追加する場合はpoetryを使う。langchain-openaiを追加する。
$ cd my-app # プロジェクトディレクトリに移動
$ poetry add langchain-openai
OpenAIのAPIキーを環境変数にセット。実際にやる場合は.envあたりを使うのが良いと思う。
$ export OPENAI_API_KEY="sk-..."
あとは以下で起動するだけだけども、コードはまだ何も書いてないのでエラーになる。
$ poetry run langchain serve
ということでチュートリアルのサンプルコードを反映する。
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from langserve import add_routes
# langchainをインポート
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
app = FastAPI()
@app.get("/")
async def redirect_root_to_docs():
return RedirectResponse("/docs")
# モデルやプロンプトなどの定義
model = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_template("{topic}についてのジョークを考えて。")
# /jokeでchainを実行する
add_routes(
app,
prompt | model, # LCELでチェーンを書く
path="/joke"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
では起動する。ポートを変更する場合などは以下のようにして変更できる。
$ poetry run langchain serve --port 8100
INFO: Will watch for changes in these directories: ['/workspaces/langserve-test/my-app']
INFO: Uvicorn running on http://127.0.0.1:8100 (Press CTRL+C to quit)
INFO: Started reloader process [13607] using StatReload
INFO: Started server process [13616]
INFO: Waiting for application startup.
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
LANGSERVE: Playground for chain "/joke/" is live at:
LANGSERVE: │
LANGSERVE: └──> /joke/playground/
LANGSERVE:
LANGSERVE: See all available routes at /docs/
INFO: Application startup complete.
こんな感じで起動した。
ということでとりあえずシンプルにcurlで試してみる。
$ curl -X POST 'http://localhost:8000/joke/invoke' \
-H 'Content-Type: application/json' \
--data-raw '{
"input": {
"topic": "猫"
}
}' | jq -r .
{
"output": {
"content": "もちろん、猫についてのジョークを考えました:\n\nどうして猫はインターネットが好きなのか知ってる?\nだって、そこには\"キャット\"フィッシュがたくさんいるからさ!\n\nどうでしょうか?気に入っていただけたら嬉しいです。",
"additional_kwargs": {},
"response_metadata": {
"token_usage": {
"completion_tokens": 64,
"prompt_tokens": 16,
"total_tokens": 80
},
"model_name": "gpt-4o",
"system_fingerprint": "fp_319be4768e",
"finish_reason": "stop",
"logprobs": null
},
"type": "ai",
"name": null,
"id": "run-f0ce5e48-5901-43b3-80fc-b178be4d85dc-0",
"example": false,
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 16,
"output_tokens": 64,
"total_tokens": 80
}
},
"metadata": {
"run_id": "43f32b90-4ba6-4d8b-b116-8b567c686987",
"feedback_tokens": []
}
}
/runnable名/
がエンドポイントのベースになって、invoke
やstream
とかで叩けるっぽい。
pythonでinvoke。
import requests
response = requests.post(
"http://localhost:8100/joke/invoke",
json={'input': {'topic': '猫'}}
)
print(response.json()["output"]["content"])
langserveはクライアントライブラリとしても使用できる。streamの例。
from langserve import RemoteRunnable
import asyncio
joke_chain = RemoteRunnable("http://localhost:8100/joke/")
async def jokes(topic: str):
async for msg in joke_chain.astream({"topic": topic}):
print(msg.content, end="", flush=True)
asyncio.run(jokes("猫"))
/runnable名/playground
にアクセスすると、チャットUIで簡単に試せる。ただし、よくあるplaygroundのような自由度はなく、runnableの入力スキーマが辞書であること、かつ、以下のどちらかの条件を満たす必要がある。
- 1つのキーがあり、そのキーの値はチャットメッセージのリストでなければならない。
- 2つのキーがあり、1つはメッセージのリスト、もう1つは最新のメッセージでなければならない。
playground_type="chat"
をapp_routesに設定する必要がある、と書いてあるが、特に指定しなくてもアクセスできた。
AWS SAMでLambdaにデプロイするのはこの辺。参考になる。
シンプルにLCELのチェーンを渡せるのは楽で良き。
スキーマが決まっていてあんまりカスタマイズの余地はないのかなーと思いきや、APIHandler
を使えばカスタマイズできる余地はありそう。