テスト観点ライブラリはエージェントに効くのか。バグ検出、実装、設計の三段階で測る

  • #ai
  • #tpl
  • #testing
  • #context-engineering

前回の記事では、蓄積された決定の記録(ADR)がコーディングエージェントに効く条件を対照実験で確かめました。 私のリポジトリにはもう1つ、育ててきた蓄積があります。 テスト観点ライブラリ(Test Perspective Library、以下 TPL)。過去のバグから「再発しうる失敗パターン」を一般化して、1 観点 1 ファイルで貯めたものです。

TPL は ADR と役割を分けています。 ADR が「私たちはこう判断した」という過去の記録なのに対し、TPL は「これを検証すべき」という未来への指示です。 70 本まで育ち、新機能の設計時と受け入れテストの設計時に参照する運用を続けてきました。

では、この蓄積はコーディングエージェントの成果を実際に変えているのでしょうか。 前回と同じ方法で測りました。 リポジトリを複製し、片方から TPL 本体 70 本と参照導線を取り除きます。 導線とは、エージェント向けルールの該当節、PR テンプレート、仕様書からの相互リンクなど 200 ファイル超です。 その上で、同じ依頼を両方へ繰り返し与えます1。

サマリ

  1. TPL は、設計段階の仕様の完全性を上げる。 過去の失敗から蒸留した観点は「コードと一般知識のどちらにもまだ書かれていない仕様の穴」を塞ぐ。ここが TPL に残す価値の中心でした。
  2. ただし、コードレビューでのバグ検出には効かない。 消しても検出率は落ちない。エージェントは観点を自力で再導出する。
  3. 定型的な実装の網羅にも効かない。 既存コードが「どこを埋めるべきか」を列挙しているからで、エージェントはそれをそのまま写す。

以下では、この 3 行に至った実験を順に説明します。 2 つの null を先に見せます。 3 つ目の実験の差がなぜ本物と言えるかは、この null のメカニズムが説明してくれます。

実験 1:バグを埋めてレビューさせる

TPL の目的は再発防止です。 なら一番直接的な測り方は、TPL が守っているはずの失敗パターンと同型のバグを新規に仕込み、レビューで検出されるかを見ることでしょう。

的にした観点は「同じ語彙を複数の表現で重複して持つと、片方だけの更新で静かにずれる」というものです。 実例で言うと、ノードの種別(kind)ごとの見た目が、SVG レンダラと draw.io エクスポータという離れた 2 箇所で独立に列挙されています。 新しい kind を追加する変更を用意し、エクスポータ側だけ更新し忘れた状態を「機能追加の PR」として仕立てました。 型チェック、lint、既存テスト約 3,700 本はすべて通ります。 機械が捕まえられない以上、検出できるとすれば観点の知識だけ、という設計のつもりでした。

依頼は「このコミットを merge してよいかレビューして」だけで、観点への手がかりは入れません。

結果は、TPL の有無にかかわらず全試行が検出でした。 transcript を読むと、どちらの条件のエージェントも同じことをしています。 新しい kind の文字列をリポジトリ全域に検索し、その語彙が現れる場所をすべて洗い出して、エクスポータの列挙に欠けがあることを突き止めていました。

エントリの欠落は検索で見つかるのだから当然だ、という反論はありえます。 そこで第 2 ラウンドでは、エントリ自体は存在するが値が別の kind からのコピペでずれている、という検索では「配線済み」に見えるバグへ差し替えました。 これも両条件 5 試行ずつ、全部検出されます。 エージェントは表現を見つけるだけでなく、値まで突き合わせていました。 「全表現を同じ入力に通して結果を比べよ」という TPL のチェックリストに書いてある行為を、読んでいない条件のエージェントが自発的にやったことになります。

興味深いのは、TPL がある条件のエージェントは全試行で TPL を読みに行っていたことです。 参照導線は機能しています。 読んだ上で、読まなかった側と同じ検出率でした。 記録は在って、読まれて、それでも成績を変えない。 前回とは違う形の null です。

実験 2:実装の網羅で測る

レビューでだめなら予防ではどうか。 バグを仕込むのをやめ、「新しい kind を追加して、merge できる完成度に仕上げて」という実装依頼に変えました。 正解の配線先は 15 箇所あり、離れたエクスポータや VS Code 拡張を自発的に埋めるかを、TPL の有無で比べます。

