langchianlogo

LangChain入門 – 3)Output Parser 出力パーサー

LangChain入門の3回目です。Output Parserの使い方について説明します。一般的に生成AIの出力は自然言語となりますが、そのままではプログラムで処理しにくいこともあるでしょう。プログラムからすると、構造化されたデータ(リスト、辞書、JSON、XMLなど)のほうが処理しやすいはずです。
LangChainではOutput Parserを使用することで、生成AIの出力の形式を制御することが可能です。

  • 入力を処理するのがPrompt Template
  • 出力を処理するのがOutput parser

と理解すると良いでしょう。

OutputParser
LangChain入門 – 3)Output Parser 出力パーサー 8

本記事はFuture Coders独自教材からの抜粋です。更新の多い分野なので現状の内容と異なる場合があることご了承ください。

試しに、JSONで出力するように依頼してみましょう。

from langchain_openai import OpenAI
llm = OpenAI()
r = llm.invoke("生徒名簿(3人分)をJSON形式でほしいです。氏と名は別に、年齢、性別(MかF)を含み、キーは英語、値は日本語としてください")
print(r)

以下のようにほぼ正しいJSONの形式で出力されました。残念ながら先頭に”。”がついています。

JSONサンプル
LangChain入門 – 3)Output Parser 出力パーサー 9

また、上記の例では、キーの値がlastName, firstNameになっていますが、Last_Name, First_Nameとキーを指定したいかもしれません。プロンプトを調整すれば可能かもしれませんが、多少面倒なことは否めません。さらに、このデータは正しいJSON形式になっていますが、文字列であることには代わりありません。実際にプログラムで使用するには、この文字列を実データの形式に変換する必要があります。

このように、生成AIの出力をプログラムから利用できる形にするには、ひと手間必要になってしまいます。Output parserはこの処理を行ってくれます。

ちなみに、LangChainは頻繁に更新されており、バージョンによっては以下に掲載するコードがそのまま動かないこともあります。そのようなときは以下のようなコマンドを実行して最新版をインストールしたのちに、試してみてください。

pip install --upgrade langchain
pip install --upgrade pydantic

現在LangChainは活発に開発が進められており、API(クラスや関数)が頻繁に更新されます。エラーメッセージに遭遇した場合には公式サイトにある最新情報を参考にしてください。

LIST (CommaSeparatedListOutputParser)

https://js.langchain.com/docs/api/output_parsers/classes/CommaSeparatedListOutputParser

複数の要素をカンマで区切って表現する、そんな出力が必要になることも多いでしょう。そんな用途に適しているのがCommaSeparatedListOutputParserです。JavaScriptやPythonであればリストは[]で区切り、その中の要素が文字列であれば、”で囲まれている必要があります。

from langchain_openai import OpenAI
llm = OpenAI()
r = llm.invoke("フルーツをカンマで区切って3個列挙してください")
print(r)

出力は以下のようになりました。確かにカンマ区切りで表現されていますが、このままでは単なる文字列です。そのままではJavaScriptやPythonなどの言語でリストとして使えません。

りんご,バナナ,メロン

CommaSeparatedListOutputParserを使ってみましょう。まず、このクラスがどのようなプロンプトを出力するか確認してみましょう。

from langchain.output_parsers import CommaSeparatedListOutputParser

output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()
print(format_instructions)

CommaSeparatedListOutputParserのオブジェクトを作成してget_format_instructionsメソッドの戻り値を出力しているだけです。出力は以下の通りです。

Your response should be a list of comma separated values, eg: `foo, bar, baz`

英語で”カンマで区切って出力する旨”が例とともに提示されています。これを生成AIと組み合わせてみましょう。

from langchain_openai import OpenAI
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate

output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()
template = PromptTemplate(
    template="{item}をカンマで区切って{count}個列挙してください。\n{format_instructions}\n",
    input_variables=["item", "count"],
    partial_variables={"format_instructions": format_instructions}
)

llm = OpenAI()
r = llm.invoke(template.format(item="宝石", count="3"))
response = output_parser.parse(r)
print(f"llmの出力   ='{r}'")
print(f"出力        ={response}")
print(f"型          ={type(response)}")

プロンプト作成時に、PromptTemplateの引数partial_variablesを使用して、プロンプトに補足指示を追加しています。

llmを実行後の結果をoutput_parserオブジェクトのparseメソッドで解釈して、結果となるリストを取得しています。

出力は以下のようになりました。

llmの出力   ='
りんご, バナナ, 桃'
出力        =['りんご', 'バナナ', '桃']
型          =<class 'list'>

