【０から目指すAIエンジニア】Day6：OutputParserの基礎

# はじめに
こんにちは、慶應義塾大学理工学部機械工学科3年の金賢佑です！
私は東大発AIベンチャーの株式会社2WINSの元でインターンをしています！

これからの記事では自分がインターンを通して最強のAIエンジニアを目指していきます！
第六回はOutputParserの基礎を記録しました！

# OutputParserとは？
`OutputParser`とは`LangChain`のコンポーネントの一つ！\
言語モデルの出力を特定の形に変換してくれる！

いろんな種類があって、たとえば\
`StructuredOutputParser`は出力を`JSON`形式にしてくれる！\
`CommaSeparatedListOutputParser`はリストに変換してくれる！\
`PydanticOutputParser`は型のついたデータにしてくれる！

今回は`PydanticOutputParser`を使ってみるよ！

# PydanticOutputParserを使ってみよう


以下では急に`Pydantic`ライブラリが出てくるから先に説明しておくよ！
### Pydanticって？
`Pydantic`は`Python`でデータの型を決めて扱うためのライブラリ📦！

例えば`Pydantic`ライブラリの中の`BaseModel`というクラスを使ってみるよ！
``` python
from pydantic import BaseModel

class Recipe(BaseModel):  # ← BaseModel を継承！
    ingredients: list[str]
    steps: list[str]
```
だから以下のようにルール通りにデータを渡すとちゃんとうまくいきますね！
``` py
data = {
  "ingredients": ["たまねぎ", "にんじん"],
  "steps": ["切る", "炒める"]
}

recipe = Recipe(**data)

print(recipe.ingredients)  # → ['たまねぎ', 'にんじん']
print(recipe.steps)        # → ['切る', '炒める']

```
だけどルールと違う以下のような場合もエラーになる！
``` python
Recipe(
    ingredients="たまねぎ", 'にんじん'
    steps=["切る", 1000]
)
```
### まずは`Recipe`クラスを作成しよう！
``` python
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`で説明を付け加えている。

次はこのコードを追加してください！
``` python
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)`での出力は「正しい出力」と「間違った出力」の例を出している！

``` text
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`を作成します。

``` python
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`で固定しちゃう！\
ちなみに復習するとこれが入っているよ
``` python
format_instructions = output_parsers.get_format_instructions()
```

では次！
``` python
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形式で答えて〜」ってお願いしている！
``` python
recipe = output_parsers.invoke(ai_message)
print(type(recipe))
print(recipe)
```
最終的に返ってきたテキストを`Recipe`オブジェクトに変換しました！

``` python
#出力結果
<class '__main__.Recipe'>
ingredients=['鶏肉 300g', '玉ねぎ 2個', 'にんじん 1本', 'じゃがいも 2個', 'カレールー 1箱', '水 600ml', 'サラダ油 大さじ1', '塩 少々', 'こしょう 少々'] steps=['鶏肉、玉ねぎ、にんじん、じゃがいもを一口大に切る。', '鍋にサラダ油を熱し、鶏肉を炒める。', '鶏肉の色が変わったら、玉ねぎ、にんじん、じゃがいもを加えて炒める。', '全体に油が回ったら、水を加えて煮立たせる。', 'アクを取り除き、弱火で野菜が柔らかくなるまで煮る。', '火を止めてカレールーを加え、溶かす。', '再び弱火で煮込み、塩とこしょうで味を調える。', 'お好みのとろみになるまで煮込んで完成。']
```
これでPythonでデータとして扱うことができます！

# format_instructionsがないとどうなるのか
なんだか`PydanticOutputParser`で「`Recipe`型で返してね」って定義しているのに`format_instruction`でもまた、「`Recipe`型で返してね〜」って指示を出してるの、なんだか気持ち悪くありませんか？

そこで試しに`format_instructions`を消してみました。\
実行例は以下の通りです。

``` python
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で詳細を確認してみましょう。\
左が前回まで、右が今回です。


![alt text](<スクリーンショット 2025-04-16 15.28.00.png>) ![alt text](<スクリーンショット 2025-04-16 15.29.56.png>)


まず一枚目のシステムプロンプトについてですが、右だと、ただLLMに質問を投げかけているだけですね。\
一方左はこういう型で返してねーとちゃんと伝えています。

二枚目の返答についても、ぱっと見二つとも返せているように見えますが、左とは違って右はただ文章を生成しているだけになっていますね。

（筆者はここで、また頭がこんがらがったので一応メモ程度に記しておきます。左の方、もう`"ingredients"`と`"steps"`にまとまっていてこれ以上何をパースするんだっけ？ってなりましたが、この`"ingredients"`はまだ文字列ですよね。だからこの後レシピ型にパースすることでしっかり`ingredients`と言うリストに入るんですね。）

これらのことから`OutputParser`は形の定義そのものであり、`format_instructions`で指示をして初めて正常に動くことがわかりました！\
いろいろ複雑ですね。。。

# おさらい
``` python
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")
```
レシピの型を定義した！

``` python
from langchain_core.output_parsers import PydanticOutputParser
output_parsers = PydanticOutputParser(pydantic_object=Recipe)
```
出力をこの型にしてくれる`Parser`を作成！

 ``` python
 format_instructions = output_parsers.get_format_instructions()
```
 LLMにこの形式で返答して！と指示書を作る！

``` python
prompt = ChatPromptTemplate([
    ('system', 'レシピを出力してください。\n\n{format_instructions}'),
    ('human', '{dish}')
])

prompt_with_format_instructions = prompt.partial(format_instructions=format_instructions)

```
プロンプトテンプレートを作成！

``` python
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に質問を投げる！

``` python
recipe = output_parsers.parse(ai_message.content)
print(recipe)
```
最後に返ってきたテキストを`Recipe`型に変換しておしまい！

# まとめ
今回は`LangChain`コンポーネントの一つの`Outputparser`を使ってみました！

お疲れ様でした！