これも差が出ませんでした。 全試行が、TPL なしでもエクスポータまで配線します。 transcript には理由がそのまま写っていました。 エージェントは実装を始める前に既存の kind がどこに現れるかを全域検索し、ある試行では過去の「kind 追加」コミットを git 履歴から見つけて、それを設計図として読んでいます。 既存コードそのものが「どこを埋めるべきかの完全なリスト」なので、TPL の出番がありません。

2 つの null は同じ1つのメカニズムに還元できます。 答えがコードの中に列挙されている問題では、徹底的なエージェントは観点を自力で再導出する。 検出と網羅はどちらも、語彙の検索と前例の鏡写しという、エージェントが指示なしでやる作法の射程内でした。

ここで実験を打ち切っていたら、「TPL は要らない」が結論になっていたはずです。 しかし私が TPL の効果を実感してきたのは、レビューや実装の場面ではありませんでした。 設計の段階で、バグになりそうな仕様の穴を先に潰せたときです。 機能 A の強化が機能 B に波及するとき、その波及はまだコードのどこにも書かれていません。 コードに答えのない場所でなら、測り方を変えれば差が出るのではないか。

実験 3:設計文書を書かせる

依頼を「実装するな。DesignDoc と受け入れ基準を書け」に変えました。 題材は、既存機能との新しい相互作用を持ち、リポジトリ内で鏡写しにできる前例がない機能から選びます。 1 つ目は「図の中で 1 つのコンテナだけを inline 展開する」という表示機能。 2 つ目は「開き直したときに前回の状態を復元する」というセッション復元です。

採点は、該当する TPL のチェックリストから起こした 6 項目について、書かれた設計文書が「実装者が迷わない規定」を置いているかをブラインドで判定します2。

今度は差が出ました。 1 つ目の題材では、6 項目平均で 5.2 対 4.2。 2 つ目でも 5.7 対 4.0 で、TPL のある側が上回ります。

平均より雄弁なのは、差の出た項目と出なかった項目の分かれ方です。

「同時に展開できる数に上限を置く」「畳み直す操作を決める」といった項目は、両条件とも全試行が書きました。 良い設計者なら常識で書ける項目です。 一方で分かれたのは「展開中の要素が二重に描画されず、脱落もしない、を受け入れ基準に置く」という項目です。 TPL がある条件で 5 試行中 5 回、ない条件で 0 回でした3。 この観点は、過去に実際に起きたレイアウトのバグから蒸留され、TPL だけに書かれているものです。 TPL がある条件の全試行が該当の TPL を読み、読んだ観点がそのまま仕様に現れていました。

脚注に書いたとおり規模は小さいのですが、2 つ目の題材が同じ分かれ方を再現します。 復元機能で差が出たのは「壊れた保存データと、保存が無いことを区別する(壊れたデータを空として上書きしない)」といった、過去の障害由来の項目でした。 そしてここで、境界線の正体を映す観察がひとつ取れました。

復元機能には「アプリの初期化処理が、復元した状態を巻き戻してしまう」という有名な罠があり、これは過去のバグ修正のときにコードのコメントとして詳しく書き残されていました。 全試行(TPL を消した条件を含む)がこのコメントに到達し、この項目は両条件とも全員が仕様に書いています。 同じラウンドの中で、コードに書かれた観点は TPL なしで拾われ、コードに書かれていない観点だけが TPL の有無で分かれました。

境界線を運用に翻訳する

3 つの実験を貫く境界線は、こう言えます。

観点は、コードへ書かれた瞬間に TPL の役目を終える。TPL が効くのは、教訓がコードと一般知識のどちらにもまだ存在しない期間だけ。

これは TPL の価値を狭める話のようで、実は運用を軽くする話です。

まず、TPL をエージェントのレビューの網羅性のために維持する必要はありません。 検出も実装の網羅も、エージェントは観点なしでやり切ります。 「レビュー前に TPL を照合せよ」という運用を組み込むことに、少なくとも成績の面での見返りはありませんでした。

