LLMアプリケーション開発のためのLangChain 中編④ Output parsers

LangChainは、大規模な言語モデルを使用したアプリケーションの作成を簡素化するためのフレームワークです。言語モデル統合フレームワークとして、LangChainの使用ケースは、文書の分析や要約、チャットボット、コード分析を含む、言語モデルの一般的な用途と大いに重なっています。

LangChainは、PythonとJavaScriptの2つのプログラミング言語に対応しています。LangChainを使って作られているアプリケーションには、AutoGPT、LaMDA、CodeAnalyzerなどがあります。
- AutoGPTは、文章生成、翻訳、コード生成などの機能を持つアプリケーションです。
- LaMDAは、対話や文章生成を行うチャットボットです。
- CodeAnalyzerは、コードを分析するアプリケーションです。

# 記事概要

この記事では、LangChainでの出力結果の解析について紹介します。LangChainのオフィシャルサイトは以下の通りです。

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

Output parsersは以下の右側Parseの部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177240/a879a87e-c9aa-eea9-fca4-c3931c98e866.png)

言語モデルはテキストを出力しますが、より構造化された情報も必要な場合があります。出力パーサーはこれを支援するクラスで、
- モデルの出力のフォーマット方法を指示するメソッドと、
- 出力を構造に解析するメソッドを持っています。

さらに、出力を調整するためにプロンプトから情報を使用するオプションのメソッドもあります。


# はじめに

このコードの主要な目的は、OpenAIからの事前学習済みの言語モデルを使用して、ジョークを生成し、それを検証することです。

```python 
# 必要なモジュールとクラスをインポートする
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List

# モデルの名前と温度（モデルのランダム性に影響）を定義
model_name = 'text-davinci-003'
temperature = 0.0

# OpenAIモデルを初期化する
model = OpenAI(model_name=model_name, temperature=temperature)

# 希望するデータ構造を定義する。ここではジョークの構造で、セットアップとポイントが含まれる
class Joke(BaseModel):
    setup: str = Field(description="ジョークのセットアップ部分")  # ジョークをセットアップするための質問
    punchline: str = Field(description="ジョークを解決する答え")  # ジョークのポイント部分

    # Pydanticを使用してカスタムの検証ロジックを追加し、セットアップ部分が疑問符で終わることを確認する
    @validator('setup')
    def question_ends_with_question_mark(cls, field):
        if field[-1] != '？':
            raise ValueError("質問の形式が正しくありません！")
        return field

# パーサーを設定し、指示をプロンプトテンプレートに注入する
parser = PydanticOutputParser(pydantic_object=Joke)

# プロンプトテンプレートを定義する
prompt = PromptTemplate(
    template="ユーザーのクエリに答えてください。\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

# 言語モデルに上記のデータ構造を埋め込むように指示するクエリを定義する
joke_query = "日本語でのジョークを教えてください。"

# プロンプトをフォーマットする
_input = prompt.format_prompt(query=joke_query)

# モデルを使用して出力を生成する
output = model(_input.to_string())

# パーサーを使用して出力を解析する
parser.parse(output)

```

```python 
Joke(setup='なぜインド人は象を恐れているの？', punchline='なぜなら、象はインド人を見ると「おお、インド人だ！」と叫んでしまうからだ！')
```

# List parser

カンマ区切りを使ったリストがほしい時に、List Parserを利用します。

```python 
# 必要なモジュールとクラスをインポートする
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI

# カンマで区切られたリストを解析するパーサーを初期化する
output_parser = CommaSeparatedListOutputParser()

# パーサーのフォーマット命令を取得する
format_instructions = output_parser.get_format_instructions()

# 特定のトピックに関するカンマで区切られたリストを生成するためのプロンプトテンプレートを定義する
prompt = PromptTemplate(
    template="5つの{subject}をリストしてください。\n{format_instructions}",
    input_variables=["subject"],
    partial_variables={"format_instructions": format_instructions}
)

# OpenAIモデルを初期化する。温度は0に設定（生成される答えはより確定的で、ランダム性が少ない）
model = OpenAI(temperature=0)

# プロンプトをフォーマットする。ここではトピックは「アイスクリームのフレーバー」（冰淇淋口味）
_input = prompt.format(subject="アイスクリームのフレーバー")

# モデルを使用して出力を生成する
output = model(_input)

# パーサーを使用して出力を解析する
output_parser.parse(output)
```

出力結果：
```python 

['チョコレート', 'バニラ', 'ストロベリー', 'ピスタチオ', 'マンゴー']
```

# Datetime parser

```python 
# 必要なモジュールとクラスをインポートする
from langchain.prompts import PromptTemplate
from langchain.output_parsers import DatetimeOutputParser
from langchain.chains import LLMChain
from langchain.llms import OpenAI

# 日付時刻出力パーサーを初期化する
output_parser = DatetimeOutputParser()

# モデルにユーザーの質問に答えるよう指示するプロンプトテンプレートを定義する
template = """ユーザーの質問に答えてください:

{question}

{format_instructions}"""

# テンプレートを使用してプロンプトインスタンスを作成する
prompt = PromptTemplate.from_template(
    template,
    partial_variables={"format_instructions": output_parser.get_format_instructions()},
)

# プロンプトとOpenAIモデルを組み合わせて出力を生成するLLMChainを初期化する
chain = LLMChain(prompt=prompt, llm=OpenAI())

# 特定の質問に関する答えを取得するためにチェーンを実行する。ここでの質問は「ビットコインはいつ設立されましたか？」
output = chain.run("ビットコインはいつ設立されましたか？英語の形式で時間を出力してください")
```

