ChatGPT PlugIn が発表され、その実体が見えてきて「そもそも自分たちで API を叩く必要なんてないんじゃないか……?」と思われてきているんじゃないかと思います。私もそう思います。
とはいえ、日本のローカルな(業務用の) Web API などは、まだまだ Zapier などにも対応していない以上、直接 API を叩く機会はあるのではないかと思います。その他、自社ツールなどですね(広く一般に提供されるサービスでなければ、ChatGPT Plugin とする利点がまだまだ薄そうに思います、費用体系によりますが)。
さてそういう訳で独自ツールの活用に LLM を使用する場合、既存の非 AI 的なツールと LLM の通信手段・データのやりとりです。 LLM 自身は Prompt でフォーマットを指示してやれば大体のことを上手いことやってくれますが、既存システム側はそうはいきません。
以前やった、 SQLite の発行はまさにその一例ですね。
今回は、 LangChain の Output Parser を使って LLM の出力を汎用性の高い JSON にしてみます。
参考:https://langchain.readthedocs.io/en/latest/modules/prompts/examples/output_parsers.html
- ChatGPT プラグインに対応してくれないニッチな WebAPI との通信
- ローカルアプリケーションへのデータの受け渡し
- RPA や マクロ(スクリプト)へのデータの受け渡し
※個人的には 3番目が向こう 2, 3 年くらいは需要があるんじゃないかなと思っています。
LangChain の Output Parser
LangChain の Ouput parser では、オブジェクトの型定義のため、typing と pydantic を用います。
py -m pip install -U typing pydantic
Pydantic は、本来型に対して寛大(緩い)言語である Python に、型推論を実現するものです。
同じく、型に対して厳密でなかった JavaScript も型を指定できるように拡張されていったように、大規模に用いられるようになると型に対して厳密でないというのは、 IDE の型推論を利用したコードアシストが使えずに逆に不便な面があるからと考えられます(実行速度の面でも不利ですが、pydantic で改善するかは分からないです)。
copilot のような言語モデルによるコードアシストで、この流れも変化するかもしれません。
LangChainのPydantiOutputParserの基本
諸々のimport
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
# env に読み込ませるAPIキーの類
import key
# 環境変数にAPIキーを設定
import os
os.environ["OPENAI_API_KEY"] = key.OPEN_API_KEY
llm = ChatOpenAI(temperature=0)
JSON の構造の元になる class の定義(Pydantic利用)
class Joke(BaseModel):
setup: str = Field(description="question to set up a joke")
punchline: str = Field(description="answer to resolve the joke")
# カスタムバリデーションを pydantic を使って追加可能
def question_ends_with_question_mark(cls, field):
if field[-1] != '?' and field[-1] != '?':
raise ValueError("Badly formed question!")
return field
pydantic を利用して、型と、description を有したメンバを持つクラスを定義します(description が LLM が内容を把握する手助けとして用いられていると思われます)。
また、カスタムバリデーションを作成することもできます。ここでは、「問いかけ」であることを判定するために、末尾が?または?で終わることを要求しています。
実行する
# 言語モデルにデータ構造を埋めるように促す Query
joke_query = "Tell me joke."
# Parser に元になるデータの型を提供する
parser = PydanticOutputParser(pydantic_object=Joke)
# input_variables ではなく、 partial_variables に parser からオブジェクトの説明を入力する
prompt = PromptTemplate(
template="Answer the user query. \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 = llm(_input.to_messages())
print(parser.parse(output.content))
まずは、parser として Pydantic OutputParserのコンストラクタに 先ほど定義したクラスを pydantic_object として与えて実体化します。先ほどのクラス情報を元に JSON のパースを行います。
続いて、 PromptTemplate の作成ですが、ユーザーからの入力である query の他に、dict型(input_variables と異なり、キーだけではなく値も入れるため)のpartial_variablesに、format_instructionsを設定します。値は、get_fomat_instructions メソッドで取得した LLM に対するオブジェクトの取り扱いに関する説明です。人間向けにコメントからドキュメントを自動生成するようなものですね。
ここまで準備ができたら、prompt と query から message を作成します。ChatOpenAI では、メソッド名やプロパティが LangChain のドキュメントのものと異なることに気をつけてください(Chat型のため)。
※多分、サラダが着替え( dressing )してるところを見たからトマトが赤くなったんだぜ、というジョーク
もうちょっと複雑な構造
class Actor(BaseModel):
name: str = Field(description="name of an actor")
film_names: List[str] = Field(description="list of names of films they starred in")
actor_query = "Generate the filmography for a random actor."
parser = PydanticOutputParser(pydantic_object=Actor)
prompt = PromptTemplate(
template="Answer the user query.\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 = llm(_input.to_messages())
print(parser.parse(output.content))
クラスの中に List 構造が含まれる少し複雑な構造のオブジェクトです。
LLM による文法エラーの修正
基本的に ChatGPT 以降の LLM は充分に高性能なので、 LangChain が Pydantic から生成する情報で問題なく JSON をフォーマットしてくれるはずです。しかし、それでもフォーマットエラーが出た場合、これを LLM に修正させる方法も用意されています。
OutputFixingParser
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"
parser = PydanticOutputParser(pydantic_object=Actor)
# parser.parse(misformatted)
from langchain.output_parsers import OutputFixingParser
new_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
print(new_parser.parse(misformatted))
ぱっと見、どこが違うのか混乱しますが、JSON なので dictのkey と値の区切りは = となります。
修正には、parse を行う PydanticOutputParser と、修正に使用する LLM インスタンスを、 OutputFixingParserのfrom_llm メソッドに与えて新しい parser を作成します。
Prompt から修正を行う RetryWithErrorOutputParser
明らかな文法エラーなど、出力を見るだけでエラーを修正できる場合もありますが、部分的にあっている場合など、それだけでは修正できない場合があります。
そういった場合には、出力だけではなく、Prompt も用いる RetryWithErrorOutputParser を使います。これは、名前の通り修正するだけではなく、 Promptについても Retry する? ようです。
prompt_value = prompt.format_prompt(query="who is leo di caprios gf?")
bad_response = '{"action": "search"}'
#parser.parse(bad_response)
from langchain.output_parsers import RetryWithErrorOutputParser
retry_parser = RetryWithErrorOutputParser.from_llm(parser=parser, llm=ChatOpenAI())
print(retry_parser.parse_with_prompt(bad_response, prompt_value=prompt_value))
まとめ
- LangChain 経由で LLM に整形されたデータを出力させるには、LangChain だけでなく、 Pydantic も使う
- 型とdescriptionを利用し、それを元に LLM 向けの説明文を生成させる(日本語の取り扱いに注意)
- 通常のアプリケーションと異なり、LLM が出力・整形するため、フォーマットエラーが発生し得る
- 通常のエラーはOutputFixingParserで修正できる
- 文法はぱっと見あっているが何かがおかしい…不完全……のような場合には、promptも含めて RetryWithErrorOutputParser を使う
- 既存システムに LLM の処理能力を適用する手段だが、ChatGPT pluginが普及してくると限定的になるかも……?