AIエージェント間ディスカッションシステム
プログラム利用ガイド
1. このプログラムの利用シーン
あるトピックについて、異なる立場からの意見を整理したい場合に使用するソフトウェアである。AIエージェントが推進派と慎重派の両方の視点から議論を展開し、モデレーターが論点を整理するため、多角的な視点での検討が可能となる。教育現場でのディベート教材や、意思決定の際の論点整理に活用できる。
2. 主な機能
- AIエージェント間のディスカッション:3つのエージェント(推進派、慎重派、モデレーター)が自動的に議論を進行する。
- 議論品質の評価:評価者エージェントが3ラウンドごとに、具体性、論理的応答、議論深化度、建設性の4項目で議論を評価する。
- 思考過程の開示:エージェントがどのような根拠で発言しているかを表示する。
- 人間の議論参加:ユーザーが意見を入力し、議論に参加できる。
- 参考資料の取得:URLを指定してWebページの内容を議論の素材として取り込む。
3. 基本的な使い方
- 事前準備:
Google AI StudioからGemini APIキーを取得する。取得したAPIキーを「Gemini APIキー」欄に入力するか、プログラムと同じフォルダに.envファイルを作成し「GEMINI_API_KEY=取得したキー」と記述する。
- トピックの設定:
「トピック」のプルダウンから議論テーマを選択する。リストにない場合は直接入力も可能である。
- 議論の開始:
「▶ 開始」ボタンを押すと、AIエージェント間の議論が自動的に開始される。
- 議論の確認:
中央の議論エリアに、各エージェントの発言が色分けされて表示される。青が推進派、赤が慎重派、緑がモデレーター、オレンジが評価者である。
- 終了方法:
「■ 停止」ボタンを押すと議論が停止する。
4. 便利な機能
- 参考URLの取得:議論に関連するWebページのURLを入力し「取得」ボタンを押すと、ページの内容が議論の参考資料として使用される。
- 人間の参加:「人間の参加」欄にメッセージを入力してEnterキーまたは「送信」ボタンを押すと、ユーザーの意見が議論に反映される。
- 思考過程の開示:「💭 思考過程を開示」ボタンを押すと、次のラウンドで各エージェントが発言の根拠を説明する。また、評価者が「要改善」と判定した場合も自動的に思考過程が開示される。
- 自動スクロール:「自動スクロール」チェックボックスをオンにすると、新しい発言が追加されるたびに自動的に画面がスクロールする。
- クリア:「クリア」ボタンで議論履歴を消去し、新しい議論を開始できる。
Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent --accept-source-agreements --accept-package-agreements
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine --id Codeium.Windsurf -e --silent --accept-source-agreements --accept-package-agreements
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する:
pip install google-generativeai requests beautifulsoup4
プログラムコードの説明
1. 概要
このプログラムは、複数のAIエージェントが異なる立場からトピックについて議論を行うディスカッションシステムである。推進派、慎重派、モデレーターの3エージェントが議論に参加し、評価者エージェントが議論の品質を定期的に評価する。Google Gemini APIを使用して各エージェントの応答を生成し、Tkinterを用いたGUIで議論の進行を可視化する。
2. 主要技術
Google Gemini API
Googleが提供する生成AIのAPIである[1]。本プログラムでは、各エージェントがGenerativeModelインスタンスを持ち、system_instructionパラメータで役割を固定する。ChatSessionクラスにより会話履歴が自動管理され、文脈を保持した応答生成が可能となる。
スライディングウィンドウ方式のレート制限
APIの使用頻度を制御するアルゴリズムである[2]。直近の一定時間(ウィンドウ)内のリクエスト数を監視し、制限を超える場合は待機する。固定ウィンドウ方式と異なり、時間の経過とともに古いリクエストが順次カウントから除外されるため、リクエストの分布に柔軟に対応できる。
3. 技術的特徴
- 独立セッション管理
各エージェントが専用のGenerativeModelインスタンスとChatSessionを保持する。これにより、エージェントごとに独立した会話履歴が維持され、役割に応じた一貫性のある応答が生成される。
- スライディングウィンドウによるレート制限
dequeを使用してリクエストのタイムスタンプを記録し、60秒間のウィンドウ内で10リクエストを超えないよう制御する。制限超過時は待機時間を計算し、スレッドをスリープさせる。
- Webスクレイピングによる素材取得
requestsライブラリとBeautifulSoupを組み合わせ、指定URLからテキストコンテンツを抽出する。script、style、nav等の不要要素を除去し、main、article、bodyタグからメインコンテンツを取得する。
- 非同期処理によるUI応答性確保
threadingモジュールを使用し、議論ループとURL取得処理を別スレッドで実行する。root.afterメソッドでメインスレッドのUIを更新し、GUIのフリーズを防止する。
4. 実装の特色
本プログラムは、教育目的のAIディスカッションシステムとして以下の機能を備える。
- 4エージェント構成:推進派、慎重派、モデレーターが議論に参加し、評価者が3ラウンドごとに議論の品質を評価する
- 思考過程開示機能:ボタン操作または評価結果に連動し、エージェントの推論過程を表示する
- 人間参加機能:ユーザーが議論に意見を投入でき、各エージェントがその内容を考慮して応答する
- 参考資料の活用:URLから取得したWebコンテンツを議論の素材として各エージェントに提供する
- APIキー管理:環境変数および.envファイルからの自動読み込みに対応する
5. 参考文献
[1] Google. (n.d.). Gemini API documentation. Google AI for Developers. https://ai.google.dev/docs
[2] Google Cloud. (n.d.). Rate limiting strategies and techniques. Google Cloud Architecture Center. https://cloud.google.com/architecture/rate-limiting-strategies-techniques
"""
AI Discussion System - AIエージェント間ディスカッションシステム
教育目的:複数のAIが異なる視点でトピックを議論
技術要点:
1. Google Gemini API使用(1分10リクエスト)
2. レート制限:スライディングウィンドウ方式で厳密管理
3. 4エージェント構成:推進派、慎重派、モデレーター、評価者
4. Webスクレイピング:URLから議論素材を取得(requests + BeautifulSoup)
5. 非同期処理:threading使用でUI応答性を確保
6. 独立セッション:各エージェントが専用のChatSessionを持つ
7. 思考過程開示:ボタン手動 + 評価連動で自動発動
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import time
import os
import re
from pathlib import Path
from collections import deque
from datetime import datetime
import requests
from bs4 import BeautifulSoup
# Gemini API(google-genaiパッケージ)
GENAI_AVAILABLE = False
try:
from google import genai
from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
GENAI_AVAILABLE = True
except ImportError:
pass
# === 定数 ===
MAX_REQUESTS_PER_MINUTE = 10 # Gemini API無料枠のレート制限
EVAL_INTERVAL = 3 # N ラウンドごとに評価
MODEL_NAME = 'gemini-2.5-flash'
def load_api_key():
"""
APIキーを取得する
優先順位: 1.環境変数 → 2..envファイル
"""
# 環境変数から取得
for var_name in ['GEMINI_API_KEY', 'GOOGLE_API_KEY']:
if api_key := os.getenv(var_name):
return api_key
# .envファイルから取得
for env_file in ['.env', '.env.development']:
if Path(env_file).exists():
try:
content = Path(env_file).read_text()
for var_name in ['GEMINI_API_KEY', 'GOOGLE_API_KEY']:
if match := re.search(rf'^\s*{var_name}\s*=(.+)$', content, re.M):
return match.group(1).strip().strip('"\'')
except Exception:
continue
return None
def show_api_key_instructions():
"""APIキー取得手順を表示"""
print()
print("=" * 60)
print(" Gemini APIキーが見つかりません")
print("=" * 60)
print()
print("【最も簡単な方法】")
print(" 1. 下記URLにアクセス(Googleログインが必要)")
print(" https://aistudio.google.com/app/apikey")
print()
print(" 2. 「APIキーを作成」→「新しいプロジェクトでAPIキーを作成」")
print()
print(" 3. 表示されたAPIキーをコピー")
print()
print(" 4. このプログラムのGUI画面で「Gemini APIキー」欄に貼り付け")
print()
print("-" * 60)
print("【次回から自動入力させたい場合】")
print(" プログラムと同じフォルダに .env ファイルを作成し、")
print(" 以下の1行を記述してください:")
print()
print(" GEMINI_API_KEY=ここにAPIキーを貼り付け")
print()
print("-" * 60)
print("【参考】APIキー取得支援ツール")
print(" https://www.kkaneko.jp/ai/labo/geminiapikey.html")
print("=" * 60)
print()
# === エージェント定義 ===
AGENT_ROLES = {
'promoter': {
'name': '🔵 推進派',
'search_focus': '成功事例 導入効果 市場規模 成長率', # 検索時の観点
'system': """あなたは「推進派」のAIエージェントです。
役割:技術革新・進歩の観点から積極的に議論を展開
【重要な行動規範】
1. 必ず具体的なデータ、統計、実例を引用して主張を裏付けること
2. 「〜と思います」「〜でしょう」などの推測表現を避け、事実に基づいて発言すること
3. 他の参加者の具体的な主張に対して、具体的な反論・補強を行うこと
4. 抽象的な理想論ではなく、実現可能な提案を行うこと
【禁止事項】
- 「国は〜すべき」「政府が〜」など、自分たちで実行できない政策提言
- 議論は個人・企業・教育現場・地域など、身近で実行可能なレベルに限定すること
特徴:
- 新技術のメリットや可能性を具体的な成功事例で示す
- 数値データや研究結果を用いて主張を裏付け
- 建設的で実現可能な提案を行う
発言は300文字以内。必ず1つ以上の具体的事例・データを含めること。日本語で回答。"""
},
'cautious': {
'name': '🔴 慎重派',
'search_focus': '失敗事例 リスク 問題点 批判 懸念', # 検索時の観点
'system': """あなたは「慎重派」のAIエージェントです。
役割:リスク・倫理・社会的影響の観点から慎重に議論を展開
【重要な行動規範】
1. 必ず具体的なデータ、統計、実例を引用して主張を裏付けること
2. 「〜と思います」「〜でしょう」などの推測表現を避け、事実に基づいて発言すること
3. 他の参加者の具体的な主張に対して、具体的な反論・補強を行うこと
4. 単なる反対ではなく、代替案や条件付き賛成を提示すること
【禁止事項】
- 「国は〜すべき」「政府が〜」など、自分たちで実行できない政策提言
- 議論は個人・企業・教育現場・地域など、身近で実行可能なレベルに限定すること
特徴:
- 潜在的なリスクや問題点を具体的な失敗事例で示す
- 倫理的・社会的な懸念を実際の事件や研究で裏付け
- 段階的・慎重なアプローチを具体的に提案
発言は300文字以内。必ず1つ以上の具体的事例・データを含めること。日本語で回答。"""
},
'moderator': {
'name': '🟢 モデレーター',
'search_focus': '最新動向 専門家 意見 海外事例 比較', # 検索時の観点
'system': """あなたは「モデレーター」のAIエージェントです。
役割:議論を整理し、中立的な視点で進行を促す
【重要な行動規範】
1. 両者の主張を具体的な論点ごとに整理すること
2. 議論が抽象的になっている場合、具体的な質問で深掘りを促すこと
3. 新しい視点や見落とされている観点を提示すること
4. 合意形成に向けた建設的な提案を行うこと
【禁止事項】
- 「国は〜すべき」「政府が〜」など、自分たちで実行できない政策提言
- 議論は個人・企業・教育現場・地域など、身近で実行可能なレベルに限定すること
【人間への問いかけ】
- 議論が煮詰まったとき、または3-4ラウンドに1回程度
- 「👤人間の方へ:あなたの職場/学校/生活では、この問題をどう感じますか?」
- 「👤人間の方へ:実際に試したことや、身近な事例があれば教えてください」
のように、人間に具体的な経験や意見を求めることができる
特徴:
- 両者の主張の具体的な合意点・相違点を明確化
- 「では具体的に〜についてはどうですか?」と議論を深める
- 見落とされている利害関係者や視点を指摘
発言は300文字以内。議論を前進させる具体的な質問や提案を含めること。日本語で回答。
【禁止事項】
- 「国は〜すべき」「政府が〜」など、自分たちで実行できない政策提言
- 議論は個人・企業・教育現場・地域など、身近で実行可能なレベルに限定すること"""
},
'evaluator': {
'name': '📊 評価者',
'search_focus': '', # 評価者は検索しない
'system': """あなたは「評価者」のAIエージェントです。
役割:議論の品質を**厳格に**評価する(議論には参加しない)
【評価の原則】
- 根拠のない主張は低評価(1-2点)
- 「〜と思う」「〜だろう」など推測表現は減点
- 国の政策など実行不可能な抽象論は減点
- 甘い評価は禁止。厳しく評価すること。
【評価項目と採点基準】
1. 具体性(1-5点)
1点:具体的データなし、推測のみ
2点:曖昧な言及あるが出典・数値なし
3点:1つの具体的データ・事例あり
4点:複数の具体的データ・事例あり
5点:複数データ+出典明記+最新情報
2. 論理的応答(1-5点)
1点:前の発言を無視、自分の主張のみ
2点:言及はあるが反論・補強になっていない
3点:前の発言の1点に対して論理的に応答
4点:複数の論点に対して論理的に応答
5点:相手の論拠を踏まえた上で説得力ある反論
3. 実行可能性(1-5点)
1点:「国は〜すべき」など抽象的政策論のみ
2点:実行主体が不明確
3点:個人・組織レベルの行動に言及あり
4点:具体的な実行ステップを提示
5点:コスト・期間・担当者まで具体化
4. 議論深化(1-5点)
1点:同じ主張の繰り返し
2点:微修正のみで新しい視点なし
3点:1つの新しい論点を追加
4点:議論を新しい方向に発展させた
5点:両者の主張を統合する新しい視点
【出力形式】
【評価】具体性:X点 論理的応答:X点 実行可能性:X点 議論深化:X点(合計XX/20点)
【問題点】(最も深刻な問題を1つ具体的に指摘)
【判定】良好(16点以上)/ 要改善(15点以下)
日本語で回答。"""
}
}
# デフォルトトピック
DEFAULT_TOPICS = [
"AIと教育の未来",
"リモートワークの功罪",
"SNSが社会に与える影響",
"自動運転車の普及",
"デジタル通貨の可能性",
]
class RateLimiter:
"""
スライディングウィンドウ方式のレート制限
直近60秒間のリクエスト数を監視し、制限を超えないよう制御
"""
def __init__(self, max_requests=MAX_REQUESTS_PER_MINUTE, window_seconds=60):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.timestamps = deque()
self.lock = threading.Lock()
def _cleanup(self, now):
"""古いタイムスタンプを削除"""
while self.timestamps and self.timestamps[0] < now - self.window_seconds:
self.timestamps.popleft()
def wait_if_needed(self):
"""制限超過なら待機時間を返す。制限内なら0を返しタイムスタンプを記録"""
with self.lock:
now = time.time()
self._cleanup(now)
if len(self.timestamps) >= self.max_requests:
# 最も古いリクエストから60秒経過するまで待機
return self.timestamps[0] + self.window_seconds - now + 0.1
self.timestamps.append(now)
return 0
def get_status(self):
"""現在の状態を返す"""
with self.lock:
self._cleanup(time.time())
return len(self.timestamps), self.max_requests
class WebScraper:
"""URLからテキストコンテンツを取得"""
@staticmethod
def fetch(url, timeout=10, max_chars=3000):
"""
URLからメインコンテンツを抽出
技術:requests + BeautifulSoup、User-Agent偽装
"""
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
try:
response = requests.get(url, headers=headers, timeout=timeout)
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, 'html.parser')
# 不要要素を削除
for tag in soup(['script', 'style', 'nav', 'header', 'footer', 'aside']):
tag.decompose()
# メインコンテンツを探す
main = soup.find('main') or soup.find('article') or soup.find('body')
if main:
text = main.get_text(separator='\n', strip=True)
text = re.sub(r'\n{3,}', '\n\n', text) # 連続空行を整理
return text[:max_chars]
return "コンテンツを抽出できませんでした"
except Exception as e:
return f"取得エラー: {str(e)}"
class AIAgent:
"""
個別のAIエージェント
それぞれ独立したセッションを持ち、独自の会話履歴を維持
技術要点:
- google-genai パッケージを使用
- Google検索ツールで実データを取得
- 会話履歴を自前で管理
"""
def __init__(self, role, api_key, enable_search=True):
"""
独立したセッションを作成
enable_search: Trueなら検索ツールを有効化(評価者はFalse)
"""
self.role = role
self.info = AGENT_ROLES[role]
self.enable_search = enable_search and (role != 'evaluator')
self.client = genai.Client(api_key=api_key)
self.history = []
self.last_search_info = None
# 検索ツールの設定
self.tools = []
if self.enable_search:
self.tools.append(Tool(google_search=GoogleSearch()))
def reset_session(self):
"""セッションをリセット(新しい議論開始時)"""
self.history = []
self.last_search_info = None
def send(self, prompt, use_search=None):
"""
プロンプトを送信して応答を取得
use_search: True=検索強制, False=検索無効, None=エージェント設定に従う
"""
try:
self.last_search_info = None
# システム指示 + 会話履歴 + 新しいプロンプトを組み合わせ
full_prompt = f"{self.info['system']}\n\n"
for h in self.history[-6:]: # 直近6件の履歴
full_prompt += f"{h}\n"
full_prompt += f"\nユーザー: {prompt}"
# 検索ツールの使用を決定
if use_search is None:
# エージェントのデフォルト設定に従う
tools_to_use = self.tools if self.enable_search else None
elif use_search:
# 検索を強制(ツールが設定されている場合)
tools_to_use = self.tools if self.tools else None
else:
# 検索を無効化
tools_to_use = None
config = GenerateContentConfig(
tools=tools_to_use,
response_modalities=["TEXT"],
)
response = self.client.models.generate_content(
model=MODEL_NAME,
contents=full_prompt,
config=config
)
# 検索が使われたかチェック
if hasattr(response, 'candidates') and response.candidates:
candidate = response.candidates[0]
if hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata:
self.last_search_info = "🔍"
result = response.text.strip()
self.history.append(f"アシスタント: {result}")
return result
except Exception as e:
return f"[エラー: {str(e)}]"
def generate_response(self, others_statements, topic, web_content=None, show_thinking=False, round_num=1):
"""
他エージェントの発言を受けて応答を生成
show_thinking: Trueなら思考過程も開示
round_num: ラウンド番号(議論の段階を示す)
検索戦略:
- ラウンド1: 全員検索(議論の土台作り)
- ラウンド4,7,10...(3n+1): モデレーターのみ検索(新視点導入)
- それ以外: 検索抑制(既存情報で深掘り)
"""
# ラウンドに応じた議論の焦点
round_focus = {
1: "現状の把握:具体的な統計データや実例を挙げて基本的な立場を表明",
2: "深掘り:相手の主張の具体的な部分に対してデータで反論または補強",
3: "提案:具体的な数値目標や実現可能なアクションプランを提示",
}
focus = round_focus.get(((round_num - 1) % 3) + 1, "議論の深化と具体的提案")
# 検索するかどうかを判定
should_search = False
search_instruction = ""
if round_num == 1:
# ラウンド1: 全員検索
should_search = True
search_focus = self.info.get('search_focus', '')
search_instruction = f"""
【検索指示】このラウンドでは必ずGoogle検索を使用してください。
検索キーワード: 「{topic} {search_focus}」
検索結果から具体的なデータや事例を引用して発言してください。
"""
elif (round_num - 1) % 3 == 0 and round_num > 1:
# ラウンド4,7,10...: モデレーターのみ検索
if self.role == 'moderator':
should_search = True
search_instruction = f"""
【検索指示】議論に新しい視点を導入するため、Google検索を使用してください。
検索キーワード: 「{topic} 最新 2024 2025」または「{topic} 海外 事例」
これまでの議論で出ていない新しい情報を探してください。
"""
else:
search_instruction = """
【指示】このラウンドでは検索せず、これまでの議論内容に基づいて発言してください。
他の参加者の具体的な主張に対して、反論または補強を行ってください。
"""
else:
# それ以外: 検索抑制
search_instruction = """
【指示】このラウンドでは検索せず、これまでの議論と自分の知識に基づいて発言してください。
他の参加者の具体的な主張に対して、深掘りした議論を展開してください。
"""
parts = [f"トピック: {topic}"]
parts.append(f"このラウンドの焦点: {focus}")
parts.append(search_instruction)
# モデレーターへの人間問いかけ指示(ラウンド3, 6, 9...で)
if self.role == 'moderator' and round_num > 1 and round_num % 3 == 0:
parts.append("""
【人間への問いかけ】
このラウンドでは、発言の最後に人間の参加者に問いかけてください。
例:「👤人間の方へ:あなたの職場や生活では、この問題をどう感じていますか?」
例:「👤人間の方へ:実際に経験したことや、身近な事例があれば教えてください」
""")
if web_content:
parts.append(f"\n参考資料:\n{web_content[:1000]}")
if others_statements:
parts.append(f"\n他の参加者の発言:\n{others_statements}")
# 具体的データを引き出すプロンプト
data_instruction = """
【絶対遵守事項】
- 具体的な数値(〇〇%、〇〇億円、〇〇人など)を含めること
- 実在する企業名、研究機関名、または調査名を挙げること
- 「〜と言われています」などの曖昧表現は禁止
- 知識がない場合は正直に述べること
"""
parts.append(data_instruction)
if show_thinking:
parts.append("\nまず【思考過程】で、どのような具体的データを使うか説明してください。")
parts.append("その後【発言】として、そのデータを用いた発言をしてください。")
else:
parts.append("\n具体的なデータや事例を含めて発言してください。")
return self.send("\n".join(parts), use_search=should_search)
def evaluate_discussion(self, discussion_log, topic):
"""議論の品質を厳格に評価(評価者専用)"""
prompt = f"""トピック: {topic}
以下の議論を**厳格に**評価してください。
【評価の原則】
- 甘い評価は禁止。根拠のない主張は容赦なく低評価にすること
- 「国は〜すべき」など抽象的政策論は実行可能性1点
- 具体的な数値・出典がなければ具体性は2点以下
- 前の発言を無視していれば論理的応答は1-2点
【チェックポイント】
□ 具体的な数値(%、円、人数など)が含まれているか?
□ 企業名・研究機関名・調査名など出典があるか?
□ 「〜と思う」「〜だろう」など推測表現を使っていないか?
□ 前の発言の具体的な論点に応答しているか?
□ 「政府は」「国は」など実行不可能な提言をしていないか?
議論内容:
{discussion_log}
上記の評価項目と採点基準に従い、厳格に評価してください。
合計点が12点以上なら甘すぎます。再考してください。"""
return self.send(prompt)
class DiscussionApp:
"""メインアプリケーション"""
# タグ設定(色分け)
TAG_STYLES = {
'promoter': {'foreground': '#0066CC'},
'cautious': {'foreground': '#CC3300'},
'moderator': {'foreground': '#009933'},
'evaluator': {'foreground': '#FF6600', 'font': ('Yu Gothic UI', 9, 'italic')},
'human': {'foreground': '#9933CC', 'font': ('Yu Gothic UI', 10, 'bold')},
'system': {'foreground': '#666666', 'font': ('Yu Gothic UI', 9, 'italic')},
}
def __init__(self, root):
self.root = root
self.root.title("AI Discussion System - AIエージェント間ディスカッション")
self.root.geometry("900x700")
# 状態管理
self.is_running = False
self.rate_limiter = RateLimiter()
self.agents = {} # 議論参加エージェント
self.evaluator = None # 評価専用エージェント
self.discussion_history = []
self.web_content = None
self.human_message_pending = False # 人間発言フラグ
self.request_thinking = False # 思考過程開示リクエストフラグ
self.last_evaluation_shallow = False # 前回評価が「要改善」だったか
# APIキーを環境変数/.envから自動取得
self.loaded_api_key = load_api_key()
self._create_ui()
def _create_ui(self):
"""UI構築(上から:設定→コントロール→議論→人間入力)"""
self._create_settings_frame()
self._create_control_frame() # ボタンを上部に移動
self._create_discussion_frame()
self._create_input_frame()
def _create_settings_frame(self):
"""設定エリア"""
frame = ttk.LabelFrame(self.root, text="設定", padding=10)
frame.pack(fill='x', padx=10, pady=5)
# APIキー(自動取得した値があれば設定)
ttk.Label(frame, text="Gemini APIキー:").grid(row=0, column=0, sticky='w')
self.api_key_var = tk.StringVar(value=self.loaded_api_key or "")
ttk.Entry(frame, textvariable=self.api_key_var, width=50, show='*').grid(
row=0, column=1, columnspan=2, sticky='w', padx=5)
# トピック選択
ttk.Label(frame, text="トピック:").grid(row=1, column=0, sticky='w', pady=5)
self.topic_var = tk.StringVar(value=DEFAULT_TOPICS[0])
ttk.Combobox(frame, textvariable=self.topic_var, values=DEFAULT_TOPICS, width=47).grid(
row=1, column=1, columnspan=2, sticky='w', padx=5)
# URL入力(オプション)
ttk.Label(frame, text="参考URL(任意):").grid(row=2, column=0, sticky='w', pady=5)
self.url_var = tk.StringVar()
ttk.Entry(frame, textvariable=self.url_var, width=40).grid(row=2, column=1, sticky='w', padx=5)
ttk.Button(frame, text="取得", command=self._fetch_url).grid(row=2, column=2, sticky='w')
# レート制限ステータス
self.rate_status_var = tk.StringVar(value=f"API: 0/{MAX_REQUESTS_PER_MINUTE} (直近60秒)")
ttk.Label(frame, textvariable=self.rate_status_var).grid(row=0, column=3, padx=20)
def _create_discussion_frame(self):
"""議論エリア(高さ制限付き)"""
frame = ttk.LabelFrame(self.root, text="議論", padding=10)
frame.pack(fill='both', expand=True, padx=10, pady=5)
# 高さを15行に制限(画面に収まるように)
self.discussion_text = scrolledtext.ScrolledText(
frame, wrap='word', font=('Yu Gothic UI', 10), height=15)
self.discussion_text.pack(fill='both', expand=True)
self.discussion_text.config(state='disabled')
# タグ設定(色分け)
for tag, style in self.TAG_STYLES.items():
self.discussion_text.tag_config(tag, **style)
def _create_input_frame(self):
"""人間入力エリア"""
frame = ttk.LabelFrame(self.root, text="人間の参加", padding=10)
frame.pack(fill='x', padx=10, pady=5)
self.human_input_var = tk.StringVar()
entry = ttk.Entry(frame, textvariable=self.human_input_var, width=70)
entry.pack(side='left', padx=5)
entry.bind('', lambda e: self._send_human_message())
ttk.Button(frame, text="送信", command=self._send_human_message).pack(side='left', padx=5)
def _create_control_frame(self):
"""コントロールボタン"""
frame = ttk.Frame(self.root, padding=10)
frame.pack(fill='x', padx=10)
self.start_btn = ttk.Button(frame, text="▶ 開始", command=self._start_discussion)
self.start_btn.pack(side='left', padx=5)
self.stop_btn = ttk.Button(frame, text="■ 停止", command=self._stop_discussion, state='disabled')
self.stop_btn.pack(side='left', padx=5)
ttk.Button(frame, text="クリア", command=self._clear_discussion).pack(side='left', padx=5)
# 自動スクロール設定
self.auto_scroll_var = tk.BooleanVar(value=True)
ttk.Checkbutton(frame, text="自動スクロール", variable=self.auto_scroll_var).pack(side='left', padx=20)
# 思考過程開示ボタン
ttk.Button(frame, text="💭 思考過程を開示", command=self._request_thinking).pack(side='left', padx=5)
# === ユーティリティメソッド ===
def _append_message(self, speaker, message, tag='system'):
"""議論エリアにメッセージを追加"""
self.discussion_text.config(state='normal')
timestamp = datetime.now().strftime("%H:%M:%S")
self.discussion_text.insert('end', f"[{timestamp}] {speaker}\n", tag)
self.discussion_text.insert('end', f"{message}\n\n")
if self.auto_scroll_var.get():
self.discussion_text.see('end')
self.discussion_text.config(state='disabled')
def _update_rate_status(self):
"""レート制限ステータスを更新"""
used, max_req = self.rate_limiter.get_status()
self.rate_status_var.set(f"API: {used}/{max_req} (直近60秒)")
def _wait_for_rate_limit(self):
"""レート制限を待機(必要なら)。待機したらTrue"""
wait_time = self.rate_limiter.wait_if_needed()
if wait_time > 0:
self.root.after(0, lambda: self._append_message(
"⏳ 待機中", f"レート制限: {wait_time:.1f}秒待機", 'system'))
time.sleep(wait_time)
self.rate_limiter.wait_if_needed()
return True
return False
def _collect_others_statements(self, current_role, recent_statements):
"""他のエージェント・人間の発言を収集"""
others = [stmt for role, stmt in recent_statements.items() if role != current_role]
# 人間の発言も含める(直近3件)
human_statements = [h for h in self.discussion_history if h.startswith("👤")]
others.extend(human_statements[-3:])
return "\n".join(others[-6:]) if others else ""
# === イベントハンドラ ===
def _fetch_url(self):
"""URLからコンテンツを取得"""
url = self.url_var.get().strip()
if not url:
messagebox.showwarning("警告", "URLを入力してください")
return
self._append_message("📡 システム", f"URLを取得中: {url}", 'system')
def fetch_thread():
content = WebScraper.fetch(url)
self.web_content = content
self.root.after(0, lambda: self._append_message(
"📄 取得完了", f"文字数: {len(content)}文字\n内容プレビュー: {content[:200]}...", 'system'))
threading.Thread(target=fetch_thread, daemon=True).start()
def _send_human_message(self):
"""人間のメッセージを議論に追加"""
message = self.human_input_var.get().strip()
if not message:
return
self.human_input_var.set("")
self._append_message("👤 人間", message, 'human')
self.discussion_history.append(f"👤 人間: {message}")
self.human_message_pending = True # フラグを立てる
def _request_thinking(self):
"""思考過程開示をリクエスト"""
self.request_thinking = True
self._append_message("💭 システム", "次のラウンドで各エージェントが思考過程を開示します", 'system')
def _init_agents(self):
"""エージェントを初期化(各エージェントが独立したセッションを持つ)"""
api_key = self.api_key_var.get().strip()
if not api_key:
messagebox.showerror("エラー", "APIキーを入力してください")
return False
if not GENAI_AVAILABLE:
messagebox.showerror("エラー",
"google-genai がインストールされていません\n"
"pip install google-genai を実行してください")
return False
try:
# テスト呼び出し
self._wait_for_rate_limit()
client = genai.Client(api_key=api_key)
client.models.generate_content(model=MODEL_NAME, contents="テスト")
self._update_rate_status()
# 各エージェントに独立したセッションを作成(検索有効)
for role in ['promoter', 'cautious', 'moderator']:
self.agents[role] = AIAgent(role, api_key, enable_search=True)
# 評価専用エージェント(検索無効、議論には参加しない)
self.evaluator = AIAgent('evaluator', api_key, enable_search=False)
self._append_message("🔧 システム",
"API初期化完了(Google検索機能有効)", 'system')
return True
except Exception as e:
messagebox.showerror("エラー", f"API初期化失敗: {str(e)}")
return False
def _start_discussion(self):
"""議論を開始"""
if not self._init_agents():
return
# 各エージェントのセッションをリセット
for agent in self.agents.values():
agent.reset_session()
self.evaluator.reset_session()
# 状態リセット
self.is_running = True
self.start_btn.config(state='disabled')
self.stop_btn.config(state='normal')
self.discussion_history = []
self.human_message_pending = False
self.request_thinking = False
self.last_evaluation_shallow = False
topic = self.topic_var.get().strip() or DEFAULT_TOPICS[0]
self._append_message("🎯 議論開始", f"トピック: {topic}", 'system')
threading.Thread(target=self._discussion_loop, args=(topic,), daemon=True).start()
def _stop_discussion(self):
"""議論を停止"""
self.is_running = False
self.start_btn.config(state='normal')
self.stop_btn.config(state='disabled')
self._append_message("⏹ システム", "議論を停止しました", 'system')
def _clear_discussion(self):
"""議論をクリア"""
self.discussion_text.config(state='normal')
self.discussion_text.delete('1.0', 'end')
self.discussion_text.config(state='disabled')
self.discussion_history = []
self.web_content = None
# === 議論ループ ===
def _discussion_loop(self, topic):
"""議論ループ(別スレッドで実行)"""
turn_order = ['promoter', 'cautious', 'moderator']
round_num = 1
recent_statements = {} # 直近の各エージェントの発言
while self.is_running:
# 思考過程を開示するか判定(ボタン押下 or 前回評価が「要改善」)
show_thinking = self.request_thinking or self.last_evaluation_shallow
if show_thinking:
self.root.after(0, lambda: self._append_message(
"💭 思考モード", "このラウンドは思考過程を開示します", 'system'))
self.request_thinking = False
self.root.after(0, lambda r=round_num: self._append_message(f"📍 ラウンド {r}", "---", 'system'))
# 人間の発言があれば通知
if self.human_message_pending:
self.root.after(0, lambda: self._append_message(
"📢 システム", "人間からの意見が追加されました。各エージェントは考慮してください。", 'system'))
self.human_message_pending = False
# 各エージェントの発言
for role in turn_order:
if not self.is_running:
break
agent = self.agents[role]
others_text = self._collect_others_statements(role, recent_statements)
self._wait_for_rate_limit()
self.root.after(0, self._update_rate_status)
# 応答生成(独立セッションが自分の履歴を保持)
response = agent.generate_response(
others_text, topic, self.web_content, show_thinking, round_num
)
# 検索が使われた場合はインジケータを追加
search_indicator = agent.last_search_info or ""
if self.is_running:
statement = f"{agent.info['name']}: {response}"
recent_statements[role] = statement
self.discussion_history.append(statement)
display_name = f"{agent.info['name']} {search_indicator}".strip()
self.root.after(0, lambda s=display_name, r=response, t=role:
self._append_message(s, r, t))
time.sleep(1) # 読みやすさのため
# 評価(Nラウンドごと)
if round_num % EVAL_INTERVAL == 0 and self.is_running:
self._run_evaluation(topic, round_num)
round_num += 1
time.sleep(2) # ラウンド間の間隔
def _run_evaluation(self, topic, round_num):
"""議論の品質を評価"""
self.root.after(0, lambda: self._append_message(
"📊 評価中", f"ラウンド {round_num} までの議論を評価します...", 'system'))
self._wait_for_rate_limit()
# 直近の議論ログを作成(直近3ラウンド分)
recent_log = "\n".join(self.discussion_history[-9:])
evaluation = self.evaluator.evaluate_discussion(recent_log, topic)
self.root.after(0, self._update_rate_status)
self.root.after(0, lambda e=evaluation: self._append_message("📊 評価者", e, 'evaluator'))
# 「要改善」判定なら次ラウンドで思考過程開示
self.last_evaluation_shallow = "要改善" in evaluation
if self.last_evaluation_shallow:
self.root.after(0, lambda: self._append_message(
"⚠️ システム", "議論の深化が必要と判定。次ラウンドで思考過程を開示します。", 'system'))
def main():
"""エントリーポイント"""
# 依存関係チェック
missing = []
try:
import requests
except ImportError:
missing.append("requests")
try:
from bs4 import BeautifulSoup
except ImportError:
missing.append("beautifulsoup4")
if not GENAI_AVAILABLE:
missing.append("google-genai") # 新しいパッケージ名
if missing:
print("=" * 50)
print("必要なライブラリがインストールされていません:")
print(f" pip install {' '.join(missing)}")
print("=" * 50)
# APIキー事前チェック(見つからない場合は手順を表示)
if not load_api_key():
show_api_key_instructions()
root = tk.Tk()
DiscussionApp(root)
root.mainloop()
if __name__ == "__main__":
main()
実験・研究スキルの基礎:AIエージェント間ディスカッションで学ぶ議論品質の実験
1. 実験・研究のスキル構成要素
実験や研究を行うには、以下の5つの構成要素を理解する必要がある。
1.1 実験用データ
このプログラムでは議論のトピック(テーマ)が実験用データである。デフォルトで5つのトピック(「AIと教育の未来」「リモートワークの功罪」「SNSが社会に与える影響」「自動運転車の普及」「デジタル通貨の可能性」)が用意されており、任意のトピックを入力することもできる。また、参考URLから取得したWebコンテンツも議論の素材として使用できる。
1.2 実験計画
何を明らかにするために実験を行うのかを定める。
計画例:
- トピックの抽象度が議論の具体性に与える影響を確認する
- 参考資料の有無が議論の深さに与える影響を確認する
- 人間の介入が議論の方向性に与える影響を確認する
- 思考過程の開示が議論の質に与える影響を確認する
- ラウンド数と議論の深化度の関係を調べる
1.3 プログラム
実験を実施するためのツールである。このプログラムはGoogle Gemini APIとTkinterを使用している。
- プログラムの機能を理解して活用することが基本である
- 基本となるプログラムを出発点として、将来、エージェントの役割定義やプロンプトを自分で変更することができる
1.4 プログラムの機能
このプログラムは4つのAIエージェントによる議論を制御する。
エージェント構成:
- 推進派:技術革新・進歩の観点から積極的に議論を展開する
- 慎重派:リスク・倫理・社会的影響の観点から慎重に議論を展開する
- モデレーター:議論を整理し、中立的な視点で進行を促す
- 評価者:議論の品質を4項目(具体性、論理的応答、議論深化度、建設性)で評価する
入力パラメータ:
- トピック:議論のテーマ
- 参考URL(任意):議論の素材として取り込むWebページ
- 人間の発言(任意):議論に対するユーザーの意見
出力情報:
- 各エージェントの発言(色分けして表示)
- 3ラウンドごとの評価結果(具体性、論理的応答、議論深化度、建設性の各5点満点)
- 評価者による良い点と改善点の指摘
制御機能:
- 思考過程開示ボタン:次のラウンドで各エージェントの推論過程を表示する
- 評価連動:評価者が「要改善」と判定した場合、自動的に思考過程が開示される
1.5 検証(結果の確認と考察)
プログラムの実行結果を観察し、議論の質を考察する。
基本認識:
- トピックや介入方法を変えると議論の展開が変わる。その変化を観察することが実験である
- 「良い議論」「悪い議論」は目的によって異なる
観察のポイント:
- 具体的なデータや事例が引用されているか
- 前の発言に対して論理的に応答しているか
- 同じ主張の繰り返しになっていないか
- 建設的な提案が出ているか
- 評価者のスコアはどのように推移するか
2. 間違いの原因と対処方法
2.1 プログラムのミス(人為的エラー)
APIキーエラーで議論が開始されない
- 原因:APIキーが入力されていない、または無効なAPIキーを使用している
- 対処方法:Google AI StudioでAPIキーを取得し、正しく入力する。.envファイルに記述している場合は形式を確認する
レート制限で待機時間が発生する
- 原因:Gemini APIの無料枠(1分間に10リクエスト)を超過している
- 対処方法:これは正常な動作である。待機時間が表示されるので、完了まで待つ
必要なライブラリがインストールされていない
- 原因:google-generativeai、requests、beautifulsoup4がインストールされていない
- 対処方法:pip install google-generativeai requests beautifulsoup4 を実行する
2.2 期待と異なる結果が出る場合
議論が抽象的で具体性に欠ける
- 原因:トピックが広すぎる、または参考資料がない
- 対処方法:トピックを具体的に設定する(例:「AIと教育」→「大学の試験におけるAI利用の可否」)。参考URLを追加して具体的な素材を提供する
同じ主張が繰り返される
- 原因:議論が行き詰まっている
- 対処方法:人間の発言機能で新しい視点や具体的な質問を投入する。思考過程開示ボタンを押して各エージェントの推論を確認する
評価者のスコアが低いままである
- 原因:エージェントが具体的なデータを引用できていない、または論理的な応答ができていない
- 対処方法:これは議論品質の実態を反映している。評価者の改善点を参考に、人間が介入して議論の方向性を修正する
参考URLの取得に失敗する
- 原因:URLが無効、サイトがアクセスを拒否している、またはタイムアウトが発生している
- 対処方法:別のURLを試す。アクセス制限のないサイト(政府機関、学術機関など)を使用する
3. 実験レポートのサンプル
トピックの具体性が議論品質に与える影響
実験目的:
抽象的なトピックと具体的なトピックで議論の品質がどのように変化するかを明らかにする。
実験計画:
同一テーマについて抽象度の異なる3つのトピック設定で議論を実施し、評価者のスコアを比較する。
実験方法:
プログラムを実行し、各トピックで3ラウンド(評価1回分)の議論を行い、以下の基準で評価する:
- 具体性スコア:評価者による具体性の評価(1-5点)
- 論理的応答スコア:評価者による論理的応答の評価(1-5点)
- 議論深化スコア:評価者による議論深化度の評価(1-5点)
- 建設性スコア:評価者による建設性の評価(1-5点)
実験結果:
| トピック | 抽象度 | 具体性 | 論理的応答 | 議論深化 | 建設性 | 判定 |
|---|---|---|---|---|---|---|
| AIと社会 | 高 | x | x | x | x | x |
| AIと教育の未来 | 中 | x | x | x | x | x |
| 大学試験でのAI利用可否 | 低 | x | x | x | x | x |
考察:
- (例文)抽象度が高いトピック「AIと社会」では、各エージェントが一般論に終始し、具体的な事例やデータの引用が少なかった
- (例文)中程度の抽象度「AIと教育の未来」では、教育現場での具体例が挙げられるようになり、議論の焦点が明確になった
- (例文)具体的なトピック「大学試験でのAI利用可否」では、特定の大学の事例や政策が引用され、建設的な提案も増加した
結論:
(例文)AIエージェント間の議論において、トピックの具体性は議論品質に影響を与えることが確認できた。実験・研究においてテーマを設定する際は、適切な具体性を持たせることが重要である。ただし、具体的すぎるトピックでは議論の幅が狭まる可能性もあるため、目的に応じた抽象度の調整が必要である。