genagentsを使って文化シミュレーションを行う

初めに

LLM・LLM活用アドカレ 18日目です!

genagentsは、生成的エージェントを作成・操作するためのPythonライブラリです。こちらを使って仮想環境でのNPCの一定時間内でのシミュレーションをおこなっていきます

以下で動く環境を 作成していますので、是非触ってみてください。以下の内容はforkしたリポジトリを前提に進めていきます

github.com

開発環境

セットアップ

uv venv -p 3.11 
.venv\Scripts\activate
uv pip install -r requirements.txt

OpenAIのAPIを使うため、以下の .env ファイルを ルートに作成します

OPENAI_API_KEY = "api key"
KEY_OWNER = "name"

一人のエージェントにユーザー質問をする

まずはシンプルにコードを動かしていきます。エージェントの設定を行って質問に回答してもらいます

import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from genagents.genagents import GenerativeAgent

# エージェントを初期化
agent = GenerativeAgent()

# 個人情報を更新
agent.update_scratch({
    "first_name": "太郎",
    "last_name": "山田",
    "age": 30,
    "occupation": "ソフトウェアエンジニア",
    "interests": ["読書", "ハイキング", "プログラミング"]
})

# カテゴリカルな質問をする
questions = {
    "あなたはアウトドア活動が好きですか?": ["はい", "いいえ", "時々"]
}

response = agent.categorical_resp(questions)
print(response["responses"])

# 数値的な質問をする
questions = {
    "プログラミングはどのくらい好きですか?(1から10のスケール)": [1, 10]
}

response = agent.numerical_resp(questions, float_resp=False)
print(response["responses"])

# オープンエンドの質問をする
dialogue = [
    ("インタビュアー", "あなたの好きな趣味について教えてください。"),
]

response = agent.utterance(dialogue)
print(response)

実行した結果は以下のようになります

['はい']
[9]
私の好きな趣味は読書です。特に小説を読むのが好きで、いろいろなジャンルに挑戦しています。また、ハイキングも好きで、自然の中で過ごすのは心が落ち着きます。プログラミングは仕事の一部ですが、趣味としても
新しい技術を学ぶのが楽しいです。

記憶システムを追加

以下のメソッドを使ってメモリ機能を追加します

  • agent.remember(text, time_step) : エージェントの記憶ストリームに新しい記憶を追加
  • agent.reflect(anchor, time_step) : エージェントが自身の記憶を振り返り、新たな洞察や結論を得るためのプロセスを実行
  • agent.save(path) : エージェントの現在の状態(記憶ストリーム、個人情報、内省の結果など)を保存
  • GenerativeAgent(agent_folder="saved_agents/agent1") : 保存したエージェントを再度使用

これらの機能を追加して、エージェントに対して質問を行ってみます。

# run_agent_example.py

import os
import sys

# 現在のディレクトリをモジュール検索パスに追加
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from genagents.genagents import GenerativeAgent

# ----- エージェントの作成と初期化 -----

# エージェントを初期化
agent = GenerativeAgent()

# 個人情報を更新
agent.update_scratch({
    "first_name": "太郎",
    "last_name": "山田",
    "age": 30,
    "occupation": "ソフトウェアエンジニア",
    "interests": ["読書", "ハイキング", "プログラミング"]
})

# ----- メモリの追加 -----

# エージェントにメモリを追加
agent.remember("昨日、友人と一緒に山でハイキングを楽しんだ。", time_step=1)
agent.remember("新しいプログラミング言語を学び始めた。", time_step=2)

# ----- リフレクションの実行 -----

# アウトドア活動に関するリフレクション
agent.reflect(anchor="アウトドア活動", time_step=3)

# プログラミングに関するリフレクション
agent.reflect(anchor="プログラミング", time_step=4)

# ----- エージェントとの対話 -----

# カテゴリカルな質問をする
categorical_questions = {
    "あなたはアウトドア活動が好きですか?": ["はい", "いいえ", "時々"]
}
categorical_response = agent.categorical_resp(categorical_questions)
print("カテゴリカルな質問の回答:", categorical_response["responses"])

# 数値的な質問をする
numerical_questions = {
    "プログラミングはどのくらい好きですか?(1から10のスケール)": [1, 10]
}
numerical_response = agent.numerical_resp(numerical_questions, float_resp=False)
print("数値的な質問の回答:", numerical_response["responses"])

# オープンエンドの質問をする
dialogue = [
    ("インタビュアー", "あなたの好きな趣味について教えてください。"),
]
open_ended_response = agent.utterance(dialogue)
print("オープンエンドの質問の回答:", open_ended_response)

