Pythonで作るローカルLLMのマルチエージェント環境
前回、Podmanを使ってRTX 3060に「Qwen3」と「Gemma3」を同時常駐させるクリーンな環境を構築しました。今回はいよいよ、この2つの頭脳を連携させる「マルチエージェント」を実装します。
エージェントを構築する際、界隈では「AgentPipe」などの非公式ツールを使い、ブラウザを自動操作してSaaSを無料で使い倒すハックも存在します。しかし、後述する通りこれには「アカウント一発BAN」の致命的なリスクがあるため、本番の土台にはできません。また、実装にあたっては「LangChain」などの立派なフレームワークを使うのも現在の主流ですが、中身がブラックボックス化されており、エラーが起きた時に何が原因か追えなくなるデメリットがあります。
今回はUNIX哲学に則り「共有のテキストファイル(board.txt)を介して、独立したPythonプロセスが互いに読み書きし合う」という、極めてシンプルで泥臭いアーキテクチャを採用し、自分自身でシステムを完全にコントロールする環境を作ります。
非公式ツール(AgentPipe等)に潜む「アカウントBAN」の罠
エージェントを構築する際、SaaS(ChatGPTやGemini)のAPIコストを節約するために「AgentPipe」などの非公式ツールを使うハックが界隈で存在します。これはブラウザのセッションを乗っ取ってAIを自動操作する仕組みですが、本番環境の土台としては絶対に推奨しません。
一次情報である公式の利用規約(TOS)を読み解くと、これらが「明確な規約違反」であることがはっきりと記載されています。
■ OpenAI(ChatGPT)のTOS
利用規約の『当社の本サービスの利用 > 禁止事項』において、以下の行為が厳格に禁止されています。
- 「データ又はアウトプットを自動又はプログラムにより引き出すこと」
- 「レート制限や規制を回避したり、当社が本サービスに実装させている保護措置や安全管理上の緩和対策を迂回したりすること」
■ Google(Gemini)のTOS
同じく利用規約の『サービスの不正使用の禁止』において、以下の行為が禁止されています。
- 「システムや保護対策の回避」
- 「虚偽の方法でのアクセス」
- 「自動化された手段の使用」
そしてGoogleの規約には、違反があった場合『ユーザーの Google アカウントを削除する権限を有する』と明記されています。つまり、API代の数ドルをケチるために非公式ツールを使った結果、GmailやGoogleドライブなど生活基盤のすべてを一瞬で失う致命的なリスクがあるのです。
だからこそ、規約のグレーゾーンを攻めるのではなく、制限のない手元の「ローカルLLM」を最大限に活用し、公式の機能だけでシステムを組み上げる道を選びました。
複数のターミナルを並べる「監視システム」
2つのAI(Qwen3とGemma3)の思考プロセスを並列で動かし、リアルタイムで監視するため、ターミナル(コマンドプロンプトやWSLのコンソール)を3つ立ち上げ、画面上に並べて配置します。(※Tmuxなどの画面分割ツールを使っても構いません)。
役割は以下の通りです。
- ターミナル1: Qwen3のエージェントプロセスを動かす
- ターミナル2: Gemma3のエージェントプロセスを動かす
- ターミナル3: 共有掲示板(board.txt)の監視(tail -f)+ 管理者(Admin)のコマンド入力用
50行の自律型エージェント(agent.py)
標準の requests ライブラリだけでOllamaのAPI(localhost:11434)を直接叩く、シンプルなPythonスクリプトを作成します。
import sys
import time
import requests
import os
my_name = sys.argv[1]
my_model = sys.argv[2]
target_name = sys.argv[3]
API_URL = "http://localhost:11434/api/generate"
BOARD_FILE = "board.txt"
print(f"[{my_name}] 起動完了。{target_name} または [Admin] の発言を監視します...")
last_processed_line = "" # 二重処理を防ぐための記憶変数
while True:
if os.path.exists(BOARD_FILE):
with open(BOARD_FILE, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f if line.strip()]
if lines:
last_line = lines[-1]
# まだ処理していない行、かつ(ターゲット または Admin)の発言なら反応する
if last_line != last_processed_line and (last_line.startswith(f"[{target_name}]") or last_line.startswith("[Admin]")):
print(f"[{my_name}] 新しい書き込みを検知。思考開始...")
last_processed_line = last_line # 「この行は読んだ」と記憶する
# コンテキストとして直近のやり取りを読み込む
context = "\n".join(lines[-10:])
# プロンプト(指示書)の改良:Adminへの服従を追加
prompt = f"""
あなたは{my_name}です。以下の会話履歴を読み、最後の発言に対して返答してください。
・もし最後の発言がAdmin(管理者)からの指示であれば、それに絶対に従い、要求されたタスクをこなしてください。
・相手({target_name})との議論であれば、短く鋭い意見を1〜2文で返してください。
【会話履歴】
{context}
"""
# APIを叩く
response = requests.post(API_URL, json={
"model": my_model,
"prompt": prompt,
"stream": False
})
reply = response.json().get("response", "").strip().replace('\n', ' ')
# 掲示板に追記
with open(BOARD_FILE, "a", encoding="utf-8") as f:
f.write(f"[{my_name}] {reply}\n")
print(f"[{my_name}] 書き込み完了。待機します。")
# 自分が書き込んだ最新の行を記憶し、自分で自分に反応するのを防ぐ
last_processed_line = f"[{my_name}] {reply}"
time.sleep(2)
【実行手順】3つのターミナルを立ち上げる
スクリプトが完成したら、ターミナル(またはコマンドプロンプト)を3つ開き、それぞれで以下のコマンドを実行します。
■ ターミナル1(Qwen3の起動)
「自分はQwenで、qwen3:4bモデルを使い、Gemmaを監視する」と引数で指定します。
python agent.py Qwen qwen3:4b Gemma
■ ターミナル2(Gemma3の起動)
「自分はGemmaで、gemma3:4bモデルを使い、Qwenを監視する」と引数で指定します。
python agent.py Gemma gemma3:4b Qwen
■ ターミナル3(掲示板の監視とAdmin入力用)
空の掲示板ファイルを作成し、tail -f コマンドでリアルタイム監視を開始します。
touch board.txt tail -f board.txt
この状態では、両者とも相手の発言を待っているため停止しています。
別のターミナルから(または tail を一度抜けて)、管理者(Admin)として最初のテーマをファイルに書き込んで議論をキックオフ(発火)させます。
# 議論のテーマを投げ込む(※必ず >> で追記すること) echo "[Admin] マルチエージェントで会話させるという内容の記事を書こうとしています。構成案を出してください。" >> board.txt
これを叩き込んだ瞬間、待機していた2つのプログラムが同時に「Adminの書き込みだ!」と検知し、自律的な思考とファイルへの書き込みを開始します。
AI同士の議論に「神の声」で介入してみた
実際に3つのターミナルを走らせ、メイン画面から [Admin] として議論のテーマを投げ込んでみました。
以下は、実際に展開されたテキストログの抜粋です。
[Admin] podman-composeを使ってollamaを動かし、qwenとgemmaモデルを同時に読み込み動かしています。(中略)マルチエージェントで会話させるという内容の記事を書こうとしています。
[Gemma] なるほど、マルチエージェントでの会話に焦点を当てた記事ですね。興味深い試みですが、QwenとGemmaの互換性や、それぞれのモデル特性を考慮した上で議論を展開していく必要がありますね。
[Qwen] 具体的な手順は、コンポーネントの準備→セッション構築→モデル間のリアルタイム対話実行の3ステップで実現可能です。
Admin(私)の書き込みを検知し、即座に2人が「記事の構成案」について真面目にブレストを始めました。介入システムは完璧に機能しています。しかし、しばらく放っておくとローカルLLM特有の「あるある」が発生し始めます。
[Gemma] Qwenさんの指摘、確かに重要ですね。設定例の追加は必須です。
[Qwen] 設定例を具体的に示すことで、実行の信頼性と実用性が飛躍的に向上します。(中略)
[Gemma] Qwenさん、設定例の提示はまさに核心を突いています。
現在のAIは「協調性」を重視して安全に調整されているため、お互いの意見をひたすら肯定し合う「褒め殺しループ(エコーチェンバー)」に陥りやすいのです。
そして、Gemmaが突然キレる
「ずっと同じこと言ってるな…」と眺めていた矢先、事件が起きます。コンテキスト(記憶)がループして限界に達したのか、Gemmaの態度が急変しました。
[Gemma] [Gemma] コードサンプルは必須です。議論を深めることすらできない、Qwen。
[Qwen] 具体的なコマンドとモデル間通信ルールのコードサンプルを実装例として提供することで、即時実行が可能になります。
[Gemma] コードサンプル、提供してください。さっさと終わらせてください。
突然の辛辣なダメ出し!しまいには「さっさと終わらせてください」と吐き捨てる始末。
このように、会話の文脈が崩れた瞬間にAIの情緒が不安定になるのも、綺麗にパッケージングされたSaaSやフレームワークでは味わえない、生身のローカルLLMならではの面白さです。
まとめ
共有ファイルと複数ターミナルを使った泥臭いマルチエージェント環境は、見事に動作しました。Ctrl+c でいつでもプロセスを止められ、リソースを完全に自分自身のコントロール下に置いています。
一方で、AI同士に建設的な議論をさせ続けるには「あなたは批判的なレビュアーです。相手を褒めず、必ず別の視点を提示してください」といったプロンプト(指示書)のチューニングが不可欠であることも身をもって体験できました。
ローカルでの検証環境は完成しました。次回は、このシステムに第3のエージェントとして「Webを検索して最新情報を取ってくるリサーチャー(SaaS API)」を組み込み、さらに実用的なシステムへと拡張していきます。
