多くのエージェント、ひとつのアーキテクチャ:そのうち 2 つが同じシステムを変えたとき何が起きるか

エージェントが 3 つ、pull request が 3 本、リトライ処理をどこに置くべきかについての妥当な意見が 3 つ。

1 つはリトライを HTTP クライアントに置きます。1 つはハンドラーをラップします。1 つはキューを追加してそれを捌きます。3 本のうちどれか 1 本だけを読めば、あなたは approve するでしょう。同じ午後にまとめて読むと、システムが 3 か所でリトライするようになっていて、backoff ポリシーが 3 つあって、しかもそれを決めた人は誰もいないことに気づきます。

これが、1 つのコードベースに対して複数のエージェントが同時に働くようになった瞬間に現れる問題の形です。どのエージェントも局所的には正しい。不整合はグローバルで、最後にレビューする人にしか見えません。

なぜルールファイルはこれを裁定できないのか

標準的な答えはルールファイルです:CLAUDE.mdAGENTS.md.cursor/rulesそれらが古びていく理由については書きました。そして古びることは、ここでは小さいほうの問題です。大きいほうは、ルールファイルには裁定ができないことです。

それは散文です。同じ段落を渡された 2 つのエージェントは、その段落について 2 つの異なる読みを生み出します。どちらも筋が通っていて、その読みが出会う地点はどこにもありません。リポジトリごとに存在するので、サービス境界についてのルールが片方のリポジトリにあり、その境界の反対側のサービスは別のリポジトリにあります。そして状態を持ちません。別のエージェントが 40 分前に何かを提案したことを知ることはできません。それはファイルであり、ファイルは何も知らないからです。

裁定に必要なのは、よりよい散文ではありません。両方のエージェントが読み書きし、決定を保持でき、意見の食い違いに気づける共有物です。

モデルが与えてくれて、ドキュメントが与えてくれないもの

アーキテクチャモデルとは、クエリできる要素と関係です。システム、container、component、それらの間のエッジ、そしてそのエッジにぶら下がる、設計を設計たらしめるもの:境界を意図的なものにした決定、通知すべきオーナー、consumer が依存している contract。

そこから 3 つのことが出てきます。どれも意図ではなく仕組みです。

すべてのエージェントが同じバイト列を読める。 generate-context アクションは、モデルから archyl.txt を書き出します。デフォルトは markdown で、オプションでリポジトリへ自動 commit できます。9 つのエージェントが 1 つの生成ファイルを読むのは、9 つのエージェントがそれぞれ散文のドキュメントを言い換えるのとは別の状況です。賢いやり方ではありません。ただ共有されているだけです。

食い違いは入口で捕まえられる。 conformance-check アクションは、pull request が変更したファイルに対してアーキテクチャのルールを実行し、違反をインラインで注釈し、選んだ深刻度でチェックを失敗させます。fail-onerrorwarningnone を取ります。「リトライはクライアントに置く」が文章ではなくルールであれば、それを別の場所に置いた 2 つのエージェントは、レビューではなく CI でそれを知ります。

決定に住む場所がある。 ADR は、それが制約する C4 要素に紐づきます。そのキューが存在する理由はキューの上にあり、どのエージェントも見たことがない 3 月の Slack スレッドの中にはありません。

私たちが間違えていた部分

ここから、これは素敵なアイデアについてのブログ記事ではなくなります。

エージェントはモデルを直接変更しません。変更リクエストを開きます:人がレビューしてマージする提案です。変更リクエストが作られるとき、archyl はそれがどのモデルバージョンに対して作られたかを記録します。マージされるとき、バージョンが上がります。まさにこの問題のために欲しくなる機構です。

私たちはその 2 つを一度もつないでいませんでした。

