1. はじめに
LangChain、FastAPI、Reactを使用して、レシピ提案アプリのプロトタイプを作ってみました。
アプリは、ユーザーがレシピ名を入力すると、そのレシピの概要説明、材料、手順を提案してくれるものです。LLM モデルに構造化されたデータを出力してもらうために、LangChain で提供されている Pydantic モデルのサポート機能を使ってみました。また、せっかくPydantic モデルが使えるので、その応用先として FastAPI と連携して API 化し、React アプリからの利用も試してみました。
本記事全体を通じて、得られた成果と課題について紹介します。
2. アプリの基本的な機能と動作
まず初めに、構造化したレシピ情報を扱うために、Pydanticモデルを定義します。料理名、料理の概要説明、材料、手順を出力してもらえるように定義しました。以下がモデルの定義例です。
from typing import List
from pydantic import BaseModel, Field
class Recipe(BaseModel):
dish_name: str = Field(description="name of the dish")
description: str = Field(description="description of the dish")
ingredients: List[str] = Field(
description="ingredients of the dish. Ingredients can be includuding imaginary ingredients"
)
steps: List[str] = Field(description="steps to make the dish")
LangChain の Pydantic モデルを扱う機能を利用すると、以下のような結果を返してくれます。
{
"dish_name": "カツカレー",
"description": "カツカレーは、カツとカレーを組み合わせた人気のある日本の料理です。カツは揚げた肉や魚のカットレットであり、カレーはスパイスを使った濃厚なソースです。",
"ingredients": [
"カツ用の肉(豚肉や鶏肉)",
"パン粉",
"小麦粉",
"卵",
"カレールー",
"玉ねぎ",
"にんじん",
"じゃがいも",
"水",
"油"
],
"steps": [
"肉を適当な大きさに切り、塩とこしょうで下味をつける。",
"小麦粉、溶き卵、パン粉の順に肉をまぶす。",
"フライパンに油を熱し、肉を両面こんがりと揚げる。",
"玉ねぎ、にんじん、じゃがいもを適当な大きさに切る。",
"切った野菜をフライパンに入れて炒める。",
"水を加えて野菜を煮る。",
"カレールーを加えて溶かし、ソースがとろみがつくまで煮込む。",
"揚げたカツをソースの上にのせて完成。"
]
}
BaseModel で定義した通りの結果が出力されていますね。
出力結果が構造化されているので、アプリでも整形された形で出力できるようになります。
なお、スキーマの説明で架空の材料を含めてもよいと指示しているので、「一角獣ステーキ」や「魔物カレー」もそれっぽい結果を出してくれます。
次節以降でそれぞれのステップについて説明していきます。
3. Pydantic スキーマを使用したデータの出力
Pydanticスキーマは、python 内でデータモデルを定義するのに使われます。レシピ情報を料理名、説明、材料、手順といった要素に分解し、それぞれの情報が整合性を持つよう保証します。以下が、Pydanticスキーマを使用した python クラスです。
from typing import List
from pydantic import BaseModel, Field
class Recipe(BaseModel):
dish_name: str = Field(description="name of the dish")
description: str = Field(description="description of the dish")
ingredients: List[str] = Field(
description="ingredients of the dish. Ingredients can be includuding imaginary ingredients"
)
steps: List[str] = Field(description="steps to make the dish")
このモデルを LangChain を使って LLM モデルに出力させることができます。
import langchain
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
langchain.verbose = True
chat = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
openai_api_key="******",
)
def generate_recipe(dish: str) -> Recipe:
template = """料理のレシピを教えてください。
{format_instructions}
料理名: {dish}
"""
parser = PydanticOutputParser(pydantic_object=Recipe)
prompt = PromptTemplate(
template=template,
input_variables=["dish"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
chain = LLMChain(llm=chat, prompt=prompt)
output = chain.run(dish=dish)
recipe = parser.parse(output)
return recipe
generate_recipe
関数は、Recipe クラスで回答を埋め込んだ形式で返してくれます。
この関数を実行した場合、以下のように過程が出力されます。
> Entering new LLMChain chain...
Prompt after formatting:
料理のレシピを教えてください。
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
``
{"properties": {"dish_name": {"title": "Dish Name", "description": "name of the dish", "type": "string"}, "description": {"title": "Description", "description": "description of the dish", "type": "string"}, "ingredients": {"title": "Ingredients", "description": "ingredients of the dish. Ingredients can be includuding imaginary ingredients", "type": "array", "items": {"type": "string"}}, "steps": {"title": "Steps", "description": "steps to make the dish", "type": "array", "items": {"type": "string"}}}, "required": ["dish_name", "description", "ingredients", "steps"]}
``
料理名: 一角獣ステーキ
> Finished chain.
※一部マークダウン記法と競合するので改変しています。
LangChain は回答方法を自然言語で補完し、LLMモデルに問い合わせているようです。
LLMモデルは指定したスキーマで返してくるので、あとは Recipe モデルとしてパースして関数の出力値として返します。
4. FastAPI による API 実装
せっかく Pydantic モデルで返される関数があるので、Pydantic と親和性の高い FastAPI と組み合わせて API を提供してみます。
from fastapi import APIRouter, FastAPI
router = APIRouter(prefix="")
@router.post("/recipe", operation_id="generate_recipe", response_model=Recipe)
async def api_generate_recipe(dish: str):
return generate_recipe(dish)
app.include_router(router)
これにより以下の API ドキュメントが生成されました。
あとはこの仕様に従ったり、仕様からAPI利用ライブラリを自動作成するツールを使うなどして UI を組むだけです。
本記事ではUIの作成過程については割愛します。
5. 所感と課題
成果に対する所感は、概ね期待通りです。
- レシピ情報の生成: 大まかな期待通りの結果を出してくれました。ユーザーがレシピ名を入力すると、概要説明や材料、手順がそれっぽいものが出力されました。
- Pydanticスキーマの効果: Pydanticスキーマの導入により、データの整形と管理が簡単に行えるようになりました。また、FastAPI と組み合わせることにより、APIのリクエストとレスポンスのデータ形式が明確に定義され、コードの可読性と保守性が向上しました。
一方で、以下のような課題もありました。
- 材料の分量情報の不安定さ: LangChainモデルの生成結果において、材料の分量情報が一貫して出力されない場合がありました。今回は、材料に関する具体的な情報を与えていないことに起因していそうです。適切な指示への変更や、pydantic スキーマの修正により解決が期待されます。
- パースエラーとリトライ処理の必要性: 入力によっては指定した形式を無視して結果を返すことがあります。詳細な原因の分析はまだできていませんが、実際にサービスとして提供する場合には適切なリトライ処理やプロンプトエンジニアリングの改善が必要そうです。
6. 結論
LangChain、FastAPI、Reactの組み合わせによるレシピ提案アプリの開発プロセスを検証し、その結果を報告しました。
Pydanticスキーマを利用して構造化されたレシピ情報を操作し、LangChainを通じてLLMモデルから出力を得る手法を実証しました。
さらに、FastAPIと組み合わせてAPIを提供し、ユーザーが利用できる形でアプリケーションをデモしました。
特定のスキーマを持つデータをLangChainモデルに生成させるアプリケーションを検討する場合、LangChain + Pydantic + FastAPIの組み合わせにより非常に効率よくプロトタイピングでき、役に立ちそうです。