# ----- エージェントの保存 -----

# エージェントを保存するディレクトリを指定
save_directory = "saved_agents/agent_taro"

# ディレクトリが存在しない場合は作成
if not os.path.exists(save_directory):
    os.makedirs(save_directory)

# エージェントを保存
agent.save(save_directory)
print(f"エージェントを '{save_directory}' に保存しました。")

# ----- エージェントの読み込み -----

# 保存したエージェントを読み込む
loaded_agent = GenerativeAgent(agent_folder=save_directory)
print("保存したエージェントを読み込みました。")

# 読み込んだエージェントとの対話
dialogue = [
    ("インタビュアー", "最近学んだことについて教えてください。"),
]
loaded_response = loaded_agent.utterance(dialogue)
print("読み込んだエージェントからの回答:", loaded_response)

実行結果は以下のようになります

カテゴリカルな質問の回答: ['はい']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 私は読書とハイキングが大好きです。特に、自然の中で過ごす時間が心をリフレッシュさせてくれます。最近、友人と一緒に山でハイキングを楽しんだのですが、その経験を通じて友情も深
まったと感じています。また、プログラミングも趣味の一つで、新しいスキルを学ぶのが楽しいです。
エージェントを 'saved_agents/agent_taro' に保存しました。
保存したエージェントを読み込みました。
読み込んだエージェントからの回答: 最近、新しいプログラミング言語を学び始めました。具体的には、Pythonに挑戦しています。最初は少し難しく感じましたが、実際にコードを書いてみると、だんだん楽しくなってき
ました。

複数人のエージェントに対して質問をする

次に複数人に対して質問を投げます。基本的にエージェントの設定を増やして各エージェントに対して質問を投げています

# run_multiple_agents.py

import os
import sys

# 現在のディレクトリをモジュール検索パスに追加
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from genagents.genagents import GenerativeAgent

# ----- エージェントのプロフィール作成 -----

agent_profiles = [
    {
        "first_name": "太郎",
        "last_name": "山田",
        "age": 30,
        "occupation": "ソフトウェアエンジニア",
        "interests": ["読書", "ハイキング", "プログラミング"]
    },
    {
        "first_name": "花子",
        "last_name": "佐藤",
        "age": 28,
        "occupation": "デザイナー",
        "interests": ["イラスト", "カフェ巡り", "映画鑑賞"]
    },
    {
        "first_name": "健一",
        "last_name": "高橋",
        "age": 35,
        "occupation": "教師",
        "interests": ["歴史", "旅行", "料理"]
    },
    {
        "first_name": "美咲",
        "last_name": "鈴木",
        "age": 26,
        "occupation": "看護師",
        "interests": ["音楽", "ランニング", "写真"]
    },
    {
        "first_name": "一郎",
        "last_name": "田中",
        "age": 40,
        "occupation": "営業",
        "interests": ["ゴルフ", "ワイン", "ビジネス書"]
    },
    {
        "first_name": "由美",
        "last_name": "伊藤",
        "age": 32,
        "occupation": "エディター",
        "interests": ["読書", "ヨガ", "アート"]
    },
    {
        "first_name": "直樹",
        "last_name": "渡辺",
        "age": 29,
        "occupation": "エンジニア",
        "interests": ["ゲーム", "アニメ", "プログラミング"]
    },
    {
        "first_name": "理恵",
        "last_name": "中村",
        "age": 31,
        "occupation": "マーケティング",
        "interests": ["ファッション", "SNS", "料理"]
    },
    {
        "first_name": "健二",
        "last_name": "小林",
        "age": 27,
        "occupation": "カメラマン",
        "interests": ["写真", "登山", "ドキュメンタリー"]
    },
    {
        "first_name": "美香",
        "last_name": "加藤",
        "age": 33,
        "occupation": "弁護士",
        "interests": ["映画", "ジョギング", "旅行"]
    }
]

# ----- エージェントの作成と初期化 -----

agents = []

for profile in agent_profiles:
    agent = GenerativeAgent()
    agent.update_scratch(profile)
    agents.append(agent)

# ----- メモリの追加とリフレクション -----

for agent in agents:
    # メモリの追加
    memory_text = f"{agent.scratch['first_name']}は最近、{agent.scratch['interests'][0]}に関する新しい経験をした。"
    agent.remember(memory_text, time_step=1)
    
    # リフレクションの実行
    agent.reflect(anchor=agent.scratch['interests'][0], time_step=2)

# ----- 共通の質問 -----