```python 
output
```
出力結果が：`'\n\n2008-01-03T18:15:05.000000Z'`

```python 
output_parser.parse(output)
```
出力結果が：`datetime.datetime(2008, 1, 3, 18, 15, 5)`


上記のリストや時間のParser以外に、Enum parserもあります。使い方が似ていますので、省略します。

# Auto-fixing parser

この出力パーサは、別のパーサをラップし、エラーが発生した場合にはLLMを使って修正します。エラーだけでなく、不正確な出力をモデルに渡し、修正することもできます。例として、Pydantic出力パーサが示されています。

```python 
# 必要なライブラリとモジュールをインポート
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List

# 俳優を表すデータ構造を定義。名前と出演した映画のリストを含む
class Actor(BaseModel):
    name: str = Field(description="俳優の名前")                   # 俳優の名前
    film_names: List[str] = Field(description="出演した映画の名前のリスト")  # 出演した映画のリスト

# ランダムな俳優のフィルモグラフィを生成するためのクエリを定義
actor_query = "ランダムな俳優のフィルモグラフィを生成してください。"

# `Actor`モデルを使用してパーサーを初期化
parser = PydanticOutputParser(pydantic_object=Actor)

# 間違ったフォーマットの文字列データを定義
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"

# 上記のデータを解析するためにパーサーを使用
parser.parse(misformatted)
```

上記のParseですと、エラーが発生します。
misformatted文字列を正しくフォーマットするには、それが有効なJSON文字列であることを確認する必要があります。これは、キーと値を囲むために`シングルクォート（'）`の代わりに`ダブルクォート（"）`を使用する必要があることを意味します。以下は修正後のものです：

```python 
parser.parse('{"name": "Tom Hanks", "film_names": ["Forrest Gump"]}')
```

出力結果：
```python 
Actor(name='Tom Hanks', film_names=['Forrest Gump'])
```

もし`Auto-fixing parser`を利用すると、misformatted文字列でも解析することができます。
```python 
from langchain.output_parsers import OutputFixingParser

new_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
new_parser.parse(misformatted)
```

出力結果：
```python 
Actor(name='Tom Hanks', film_names=['Forrest Gump'])
```


# Pydantic (JSON) parser

この出力パーサーは、ユーザーが特定のJSONスキーマに基づいた出力をLLMに問い合わせることができます。大規模な言語モデルは完璧ではないため、整形されたJSONを生成するにはDaVinciのような高性能なモデルが必要です。Pydanticを使用してデータモデルを宣言できます。

```python 

# 必要なモジュールとクラスをインポート
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List

# モデルの名前と温度（モデルのランダム性に影響）を定義
model_name = 'text-davinci-003'
temperature = 0.0

# OpenAIモデルを初期化
model = OpenAI(model_name=model_name, temperature=temperature)

# 望むデータ構造を定義。ここではジョークの構造で、セットアップとパンチラインを含む
class Joke(BaseModel):
    setup: str = Field(description="ジョークのセットアップ部分")  # ジョークのセットアップ
    punchline: str = Field(description="ジョークのポイント")  # ジョークのパンチライン

    # Pydanticを使用してカスタムの検証ロジックを追加。セットアップが疑問符で終わることを確認
    @validator('setup')
    def question_ends_with_question_mark(cls, field):
        if field[-1] != '?':
            raise ValueError("質問の形式が不正です！")
        return field

# 言語モデルに上記のデータ構造を埋め込むように指示するクエリを定義
joke_query = "ジョークを教えてください。"

# パーサーを設定し、指示をプロンプトテンプレートに注入
parser = PydanticOutputParser(pydantic_object=Joke)

# プロンプトテンプレートを定義
prompt = PromptTemplate(
    template="ユーザーのクエリに答えてください。\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

# プロンプトをフォーマット
_input = prompt.format_prompt(query=joke_query)

# モデルを使用して出力を生成
output = model(_input.to_string())

# パーサーで出力を解析
parser.parse(output)

```

出力結果：
```python 

Joke(setup='Why did the chicken cross the road?', punchline='To get to the other side!')
```

もう一つの例を見てみます：

```python 
# 必要なモジュールとクラスをインポート
from pydantic import BaseModel, Field
from typing import List

# 俳優とその出演映画のリストを表すデータ構造を定義
class Actor(BaseModel):
    name: str = Field(description="俳優の名前")                   # 俳優の名前
    film_names: List[str] = Field(description="出演した映画の名前のリスト")  # 出演した映画のリスト

# ランダムな俳優のフィルモグラフィを生成するためのクエリを定義
actor_query = "ランダムな俳優のフィルモグラフィを生成してください。"

# `Actor`モデルを使用してパーサーを初期化
parser = PydanticOutputParser(pydantic_object=Actor)

# ユーザーのクエリに答えるようモデルを指示するプロンプトテンプレートを定義
prompt = PromptTemplate(
    template="ユーザーのクエリに答えてください。\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

# プロンプトをフォーマット
_input = prompt.format_prompt(query=actor_query)

# モデルを使用して出力を生成
output = model(_input.to_string())

# パーサーで出力を解析
parser.parse(output)

```

出力結果：
```python 
Actor(name='John Doe', film_names=['The Matrix', 'The Godfather', 'Star Wars'])
```

# 最後に

Output parsersには、更に`Retry parser`や`Structured output parser`、`XML parser`などがあります。詳細はオフィシャルサイトを参照してください。