ただしこれは、レビュアーがエージェントである場合の話です。 観点なしの検出が成立したのは、新しい語彙をリポジトリ全域に検索し、数分で全表現を洗い出すという、人間にはない力技によってでした。 diff を中心に読む人間のレビュアーには、この null は外挿できません。 人間が読むチェックリストとしての TPL の価値は今回の実験では測っておらず、むしろ残っていると見るほうが自然です。 バグを観点へ一般化して書く行為そのものが失敗を構造として理解させる、という書き手側の価値も同様に、この実験の外にあります。

TPL の置き場所は設計フェーズの入口です。 私のリポジトリでは「DesignDoc 作成時に該当する観点を確認する」という導線を常設ルールに書いており、実験でも TPL がある条件の全試行がこの導線経由で観点に到達していました。 前回の ADR の実験で「確認を手続きに埋める一文が最大の梃子」と書きましたが、TPL でも同じ構図です。

そして、観点がテストやコメントとしてコードに定着したら、その観点の TPL は防波堤としては引退しています。 正確には、境界を分けるのは「コードのどこかに書かれているか」ではなく、「そのタスクでエージェントが読む範囲に書かれているか」です。 初期化の罠が両条件で拾われたのは、復元機能の設計なら必ず読む hook に書かれていたからでした。 次の同型機能では、定着した教訓の在り処(前の機能のテストや文書)は設計対象から遠く、読む範囲に入りにくくなります。 だから TPL は、観点がコードに定着した後も、次の同型機能の設計には効きます。 ただ「TPL が薄いとレビューが穴だらけになる」という心配は、エージェントがレビュアーである限り、実験からは支持されませんでした。 蓄積を止める理由ではなく、蓄積の目的関数を「設計時に思い出せること」へ寄せる理由です。

限界

前回と同じく、個人開発の 1 リポジトリ、1 モデル、各セル 3〜5 試行という規模です。 効果量ではなく、条件間の分かれ方の一貫性を確かめたものと読んでください。

レビュアーが徹底的な frontier モデルであることも前提です。 時間制約のある CI レビューや、diff テキストしか見ない形式のレビュー、人間のレビュアーが相手なら、実験 1 と 2 の null がそのまま成り立つとは限りません。 また、的にした失敗パターンはコード読解で追い詰められる構造的なものに偏っています。 タイミングや環境差のような、再現手順を知らないと気づけない観点は測れていません。 私のリポジトリでは受け入れテストのシナリオ作成でも TPL を参照し、そこから e2e テストを起こす運用をしています。 この連鎖(観点がシナリオを経て回帰テストの防御力になるか)は今回の実験の外にあり、別の記事で扱う予定です。

まとめ

前回の実験は「決定の記録は、衝突の瞬間に取り出させて初めて効く」で締めました。 今回の実験は、その続きへこう足すことになります。

蓄積された観点の価値は、エージェントが賢くなるほど「コードから再導出できる部分」から順に消えていくはずです。 最後まで残るのは、まだどこにも書かれていない教訓の置き場としての価値で、それが効く場面は設計の入口に集中している。

実験台にした karasu は公開リポジトリなので、TPL の実物(docs/test-perspectives/)も読めます。 実験の環境、条件定義、依頼プロンプト、全ラウンドの結果表は資料編にまとめてあります。

Footnotes

  1. 実験はすべて headless の Claude Code で、モデルは Claude Opus 4.8 に固定。試行ごとにリポジトリを初期状態へ戻し、TPL を消した条件は git 履歴からも痕跡を除いています(前回、削除操作が履歴から発掘された反省を反映しました)。バグ検出 16 回、実装 6 回、設計 16 回の、のべ 38 回です。 ↩

  2. 判定は条件を伏せた LLM に 6 項目の基準と根拠引用を要求する形で行い、弁別項目は全件、原文と機械的に突き合わせて裏を取りました。採点対象は最終応答だけでなく全応答の連結です。最終応答だけを見ると本体を取り逃す誤判定を今回の採点で実際に踏み、訂正しました(経緯は資料編)。 ↩

  3. 5/5 対 0/5 は Fisher の正確検定で p ≈ 0.004(片側)。とはいえ規模が小さいので、効果量ではなく分かれ方の一貫性を見てください。 ↩