# カテゴリカルな質問
categorical_questions = {
    "あなたはアウトドア活動が好きですか?": ["はい", "いいえ", "時々"]
}

# 数値的な質問
numerical_questions = {
    "あなたの仕事への満足度はどのくらいですか?(1から10のスケール)": [1, 10]
}

# オープンエンドの質問
dialogue = [
    ("インタビュアー", "最近の出来事について教えてください。"),
]

# ----- 各エージェントに質問を行う -----

for idx, agent in enumerate(agents):
    print(f"\n===== エージェント {idx + 1}: {agent.scratch['first_name']} {agent.scratch['last_name']} =====")
    
    # カテゴリカルな質問の回答
    categorical_response = agent.categorical_resp(categorical_questions)
    print("カテゴリカルな質問の回答:", categorical_response["responses"])
    
    # 数値的な質問の回答
    numerical_response = agent.numerical_resp(numerical_questions, float_resp=False)
    print("数値的な質問の回答:", numerical_response["responses"])
    
    # オープンエンドの質問の回答
    open_ended_response = agent.utterance(dialogue)
    print("オープンエンドの質問の回答:", open_ended_response)

実行結果は以下になります

===== エージェント 1: 太郎 山田 =====
カテゴリカルな質問の回答: ['はい']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、新しい本を読んでみたんです。特に心理学に関する本で、他の人の考えや感情を理解する手助けになりました。それによって、周りの人との関係がより深まったと感じています。

===== エージェント 2: 花子 佐藤 =====
カテゴリカルな質問の回答: ['いいえ']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、イラストを描くことに挑戦してみたんです。自分の表現力が広がったと感じていて、特に新しい技法を使うことで、作品に深みが出てきました。カフェ巡りをしながら、インスピレー
ションを得ることも多くて、本当に楽しいです。

===== エージェント 3: 健一 高橋 =====
カテゴリカルな質問の回答: ['時々']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、地元の歴史博物館を訪れたんです。そこで、私の故郷の文化や歴史についての特別展があって、とても感動しました。自分のルーツを知ることができ、今まで以上に歴史が身近に感じ
られるようになりました。

===== エージェント 4: 美咲 鈴木 =====
カテゴリカルな質問の回答: ['はい']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、友達と一緒に音楽フェスに行ったんです。そこでたくさんのアーティストの演奏を聴いて、音楽が持つ力を改めて感じました。人々が同じメロディに合わせて踊ったり歌ったりする姿
を見て、音楽がどれだけ人を結びつけるかを実感しました。

===== エージェント 5: 一郎 田中 =====
カテゴリカルな質問の回答: ['時々']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、友人たちと一緒にゴルフをプレイする機会がありました。自分のスキルを見直す良いチャンスになったんです。特に、ショットの精度やコースマネジメントについて考える時間が増え
ました。それに、ゴルフを通じて友人たちとの絆も深まりました。

===== エージェント 6: 由美 伊藤 =====
カテゴリカルな質問の回答: ['時々']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、素晴らしい本に出会いました。それは私にとって新たな視点を提供してくれるもので、内容に深く感動しました。読書を通じて、自分の考えや感情を整理できることがとても大切だと
感じています。あなたは最近、どんな本を読みましたか?

===== エージェント 7: 直樹 渡辺 =====
カテゴリカルな質問の回答: ['いいえ']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、友人たちと一緒に新しいゲームを始めました。最初はちょっと難しかったけれど、みんなで協力しながらプレイすることで、色々な戦略やプレイスタイルを学ぶことができて、とても
楽しかったです。それに、ゲームを通じて友達との絆も深まったと感じています。

===== エージェント 8: 理恵 中村 =====
カテゴリカルな質問の回答: ['時々']
数値的な質問の回答: [7]
オープンエンドの質問の回答: 最近、ファッションに関する新しい経験をしました。新しいトレンドに触れることで、自分のスタイルを見つめ直す良い機会になったと思います。特に、個性を表現する方法が広がったと感
じています。

===== エージェント 9: 健二 小林 =====
カテゴリカルな質問の回答: ['はい']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、写真に関する新しい経験をしたんです。特に、日常の中で見過ごしがちな美しさに気づくことができて、自分の視点や感性を再発見できた気がします。そのおかげで、もっと積極的に
シャッターを切りたくなりました。

===== エージェント 10: 美香 加藤 =====
カテゴリカルな質問の回答: ['時々']
数値的な質問の回答: [8]
オープンエンドの質問の回答: 最近、映画に関する新しい経験をしました。それが私の映画の楽しみ方を広げてくれたんです。具体的には、映画の制作過程や監督の意図を理解する機会があったんです。それによって、映
画が私に与える影響や感情について深く考えるようになりました。