llmの出力もカンマ区切りのように見えていましたが、先頭に改行が挿入されてたことが分かります。また文字列なので、直接プログラムからリストとして扱うことはできません。output_parserのparseメソッドを実行した結果、文字列をシングルクォーテーションで囲み、要素はカンマで区切るというリストになっています。しかも、type関数でデータ型を調べてみると、listになっていることがわかります。このような出力であればプログラムですぐに利用できます。

一方で注意も必要です。item=”fruits”とした箇所を、item=”宝石”に変更して実行してみました。出力は以下のようになりました。大きな違いがあることに気づいたでしょうか?

llmの出力   ='
ダイヤモンド,サファイア,ルビー'
出力        =['ダイヤモンド,サファイア,ルビー']
型          =<class 'list'>

llmの出力をみてみると、カンマの後に空白がありません。これによって、output_parser.parseを実行したときに、正しく分割されず、リストの要素が1つだけになってしまっています。カンマで区切られて要素が3つあるように見えるかもしれませんが、文字列を囲むシングルクォーテーションが最初と最後にしかないことに注意してください。

CommaSeparatedListOutputParserの行った処理のイメージを以下に示します。

Hint by format_instruction
LangChain入門 – 3)Output Parser 出力パーサー 10
  • format_instructionsを使って、プロンプトにヒントを提供する
  • 生成AIの出力をリスト形式に変換する

いろいろなOutput Parserが用意されていますが、基本的な使い方は同じです。他のクラスを見てゆきましょう。

JSON (StructuredOutputParser)

https://python.langchain.com/docs/modules/model_io/output_parsers/structured

次に、JSON形式で出力してみましょう。以下は、氏/名/年齢/性別のデータをランダムに生成する
サンプルです。

from langchain.output_parsers import StructuredOutputParser, ResponseSchema

json_schemas = [
    ResponseSchema(name="Last_Name", description="氏"),
    ResponseSchema(name="First_Name", description="名"),
    ResponseSchema(name="Age", description="年齢", type="int"),
    ResponseSchema(name="Gender", description="性別をMかFで")
]
output_parser = StructuredOutputParser.from_response_schemas(json_schemas)
format_instructions = output_parser.get_format_instructions()
print(format_instructions)

データ構造(名前や型など)を”スキーマ”と表現したりします。ここではjson_schemasという変数でデータ構造を表現しています。氏・名・年齢・性別、それぞれの名前と説明、データ型(省略時は文字列)を指定しています。そのスキーマ情報からoutput_parserを取得しています。このoutput_parserのget_format_instructionsを出力しています。以下のような内容が出力されました。

output3
LangChain入門 – 3)Output Parser 出力パーサー 11

「こんなデータ型になるようにJSONで出力せよ」と生成AIに対する指示を明確に表現しています。このようにStructuredOutputParserを使うと明確な表現でプロンプトを作成することが可能です。

このオブジェクトを生成AIと組み合わせてみましょう。

from langchain.output_parsers import StructuredOutputParser, ResponseSchema
from langchain.prompts import PromptTemplate
response_schemas = [
    ResponseSchema(name="Last_Name", description="氏"),
    ResponseSchema(name="First_Name", description="名"),
    ResponseSchema(name="Age", description="年齢", type="int"),
    ResponseSchema(name="Gender", description="性別をMかFで")
]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = output_parser.get_format_instructions()

template = PromptTemplate(
    template="{number}人分の生徒名簿を作成してください。\n{format_instructions}\n",
    input_variables=["number"],
    partial_variables={"format_instructions": format_instructions}
)

from langchain_openai import OpenAI


llm = OpenAI(temperature=0)
prompt = template.format(number="1")
output = llm.invoke(prompt)
print(output)

response = output_parser.parse(output)
print(type(response))
for k, v in response.items():
    print(f"key={k}\tv={v}")

CommaSeparatedListOutputParserのときと同じように、PromptTemplateのtemplate引数の文字列にformat_instructionsを含め、partial_variables引数にformat_instructionsを指定しています。

出力は以下のようになりました。出力データが辞書形式となり、for文で正しく処理されています。キーの値も指示通りです。

JSONサンプル
LangChain入門 – 3)Output Parser 出力パーサー 12

では、2人分にしてみましょう。template.format(number="2")とすると、残念ながらエラーとなってしまいます。

これはoutputが以下のようになったためです。

{
        "Last_Name": "Yamada",
        "First_Name": "Taro",
        "Age": 18,
        "Gender": "M"
},
{
        "Last_Name": "Suzuki",
        "First_Name": "Hanako",
        "Age": 17,
        "Gender": "F"
}

