LangChainでWikipediaは扱いづらいかもしれないよ、という事例
はじめに
しばらく前になるのだが、LangChainのv0.0.107がリリースされたとき、toolにWikipediaが組み込まれたという話が出てきた。
ちょうどserpapiの月の無料枠が上限に達していたので、Wikipediaを利用してみようと思い、早速試してみた。しかし、これが予想以上に難しいもので難儀した。もしかすると、自分の使い方が悪いのかもしれないもしそうであれば、指摘いただけるとうれしい。
やってみたこと
これまでしつこく、Wikipediaのナポレオン戦争の項から、指定した情報を抽出して指定のJSON形式にまとめる、ということをあれこれやっている。
文章を提示し、必要な情報を抽出してJSON形式で出力することは、ほぼ実現できている。現在は出力に少しブレがあるものの、必要な情報をかなり抽出できるようになってきた。
検索を使って得た情報だけでは足りない情報があるかもしれない。そこで、次のアイデアとして、情報を補完するためにWikipediaを利用することにした。実は、この方法もすでにserpapiで試してみたのだが、試行回数が多すぎて、APIの利用可能な無料枠をほぼ使い切ってしまった。金額はそれほど高くないので、支払えば済む話なのだが、できれば少ない費用で済ませたいと思っている。
コード
エージェントの利用にあたってはこちらのnoteを参考にした。
いつもありがとうございます。
# 環境変数の準備
import os
os.environ["OPENAI_API_KEY"] = "INPUT_YOUR_OWN_API_KEY"
# langchain
from langchain.llms import OpenAI, OpenAIChat
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
import wikipedia
# エージェント
from langchain.agents import ZeroShotAgent, Tool, AgentExecutor, load_tools, initialize_agent
# wikipediaの言語を日本語にセット
wikipedia.set_lang('ja')
# 例示付きプロンプトの設定
# 指定のJSON形式を短くしてみる。特に複数出してほしいreralted_eventsとbattleをひとつにしたらどうなるか。プロンプトは短い方が良い。
# 同じ形式で複数出力してほしい場合は"$複数ある場合は同じ形式で繰り返す"を入れると良い
# prefixで基本指示およびツール利用が可能であることを指示する
prefix = """あなたは入力された文章をもとに、関連する情報を調査し拡張するAIです。調査した結果は日本語でまとめてください。
次のツールにアクセスすることができます。入力された文章からはわからない情報は検索してください。
"""
# suffixに出力形式の指定
suffix = """# 指定のJSON形式
---
(
"event":(
"name":"$本文のタイトル(入力された文章をもっともよく表すワードを選択)",
"start_year":$入力された出来事が発生した年を西暦で出力,
"end_year":$入力された出来事が終結した年を西暦で出力,
"related_persons":["$関与した人名を出力","$関与した人名を出力"],
"related_events":[
(
"name":"$関連する出来事を出力",
"summary":"$出力した関連する出来事の概要を50文字以内で出力"
),
$eventが複数ある場合は同じ形式で出力
],
"place":["$関与した地名を出力","$関与した地名を出力"],
"related_nations":["$関与した国名を出力","$関与した国名を出力"],
"battles":[
(
"name":"$発生した戦闘の名称を出力",
"start_date":"$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力",
"end_date":"$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力、日付が明示されない場合はstart_dateと同じ年月日を出力",
"result":"$フランス軍の勝敗を出力",
),
$battleが複数ある場合は同じ形式で出力
],
)
)
---
Human: {input}
AI:{agent_scratchpad}"""
llm = OpenAIChat(model_name="gpt-3.5-turbo",
temperature = 0.2)
# ツールの準備、個々ではwikipediaのみを入れている
tools = load_tools(['wikipedia'], llm = llm)
# プロンプトの設定
prompt = ZeroShotAgent.create_prompt(
tools,
prefix = prefix,
suffix = suffix,
input_variables = ['input', 'agent_scratchpad'],
)
llm_chain = LLMChain(
llm = llm,
verbose = True,
prompt = prompt
)
agent = ZeroShotAgent(llm_chain = llm_chain,
tools = tools,
return_intermediate_steps=True)
# toolの繰り返し利用が多くなると入力token数が最大値を超える可能性があるため、max_iteration=2としている
# また、toolの繰り返し利用上限に達した際に出力が行われるよう、early_stopping_method="generate"とする
agent_executor = AgentExecutor.from_agent_and_tools(agent = agent,
tools = tools,
verbose = True,
max_iterations=2,
early_stopping_method="generate")
以上までやったところで、agent_executorで処理を実行する。今回は第一次イタリア遠征のみの実験である。
output = agent_executor.run(input = """第一次イタリア遠征(第一次対仏大同盟)
1792年のフランス革命戦争の勃発により、1793年にイギリス、オーストリア、プロイセン、スペインなどによって第一次対仏大同盟が結成された。この戦いにおいてフランスの総裁政府は、ライン方面から2個軍、北イタリア方面から1個軍をもってオーストリアを包囲攻略する作戦を企図していた。
1796年3月、イタリア方面軍の司令官に任命されたナポレオン・ボナパルトは攻勢に出る。まず、これまで最前線でフランス軍と対峙してきたサルデーニャ王国をわずか1か月で降伏させ、オーストリア軍の拠点マントヴァを包囲した。オーストリア軍はマントヴァ解放を目指して反撃に出るが、ナポレオンの前にカスティリオーネの戦い(8月5日)、アルコレの戦い(11月15日-17日)、リヴォリの戦い(1797年1月14日)で敗北する。2月2日にマントヴァは開城。オーストリアは停戦を申し入れ、4月18日にレオーベンの和約が成立した。
10月17日、フランスとオーストリアはカンポ・フォルミオの和約を締結。フランスは南ネーデルラントとライン川左岸を併合し、北イタリアにはチザルピーナ共和国などのフランスの衛星国が成立した。オーストリアの脱落で第一次対仏大同盟は崩壊した。""")
そのとき何が起こったか?
途中出力を全部だすと長いので、一部抜粋しながら見ていこう。
> Entering new LLMChain chain...
Prompt after formatting:
あなたは入力された文章から必要な情報を抽出し、指定のJSON形式に整形するAIです。以下に示す形式に従い、結果を出力してください。
次のツールにアクセスすることができます。入力された文章からはわからない指定形式のJSONに必要な情報は検索してください。
Wikipedia: A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, historical events, or other subjects. Input should be a search query.
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Wikipedia]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
# 指定のJSON形式
---
(
"event":(
プロンプトで指定したように、prefixとsuffixの間にちゃんとtool利用に関する指示が明記されている。ここは問題ない。途中を飛ばして、toolが利用されるところを見る。
> Finished chain.
Thought: Let's extract information about the First Italian Campaign during the French Revolutionary Wars
Action: Wikipedia
Action Input: First Italian Campaign
まぁ、LangChainに収載されているプロンプトが基本的に英語なので、英語で問い合わせがかかることは仕方ない。どうしてもそれが嫌ならば、カスタムエージェントにするか、コードをforkするなりして日本語向けに書き換えるか、すると良いだろう。
さて、問題はここからである。
Observation: Page: バルカン戦線 (第一次世界大戦)
Summary: バルカン戦線(バルカンせんせん、英語:Balkans campaign / Balkans theatre、1914年7月28日〜1918年11月11日)は、第一次世界大戦(東部戦線)のバルカン半島をめぐる中央同盟国と連合国の戦闘。
バルカン半島では多様な民族対立があるのみならず、汎スラヴ主義と汎ゲルマン主義、イスラム教と正教会・カトリックなどといった様々な対立のために「ヨーロッパの火薬庫」と呼ばれたほど複雑な情勢下にあり、大戦の発端であるサラエボ事件の舞台ともなった。
Page: 中道右派
Summary: 中道右派(ちゅうどううは、英語: Centre-right)とは、政党あるいは政党グループの分類。穏健な右派・保守派のこと。
一般的には、オルド自由主義、自由保守主義、保守自由主義、保守中道主義、改革保守主義、緑の保守主義、キリスト教民主主義といった主義を掲げる政党に対して用いられる。アメリカの共和党やイギリスの保守党、ドイツのCDUが代表格である。
中道右派と特徴づけられるイデオロギーには、自由保守主義や自由主義のいくつかの変種、キリスト教民主主義などがある。現代の中道右派の経済的側面は経済的自由主義の影響を受けており、一般に自由市場、限られた歳出など新自由主義に大きく関連する政策を支持している。穏健右派は社会保守主義でも文化的自由主義でもなく、しばしば市民的自由の支持や伝統主義の要素と両方の信念を併せ持っている。
Page: レディオヘッドの作品
Summary: イギリスのロック・バンド、レディオヘッドの作品。
Thought:
バルカン戦線???
レディオヘッド???
なんでそうなる???
そこで、「この結果はおかしいよね」と認識して次のように進んでいる。このあたりはさすがに賢いと思った。
> Finished chain.
The Wikipedia search for "First Italian Campaign" did not provide relevant information. Let's try again.
Action: Wikipedia
Action Input: First Italian Campaign French Revolutionary Wars
しかし、ここで出てきたのは以下の結果であった。
Observation: Page: ネールウィンデンの戦い (1793年)
Summary: (長いので中略)
Page: パレスチナ独立戦争
Summary:(長いので中略)
Page: 人為的な要因による死者数一覧
Summary: (長いので中略)
ここでmax_iteration=2の上限に到達し処理は終了。
しかしながら、入力トークン数の最大値を超えてしまい、エラー終了した。
InvalidRequestError: This model's maximum context length is 4097 tokens. However, your messages resulted in 4290 tokens. Please reduce the length of the messages.
※もちろん、毎回この結果となるとは限らない。LLMも結局のところは確率的言語モデルなので、出力にぶれが生じるのは致し方ないのである。
調べてみた
もう一度、LangChain側の挙動を見てみよう。
Action: Wikipedia
Action Input: First Italian Campaign
つまり、日本語Wikipediaに対して"First Italian Campaign"と問い合わせをかけているわけだ。さて、Wikipedia側はどうなるだろうか?
このように、日本語Wikipediaでは結果がヒットしなかった。その代わりに出てきたのが「バルカン戦線」「中道左派」「レディオヘッドの作品」だったわけだ。
この時点で、関連する該当ページが見つからなかったため、LangChainは以下のように判断し、検索用語を変えてきている。
The Wikipedia search for "First Italian Campaign" did not provide relevant information. Let's try again.
や、改めて考えてみるとこの挙動はなかなかすごいな、と思う。おそらく「見つかりませんでした」を見て判断しているのだろうが、通常のスクレイピングだったらいちいちこの文言があることを明記しないと、この処理はしてくれないだろう。
次いでLangChainがとったアクションは以下のものだった。
Action: Wikipedia
Action Input: First Italian Campaign French Revolutionary Wars
"First Italian Campaign"でだめなら"French Revolutionary Wars"(フランス革命戦争)とつけてやれば良いんじゃね?と判断したわけだ。もちろんこれは入力文に明記している内容であるため、ある意味順当な動作なのだろうが、それにしてもなかなかやるではないか。
しかし、残念ながらこの検索結果もハズレだった。残念。
ここでmax_iterationsに達したために検索処理が終了している。
どうやったら改善できるか
おしむらくは、最後に入力トークン数の上限に達してエラー終了してしまったことである。どうやらLangChain側では適切にヒットしなかった結果の上位3つのsummaryは保持してしまうようだ。そのため、不要なトークンが蓄積されてしまい、最終的にはエラー終了してしまう。
ヒットしなかった検索結果を蓄積しないようにできれば良いのだが、良いやり方が思いつかなかった。
※GPT-4であれば最大トークン数が8kまたは32kになっているようなので試してみる価値はあると考えるが、いかんせん良いお値段がするので気軽に実験するのはためらわれる。
また、そもそもWikipediaに収載されていない語はどうあっても探すことができない。なので、より収載項目数の多い英語版Wikipediaを使うことも考えたが、今回はそこまで検証していない。
まとめ
LangChainのwikipedia機能を試してみた。ややクセがあるというか、実現したいことによっては正常に機能しない可能性がある。日本語Wikipediaの特性もある程度理解したうえで、適切なプロンプトを組む必要があるだろう。
扱いづらい点もあり、serpapiの代わりになるかと言われると、用途による、としか言いようがない。しかしながら、toolの選択肢が増えるのは良いことだろうと考える。
この記事が気に入ったらサポートをしてみませんか?