LangChain・FastAPI・ReactでGPTチャットアプリを作成する
都内でAIエンジニアをやっています、Buchioです。
近年話題のChatGPTやBingAIは非常に強力なツールで知的生産に役立つものだと思います。ただ、あくまで誰でも無料で使える汎用的なアプリケーションであり、専門的な文章を読み込ませてたり、ドキュメント丸々を要約させるなどの柔軟なことはできません。そのため、特定のドメインやシナリオに合わせてカスタマイズできる雛形を手元に置いておきたいと考えました。
LangChainは、OpenAIのAPIにアクセスして任意のモデルを利用できるだけでなく、プロンプトの管理、独自の質問応答ロジックや検索機能も提供してくれるため、自分用のチャットボットを作成するのに非常に便利なツールです。
この記事では、LangChainを使ってAIチャットボットを作成し、FastAPIとReactでWebUIとして扱いやすい形式にするテンプレート的なものを作成します。
前提条件
本記事ではLangChainのUI部分を作成する全体感を伝えることに主眼におくため、LangChain自体の具体的な解説や細かな環境構築は省きます。また、実サービスを前提とした設計としてはいろいろ穴があると思われるので、簡易的なプロトタイプとして捉えてください。
やること
- FastAPI上でLangChainを用いたAPIを作成
- OpenAPIを用いたフロントのAPIClientの自動生成
- React-typescript-tailwind.cssを用いた簡易的なUIの作成
やらないこと
- LangChainの具体的な使用方法解説
- web socketを用いた会話履歴を考慮したChatbot
前提とする環境
- Linux or Mac
- Dockerがインストール済み(
docker compose
が実行可能) - yarn, poetryがインストール済み
- OpenAIのアカウントを保有
完成物
backend
frontend
backend
backendは以下のように構成します。
- poetry
- FastAPI
- LangChain
poetryはpythonの仮想環境や依存関係を管理するためのツールであり、javascriptでいうnpmのようなものです。近年は、Python開発において、poetryが広く採用されるようになっています。
FastAPIはRESTful APIを開発するためのモダンで高速なWebフレームワークです。FastAPIはPythonの型ヒントに基づいてデータのバリデーション、シリアライズ、デシリアライズを行い、自動的にOpenAPIドキュメントを生成します。また、ASGIサーバに対応しており、非同期処理やWebSocketなどもサポートしています。また作成したAPIのスキーマを、OpenAPI形式で出力可能です。
LangChainは、大規模言語モデル(LLM)と外部リソース(データソースや言語処理系など)を組み合わせたアプリケーションの開発支援を目的としたPythonライブラリです。LangChainでは、LLMにプロンプトを与えてテキスト生成や質問応答などのタスクを実行できます。また、チェーンという機能を使って、複数のLLMや外部リソースを連携させることもできます。
FastAPIを環境を構築
こちらの記事を参考にFastAPIの環境を構築しました。(一部省略)
以下ではpoetryがインストールされている前提で進めます。
poetry new --src llm-server
poetry config virtualenvs.path ".venv" --local
poetry config virtualenvs.in-project true --local
poetry add --dev mypy black flake8
poetry add fastapi uvicorn gunicorn
langchainの追加
LangChainを用いてAIモデルを動かします。ここではシンプルにOpenAIのGPT3に対してQ,Aのプロンプトを与えて回答するようにしています。この部分を別のLLMに置き換えることでOpenAPIのサーバを叩かず自前のLLMを用いることも可能です。
依存関係の追加
poetry add langchain
poetry add openai
実装
src/llm_server/simple_agent.py
from langchain import PromptTemplate, LLMChain
from langchain.llms import OpenAI
def ask_question(question: str) -> str:
llm = OpenAI(temperature=0.9)
template = """Question: {question}
Answer:"""
prompt = PromptTemplate(template=template, input_variables=["question"])
llm_chain = LLMChain(prompt=prompt, llm=llm)
answer = llm_chain.run(question)
return answer
APIの作成
FastAPIではAPIを作成するために以下の3つを用意します。
- リクエストボディ
- レスポンスボディ
- APIメソッドに対応する関数
今回は単純なtextを受け取り、先ほど作成したask_questionに渡して、返り値をtextを返せばよいので、以下のようになります。
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from llm_server.simple_agent import ask_question
app = FastAPI()
origins = ["http://localhost:5173"]# Frontendのオリジンをここに書く
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Message(BaseModel):
text: str = Field(title="Request message to LLM.", max_length=1000)
class LLMResponse(BaseModel):
text: str
@app.get("/healthcheck")
def healthcheck():
return {}
@app.post("/llm")
async def run_llm(message: Message) -> LLMResponse:
answer = ask_question(message.text)
return LLMResponse(text=answer)
以下のコマンドで起動します。 OpenAIのアクセストークンはOpenAIのサイトから発行できます。(一定を超えると有料になるので注意)
export OPENAI_API_KEY="OpenAIのアクセストークン"
uvicorn llm_server:main:app --reload
ブラウザでlocalhost:8000/docsを開くと、APIドキュメントが表示されるので、以下のようにtryoutでAPIを叩いて動作を確認します。
Frontend
フロントエンドは以下のような構成で実装します。
- React
- typescript
- tailwind.css
- vite
- storybook
設定周りは以下の記事を参考にさせていただきました。
yarn create vite llm-interface-react --template react-ts
yarn add -D prettier eslint eslint-config-prettier eslint-plugin-{import,prettier,react,react-hooks}
yarn add -D @typescript-eslint/{parser,eslint-plugin}
yarn add -D npm-run-all
touch .eslintrc .prettierrc .eslintignore .prettierignore
tailwind.css導入
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
storybook追加
npx storybook init
yarn add -D @storybook/addon-postcss
諸々の設定などは完成物を参照ください。
frontend側のClientの自動生成
次にFrontend側のAPIアクセス部分をFastAPIから生成されるスキーマファイルから自動生成します。
スキーマ駆動開発では以下のように、スキーマに対してそれぞれが依存するため、schemaファイルや、生成コードはbackendやfrontendの外側に置くべきです。
しかし、FastAPIではbackendコードからスキーマが生成されるのでshemaはbackendに依存すると言えます。
この理由から、ここでは生成用のコードなどはfrontend側におくこととします。
自動生成は以下の記事を参考にしつつおこないました。
まず、 前章の手順でbackendを立ち上げてlocalhost:8000/openapi.jsonにアクセスし、openapi.jsonをfrontendのルートディレクトリに置きます。
次に以下のようなdocker-compose.ymlをフロントエンドのルートディレクトリに作成、docker compose up
を実行して自動生成を行います。
version: "3.7"
services:
openapi-generator:
image: openapitools/openapi-generator-cli
volumes:
- ./:/frontend
working_dir: /frontend
command:
- generate
- -g
- typescript-axios
- -i
- ./openapi.json
- -o
- /frontend/src/api-client
- --additional-properties=supportsES6=true,modelPropertyNaming=original
これによりsrc/api-client以下にクライアントアプリが生成できました。
フロントエンドの実装
chatbotの基本的な動作としては以下のような動作になります。
- メッセージログの表示
- メッセージの入力
- chatbotへのメッセージの送信、レスポンスをログに追加
ログの表示としては以下のようにMessageオブジェクトのリストからログを表示するコンポーネントを作成します。
Messageオブジェクトはテキストの内容と発言者のIDを持ち、発言者のIDが0だと左、それ以外は右になるようにしています。
import clsx from 'clsx'
export type DialogProps = {
messages: Message[]
}
export type Message = {
speakerId: number
text: string
}
export default function Dialog({ messages }: DialogProps) {
return (
<ul className=' flex flex-col h-full w-full gap-2'>
{messages.map((message) => (
<li
className={clsx(
'flex ',
message.speakerId === 0 ? 'justify-start' : 'justify-end'
)}
key={message.text}
>
<div
className={clsx(
'relative max-w-[80%] px-4 py-2 rounded shadow dark:bg-gray-800'
)}
>
<span className='block'>{message.text}</span>
</div>
</li>
))}
</ul>
)
}
全体のアプリケーションとしては以下のようになります。
import clsx from 'clsx'
import { DefaultApi, Configuration } from './api-client'
import Button from './components/Button'
import Dialog from './components/Dialog'
import Header from './components/Header'
import { useChat } from './useChat'
const config = new Configuration({ basePath: 'http://localhost:8000' }) // TODO: This is for dev
export const apiClient = new DefaultApi(config)
function App() {
const { inputText, setInputText, messages, onSubmit } = useChat(apiClient)
return (
<div className='flex flex-col h-full w-full justify-center'>
<Header />
<div className='flex flex-col h-full w-full items-center'>
<div
className={clsx(
'flex flex-col justify-center h-full w-5/6 max-w-screen-md gap-5 p-5'
)}
>
<Dialog messages={messages} />
<div id='input_form' className={clsx('flex flex-row gap-3')}>
<input
className='w-full'
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<Button onClick={onSubmit} />
</div>
</div>
</div>
</div>
)
}
export default App
ここでバックエンドとの通信やログの管理のようなチャットのロジックとなる部分はuseChatとしてまとめています。
バックエンドへのアクセスは自動生成されたapiClientオブジェクトが各種APIに対応するメソッドを持つため、これを利用します。ここではapiClient.runLlmLlmPost({text:inputText})
のように実行しています。(エラーキャッチは省略)
import { useCallback, useState } from 'react'
import { DefaultApi } from './api-client'
import { Message } from './components/Dialog'
export const useChat = (apiClient: DefaultApi) => {
const [inputText, setInputText] = useState<string>('')
const [messages, setMessages] = useState<Message[]>([
{ speakerId: 0, text: 'Hello!' },
])
const addMessage = useCallback(
(speakerId: number, text: string) => {
setMessages((ms) => [...ms, { speakerId: speakerId, text: text }])
},
[setMessages]
)
const onSubmit = useCallback(() => {
setMessages((ms) => [...ms, { speakerId: 1, text: inputText }])
apiClient.runLlmLlmPost({ text: inputText }).then((x) => {
addMessage(0, x.data.text)
})
setInputText('')
}, [addMessage, setInputText, inputText])
return { inputText, setInputText, messages, onSubmit }
}
異常の工程でフロントエンドが完成しました。以下のコマンドでフロントエンドを立ち上げます。
yarn dev
動作確認
標準だとlocalhost:5173にアクセスするとフロントエンドが見れると思います。実行すると以下のようになりました。GPTくんはO型であり、人狼ではないらしいです。
おわりに
本記事ではLangChainで作成したAIbotをFastAPIとReact経由でアプリケーション化するということを行いました。 LangChainには、AIbotに検索エンジンをしようさせたり、DBのデータを知識として活用させるなどの機能があるため、これをベースにプロンプトの工夫次第でさまざななアプリケーションが作成できます。
しかしながら、ここでは単純なRestAPIを作成しており、会話の履歴情報はこのままだと保持が難しいです。これに対しては、各チャット接続に対してソケット通信をしたり、会話履歴はフロントで管理させるなどが必要になります。ちょうどこれを作成した日に、LangChainのTypescript実装も発表されたので、近々触ってみたいとおもいます。
最後になりますが、本記事は何かと間違いやbad practiceを含んでいる可能性があります。有識者の方は是非コメントでフィードバックをいただければ嬉しいです。
Discussion