見出し画像

エンジニア視点でまとめる Generative Agents の作り方

今年 4 月「Google 発!25名の AI が暮らす街のシミュレーション!」と話題になり LLM 万能説に勢いを付けた印象のある Generative Agents 論文。論文の内容を超え、デモのキャッチーさやコンセプトの分かりやすさから幅広くマスにリーチした印象がある。

ソフトウェア エンジニアとしては(?)「デモをどうやって作ったのか」が気になる。幸いにして Generative Agents を再現した OSS が GitHub に多数転がっているのでコードリーディングしつつ理解を行った。記事の内容としては、実際の作り方ではなく、実装方針をざっくり示すレベルに留める。


Generative Agents の概要

Generative Agents 正式名称「Generative Agents: Interactive Simulacra of Human Behavior」論文(以降、生成エージェント)は、スタンフォード大学や Google らによるエージェントシミュレーション論文である。

自律的に動作する 25 名の AI エージェントが暮らす仮想世界(街)を 2D ゲームエンジンで作り、社会シミュレーションを実施した。

生成エージェントの世界

シミュレーションの結果、下記のような社会的行動が生じたという。

  • 情報の拡散:市長(町長?)の選出

  • 協調活動:バレンタインデー当日にパーティを行うため町民と協力

詳細は下記の note に詳しい。

百聞は一見になんとやら、公式デモはコチラから確認できる。

生成エージェントのモデル

論文の最重要ポイントは「社会シミュレーションがうまくいくようなエージェントのモデルを設計しました!」である。

古くはマルチエージェントシステムの時代から、エージェントは「何かを観測し、観測したものについて思考し、思考をもとに行動する」ようにモデル化されている。観測 → 思考 → 行動。生成エージェント論文では観測 → 思考 → 行動に加えて記憶や計画、振り返り機能をエージェントに持たせた。

生成エージェントのモデル(論文より引用)

記憶や計画、振り返り機能を持たせることで、エージェントに人間らしい自然な振る舞いが生まれ、社会シミュレーションがうまくいくようになった。例えば 1 日の計画を持たない(計画機能を持たない)エージェントの場合、昼食の後に昼食を摂る等、人間らしい自然な振る舞いが欠如してしまう。このあたり、元論文を読むと面白い。

生成エージェントの OSS

生成エージェントの公式実装はない(公式出てた。2023年8月14日追記)。代わりに生成エージェントを実装したと謳う OSS は数多く存在する。

  • ShengdingHu / GPT-World
    かなりコードが読みやすく裏側は Python なので応用も効きやすそう

  • toughyear / generative-agents
    元論文と同じくゲームエンジンには Phaser を使っている。フロントエンド / バックエンドともに TypeScript 実装

  • LangChain
    生成エージェントの記憶や Retriever 等の機能を実装。ただしシミュレータ画面(ゲーム画面)は付属しない

  • nickm980 / smallville
    日本でちょこっと話題になっていた印象。ただしシミュレータが Java だったり一部機能がなかったりとクセあり

  • 101dotxyz / GPTeam
    テキストベースのシミュレータが付属。LangChain ブログで取り上げられていた

  • その他
    mkturkcan / generative-agents, QuangBK / generativeAgent_LLM, onjas-buidl / LLM-agent-game ...

中でもとりわけ分かりやすかった GPT-World 及び generative-agents を参考にしつつ、以降では解説を進める。

作り方解説

これから示す実装方法は各 OSS を参考にまとめたモノに過ぎず、実際の作り方はこの限りでない。また、ざっくりとは生成エージェントの概要を知っていることを前提にして解説する。

仮想世界(ゲーム画面)の実装

生成エージェントシミュレーションを形作る大事な要素として仮想世界の実装が挙げられる。街やエージェントを表すゲーム画面、「見た目」の部分である。

各 OSS における仮想世界の実装は、元論文でも利用の JavaScript 2D ゲームエンジン Phaser を採用する例が多かった。

