見出し画像

【前編】LangChainによるGenerative Agents実装まとめ

先週、LLM(GPT-3.5)によって固有の性格や属性を持たせた25人のAIエージェントによる小規模社会シミュレーションの実現を試みたGenerative Agents論文が話題になりました。

本論文では、信憑性のある人間の行動をシミュレートするジェネラティブエージェントを紹介します。大規模言語モデルを拡張したアーキテクチャを用いて、エージェントの経験を自然言語で記録し、リフレクションにより統合し、それを行動計画に利用します。インタラクティブな環境でユーザーと自然言語で対話できるようにし、信憑性のある個々の行動と新たに現れる社会的行動を実現します。今後の研究では、生成エージェントのアーキテクチャをさらに改良し、人間の行動のシミュレーションをさらに向上させることが求められます。またこの技術は、教育、ゲーム、ビジネスコミュニケーションなど、さまざまな分野での応用が期待されています。

アブストラクトの要約

論文そのものの内容についてはteftefさんの以下の記事に詳しいため、あわせてご参考ください。

で、この論文内にあるリフレクションを踏まえた長時間記憶の実装のアイデアはかなり役に立ちそうだよね、というところで前回のLangChainもくもく会でも話題になっていたのですが、昨晩早速LangChainのhwchase17氏がLangChainを用いたGenerative Agentsの実装について連続ツイートで解説されていました。

hwchase17氏の解説がかなり分かりやすかったので、前編ではその和訳と補足、また新しく追加されたTimeWeightedVectorStoreRetrieverについて見て行き、後編ではこの実装の中心となるGenerativeAgentクラスの設計と活用について見ていきたいと思います。

hwchase17氏による解説(和訳)

先週、Parkらによって「Generative Agents」という論文が公開されました。この論文では、数十のエージェント間の相互作用をシミュレートしています。

私たちはこの論文を熟読し、紹介された新しいコンポーネントの1つ、長期的なリフレクションに基づくメモリシステムを実装しました。

https://twitter.com/hwchase17/status/1647987713449263106

まだ論文を読んでいない場合は、ぜひ読んでみてください。

Link: https://arxiv.org/abs/2304.03442

「私たちのエージェントアーキテクチャの構成要素である観察、計画、リフレクションがそれぞれ、エージェントの行動の信憑性に重要な寄与をしていることを、アブレーションによって示します」

https://twitter.com/hwchase17/status/1647987717576478725

このツイートを読んだときはアブレーションの意味が分からなかったのですが、ここで言っている「アブレーション」とはアブレーション研究、またはアブレーション分析という研究方法を指しているようです。

アブレーション研究とはシステムの各機能またはコンポーネントを一時的に削除し、その削除がシステム全体のパフォーマンスにどのような影響を与えるかを調べる方法で、Generative Agents論文ではその手法で観察・計画・リフレクションが重要な寄与をしていることを示すと言っています。

新しいコンポーネントの1つは、「生成エージェントが記憶し、取り出し、リフレクションし、他のエージェントと相互作用することができるアーキテクチャ」であり、これを再現しようと試みました。

Notebook here: https://python.langchain.com/en/latest/use_cases/agents/characters.html

https://twitter.com/hwchase17/status/1647987718977368064

下の図に示すように、多くの部分があります。特筆すべきことは2つあります:
・リフレクションステップ
・リトリーバルステップ

リフレクションはエージェントのメモリストリームに寄与し、その後取り出されて行動に使用されます。

https://twitter.com/hwchase17/status/1647987720332140544

まず、リトリーバルについて話しましょう。過去数週間で多くの異なるリトリーバーを紹介しましたが、この論文で使用されているものはどのように比較されますか?

本質的には、「Time Weighted VectorStore Retriever」とみなすことができます。これは、最近性(recency)と関連性(relevance)を組み合わせたリトリーバーです。

https://twitter.com/hwchase17/status/1647987722030817286

したがって、LangChainで独立したTimeWeightedVectorStoreRetrieverを実装しました。

