本記事では私が考えているマルチレイヤーLLMのメリットと、それに活用できるようLangChainにコミットしたメモリ共有機能「ReadOnlySharedMemory」について紹介します!
※LLM MeetupでのLT資料
マルチレイヤーLLM
マルチレイヤーLLMとは?
本記事では、LLMによるエージェントを複数層に渡って配置し、問題解決に当たらせることをマルチレイヤーLLMと呼びます(既に名前があったら恥ずかしいですが教えてください)。具体イメージとして、大学のすべての科目に対する適切な質問回答が可能な「大学生アシスタントAI」を記載します。
この構成では、エージェントとエキスパートにそれぞれ独自のプロンプトを持たせ、ツリー状組織にし、以下のようにタスクを分担します。
- 大学生アシスタントAI: 質問が「理系か、文系か、事務か」だけを考える
- 理系エージェント: 質問が(理系のうち)どの分野の話なのかだけを考える
- 文系エージェント: 質問が(文系のうち)どの分野の話なのかだけを考える
- 事務エージェント: 質問が(事務のうち)どの分野の話なのかだけを考える
- 分野エキスパート: その分野の専門家情報をプロンプトとして保持した上で、質問への回答を考える(例: 法学なら六法全書をプロンプトに持つ、など)
なにが嬉しいのか?
一番重要なポイントは、マルチレイヤー化により一つ一つのLLMが処理するタスクを単純化していることです。これをどんどんと結合していけば、万能のAIアシスタントを作り出すことも夢物語ではないと考えています。この構造では、LLMが持つ以下の弱点を緩和できると考えています。
- コンテキスト窓制約
- 推論コスト
- 出力の解釈性
1. コンテキスト窓制約
GPTなどのLLMには、コンテキスト窓と呼ばれる最大入出力量の制約が存在し、含めることのできる情報量は制限されています。マルチレイヤーを作り上げることで、各エージェントが解くべきタスクに合わせてコンテキストを絞ることができます。
「コンテキストとは?」という疑問に対しては以下の記事がお勧めです。
また、これは完全に主観ですが、一つのプロンプトに多くの機能を持たせ過ぎると単機能の場合よりも専門家力が低下する傾向にあると感じています。これについては以下の要因があるのではないかと考えています。
- コンテキスト窓の制約により、few-shotの例文量が犠牲になっている
- 人間のスペシャリストの方がジェネラリストよりも深い回答が可能であることを模倣している
各エージェントを特定の機能に特化させることができるため、これらの要因についても対処が可能になっています。
2. 推論コスト
高性能なgpt-4はgpt-3.5-turboと比較して約15倍のコストがかかります。マルチレイヤーにより、各エージェントの役割分担を作り上げると、簡単なタスクには安価なモデルを使い、難易度の高いタスクに対しては高度なモデルを使う、といった構造にすることで、推論コストを抑えることができます。
また、単純にコンテキスト量の問題から言っても、適切にマルチレイヤーを設定できていれば、入力するコンテキストを抑えることが可能です。GPTの場合は、入出力量に比例してコストが決定されるため、一回あたりの推論コストを低減できると考えられます。
3. 出力の解釈性
LLMを単体で用いる場合、一つ一つの入力に対してブラックボックスのように振る舞い、出力を返します。マルチレイヤーでは各エージェントが特定の機能や知識に特化しており、それぞれが単純で明確な役割を果たします。
そのため、以下の点でモデルの解釈性が向上します。
- 個別のエージェントの分析: 各エージェントの役割が明確であるため、個別のエージェント動作の分析・理解が容易になります。これにより、問題が発生した場合に特定のエージェントに焦点を当てて修正や改善が行えるようになります。
- エージェント間連携の可視化: マルチレイヤーでは、エージェント間の情報のやり取りが明確になるため、システム全体の処理フローを追跡できます。これにより、どのエージェントがどのタスクに対してどのような貢献をしているのかを把握しやすくなり、全体の動作を理解しやすくなります。
複数タスクを一つのプロンプトから実行させる場合、「Aタスクに対して調整したらBタスクへの対応力が下がってしまった。。」ということが起こりがちですが、マルチレイヤーで役割を分割しておけば、それぞれの対応力が上がるように考えていくだけで、全体としての機能向上を図ることが可能になります。
LangChainによるLLMのマルチレイヤー
それでは、ここまでお話ししてきたマルチレイヤーLLMを実際に構築するための機能として、私がコミットした機能を簡単に紹介します。
LangChainの持っていた問題
Langchainでは、人間とエージェントの会話を記録する仕組みとしてMemory機能が用意されています。この機能は非常に便利で、ほとんど頭を使わずに会話の流れを意識したエージェントが作れます。また、エージェントやエキスパートを簡単にToolとして扱うことができるため、上で説明したマルチレイヤーを非常に簡単に作り上げることが可能です。
ただし、一点だけ問題があり、マルチレイヤーを作って、後段のLLMにもその会話情報を見てもらうためにMemoryを渡すと、後段のLLMの出力が人間の発言としてMemoryに書き込まれてしまうという事象が発生していました。この問題を解消するためにプルリクエストを出してみたところ、マージ頂けたので、簡単に紹介します。
ReadOnlySharedMemoryとは?
ほぼ公式側にあげたexampleのままですが、以下に利用例を示します。この例では二段目のサマリーツールがReadOnlySharedMemoryを介してMemoryにアクセスしています。これにより、意図しないMemoryの変更を防ぐことができ、安全な共有が実現できます。
(利用例におけるエージェント構成)
from langchain.agents import ZeroShotAgent, Tool, AgentExecutor
from langchain.memory import ConversationBufferMemory, ReadOnlySharedMemory
from langchain import OpenAI, LLMChain, PromptTemplate
from langchain.utilities import GoogleSearchAPIWrapper
# プロンプトテンプレートを作成
template = """This is a conversation between a human and a bot:
{chat_history}
Write a summary of the conversation for {input}:
"""
prompt = PromptTemplate(
input_variables=["input", "chat_history"],
template=template
)
# Memoryオブジェクトを作成
memory = ConversationBufferMemory(memory_key="chat_history")
# 読み取り専用のメモリオブジェクトを作成
readonlymemory = ReadOnlySharedMemory(memory=memory)
# サマリー用のLLMChainを作成
# ツールがMemoryを変更しないように、ReadOnlySharedMemoryを使用
summry_chain = LLMChain(
llm=OpenAI(),
prompt=prompt,
verbose=True,
memory=readonlymemory,
)
# ツールを作成
search = GoogleSearchAPIWrapper()
tools = [
Tool(
name = "Search",
func=search.run,
description="useful for when you need to answer questions about current events"
),
Tool(
name = "Summary",
func=summry_chain.run,
description="useful for when you summarize a conversation. The input to this tool should be a string, representing who will read this summary."
)
]
# エージェント用のプロンプトを作成
prefix = """Have a conversation with a human, answering the following questions as best you can. You have access to the following tools:"""
suffix = """Begin!"
{chat_history}
Question: {input}
{agent_scratchpad}"""
prompt = ZeroShotAgent.create_prompt(
tools,
prefix=prefix,
suffix=suffix,
input_variables=["input", "chat_history", "agent_scratchpad"]
)
llm_chain = LLMChain(llm=OpenAI(temperature=0), prompt=prompt)
# エージェントを作成
agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True)
# Memoryを含むAgentExecutorチェーンを作成
# エージェントはMemoryを編集して良いので、そのまま渡す
agent_chain = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True, memory=memory)
# あとは普通に実行するだけ
実行結果などは以下ノートブックからご確認ください。
なにが嬉しいのか?
メモリをラップして、ReadOnlyにして後段に渡すという工夫を入れることで、会話の文脈は読めつつも、Memoryの状態を破壊しない状況を実現しました。
(LangChain公式twitterでも解説されてるので、ぜひご覧下さい)
今後
LLMは、深く考えれば考えるほど、「人間ってどう働いているのか?」に近づいてくるのが本当に面白いと思います。
- ジェネラリストを育てるべきか、スペシャリストを育てるべきか
- パフォーマンス向上を図るためにはどのような報酬制度があるべきか
- 倫理や価値観のトレーニング、その効果の評価はどのようにすべきか
- 役割分担や組織体系はどのように構築するべきか
- 組織の意思決定において、どのように多様な意見や専門知識を活用するか
- 組織内外の情報伝達はどのようにすべきか
- 組織知を継続的に蓄積・更新し、活用するにはどのような仕組みがあるべきか
LLMの周辺技術はまさに日進月歩でついていくのもやっとではありますが、そこ以外にも人間に対する組織論や人材育成などの知識を得たり、そういった点に強い方々とのコラボレーションを行うことで、より未来に向けた思考・トライが可能になると思います。
巨人の肩に乗りつつも、皆さんとの切磋琢磨を通じて、自分自身の肩に乗る人を増やせるよう、頑張っていきたいと思います!引き続きよろしくお願いします。