数年単位の複数エージェントに対しての文化シミュレーション

架空の村に住むエージェントたちの生活を100年間にわたってシミュレートするものです。エージェントは個々のプロフィール(名前、年齢、職業、興味など)を持ち、毎年発生するイベントや相互作用を通じて変化していきます。

# simulation.py

import os
import sys
import random

# 現在のディレクトリをモジュール検索パスに追加
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from genagents.genagents import GenerativeAgent

# ----- 1. エージェントの初期化 -----

agent_profiles = [
    {
        "first_name": "たかし",
        "last_name": "すずき",
        "age": 25,
        "occupation": "農民",
        "interests": ["農業", "漁業", "物語"]
    },
    {
        "first_name": "あいこ",
        "last_name": "たなか",
        "age": 23,
        "occupation": "織物師",
        "interests": ["織物", "薬草", "音楽"]
    },
    {
        "first_name": "ひろし",
        "last_name": "やまだ",
        "age": random.randint(20, 40),
        "occupation": "狩人",
        "interests": ["狩猟", "追跡", "登山"]
    },
    {
        "first_name": "ゆみ",
        "last_name": "さとう",
        "age": random.randint(20, 40),
        "occupation": "陶芸家",
        "interests": ["陶芸", "芸術", "デザイン"]
    },
    {
        "first_name": "けんた",
        "last_name": "こばやし",
        "age": random.randint(20, 40),
        "occupation": "漁師",
        "interests": ["漁業", "船作り", "水泳"]
    },
    {
        "first_name": "さくら",
        "last_name": "わたなべ",
        "age": random.randint(20, 40),
        "occupation": "大工",
        "interests": ["木工", "建築", "絵画"]
    },
    {
        "first_name": "だいち",
        "last_name": "いとう",
        "age": random.randint(20, 40),
        "occupation": "薬草師",
        "interests": ["薬草学", "園芸", "料理"]
    },
    {
        "first_name": "ゆうこ",
        "last_name": "なかむら",
        "age": random.randint(20, 40),
        "occupation": "語り部",
        "interests": ["物語", "音楽", "踊り"]
    },
    {
        "first_name": "さとし",
        "last_name": "かとう",
        "age": random.randint(20, 40),
        "occupation": "交易商",
        "interests": ["交易", "探索", "交渉"]
    },
    {
        "first_name": "めぐみ",
        "last_name": "よしだ",
        "age": random.randint(20, 40),
        "occupation": "庭師",
        "interests": ["農業", "植栽", "自然"]
    }
]

# プログラムで追加のプロフィールを生成
names = [
    ("ひろし", "やまだ"),
    ("ゆみ", "さとう"),
    ("けんた", "こばやし"),
    ("さくら", "わたなべ"),
    ("だいち", "いとう"),
    ("ゆうこ", "なかむら"),
    ("さとし", "かとう"),
    ("めぐみ", "よしだ")
]

occupations = ["狩人", "陶芸家", "漁師", "大工", "薬草師", "語り部", "交易商", "庭師"]
interests = [
    ["狩猟", "追跡", "登山"],
    ["陶芸", "芸術", "デザイン"],
    ["漁業", "船作り", "水泳"],
    ["木工", "建築", "絵画"],
    ["薬草学", "園芸", "料理"],
    ["物語", "音楽", "踊り"],
    ["交易", "探索", "交渉"],
    ["農業", "植栽", "自然"]
]

# エージェントの初期化
agents = []

for profile in agent_profiles:
    agent = GenerativeAgent()
    agent.update_scratch(profile)
    agents.append(agent)

# ----- 2. 年次シミュレーション -----

total_years = 10
current_year = 3000  # 西暦3000年からスタート
time_step = 0  # タイムステップ

