LangChain

【Python】LangChain: OutputParserで出力形式を制御する

【Python】LangChain OutputParserで出力形式を制御する

Pythonで大規模言語モデル(LLM: Large Language Model)を活用する際に使用できるLangChainでOutputParserを使用する方法を解説します。

LangChainのOutputParser機能

LangChainは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。LangChain自体の概要については「LangChainで大規模言語モデル(LLM)を活用する」で説明していますので参考にしてください。

Langchainは、LLMの複雑なタスクを実行するためのライブラリでワークフローを簡単に実現することが可能です。モデルが出力した結果を適切な形式に変換したい場合はOutputParserを使用することができます。

この記事では、OutputParserの基本的な使用方法について紹介します。

この記事で使用されているプログラムは以下のバージョンでの動作を確認済みです。生成AIの分野は進歩が速いため、API等の変更により最新バージョンではプログラムがそのままでは動作しない可能性もありますので注意してください。

  • Python: 3.11.5
  • langchain: 0.2.11
  • langchain-core: 0.2.26
  • langchain-community: 0.2.10
  • langchain-openai: 0.1.16

OutputParser機能

OutputParserは、モデルからの出力を適切な形式に変換する機能で、JSON、XML、CSV等の様々な種類のOutputParserが定義されています。公式ページのこちらで各種OutputParserの種類を確認することが可能です。

OutputParserは、モデルからの出力を構造化するために以下のような主要な機能を持っています。

  1. get_format_instructions: モデルの出力がどのようにフォーマットされるべきかの指令を含む文字列を返却する機能
  2. parse: 言語モデルからのレスポンスを受け取り、それを目的の構造に解析する機能

この記事では、いくつかの例を取り上げて使い方を紹介します。

StrOutputParser

モデルの出力から単一の文字列を出力する場合には、StrOutputParserクラスを使用できます。StrOutputParserは以下のように使用します。

以降の紹介で基本的なモデル実行に関する説明は省略します。「LangChainで大規模言語モデル(LLM)を活用する」を参考にしてください。

import configparser

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# パーサーを準備する
parser = StrOutputParser()

# コンフィグファイルからキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# モデルを生成する
llm = ChatOpenAI(
    openai_api_key=config["OPENAI"]["key"],
    model="gpt-3.5-turbo",
)
# 問い合わせを実行する
response = llm.invoke("Pythonの概要について教えてください。")

# パーサーを使用して文字列に変換する
result = parser.invoke(response)

# 結果を表示する
print(type(result))
print(result)
【実行結果】
<class 'str'>
Pythonは汎用プログラミング言語であり、高レベル言語として知られています。Pythonは直感的で読みやすい文法を持ち、初心者にも扱いやすいため、教育や研究、Web開発、データ分析、AIなどのさまざまな分野で広く利用されています。Pythonはオブジェクト指向プログラミングや関数型プログラミングの要素を持ち、豊富な標準ライブラリやサードパーティライブラリが利用できるため、効率的にプログラムを開発することができます。また、クロスプラットフォーム対応であり、Windows、Mac、Linuxなどさまざまな環境で動作します。

パーサーを使用する際には、langchain_core.output_parsersから対象のパーサーをインポートします。今回はStrOutputParserをインポートして「parser = StrOutputParser()」といったようにインスタンス化します。

OpenAIのAPIからの回答結果をパーサーのinvokeメソッドに渡すことで、文字列として回答結果を取得することが可能です。StrOutputParserは、モデルの出力結果を文字列(str)に変換する単純なパーサーなので、format_instructionsを考慮しなくても使用可能です。

JsonOutputParser

モデルの出力をJSON形式で取得したい場合には、JsonOutputParserクラスを使用できます。

JsonOutputParserの使用では、pydanticのクラスを継承したクラスを用いることで出力を整形します。pydanticは、データの型チェックとバリデーションを容易にするために設計されたモジュールです。

以下は、質問で指示された内容のプログラムコードを複数プログラミング言語で出力させるようなものになっています。以下例で、JsonOutputParserの使い方を見ていきましょう。

import configparser

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI

# コンフィグファイルからキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# モデルを初期化する
model = ChatOpenAI(
    openai_api_key=config["OPENAI"]["key"],
    model="gpt-4-turbo",
)


# ベースモデルを継承して出力用のクラスを生成する
class CodeGenResult(BaseModel):
    python: str = Field(description="Pythonでの結果")
    java: str = Field(description="Javaでの結果")


# JSONパーサーを生成する
parser = JsonOutputParser(pydantic_object=CodeGenResult)

prompt_template = ChatPromptTemplate(
    messages=[
        SystemMessagePromptTemplate.from_template(
            "与えられた指示を以下のプログラムで実装してください。\n{format_instructions}\n"
        ),
        HumanMessagePromptTemplate.from_template("{query}"),
    ],
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)
print("===== format_instructions")
print(parser.get_format_instructions())

# プロンプトに指示を与える
prompt = prompt_template.format_prompt(query="HelloWorldを標準出力に出力する。")
print("===== プロンプト")
print(prompt)

# モデルに問い合わせを実行
output = model.invoke(prompt.to_messages())
print("===== モデルからの回答")
print(output)

# パーサーに通す
result = parser.invoke(output)
print("===== Jsonパーサー適用後の回答")
print(result)
【実行結果】
===== 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:
```
{"properties": {"python": {"title": "Python", "description": "Python\u3067\u306e\u7d50\u679c", "type": "string"}, "java": {"title": "Java", "description": "Java\u3067\u306e\u7d50\u679c", "type": "string"}}, "required": ["python", "java"]}
```
===== プロンプト
messages=[SystemMessage(content='与えられた指示を以下のプログラムで実装してください。\nThe output should be formatted as a JSON instance that conforms to the JSON schema below.\n\nAs an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}\nthe object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.\n\nHere is the output schema:\n```\n{"properties": {"python": {"title": "Python", "description": "Python\\u3067\\u306e\\u7d50\\u679c", "type": "string"}, "java": {"title": "Java", "description": "Java\\u3067\\u306e\\u7d50\\u679c", "type": "string"}}, "required": ["python", "java"]}\n```\n'), HumanMessage(content='HelloWorldを標準出力に出力する。')]
===== モデルからの回答
content='```json\n{\n  "python": "print(\'HelloWorld\')",\n  "java": "System.out.println(\\"HelloWorld\\");"\n}\n```' response_metadata={'token_usage': {'completion_tokens': 31, 'prompt_tokens': 247, 'total_tokens': 278}, 'model_name': 'gpt-4-turbo-2024-04-09', 'system_fingerprint': 'fp_0993c4a4c0', 'finish_reason': 'stop', 'logprobs': None} id='run-def3b3cf-5607-4511-8c78-ff29a0a4d31c-0' usage_metadata={'input_tokens': 247, 'output_tokens': 31, 'total_tokens': 278}
===== Jsonパーサー適用後の回答
{'python': "print('HelloWorld')", 'java': 'System.out.println("HelloWorld");'}
プログラムの詳細説明

まずは、Pythonのクラスを作成します。langchain_core.pydantic_v1からBaseModelFieldをインポートします。

# ベースモデルを継承して出力用のクラスを生成する
class CodeGenResult(BaseModel):
    python: str = Field(description="Pythonでの結果")
    java: str = Field(description="Javaでの結果")

上記のようにBaseModelクラスを継承したクラスCodeGenResultを定義し、クラス内でFieldを用いた説明をdescriptionで記載します。上記の例では「Pythonでの結果」と「Javaの結果」という異なる言語での結果を入れる場所として定義しています。

# JSONパーサーを生成する
parser = JsonOutputParser(pydantic_object=CodeGenResult)

JSONパーサーを生成する際には、JsonOutputParserpydantic_object引数に先ほど作ったクラスを指定します。

prompt_template = ChatPromptTemplate(
    messages=[
        SystemMessagePromptTemplate.from_template(
            "与えられた指示を以下のプログラムで実装してください。\n{format_instructions}\n"
        ),
        HumanMessagePromptTemplate.from_template("{query}"),
    ],
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)
print("===== format_instructions")
print(parser.get_format_instructions())