下に示すように、最近性と関連性の間を調整するために減衰率を指定できます。下に示すように、最近性と関連性の間を調整するために減衰率を指定できます。

Docs: https://python.langchain.com/en/latest/modules/indexes/retrievers/examples/time_weighted_vectorstore.html…

では、このリトリーバーがメモリでどのように使用されるのでしょうか?

https://twitter.com/hwchase17/status/1647987723251367936

Generative Agents論文で実装されていた、記憶が減衰していくメモリモデルを作成するために必要なリトリーバーを開発したとのことで、これだけでもだいぶありがたいことだなーと感じました。

主要なメソッドは2つあります:
add_memorysummarize_related_memories

エージェントが観察(observation)を行うと、メモリを格納します:
1. LLMがメモリの重要度を評価します(平凡なものは1、感動的なものは10)
2. 観察と重要度が取り出しシステムに格納されます。

https://twitter.com/hwchase17/status/1647987725201727489

エージェントが観察(observation)に対して応答するとき:

1. リトリーバーのためのクエリを生成し、関連性、最近性、重要度に基づいてドキュメントを取得します。
2. 取得した情報を要約します。
3. 使用されたドキュメントのlast_accessed_timeを更新します。

https://twitter.com/hwchase17/status/1647987726329974784

それでは実際にこれを見てみましょう!エージェントに観察(observations)を与えて、エージェントの要約が時間とともにどのように更新されるかをシミュレートできます。

ここでは、いくつかの観察(observations)だけを更新する簡単な例を示します。

https://twitter.com/hwchase17/status/1647987727500193792

さらに極端なケースとして、約20の観察(1日分)を更新することもできます。

その後、エージェントを1日の前後で「インタビュー」し、エージェントの回答の変化に注目できます。

https://twitter.com/hwchase17/status/1647987729261801472

最後に、お互いに話す2つのエージェントのシミュレーションを作成できます。

これは論文でシミュレートされた約20のエージェントからは程遠いですが、会話を見るのも興味深いですし、それらの前後でインタビューするのも面白いでしょう。

https://twitter.com/hwchase17/status/1647987731237314560

TimeWeightedVectorStoreRetriever

ツイート内で紹介されているリトリーバーは、利用する分にはシンプルなモデルです。以下の計算式に基づいてストアされているドキュメントを評価し、その重み付けを反映した形でデータ抽出します。

semantic_similarity + (1.0 - decay_rate) ** hours_passed
意味類似性 + (1.0 - 減衰係数) ** 経過時間(hour)

意味類似性はベクトルDB側で計算されるので、アルゴリズムとして実装されるのは時間経過による減衰のみです。

例えば次のコードのように減衰係数を0.999のような大きめの数字に設定し、

# Define your embedding model
embeddings_model = OpenAIEmbeddings()
# Initialize the vectorstore as empty
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model.embed_query, index, InMemoryDocstore({}), {})
retriever = TimeWeightedVectorStoreRetriever(vectorstore=vectorstore, decay_rate=.999, k=1) 

1日前のデータとして「hello world」、現在時刻のデータとして「hello foo」というデータを設定したとき、

yesterday = datetime.now() - timedelta(days=1)
retriever.add_documents([Document(page_content="hello world", metadata={"last_accessed_at": yesterday})])
retriever.add_documents([Document(page_content="hello foo")])

「hello world」というクエリによってデータを取得する場合、通常は意味類似性のみで評価されるため「hello world」が抽出されますが、リトリーバーによって昨日のデータの優先順位は低くなるため「hello foo」が抽出されます。

# "Hello Foo" is returned first because "hello world" is mostly forgotten
retriever.get_relevant_documents("hello world")

[Document(page_content='hello foo', metadata={'last_accessed_at': datetime.datetime(2023, 4, 16, 22, 9, 2, 494798), 'created_at': datetime.datetime(2023, 4, 16, 22, 9, 2, 178722), 'buffer_idx': 1})]

現場からは以上です。

後編はコチラ↓

この記事が気に入ったらサポートをしてみませんか?