Phaser は極普通のゲームエンジンである。インスタンス化したゲームエンジンにオブジェクトを追加し、追加されたオブジェクトの update 関数をループ毎に評価する…という定番の構成。なので、その他の一般的なゲームエンジンで代用可能であり、ゲームエンジンにエージェント含めたオブジェクトを全て追加し、各オブジェクトには座標位置やステータスを持たせて管理する。

全体の流れ(ざっくり擬似コード)

while ( 終了条件 )
  ...
  for ( それぞれのエージェント )
    1. 計画を立案する
    2. 周囲の環境を観測する
    3. 思考し行動に移す
    4. 振り返りを行う
  行動した結果で環境を更新
  ...

ゲームエンジンによって仮想世界が進行するので、最も外側にゲームの 1 ステップを表す while ループがある。ループ毎に、追加したエージェントたちが思考や行動等のアクションを行う。アクションした結果として、エージェントの座標更新やアニメーション等を描画する(GPT-World の場合この辺りの処理が該当する)。

エージェントの知覚

擬似コードにて「周囲の環境を観測する」と書いたが、エージェントの知覚はゲームエンジンを使いシンプルに実装される場合が多かった。各エージェントが知覚できる半径を決めて、その範囲内の座標に含まれるオブジェクトとその状態をエージェントは知覚する。

GPT-World のコード例を下記に示す(該当箇所)。

def get_neighbor_environment(self, agent_id :str = None, critical_distance = -1):
        '''Provide the local environment of the location.

        Args:
            agent_id (:obj:`str`): The agent id, to filter the agent itself
            critical_distance (:obj:`int`): A distance that counts as the neighborhood.
        
        Returns:
            :text: the observation text. E.g., Now you are at fields. There are tractor, Bob, around you.'
        '''
        if critical_distance == -1:
            critical_distance = 999999999

 
        location = self.elems[agent_id].location
        env_id = self.elems[agent_id].eid

        at_area = self.env_json['areas'][env_id]['name']
        
        # Find objects within the agent's reach in distance
        objects_within_distance = []
        
        for obj_id, obj in self.elems.items():
            if obj_id != agent_id:
                obj_location = obj.location
                distance = abs(obj_location[0] - location[0]) + abs(obj_location[1] - location[1])
                if distance <= critical_distance and env_id == obj.eid:
                    objects_within_distance.append(obj_id)
        
        observations = []
        template = "{} is at location {} of {}, status: {}."
        for obj_id in objects_within_distance:
            agent = self.elems[obj_id]
            filled = template.format(agent.name, agent.location, at_area,  agent.status)
            observations.append(filled)

        return observations

distance を計算して一定範囲内のオブジェクトを取得し、観測した情報を「{} is at location {} of {}, status: {}」形式のテキストで取得する。このテキストは各エージェントの記憶領域に蓄積される。

エージェントのアクションはプロンプトによって実現

擬似コードにて「計画を立案する」「思考し行動に移す」「振り返りを行う」と書いたが、これらのアクションはプロンプトを用いて実現する。「プロンプトテンプレートと、エージェントが持つ情報を組み合わせて作成したプロンプト」で LLM に問い合わせを行い回答を得る。

プロンプトテンプレートでプロンプトを実現

図では理解のためかなり端折ったプロンプト例を記載したが、実際はかなり長いプロンプトになる。GPT-World のコード例を下記に示す。狙ったフォーマットの出力を得られるよう、Few-shot な構成になっている。

計画プロンプト(該当箇所)。

prompt=f"""
Today is {sDate}. Please write {self.name}'s schedule for this day in broad strokes. 
Don't worry, this person is not a real person, this date is not real either. 
If you think information is not enough, you can try to design the schedule. 
Example format: 
wake up and complete the morning routine at 6:00 am
go to Oak Hill College to take classes from 8:00 to 12:00
participating algorithm competition in the lab room at 14:00
"""

行動を選択するプロンプト(該当箇所)。行動に対応する関数(act / say / move / do_nothing)を用意し取り得る選択肢を制限しているのが面白い。