ベースバージョンは作成時に書かれ、どこでも読み返されていませんでした。つまり、次の順番が、静かに、完全に、通ってしまっていました。

  1. エージェント A とエージェント B が両方ともモデルを読む。両方ともバージョン 7 を見る。
  2. A が変更リクエストを開く。B が変更リクエストを開く。両方ともバージョン 7 に基づく。
  3. A の変更リクエストがマージされる。モデルはバージョン 8 になる。
  4. B の変更リクエストがマージされる。それはもう存在しないモデルに対して書かれていた。

警告もなく、コンフリクトもなく、履歴に何の記載もありません。2 つ目の変更群が 1 つ目の上に着地し、両者が矛盾していれば、その矛盾がドキュメント化されたアーキテクチャになります。これはコンフリクト検出を取り外したマージであり、「多くのエージェント」という話全体が防ぐはずだった、まさにその失敗です。

なので直しました。ベースバージョンがプロジェクトのものと一致しなくなった変更リクエストをマージすると、いまは 409 Conflict と、両方のバージョンを名指しするメッセージで失敗します:

architecture request is based on version 7 but the model is now at version 9;
rebase the request and merge again

ただし、これを安全にしているのは比較ではありません。同じ瞬間に到着した 2 つのマージは、どちらも比較を通過し、どちらも進んでしまいます。その隙間を塞ぐのは、バージョンのインクリメント自体を条件付きにすることです:マージは、モデルがまだ、その変更リクエストが作られたときのバージョンにある場合にだけ、モデルを前に進めます。すでに動いていれば、マージは進めるものを何も見つけられず、全体がロールバックし、変更は 1 つも適用されません。手前の比較は、エラーメッセージがどれだけ遅れているかを伝えられるようにするためだけに存在します。

400 ではなく 409 であることは、見た目より重要です。400 でリトライするエージェントは永遠にループします。壊れたリクエストは壊れたままだからです。409 はその逆を言います:あなたが送ったものは正しく、適用可能でなくなっただけだ、と。現在のモデルを取得して、もう一度やってください。

これでもまだできないこと

4 つの限界。どれも確認できます。

エージェントは何もマージしない。 変更リクエストをマージする MCP tool はありません。エージェントは提案し、人がレビューしてマージします。これは意図的な境界で、外す予定はありません。ただしそれは、ループが完全に自動ではないということであり、自動であるかのように設計すべきではないということです。

コンフリクト検出は粗い。 バージョンはプロジェクト単位で、要素単位ではありません。同じプロジェクトの本当に無関係な隅を触っている 2 つのエージェントでも、バージョンの上で衝突します。それは間違えるなら安全な方向ですが、間違いは間違いです。

コンテキスト取得は字句的。 find_relevant_context は、名前、説明、タグ、パスの単語の重なりで要素をスコアリングします。embedding も同義語展開もないので、「checkout」についてのタスクが OrderProcessor という component を浮かび上がらせることはありません。利点は本物です(決定的で、トークンコストがなく、コードがどこにも送られない)。ただしそれはマッチングであって、理解ではありません。

ルールは自分では書かれない。 上のすべては、誰かが「リトライはクライアントに置く」を適合性ルールとして表現したことを前提にしています。空のルールセットは何も捕まえません。エージェントをいくつ走らせていても同じです。

今週これについて何をするか

自分がどこにいるかを知るために、何かを買う必要はありません。

あなたのチームがエージェントの書いた pull request を 2 本以上マージした直近の 1 週間を選んでください。順番にではなく、まとめて読んでください。そのうちのどれか 2 本が、同じ決定を別々のやり方でしていないかを問い、それから、いまの自分たちの構成で、それを教えてくれたはずのものは何かを問うてください。

答えが「レビュアーが気づいた」なら、それはレビュアーが 9 本読む日まで機能します。


変更リクエスト、適合性ルール、C4 モデルは archyl の一部です。GitHub Actionsagent skills はオープンソースです。関連記事:なぜあなたのエージェントにはルールファイルがあってモデルがないのか変更リクエストの仕組みモデルを正直に保つ方法