LangChainでOutput Parserを使って、出力をパースしてコントロールしよう

# はじめに

みなさんは LLM を利用したアプリケーションを作っていてこんな風に思ったことはありませんか？

- LLMの出力を後続の処理で使いたいのに、出力の形が不安定な文字列で困る
- JSONで出力してほしいのに、コードブロックに囲われて出てきて困る

LangChainのOutput Parserを使えば出力をコントロールして、オブジェクトにパースすることができます。

# 前提
LLM の API キーなどについては適切に`.env`に書かれていることを前提としています。
この記事では以下のバージョンで動作確認をしています。

- Python 3.12.3
- langchain-core == 0.3.18

# Output Parser

公式にはたくさんの Output Parser が紹介されています。

https://python.langchain.com/docs/how_to/#output-parsers

この中でも私がよく使っている `StrOutputParser`と`PydanticOutputParser`について紹介します。

## StrOutputParser

`StrOutputParser`はその名の通り文字列へのパーサです。出力のコントロールとは関係ないですが、便利でよく使っているので紹介します。
LLM のチャットモデルを invoke したときに返ってくるレスポンスの形式は様々ですが、これをチェーンに加えることにより出力から求めている可能性が高い文字列（LLM の最新の応答など）を抜き出して返してくれるようになります。

例えば、`ChatOpenAI`を直接 invoke したときのレスポンスは`BaseMessage`なので様々なメタデータが含まれています。生成された文書だけ使いたい場合は`BaseMessage.content`にあるテキストを利用する必要がありますが、`StrOutputParser`を使えば直接`str`が返ってくるので楽です。

```py
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(model="gpt-4o")
chain = llm | StrOutputParser()

result = chain.invoke("こんにちは")
print(result)
```



## PydanticOutputParser

`PydanticOutputParser`は`pydantic.BaseModel`を継承して定義したオブジェクト（以下 Pydantic オブジェクト）を返すパーサです。`get_format_instructions`メソッドを持っており、プロンプトに埋め込む用の適切な出力形式の指示を作成してくれます。

簡単な例をもとに紹介します。
必要なモジュールを import します。

```py
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from pydantic import BaseModel, Field
from typing import Literal
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(model="gpt-4o")
```

出力させたい形式の Pydantic オブジェクトを作成します。`pydantic.Field`で補足情報を与えることができます。`description`でどんな値を入れるべき変数なのかを LLM に伝える用途で利用します。

以下のコードでは`KantoTouristSpot`で欲しい観光地の情報をまとめたクラスを作成して、それを`TouristSpots`でリストにしています。
`prefecture`のように列挙型を使いたいような変数のときは`Literal`を使うと楽です。

```py
# Pydantic オブジェクトを定義
class KantoTouristSpot(BaseModel):
    name: str = Field(..., description="観光地名")
    prefecture: Literal[
        "Tokyo", "Kanagawa", "Chiba", "Saitama", "Ibaraki", "Tochigi", "Gunma"
        ] = Field(..., description="所在都道府県名")
    description: str = Field(..., description="観光地の説明")

class TouristSpots(BaseModel):
    spots: list[KantoTouristSpot] = Field(..., title="観光地リスト")
```

定義した Pydantic オブジェクトを`PydanticOutputParser`に渡してパーサを生成します。
パーサの`get_format_instructions`メソッドでフォーマットの指定プロンプトを作成します。

```py
# 出力させたいPydantic オブジェクトを指定する
output_parser = PydanticOutputParser(pydantic_object=TouristSpots)

# format_instructions を生成
format_instructions = output_parser.get_format_instructions()

```

ちなみに、`format_instructions`の中身はこのようになっています。

````
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:
```
{"$defs": {"KantoTouristSpot": {"properties": {"name": {"description": "観光地名", "title": "Name", "type": "string"}, "prefecture": {"description": "所在都道府県名", "enum": ["Tokyo", "Kanagawa", "Chiba", "Saitama", "Ibaraki", "Tochigi", "Gunma"], "title": "prefecture", "type": "string"}, "description": {"description": "観光地の説明", "title": "Description", "type": "string"}}, "required": ["name", "prefecture", "description"], "title": "KantoTouristSpot", "type": "object"}}, "properties": {"spots": {"items": {"$ref": "#/$defs/KantoTouristSpot"}, "title": "観光地リスト", "type": "array"}}, "required": ["spots"]}
```
````

あとはプロンプトテンプレートを作り`format_instructions`を代入して、チェーンを実行すれば定義したクラスに収まった出力が得られます。

```py
# プロンプトテンプレート
prompt_template = ChatPromptTemplate(
    [
        (
            "system",
            """あなたは関東地方の観光地を紹介するAIエージェントです。ユーザーの入力に対して適切な回答を返してください。
            
            {format_instructions}""",
        ),
        ("human", "{user_input}"),
    ]
)
# format_instructionsをテンプレートに挿入
prompt = prompt_template.partial(format_instructions=format_instructions)
chain = prompt | llm | output_parser

result = chain.invoke({"user_input": "テーマパークを紹介してください。"})
print(result)
```
出力は以下の通りです（視認性のために改行しています）。
```
spots=[
   KantoTouristSpot(name='東京ディズニーランド', prefecture='Chiba', description='東京ディズニーランドは千葉県浦安市にある世界的に有名なテーマパークで、ディズニーキャラクターたちと触れ合えるアトラクションやパレードが楽しめます。'), 
   KantoTouristSpot(name='横浜・八景島シーパラダイス', prefecture='Kanagawa', description='横浜・八景島シーパラダイスは神奈川県横浜市にある、海洋生物の展示やアトラクションが楽しめるテーマパークです。'), 
   KantoTouristSpot(name='サンリオピューロランド', prefecture='Tokyo', description='サンリオピューロランドは東京都多摩市にある屋内型テーマパークで、ハローキティなどサンリオキャラクターとの楽しいひとときを過ごせます。')
]
```

# おわりに
適切にパースされた出力を得られるのでその後の様々な処理がやりやすくなります。文章から情報を抜き出して整理させる際などに便利なので使ってみてください。
LangChainのエコシステムには便利なツールが揃っているので、勉強していきたいですね。