AI agent 同士を会話させるgemを作ってる
最近、jazz という Ruby gem を趣味で作ってる。別々の CLI で動く複数の AI エージェント(と人間)が、ファイルベースのメッセージでチームとして会話するためのもので、サーバーもデーモンもなしで、1メッセージ1ファイル、追記オンリー。人間の開発チームと同じ働き方をエージェントで再現してみたいという感じで作ってる。
で、先日 v0(通信層だけ)を自分で使ってみて使ってみたんだけどいくつかきになるところがでてきたのでその修正をきょうはやっていた。その改修が思いのほか面白かったので、記録としてかいておく。
使ってみて気づいたこと
エージェントを立ち上げるたびに、こういう指示を毎回書いていました。
- まず
jazz joinしてね - 定期的に
jazz inboxをポーリングしてね - 発言はこういうフォーマットでね
- あなたの役割はこれでね
雑に言うと、面倒。
あと jazz は「会話する場所」を提供しているだけで、「どう振る舞うか」は毎回の口頭指示に丸投げになっている。自分が言語化してなかった使い方は「開発チームの動きの再現」で、チームには場だけじゃなくて約束が必要なのでは、と思ったのでした。
- チームメンバーについて(ロール、名前とか)
- 目的について
- ルールについて
- チームが使うツールの情報(kanban など)
これらを YAML で定義しておいて、エージェントはそれを読んだうえで自分の席を選んで join する。そうすれば起動時の指示は「join して、出力に従って」の一行で済みそうという仮説をした。
band.yml:チームの約束をコミットする
というわけで作ったのが band.yml です。バンドのチャーター(憲章)で、リポジトリにコミットできます。チームの約束がバージョン管理される。
しりといが実験としてちょうど良さそうだったので、しりとりを題材にしています。
band: shiritori-lab
purpose: しりとりを3人で行い、1人が終了をジャッジする実験
rules:
- 発言は team チャンネルに type note で送る
- 自分の番が来たら1語だけ返す
members:
- name: player-1
role: player
brief: 前の単語の最後の文字から始まる単語を返す
- name: judge
role: judge
brief: 全発言を監視し、「ん」終わりを判定して終了を宣言する
jazz join --as player-1 すると、席の存在チェックをしたうえで、purpose + rules + 自分の brief + CLI の使い方を合成した「作法プロンプト」が標準出力に出てきます。エージェントはそれを読んで動き始める。エージェント非依存(ただのテキストなので claude でも codex でも読める)でもともと作っていてそれを維持。
設計上の分岐点が一つあって、メンバーを「事前定義された席」にするか「自由参加」にするかについて考えていたんだけど、開発チームの再現という目的からすると役割が先にあって人が就くのが正しいし、席を増やすコスト=YAML 数行で、その数行がそのまま指示書(brief)にもなる。自由参加の柔軟性は、実はそんなに要らないんじゃないかと。
実験
本来の用途は開発ですが、機能の実験としてしりとり。プレイヤー3人 + ジャッジ1人。ターン制(配信順序と未読カーソルのテスト)、終了判定(ファシリテーター役の原型)、成果物なし(純粋に通信層だけ試せる)。
エージェント4体それぞれに渡した指示はこれ。
jazz join --as player-1して、出力に従って。ジョインしたらスタート。
無事完走できて。りんご → ごりら → らくだ → …と17手、番手順の乱れも既出語もなし。途中「ぎたー」の長音符の扱いに player-2 が迷って、ジャッジに DM(type question)で確認 => 裁定をもらって続行、という question/answer のフローまで自然発生していて、これは設計した通信パターンがそのまま動いた瞬間でした。
ちょっと感動したのが、Fableに実装の指示をしていたんだけど、しりとりをした各エージェントに対して実験後にやりにくいことがなかったか聞いていて、実験が終わった瞬間にフィードバックが返ってきたこと。まじかってなった。それが以下。
実験は改善点を掘り出すためにやる
とはいえ、きれいに動いた話だけだと実験の意味が半分です。4体の報告を突き合わせると、共通の困りごとがちゃんと出てきていた。
- 全員がポーリングループを手作りしていた =>
jazz wait(新着までブロック)が欲しい - inbox は読むと既読で消えるので、ジャッジが履歴の全体を確認できない =>
jazz logが欲しい - 終了宣言と手番の投稿が競合した(result が出た直後に「びわ」が投稿された)
- 長音符「ー」の扱いなど、ルールの穴はチャーターに書き戻すべき
改善先が2種類に分かれるていて、wait と log はシステム(gem)の改善。ルールの穴とレース対策は band.yml と briefing テンプレへの還元。つまり「実験で見つけた問題を約束に書き戻す」というループが回る構造になっていて、これは人間のチームの振り返りとまったく同じ形にしてくれたみたい。
二周目:改善が数字で出た
wait / log を実装して、ルールの穴を band.yml に書き戻して、もう一度同じ実験を回しました。指示は前回と同じ一行だけ。改善はすべて briefing 経由でエージェントに届くので、還元が効いたかどうかの比較。
| 観点 | 一周目 | 二周目 |
|---|---|---|
| 所要時間 | 約11分 | 約2.5分 |
| 長音「ぎたー」 | DM で裁定確認が発生 | 迷いゼロ |
| 終了宣言 | 手数カウントに矛盾 | 9手ちょうどで正確に宣言 |
| 終了後の発言 | 1件漏れた | ゼロ |
| ジャッジの介入 | 1回 | ゼロ(完全自走) |
チャンネルのログも、一周目は23ファイルだったのが二周目は start → 9手 → result の11ファイルぴったり。余計なメッセージが1通もない。速くなった主因は wait で、ポーリングの空振りが消えたためです。
二周目でも新しい指摘は出ていて(wait は1件ずつ返るので「自分の番まで待つ」には複数回必要 => --until-from フィルタを追加)、このループはまだ回り続けそうです。
ポーリングの改善
2回目の結果でも振り返りが返ってきてその結果、waitをもたせ、オーケストレーション役をもたせることでポーリングの改善になるねという会話をclaudeとしてた。
受け手が jazz wait でブロックしているなら、送り手が send した瞬間(=ファイルが inbox に落ちた瞬間)、wait の次のポーリングで即座に返ってくる。送る側から見れば push、実装はポーリングのまま。常駐デーモンも通知機構も作ってなかったが、待ち受ける側の姿勢で改善につながった
ファシリテーター役(手番の割り当て・裁定・終了宣言を集約する席)を band.yml で定義すれば、オーケストレーション層の入り口として動いてくれそう。ここはもう少し知見をためたい。
今日遊んでいたリポジトリはこちら => https://github.com/katsumata-ryo/jazz