はじめに
みなさんは LLM を利用したアプリケーションを作っていてこんな風に思ったことはありませんか?
- LLMの出力を後続の処理で使いたいのに、出力の形が不安定な文字列で困る
- JSONで出力してほしいのに、コードブロックに囲われて出てきて困る
LangChainのOutput Parserを使えば出力をコントロールして、オブジェクトにパースすることができます。
前提
LLM の API キーなどについては適切に.env
に書かれていることを前提としています。
この記事では以下のバージョンで動作確認をしています。
- Python 3.12.3
- langchain-core == 0.3.18
Output Parser
公式にはたくさんの Output Parser が紹介されています。
この中でも私がよく使っている StrOutputParser
とPydanticOutputParser
について紹介します。
StrOutputParser
StrOutputParser
はその名の通り文字列へのパーサです。出力のコントロールとは関係ないですが、便利でよく使っているので紹介します。
LLM のチャットモデルを invoke したときに返ってくるレスポンスの形式は様々ですが、これをチェーンに加えることにより出力から求めている可能性が高い文字列(LLM の最新の応答など)を抜き出して返してくれるようになります。
例えば、ChatOpenAI
を直接 invoke したときのレスポンスはBaseMessage
なので様々なメタデータが含まれています。生成された文書だけ使いたい場合はBaseMessage.content
にあるテキストを利用する必要がありますが、StrOutputParser
を使えば直接str
が返ってくるので楽です。
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 します。
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
を使うと楽です。
# 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
メソッドでフォーマットの指定プロンプトを作成します。
# 出力させたい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
を代入して、チェーンを実行すれば定義したクラスに収まった出力が得られます。
# プロンプトテンプレート
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のエコシステムには便利なツールが揃っているので、勉強していきたいですね。