はじめに
こんにちは、慶應義塾大学理工学部機械工学科3年の金賢佑です!
私は東大発AIベンチャーの株式会社2WINSの元でインターンをしています!
これからの記事では自分がインターンを通して最強のAIエンジニアを目指していきます!
第六回はOutputParserの基礎を記録しました!
OutputParserとは?
OutputParser
とはLangChain
のコンポーネントの一つ!
言語モデルの出力を特定の形に変換してくれる!
いろんな種類があって、たとえば
StructuredOutputParser
は出力をJSON
形式にしてくれる!
CommaSeparatedListOutputParser
はリストに変換してくれる!
PydanticOutputParser
は型のついたデータにしてくれる!
今回はPydanticOutputParser
を使ってみるよ!
PydanticOutputParserを使ってみよう
以下では急にPydantic
ライブラリが出てくるから先に説明しておくよ!
Pydanticって?
Pydantic
はPython
でデータの型を決めて扱うためのライブラリ📦!
例えばPydantic
ライブラリの中のBaseModel
というクラスを使ってみるよ!
from pydantic import BaseModel
class Recipe(BaseModel): # ← BaseModel を継承!
ingredients: list[str]
steps: list[str]
だから以下のようにルール通りにデータを渡すとちゃんとうまくいきますね!
data = {
"ingredients": ["たまねぎ", "にんじん"],
"steps": ["切る", "炒める"]
}
recipe = Recipe(**data)
print(recipe.ingredients) # → ['たまねぎ', 'にんじん']
print(recipe.steps) # → ['切る', '炒める']
だけどルールと違う以下のような場合もエラーになる!
Recipe(
ingredients="たまねぎ", 'にんじん'
steps=["切る", 1000]
)
まずはRecipe
クラスを作成しよう!
from pydantic import BaseModel, Field
class Recipe(BaseModel):
ingredients: list[str] = Field(description='ingredients of the dish')
steps: list[str] = Field(description='steps to make the dish')
class Recipe(BaseModel):
はRecipe
という名前のクラスを定義してるよ!
ingredients: list[str]
はingredients
には「文字列のリスト」しか加えられないことを意味している!
Field
で説明を付け加えている。
次はこのコードを追加してください!
from langchain_core.output_parsers import PydanticOutputParser
output_parsers = PydanticOutputParser(pydantic_object=Recipe)
format_instructions = output_parsers.get_format_instructions()
print(format_instructions)
PydanticOutputParser
は出力をRecipe
型に変換するよって定義している感じ!
get_format_instructions()
はLLMに「さっき定義したRecipe
型で答えてね〜」と指定している感じ!
だから、print(format_instruction)
での出力は「正しい出力」と「間違った出力」の例を出している!
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.
ではこのformat_instructions
を使ってChatPromptTemplate
を作成します。
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate(
[
(
'system',
'ユーザーが入力した料理のレシピを教えてください。\n\n'
'{format_instructions}',
),
('human', '{dish}'),
]
)
prompt_with_format_instructions = prompt.partial(
format_instructions=format_instructions
)
system
にformat_instructions
で答えてね、と伝えてる!
'human', '{dish}'
でユーザーが聞きたい料理名を入力できる
format_instructions
の部分にはいつも同じものが入るはず!いちいち形式を指定するのは面倒くさいからprompt.partial
で固定しちゃう!
ちなみに復習するとこれが入っているよ
format_instructions = output_parsers.get_format_instructions()
では次!
prompt_value = prompt_with_format_instructions.invoke({'dish':'カレー'})
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model='gpt-4o', temperature=0)
ai_message = model.invoke(prompt_value)
GPT-4oを使ってプロンプトを送信した!
今回でいうと「カレーのレシピを指定したJSON形式で答えて〜」ってお願いしている!
recipe = output_parsers.invoke(ai_message)
print(type(recipe))
print(recipe)
最終的に返ってきたテキストをRecipe
オブジェクトに変換しました!
#出力結果
<class '__main__.Recipe'>
ingredients=['鶏肉 300g', '玉ねぎ 2個', 'にんじん 1本', 'じゃがいも 2個', 'カレールー 1箱', '水 600ml', 'サラダ油 大さじ1', '塩 少々', 'こしょう 少々'] steps=['鶏肉、玉ねぎ、にんじん、じゃがいもを一口大に切る。', '鍋にサラダ油を熱し、鶏肉を炒める。', '鶏肉の色が変わったら、玉ねぎ、にんじん、じゃがいもを加えて炒める。', '全体に油が回ったら、水を加えて煮立たせる。', 'アクを取り除き、弱火で野菜が柔らかくなるまで煮る。', '火を止めてカレールーを加え、溶かす。', '再び弱火で煮込み、塩とこしょうで味を調える。', 'お好みのとろみになるまで煮込んで完成。']
これでPythonでデータとして扱うことができます!
format_instructionsがないとどうなるのか
なんだかPydanticOutputParser
で「Recipe
型で返してね」って定義しているのにformat_instruction
でもまた、「Recipe
型で返してね〜」って指示を出してるの、なんだか気持ち悪くありませんか?
そこで試しにformat_instructions
を消してみました。
実行例は以下の通りです。
from pydantic import BaseModel, Field
class Recipe(BaseModel):
ingredients: list[str] = Field(description='ingredients of the dish')
steps: list[str] = Field(description='steps to make the dish')
from langchain_core.output_parsers import PydanticOutputParser
output_parsers = PydanticOutputParser(pydantic_object=Recipe)
# format_instructions = output_parsers.get_format_instructions()
# print(format_instructions) <- 消しました
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
(
'system',
'ユーザーが入力した料理のレシピを教えてください。' # \n\n''{format_instructions}', <- も消しています
),
('human', '{dish}'),
]
)
# prompt_with_format_instructions = prompt.partial(
# format_instructions=format_instructions
# ) <- もちろん消す
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model='gpt-4o', temperature=0)
prompt_value = prompt.invoke({'dish': 'カレー'})
ai_message = model.invoke(prompt_value)
recipe = output_parsers.parse(ai_message.content)
print(recipe)
実行結果は何やらエラーが起きています。LangSmithで詳細を確認してみましょう。
左が前回まで、右が今回です。
まず一枚目のシステムプロンプトについてですが、右だと、ただLLMに質問を投げかけているだけですね。
一方左はこういう型で返してねーとちゃんと伝えています。
二枚目の返答についても、ぱっと見二つとも返せているように見えますが、左とは違って右はただ文章を生成しているだけになっていますね。
(筆者はここで、また頭がこんがらがったので一応メモ程度に記しておきます。左の方、もう"ingredients"
と"steps"
にまとまっていてこれ以上何をパースするんだっけ?ってなりましたが、この"ingredients"
はまだ文字列ですよね。だからこの後レシピ型にパースすることでしっかりingredients
と言うリストに入るんですね。)
これらのことからOutputParser
は形の定義そのものであり、format_instructions
で指示をして初めて正常に動くことがわかりました!
いろいろ複雑ですね。。。
おさらい
from pydantic import BaseModel, Field
class Recipe(BaseModel):
ingredients: list[str] = Field(description="ingredients of the dish")
steps: list[str] = Field(description="steps to make the dish")
レシピの型を定義した!
from langchain_core.output_parsers import PydanticOutputParser
output_parsers = PydanticOutputParser(pydantic_object=Recipe)
出力をこの型にしてくれるParser
を作成!
format_instructions = output_parsers.get_format_instructions()
LLMにこの形式で返答して!と指示書を作る!
prompt = ChatPromptTemplate([
('system', 'レシピを出力してください。\n\n{format_instructions}'),
('human', '{dish}')
])
prompt_with_format_instructions = prompt.partial(format_instructions=format_instructions)
プロンプトテンプレートを作成!
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model='gpt-4o', temperature=0)
prompt_value = prompt_with_format_instructions.invoke({'dish': 'カレー'})
ai_message = model.invoke(prompt_value)
LLMに質問を投げる!
recipe = output_parsers.parse(ai_message.content)
print(recipe)
最後に返ってきたテキストをRecipe
型に変換しておしまい!
まとめ
今回はLangChain
コンポーネントの一つのOutputparser
を使ってみました!
お疲れ様でした!