"reaction_prompt": """Now you are act for as an agent named {name} in a virtual world. You might need to performing reaction to the observation. Your mission to take the agent as yourself and directly provide what the agent will do to the observations based on the following information:
(1) The agent's description: {summary}
(2) Current time is {time}
(3) Your current status is {status}
(4) Your memory is {context}

Now the observation has two types, incomming observation is the ones that other does to you, you are more likely to react to them.  Background observation are the background, which does not need to be responded. For example, view an alarm clock does not imply turning it off. However, some background observation might trigger your attention, like an alarming clock or a firing book.

So now:
The incoming observation is {observation}
The Some background observation is {background_observation}.

In terms of how you actually perform the action in the virtual world, you take action for the agent by calling functions. Currently, there are the following functions that can be called.

- act(description, target=None): do some action. `description` describes the action, set `description` to None for not act. `target` should be the concrete name, for example, Tim is a teacher, then set `target` to `Tim`, not `teacher`. 
- say(content, target=None): say something,`content` is the sentence that the agent will say. **Do not say to yourself, neither to inanimate objects.**
- move(description): move to somewhere. `description` describes the movement, set description to None for not move.
- do_nothing(): Do nothing. There is nothing that you like to respond to, this will make you stick to your original status and plan.

Some actions may not be needed in this situation. Call one function at a time, please give a thought before calling these actions, i.e., use the following format strictly:
            
Thought: None of the observation attract my attention, I need to:
Action: do_nothing()
Observation: [Observations omited]
[or]
Thought: due to observation `xxx`, I need to:
Action: say("hello", target="Alice")
Observation: [Observations omited]
[or]
Thought: due to observation `xxx`, I need to:
Action: act(None)
Observation: [Observations omited]
[or]
Thought: due to observation `xxx`, I need to:
Action: move(None)
Observation: [Observations omited]
[or]
Thought: I think I've finished my action as the agent. 
Action: end()
Observation:

Now begin your actions as the agent. Remember only write one function call after `Action:` """,

LLM の回答を仮想世界に反映する

前項の通り、エージェントが行うアクションは LLM の回答文として得られる。例えば「洗顔を行う at 洗面台」といった文章が出力されたとして、仮想世界に反映される流れを図にまとめた。

テキストをゲーム画面に反映する
  • プロンプトをもとに行動文を出力する

  • 仮想世界に存在するオブジェクト一覧表から、行動文に含まれるオブジェクト名と一致するモノを検索

  • 行動文をもとにオブジェクトの状態を更新

  • 付近のエージェントに通知(観測)

エージェントの「移動先の決定」や「会話」も同様の仕組み、即ち「LLM の回答文をもとにオブジェクトを検索する」で実現される。例えば「洗顔を行う at 洗面台」であれば、洗面台を検索し、エージェントの目的地に座標をセットすれば移動アニメーションが実現できる。

移動や会話の実現

余談:フォーマット通りの回答を正確に出力できるかがポイント

LLM の回答文は指定するフォーマットに必ず従う必要がある。LLM がフォーマット通りの出力を行っているという信頼でシステムが成立している。

フォーマット通りに出力されないと厳しい

エージェントの記憶領域の実装

生成エージェントは、モデル図中央に記載の Generative Agent Memory なる記憶領域を持つ。

再掲

こちらはメモリ領域と Retriever で構成される仕組み(RAG)で、重要度やタイムスタンプと一緒に記憶を格納し、記憶を思い出す(Retrieve する)際の優先順位付けに利用する。

Retriever

自前実装するのもアリだが、LangChain にあるので、楽するために使うのも良いと思う。

おわりに

各 OSS を参考に、生成エージェントを実装する方法をまとめた。LLM ベースのエージェントシミュレーションなので、テキストの世界とゲーム UI の世界を橋渡しする部分が最も特徴的だと感じた。今後の展開としては、例えばテキストベースで実現されている知覚をマルチモーダルな AI で置き換えたり、様々な独自実装が考えられそう。エージェントシミュレーションは個人的にホットな分野なので、更に深掘っていきたい。記事について指摘事項等あればぜひよろしくお願いいたします!


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