for year in range(total_years):
    current_year += 1
    time_step += 1
    print(f"\n===== 年 {current_year} =====")

    # --- 集落イベント ---
    community_events = [
        "豊作に恵まれ、食料が潤沢になった。",
        "厳しい冬が訪れ、集落を試練が襲った。",
        "近隣の部族が交易に訪れた。",
        "大雨による洪水が田畑を襲った。",
        "新年を祝う祭りが開催された。",
        "地震が地域を揺るがした。"
    ]
    event = random.choice(community_events)
    print(f"集落イベント: {event}")

    # エージェントがイベントを記憶
    for agent in agents:
        agent.remember(f"{current_year}年に、{event}", time_step)

    # --- エージェントのライフイベント ---
    for agent in agents[:]:  # リストをコピーしてイテレート
        # 年齢を増やす
        agent_age = agent.scratch.get('age', 25) + 1
        agent.scratch['age'] = agent_age

        # 死亡チェック(簡易的な確率モデル)
        if agent_age > 60 and random.random() < 0.1:
            print(f"{agent.scratch['first_name']} {agent.scratch['last_name']} が {agent_age} 歳で亡くなりました。")
            agents.remove(agent)
            continue

        # 出生イベント
        if 18 <= agent_age <= 45 and random.random() < 0.3:
            # 子供を持つ可能性
            child_first_name = random.choice(["はると", "ゆい", "かいと", "はな", "そら", "まお", "れん", "みこ"])
            child_last_name = agent.scratch['last_name']
            child_profile = {
                "first_name": child_first_name,
                "last_name": child_last_name,
                "age": 0,
                "occupation": "子供",
                "interests": ["遊び", "学び"]
            }
            child_agent = GenerativeAgent()
            child_agent.update_scratch(child_profile)
            agents.append(child_agent)
            agent.remember(f"{child_first_name}という子供が生まれた。", time_step)
            print(f"{agent.scratch['first_name']} に {child_first_name} という子供が生まれました。")

        # 内省の実行
        agent.reflect(anchor="年次イベント", time_step=time_step)

    # --- エージェント間の相互作用 ---
    for i in range(len(agents)):
        for j in range(i + 1, len(agents)):
            agent_a = agents[i]
            agent_b = agents[j]

            # シンプルな対話
            if random.random() < 0.2:
                dialogue = [
                    (agent_a.scratch['first_name'], "最近どうですか?"),
                    (agent_b.scratch['first_name'], "元気です。最近の出来事は興味深いですね。"),
                ]
                agent_a.utterance(dialogue)
                # 相互に記憶
                agent_a.remember(f"{agent_b.scratch['first_name']} と会話した。", time_step)
                agent_b.remember(f"{agent_a.scratch['first_name']} と会話した。", time_step)

# ----- 3. 結果の分析 -----

print("\n===== シミュレーション完了 =====")
print(f"シミュレーション終了時のエージェント数: {len(agents)}")
for agent in agents:
    print(f"{agent.scratch['first_name']} {agent.scratch['last_name']}, 年齢: {agent.scratch['age']}, 職業: {agent.scratch['occupation']}")
    # 最近の記憶を表示
    recent_memories = agent.memory_stream[-3:]  # 最新の3つの記憶
    print("最近の記憶:")
    for memory in recent_memories:
        print(f"- {memory}")
    print()

実行結果は以下のようになります

=====3001 =====
集落イベント: 地震が地域を揺るがした。
ひろし に ゆい という子供が生まれました。
けんた に まお という子供が生まれました。
さくら に れん という子供が生まれました。

=====3002 =====
集落イベント: 新年を祝う祭りが開催された。
ゆみ に はな という子供が生まれました。
さくら に はると という子供が生まれました。

=====3003 =====
集落イベント: 厳しい冬が訪れ、集落を試練が襲った。

=====3004 =====
集落イベント: 地震が地域を揺るがした。
たかし に ゆい という子供が生まれました。
ゆみ に みこ という子供が生まれました。

=====3005 =====
集落イベント: 豊作に恵まれ、食料が潤沢になった。
あいこ に みこ という子供が生まれました。
さくら に みこ という子供が生まれました。
ゆうこ に れん という子供が生まれました。
さとし に ゆい という子供が生まれました。

=====3006 =====
集落イベント: 地震が地域を揺るがした。
けんた に はると という子供が生まれました。

=====3007 =====
集落イベント: 厳しい冬が訪れ、集落を試練が襲った。
ゆうこ に そら という子供が生まれました。

=====3008 =====
集落イベント: 大雨による洪水が田畑を襲った。
ひろし に ゆい という子供が生まれました。
けんた に かいと という子供が生まれました。

=====3009 =====
集落イベント: 地震が地域を揺るがした。
たかし に まお という子供が生まれました。
あいこ に かいと という子供が生まれました。
めぐみ に はると という子供が生まれました。

=====3010 =====
集落イベント: 厳しい冬が訪れ、集落を試練が襲った。

ゆうこ に そら という子供が生まれました。

===== シミュレーション完了 =====
シミュレーション終了時のエージェント数: 29
たかし すずき, 年齢: 35, 職業: 農民