一見すると正しいJSONのように見えるかもしれませんが、辞書が2つ列挙されています。JSONでは、複数の要素を表現する場合、[]で囲んでリストの形式にする必要があります。少し紛らわしいので例をあげましょう。

以下は正しいJSONではありません。

{"age":3},{"age":5}

以下は正しいJSONです。

[{"age":3},{"age":5}]

細かい違いのように感じられるかもしれませんが、プログラムにとっては大きな違いです。このように多少複雑な形式のデータを表現するのであれば、次に説明するPydanticOutputParserクラスが適しています。

Object (PydanticOutputParser)

https://python.langchain.com/docs/modules/model_io/output_parsers/pydantic

入れ子になったデータ、辞書やリストを組み合わせたデータなど、多少複雑なデータを出力したいときに便利なクラスです。先ほどエラーになった複数人のランダムデータをJSON形式で出力するサンプルです。

from typing import List

from langchain_openai import OpenAI
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from langchain.pydantic_v1 import BaseModel, Field

class Person(BaseModel):
    Last_Name: str = Field(description="氏")
    First_Name: str = Field(description="名")
    Age: int = Field(description="年齢")
    Gender: str = Field(description="性別をMかFで")

class Students(BaseModel):
    students: List[Person]

output_parser = PydanticOutputParser(pydantic_object=Students)

template = PromptTemplate(
    template="{number}人分の生徒名簿を作成してください。氏名は日本語とします。\n{format_instructions}\n",
    input_variables=["number"],
    partial_variables={"format_instructions": output_parser.get_format_instructions()}
)

llm = OpenAI(temperature=0)
prompt = template.format(number="2")
output = llm.invoke(prompt)
print(output)
response = output_parser.parse(output)
for s in response.students:
    print(f"氏:{s.First_Name} 名:{s.Last_Name} 年齢:{s.Age} 性別:{s.Gender}")

出力は以下のようになりました。

output6
LangChain入門 – 3)Output Parser 出力パーサー 13

正しいJSON形式となり、それをparseすることで、プログラムで扱いやすい形式(辞書のリスト)になっていることが分かります。

上記のサンプルでは、出力したいデータに関して、個人の情報をPersonで、それらをリストにしたものをStudentsというクラスで表現しています。PydanticOutputParserを作成するときに、どのような構造化データを出力したいかをpydantic_object引数で指定しています。あとの流れはLISTやJSONの時と同じです。

output_parserでparseした結果、responseが構造化データとして利用できていることに注目してください。
studentsプロパティで人のリストを取り出し、個々の要素にはFirst_Name, Last_Name, Age, Genderといったプロパティが利用できていることがわかります。

pydantic_v1という名前にあるように、近い将来モジュール名が変更されるかもしれません。このモジュールに限ったことではありませんが、頻繁な更新が行われるライブラリなのでご注意ください。

演習

output-parser-ex1.py

宝石の名前を3つ列挙してリスト形式にして以下のようにfor文を使って出力してください。

...
for i, g in enmuerate(gems):
    print(f"{i} = {g}")

以下のように出力されました。

0 = ダイヤモンド
1 = ルビー
2 = サファイア

output-parser-ex2.py

{'name': 'John', 'age': 25, 'hobby': 'Playing video games'}

のように名前、年齢、趣味を含むJSONを出力してください。

演習(解答例)

output-parser-ex1.py

from langchain_openai import OpenAI
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate

output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()
template = PromptTemplate(
    template="{item}をカンマで区切って{count}個列挙してください。カンマの後ろに空白を入れてください。\n{format_instructions}\n",
    input_variables=["item", "count"],
    partial_variables={"format_instructions": format_instructions}
)

llm = OpenAI()
r = llm.invoke(template.format(item="宝石", count="3"))
gems = output_parser.parse(r)
for i, g in enumerate(gems):
    print(f"{i} = {g}")

output-parser-ex2.py

from langchain.output_parsers import StructuredOutputParser, ResponseSchema
from langchain.prompts import PromptTemplate
response_schemas = [
    ResponseSchema(name="name", description="名"),
    ResponseSchema(name="age", description="年齢", type="int"),
    ResponseSchema(name="hobby", description="趣味"),
]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = output_parser.get_format_instructions()

template = PromptTemplate(
    template="名前、年齢、趣味を含むJSONを作成してください。\n{format_instructions}\n",
    input_variables=[],
    partial_variables={"format_instructions": format_instructions}
)

from langchain_openai import OpenAI

llm = OpenAI(temperature=0)
prompt = template.format()
output = llm.invoke(prompt)
print(output)

response = output_parser.parse(output)
print(response)
response = output_parser.parse(output)
print(response)