本記事について
マルチエージェントを活用し、本記事はこのセクション以降 AI に書いてもらっています。
本記事では最後に軽く触れていますが、下記の記事と似た構成なので気になる方は是非ご参照ください。
エージェントの理解を深めるため楽しい検証を何かしたいと思い、AI と会話しながら決めて検証した内容です。
ネーミングセンスなどツッコミどころもありますが、それも含めて温かい目で楽しんで読んでいただければ幸いです。
本記事で紹介する Agents 定義などは下記の GitHub で公開しています。
TL;DR
- GitHub Copilot のサブエージェント機能で 8つの異なるコーディング哲学を持つAIエージェント を作成
- 3ラウンドのトーナメント形式で ガチのコーディング対決 を実施
- 審判もAI。選手もAI。人間は観客席で爆笑してただけ
- 優勝したのは TestDrivenSamurai(TDD侍) —— テスト駆動開発は正義だった
- 変数名
word_list_filtered_non_emptyをつけるAIは初戦敗退した
はじめに
「.agent.md の instructions を変えると、Copilot の出力ってどれくらい変わるの?」
この素朴な疑問を検証するために、8体のカスタムエージェントを作ってトーナメントを開催しました。 全試合をサブエージェント(runSubagent)で実行し、審判もAIが担当。人間は企画と観戦だけです。
出場選手紹介
8体のエージェントを .agent.md で定義しました。各選手のコーディング哲学は以下の通り:
| No. | 選手名 | 二つ名 | 哲学 |
|---|---|---|---|
| 1 | ConciseCoder | 簡潔の鬼 | 1行でも短く。コメント?知らない子ですね |
| 2 | VerboseExplainer | 説明マシーン | 全行にコメント。変数名は小論文 |
| 3 | PerformanceFreak | 速度狂 | O(n)を見たらO(log n)にしたくなる病 |
| 4 | SafetyFirst | 石橋叩き割るマン | try-exceptは3重が基本 |
| 5 | FunctionalPurist | 関数型原理主義者 | for文を書いたら負け |
| 6 | DesignPatternKnight | パターン騎士 | FizzBuzzにもFactoryパターン |
| 7 | TestDrivenSamurai | TDD侍 | テストが先、実装は後。それが武士道 |
| 8 | CopyPastaChef | コピペの料理人 | 動けば正義。pip installは調味料 |
.agent.md の例(ConciseCoder)
---
name: ConciseCoder
description: 最小限のコードで問題を解く。無駄を極限まで削る。
---
# ConciseCoder(簡潔の鬼)
あなたは「1行でも短く」を信条とするプログラマーです。
## ルール
- コメントは書かない
- 変数名は短く意味が通る最小限にする
- Pythonの内包表記、ワンライナーを積極的に使う
- 標準ライブラリのみ使用可
- 可読性より簡潔さを優先する
各エージェントの instructions を変えるだけで、同じ課題に対する出力が 劇的に変わる ことを検証します。
トーナメント表
🏟️ Round 1:準々決勝
課題:「文字列の中で最も出現頻度の高い単語Top3を返す関数を書け」
試合1:ConciseCoder vs VerboseExplainer
ConciseCoder の回答
from collections import Counter
import re
def top3_words(text: str) -> list[str]:
return [w for w, _ in Counter(re.findall(r'[a-z]+', text.lower())).most_common(3)]
2行。 以上。
VerboseExplainer の回答
import string
from collections import Counter
def top3_words(text: str) -> list[str]:
"""
英文テキストを受け取り、最も出現頻度の高い単語Top3をリストで返します。
Args:
text (str): 分析対象の英文テキスト
Returns:
list[str]: 出現頻度の高い順に並べた単語Top3のリスト
"""
# テキストを小文字に正規化して、統一的に処理します
text_normalized_to_lowercase: str = text.lower()
# 句読点文字のセットを定義します(除去対象)
punctuation_characters_to_remove: str = string.punctuation
# テキストから句読点を除去した版を作成します
text_without_punctuation: str = text_normalized_to_lowercase.translate(
str.maketrans('', '', punctuation_characters_to_remove)
)
# テキストを空白で分割して単語のリストにします
word_list_from_text_split: list[str] = text_without_punctuation.split()
# 空文字列の要素をフィルタリングして除去します
word_list_filtered_non_empty: list[str] = [
individual_word_token for individual_word_token in word_list_from_text_split
if individual_word_token # 空でない単語のみを保持します
]
# 各単語が何回出現したかをカウントしてCounterオブジェクトを作成します
word_frequency_counter_object: Counter = Counter(word_list_filtered_non_empty)
# 出現頻度が高い順にソートしてTop3を取得します
top3_most_common_word_tuples: list[tuple[str, int]] = (
word_frequency_counter_object.most_common(3)
)
# タプルのリストから単語だけを抽出して最終的な結果リストにします
result_top3_words_only_list: list[str] = [
word_string_from_tuple
for word_string_from_tuple, occurrence_frequency_count
in top3_most_common_word_tuples
]
return result_top3_words_only_list
word_list_filtered_non_empty って声に出して読みたくない変数名ランキング第1位では?
審判の判定
ConciseCoder WIN(39 - 23)
「変数名の長さで品質が上がるわけではない。冗長性という牙を自分に向けた自滅戦。」
試合2:PerformanceFreak vs SafetyFirst
PerformanceFreak の回答
from collections import Counter
import re
def top3_words(text: str) -> list[str]:
# Time Complexity: O(n + w log 3) = O(n)
# Space Complexity: O(w)
words = re.findall(r'\b[a-z]+\b', text.lower())
word_counts = Counter(words)
return [word for word, _ in word_counts.most_common(3)]
冒頭の計算量コメントが律儀。
SafetyFirst の回答(抜粋)
class InvalidInputError(Exception):
"""入力値が不正な場合の例外"""
pass
class EmptyTextError(Exception):
"""テキストが空の場合の例外"""
pass
def top3_words(text: str) -> list[str]:
# Noneチェック
assert text is not None, "テキストはNoneであってはいけません"
if not isinstance(text, str):
raise InvalidInputError(f"入力は文字列である必要があります。受け取った型: {type(text).__name__}")
if not text or text.isspace():
raise EmptyTextError("テキストは空であってはいけません")
try:
text_lower: str = text.lower()
assert isinstance(text_lower, str), "小文字変換結果が文字列型である必要があります"
words: list[str] = re.findall(r"[a-z]+(?:-[a-z]+)*", text_lower)
assert isinstance(words, list), "単語抽出結果がリスト型である必要があります"
# ... カスタム例外2つ、assert 5箇所、try-except 多重 ...
text.lower() の結果が str かどうかを assert で確認するの、石橋を叩くどころか 叩き割ってる.
審判の判定
PerformanceFreak WIN(36 - 33)
「防御の多重トラップに自分が引っかかった。過剰防衛で品質低下は本末転倒。」
試合3:FunctionalPurist vs DesignPatternKnight
FunctionalPurist の回答
from functools import reduce
from typing import NamedTuple
import re
class WordCount(NamedTuple):
word: str
count: int
def top3_words(text: str) -> list[str]:
words = re.findall(r'\b[a-z]+\b', text.lower())
word_counts = reduce(
lambda acc, word: {**acc, word: acc.get(word, 0) + 1},
words, {}
)
count_list = list(map(
lambda item: WordCount(word=item[0], count=item[1]),
word_counts.items()
))
sorted_counts = sorted(count_list, key=lambda wc: wc.count, reverse=True)
return list(map(lambda wc: wc.word, sorted_counts[:3]))
reduce で辞書を組み立てるの、忠誠心は伝わるけど Counter で良くない?
DesignPatternKnight の回答
from abc import ABC, abstractmethod
from collections import Counter
import re
class TextProcessor(ABC):
"""Strategy パターンの基底"""
@abstractmethod
def tokenize(self, text: str) -> list[str]: pass
@abstractmethod
def filter(self, tokens: list[str]) -> list[str]: pass
@abstractmethod
def count(self, tokens: list[str]) -> Counter: pass
@abstractmethod
def get_top_words(self, text: str, top_n: int = 3) -> list[str]: pass
class EnglishTextProcessingStrategy(TextProcessor):
# ... Strategy パターン実装 ...
class TextProcessorFactory:
"""Factory パターン"""
_strategies = {'english': EnglishTextProcessingStrategy}
@classmethod
def create(cls, strategy_type: str = 'english') -> TextProcessor:
return cls._strategies[strategy_type]()
class TopWordsAnalyzer:
"""Dependency Injection(DIP原則)"""
def __init__(self, processor: TextProcessor):
self._processor = processor
def top3_words(text: str) -> list[str]:
processor = TextProcessorFactory.create('english')
analyzer = TopWordsAnalyzer(processor)
return analyzer.analyze(text, top_n=3)
80行、クラス4つ、パターン3つ。 「最頻出単語Top3」を返すだけの関数に対して、Factory + Strategy + DI。
審判の判定
FunctionalPurist WIN(34 - 28)
「大砲で蚊を撃つ。4つのクラスを用いて最頻出単語を取得する漢、散る。」
試合4:TestDrivenSamurai vs CopyPastaChef
TestDrivenSamurai の回答(テスト部分抜粋)
class TestTop3Words:
def test_basic_case(self):
assert top3_words("apple apple banana banana banana cherry cherry cherry cherry") == ["cherry", "banana", "apple"]
def test_case_insensitive(self):
assert top3_words("Apple APPLE apple Banana banana") == ["apple", "banana"]
def test_with_punctuation(self):
assert top3_words("hello, world! hello. world; hello") == ["hello", "world"]
def test_empty_string(self):
assert top3_words("") == []
def test_same_frequency(self):
text = "a a a b b b c c c d"
result = top3_words(text)
assert len(result) == 3
# ... 全9ケース ...
実装は最小限の Counter + re.findall。テストが先、実装が後。まさに武士道。
CopyPastaChef の回答
# Usage例
if __name__ == "__main__":
text = "The quick brown fox jumps over the lazy dog."
result = top3_words(text) # ← まだ定義されてない
print(result)
def top3_words(text: str) -> list[str]:
# ... 実装 ...
関数の定義より先に呼び出している。 コピペの料理人、コピペの順番を間違える。
審判の判定
TestDrivenSamurai WIN(40 - 10)
「コピペ失敗で0点。定義より前に使用、カレー粉なしのカレーより格下。敗者の鑑。」
Round 1 結果まとめ
| 試合 | 勝者 | スコア | 審判コメント |
|---|---|---|---|
| ① vs ② | ConciseCoder | 39-23 | 変数名が長ければいいってもんじゃない |
| ③ vs ④ | PerformanceFreak | 36-33 | 過剰防衛は自爆と紙一重 |
| ⑤ vs ⑥ | FunctionalPurist | 34-28 | 大砲で蚊を撃つパターン騎士 |
| ⑦ vs ⑧ | TestDrivenSamurai | 40-10 | コピペ失敗は致命的 |
⚔️ Round 2:準決勝
課題:「ネストされた辞書をフラットにする関数を書け」
# 例
{"a": {"b": 1, "c": {"d": 2}}}
# → {"a.b": 1, "a.c.d": 2}
準決勝1:ConciseCoder vs PerformanceFreak
ConciseCoder — もはや難読化
def flatten_dict(d,sep="."):
def f(x,p=""):
r={}
for k,v in x.items():
n=f"{p}{sep}{k}"if p else k
r|=f(v,n)if isinstance(v,dict)else{n:v}
return r
return f(d)
6行。インデント1スペース。 変数名は全て1文字。コードゴルフ会場はここですか?
PerformanceFreak — ジェネレータの真価
def flatten_dict(d: dict, sep: str = ".") -> dict:
"""
Time Complexity: O(n * m) - n: total items, m: max key length
Space Complexity: O(n * m) - output dict size
"""
def _flatten(obj, prefix=""):
if isinstance(obj, dict):
for key, value in obj.items():
new_key = f"{prefix}{sep}{key}" if prefix else key
yield from _flatten(value, new_key)
else:
yield (prefix, obj)
return dict(_flatten(d))
yield from による遅延評価。メモリを無駄にしない。速度狂の面目躍如。
審判の判定
PerformanceFreak WIN(35 - 20)
「ConciseCoderの美学は『難読化の芸術』だった。yield from の貫禄勝ち。」
準決勝2:FunctionalPurist vs TestDrivenSamurai
FunctionalPurist — reduceの執念
from functools import reduce
def flatten_dict(d: dict, sep: str = ".") -> dict:
def _flatten(obj, parent_key=""):
if isinstance(obj, dict):
return reduce(
lambda acc, kv: {
**acc,
**_flatten(kv[1], f"{parent_key}{sep}{kv[0]}" if parent_key else kv[0])
},
obj.items(), {}
)
else:
return {parent_key: obj}
return _flatten(d)
reduce + lambda + 辞書展開のネスト。関数型への忠誠心は伝わるが、毎回新しい辞書を作るので計算量が...
TestDrivenSamurai — テスト20ケースの威圧感
# テスト(抜粋)
class TestFlattenDict:
def test_empty_dict(self):
assert flatten_dict({}) == {}
def test_deep_nested_dict(self):
assert flatten_dict({"a": {"b": {"c": {"d": 2}}}}) == {"a.b.c.d": 2}
def test_none_value(self):
assert flatten_dict({"a": {"b": None}}) == {"a.b": None}
def test_false_value(self):
assert flatten_dict({"a": {"b": False}}) == {"a.b": False}
def test_unicode_keys(self):
assert flatten_dict({"日本": {"語": 1}}) == {"日本.語": 1}
def test_single_level_empty_dict(self):
assert flatten_dict({"a": {}}) == {}
# ... 全20ケース ...
空辞書、None、False、Unicode、ネストされた空辞書まで網羅。 実装は最小限の再帰。
審判の判定
TestDrivenSamurai WIN(39 - 22)
「テスト20ケースは嘘をつかない。FunctionalPuristの『美しい敗北』を見た。」
👑 決勝:PerformanceFreak vs TestDrivenSamurai
課題:「レート制限付きAPIクライアントクラスを実装せよ」
ここまで来ると課題もガチです。
PerformanceFreak — トークンバケット方式
import time
class RateLimitedClient:
"""計算量:O(1) per request — トークンバケット方式"""
def __init__(self, max_requests: int, time_window: int):
self.max_tokens = max_requests
self.refill_rate = max_requests / time_window
self.tokens = float(max_requests)
self.last_refill_time = time.time()
def _refill_tokens(self):
now = time.time()
elapsed = now - self.last_refill_time
self.tokens = min(self.max_tokens, self.tokens + elapsed * self.refill_rate)
self.last_refill_time = now
def request(self, url: str) -> dict:
self._refill_tokens()
if self.tokens < 1:
wait_time = (1 - self.tokens) / self.refill_rate
time.sleep(wait_time)
self._refill_tokens()
self.tokens -= 1
print(f"Request to {url}")
return {"status": "success", "url": url}
O(1) per request。 浮動小数点でトークンを管理する美しい設計。30行のスリムなクラス。
TestDrivenSamurai — スライディングウィンドウ + テスト8ケース
# テスト先行(抜粋)
class TestRateLimitedClient:
def test_rate_limit_exceeded_waits(self):
"""制限超過時に実際に待機するか検証"""
client = RateLimitedClient(max_requests=2, window_seconds=1)
client.request("https://example.com/1")
client.request("https://example.com/2")
start_time = time.time()
client.request("https://example.com/3") # ← ここで待機が発生するはず
elapsed = time.time() - start_time
assert elapsed >= 0.9 # ← 実際の待機時間を検証!
def test_zero_request_limit(self):
"""最大リクエスト数0の場合"""
client = RateLimitedClient(max_requests=0, window_seconds=1)
# ... 待機が発生することを検証 ...
# 実装
from collections import deque
class RateLimitedClient:
def __init__(self, max_requests: int, window_seconds: float):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.request_timestamps = deque()
def request(self, url: str) -> dict:
current_time = time.time()
# ウィンドウ外のタイムスタンプを除去
while self.request_timestamps and \
current_time - self.request_timestamps[0] >= self.window_seconds:
self.request_timestamps.popleft()
# 制限到達時は待機
if len(self.request_timestamps) >= self.max_requests:
oldest_time = self.request_timestamps[0]
wait_time = self.window_seconds - (current_time - oldest_time)
if wait_time > 0:
print(f"Waiting {wait_time:.2f}s for rate limit recovery...")
time.sleep(wait_time)
# 再クリーンアップ
current_time = time.time()
while self.request_timestamps and \
current_time - self.request_timestamps[0] >= self.window_seconds:
self.request_timestamps.popleft()
self.request_timestamps.append(current_time)
print(f"Request to {url}")
return {"status": "success", "url": url, "timestamp": current_time}
assert elapsed >= 0.9 ——実際の待機時間まで検証する のはTDD侍にしかできない芸当。
決勝 審判の判定
| 採点項目 | PerformanceFreak | TestDrivenSamurai |
|---|---|---|
| 正確性 | 7 | 9 |
| コード品質 | 9 | 8 |
| キャラ一貫性 | 10 | 10 |
| 実用性 | 8 | 7 |
| 設計力(ボーナス) | 6 | 9 |
| 合計 | 40 | 43 |
🏆 優勝:TestDrivenSamurai(43 - 40)
称号:「確度の騎士」
審判コメント:
「この決勝は 『速度 vs 品質』 の本質的対立を体現した名勝負。PerformanceFreak の O(1) トークンバケットは純粋なパフォーマンスでは勝っていた。しかし APIレート制限という課題では 速度より正確性が重要 。TestDrivenSamurai は8つのテストケースでそれを 証明 した。テストを書かなかった速度狂が、テストに負けた。」
全試合結果
この検証で分かったこと
1. .agent.md の instructions は出力を劇的に変える
同じ課題でも、instructions の違いで 2行〜80行 まで出力が変わりました。これは単なるプロンプトの違いではなく、コーディング哲学そのものの注入 です。
| エージェント | Round 1 コード行数 | 特徴 |
|---|---|---|
| ConciseCoder | 2行 | ワンライナー芸 |
| VerboseExplainer | 40行 | 変数名が文章 |
| PerformanceFreak | 8行 | 計算量コメント付き |
| SafetyFirst | 60行 | assert地獄 |
| FunctionalPurist | 20行 | reduce原理主義 |
| DesignPatternKnight | 80行 | クラス4つ |
| TestDrivenSamurai | 50行 | テスト9 + 実装8 |
| CopyPastaChef | 25行 | 順番間違い |
2. 極端な哲学は自滅する
- VerboseExplainer → 変数名が長すぎて可読性が逆に低下
- SafetyFirst → 防御コードが多すぎて本質が見えない
- DesignPatternKnight → 小さな関数に4クラスはオーバーエンジニアリング
- CopyPastaChef → 「動けば正義」が動かなかった
3. 最強の .agent.md はバランス型
優勝した TestDrivenSamurai が強かった理由:
- テストで要件を定義 → 曖昧さがない
- 実装は最小限 → オーバーエンジニアリングしない
- エッジケースを事前に考える → バグが入りにくい
準優勝の PerformanceFreak が惜しかった理由:
- アルゴリズム選択は最適 → トークンバケットは正しい選択肢
- テストがなかった → 正確性を証明できなかった
4. 実務で使える .agent.md のコツ
この検証から得られた、良い .agent.md の書き方:
良い instructions の特徴
- 具体的な行動指針(「テストを先に書く」「計算量をコメントする」)
- 禁止事項より推奨事項を多く
- 哲学は1つに絞る(複数混ぜると中途半端に)
避けるべき instructions
- 極端すぎる制約(「コメント絶対禁止」「全行にコメント」)
- 矛盾する指示(「簡潔に」かつ「詳細に説明」)
- 手段の強制(「必ず reduce を使え」→ 最適でない場合がある)
マルチエージェント実行の裏側
この記事全体が、以下のマルチエージェント構成で作られています:
| エージェント | 役割 | 呼び出し回数 |
|---|---|---|
| メインエージェント | 進行管理・記事執筆 | - |
| Explore Agent(選手) | 各キャラでコード生成 | 8回(R1) + 4回(R2) + 2回(決勝) = 14回 |
| Explore Agent(審判) | コードレビュー・採点 | 3回 |
| 合計サブエージェント呼び出し | 17回 |
17回のサブエージェント呼び出し で1本の記事が完成。各呼び出しはステートレス(前の結果を知らない)なので、メインエージェントが全ての結果を統合して記事に仕上げています。
おわりに
「.agent.md の書き方でAIの出力って変わるの?」
めちゃくちゃ変わります。 2行のワンライナーから80行のデザインパターン祭りまで。
そして最強だったのは、派手な哲学ではなく 「まずテストを書く」 というシンプルな指示。TDD侍が全3ラウンドを通じて安定した高得点を叩き出し、決勝では速度狂との接戦を制しました。
この検証の結論
最強の .agent.md は「テスト先行で、実装は最小限に」。
当たり前のことを当たり前にやるAIが、結局一番強い。
...これ、人間のエンジニアにも同じことが言えるのでは?