上記部分は、ChatPromptTemplateを使ってモデルに渡すプロンプトを生成しています。重要なポイントはSystemMessagePromptTemplateにて、{format_instructions}という指定をしている部分です。これはモデルにこの形式で出力してほしいことを指示しています。ここに埋め込む内容はpartial_variablesで設定しており、JsonOutputParserget_format_instructionsメソッドが返却する文字列をセットしています。なお、HumanMessagePromptTemplateの方は質問内容をそのまま{query}に渡すだけのようなものになっています。

PromptTemplateの使い方の基本については「LangChain: PromptTemplateで柔軟にプロンプトを定義する」でまとめていますので参考にしてください。

具体的にget_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:
```
{"properties": {"python": {"title": "Python", "description": "Python\u3067\u306e\u7d50\u679c", "type": "string"}, "java": {"title": "Java", "description": "Java\u3067\u306e\u7d50\u679c", "type": "string"}}, "required": ["python", "java"]}
```

出力はJSONである必要があることや、JSONスキーマの例、求める出力のスキーマがどういった形式かが記載されています。この出力スキーマの内容は、先ほど作成したCodeGenResultクラスに基づいていることが分かるかと思います。この指示が、テンプレートを通じて質問に反映されるわけです。

# プロンプトに指示を与える
prompt = prompt_template.format_prompt(query="HelloWorldを標準出力に出力する。")
print("===== プロンプト")
print(prompt)
【実行結果の一部抜粋】
===== プロンプト
messages=[SystemMessage(content='与えられた指示を以下のプログラムで実装してください。\nThe output should be formatted as a JSON instance that conforms to the JSON schema below.\n\nAs an example, for the schema
...(途中省略)...
{"properties": {"python": {"title": "Python", "description": "Python\\u3067\\u306e\\u7d50\\u679c", "type": "string"}, "java": {"title": "Java", "description": "Java\\u3067\\u306e\\u7d50\\u679c", "type": "string"}}, "required": ["python", "java"]}\n```\n'), HumanMessage(content='HelloWorldを標準出力に出力する。')]

上記はテンプレートに具体的に質問を埋め込んでいる部分です。今回は「HelloWorldを標準出力に出力する」という非常に簡単なプログラム作成指示をquery部分埋め込んでいます。具体的にどういったプロンプトになっているかをprintで見てみると先ほど確認したget_format_instructionsの内容もプロンプトに含まれていることが分かるかと思います。

その後、モデルにプロンプトを渡して結果にパーサーを通しているのが以下の部分になります。

# モデルに問い合わせを実行
output = model.invoke(prompt.to_messages())
print("===== モデルからの回答")
print(output)

# パーサーに通す
result = parser.invoke(output)
print("===== Jsonパーサー適用後の回答")
print(result)

パーサーを適用するのは簡単で、モデルからの回答に対してパーサーのinvokeメソッドを使用するだけです。

===== Jsonパーサー適用後の回答
{'python': "print('HelloWorld')", 'java': 'System.out.println("HelloWorld");'}

上記最終的な整形結果を見るとHelloWorldを出力するためのPythonとJavaのコードがJSON形式で整理されていることが分かるかと思います。

もし、対象プログラミング言語を増やしたかったらCodeGenResultクラスのFieldを増やしてもらえばよいですし、指示の内容を変えたければテンプレートに埋め込む内容を変えればいいだけです。このように柔軟なプログラムを作ることが可能です。もちろん、クラスの構造を工夫してもらえれば様々なケースで適用できることが分かるかと思います。

以上のようにOutputParserを使用することで出力形式を目的の形に整形することが可能です。その他のOutputParserについても公式ページのこちらで確認することができます。各パーサーのページには使用例も載っていますので是非確認してみてください。

まとめ

Pythonで大規模言語モデル(LLM: Large Language Model)を活用する際に使用できるLangChainでOutputParserを使用する方法を解説しました。

OutputParserを使用することにより、モデルからの出力を利用者が求める適切な形式に変換することが可能になります。この記事では、StrOutputParserという非常に単純な例と、JsonOutputParserを使った例で使い方の紹介をしました。

LangChainには、他にも様々な機能があります。皆さんもLangChainの使い方をぜひ覚えていってもらえるといいと思います。