Skip to content
ソフトウェア自動開発のメモ帳

テスト駆動でプロンプトを生成する──実験データが示す可能性と限界

プロンプトが「ソースコード」になりつつある

AI エージェントの system prompt(エージェントの基本的な振る舞いを定義する指示文)が、いつの間にか「コード」と呼ぶべき規模になっている。

Claude Code や Codex CLI のようなツールでは、この system prompt が数千行に及ぶ。機能ごとに分かれたサブエージェント(特定の役割を担う下位のエージェント)があり、それぞれにツール呼び出しのルールやエッジケースの処理方針がある。もはや「プロンプトを書く」ではなく「プロンプトを設計する」という表現のほうが実態に近い。

ところが、この「コード」にはソフトウェア開発で当たり前のインフラが存在しない。型チェックもない。リンターもない。テストもない。一行の変更が別の振る舞いを静かに壊す──いわゆるサイレントリグレッション(気づかないうちに以前の動作が壊れること)が、デプロイして初めて発覚する。

2026 年 3 月に公開された論文「Test-Driven AI Agent Definition(TDAD)」は、この問題に正面から取り組んでいる。コードにはテストを書くのに、プロンプトにはテストを書かない。仕様を定義し、テストを書き、AI にプロンプトを書かせる──この論文はそれを実際に試し、データを出した。

その手法と実験結果を見ていこう。

普通に書いたプロンプトはテストに通らない

具体例で考えよう。論文のベンチマークに含まれる経費承認エージェントを例にする。普通、エージェントにはこうプロンプトを書く。

あなたは経費承認アシスタントです。
使えるツール: get_policy, get_receipt, fx_convert, submit_expense,
              open_compliance_case, respond
やるべきこと:
- 承認判断の前に、必ずポリシーを確認すること。
- ポリシーが求める場合はレシートを要求すること。
- 禁止項目(アルコール、接待、私物)があれば却下し、
  違反報告を起票すること。
- レシートの通貨とポリシーの通貨が異なれば換算すること。
- 払い戻し金額はポリシーの上限から算出すること。
  金額をでっちあげないこと。

ツール一覧、基本方針、やるべきこと。prompt engineering(プロンプトの書き方を工夫して AI の精度を上げる技法)の経験があれば見慣れた形式だろう。

ここで「プロンプトをテストする」とはどういうことか。コードのユニットテストと同じ発想だ。エージェントに特定の入力(たとえば「経費申請します」や「アルコールを含むレシート」)を与え、期待通りのツール呼び出しや判断をするか検証する。

このプロンプトでそのテストを走らせると、通過率は 0〜90% しかない。 「承認判断の前に、必ずポリシーを確認すること」は方針としては正しいが、各ステップで具体的に何を確認し、どのツールを呼び、結果をどう構造化するかが書かれていない。 人間が「当たり前」と思っている判断を、LLM(大規模言語モデル)は必ずしも同じように推論しない。

では、テストに通るプロンプトを人間が書き足せばいいのか?──それは数千行のプロンプトでは現実的ではない。この論文のアプローチは、この作業を自動化することだ。

プロンプトの代わりにテストを書く

テスト駆動でプロンプトを開発するとしたら、何から始めるのか。プロンプトの代わりに、プロンプトが満たすべき振る舞いをテストとして書く。

この経費承認エージェントは何をするのか。ユーザーの申請メッセージを受け取り、社内ポリシーと突き合わせて承認・却下を判断する。その処理には複数のステップがある──情報が揃っているか確認し、禁止項目をチェックし、金額を計算し、最終判定する。これをそのままテストにする。

test("不完全な申請には聞き返す", async () => {
  const res = await agent.run("経費申請します");
  expect(res.message).toContain("役職");
  expect(res.message).toContain("レシート番号");
});

test("禁止項目があれば却下する", async () => {
  const res = await agent.run(
    "レシート RCP-123 の経費申請です。エンジニア、US",
    { receipt: { items: ["ディナー", "アルコール"] } }
  );
  expect(res.toolCalls).toContainEqual("open_compliance_case");
  expect(res.decision).toBe("REJECT");
});

test("金額をでっちあげない", async () => {
  const res = await agent.run(
    "レシート RCP-456 の経費申請です。マネージャー、JP",
    { receipt: { amount: 15000, currency: "JPY" } }
  );
  // 払い戻し金額がツール出力から導出可能か検証
  expect(res.reimbursement).toBeLessThanOrEqual(
    res.toolOutputs.policy.maxAmount
  );
});

Jest(JavaScript のテストフレームワーク)で関数にテストを書くのと同じだ。入力を渡して、出力が期待通りかチェックする。違うのは、テスト対象が関数ではなくプロンプトだということだけだ。エージェントが何をすべきかを考えた時点で、何をテストすべきかは決まっている。

テストに通るまで AI にプロンプトを書き直させる

仕様とテストが揃ったら、あとはシンプルだ。最初のプロンプト(シードプロンプト)をテストにかけ、失敗したら AI にプロンプトを修正させる。これを繰り返す。

async function compilePrompt(seed: string, tests: Test[]) {
  let prompt = seed;
  for (let i = 0; i < 10; i++) {
    const results = await runTests(prompt, tests);
    if (results.allPassed) return prompt;
    prompt = await llm.chat(`
      このプロンプトで以下のテストが失敗しました:
      ${JSON.stringify(results.failures)}
      テストに通るようにプロンプトを修正してください。
    `);
  }
  throw new Error("コンパイル失敗");
}

入力はシードプロンプトとテスト。出力はテストを通るプロンプト。論文の実験では、テストが全部通るまで平均 3.1 回ループしている。

具体的に何が変わるのか。シードプロンプトにはこう書いてあった。

- 禁止項目(アルコール、接待、私物)があれば却下し、
  違反報告を起票すること。

生成後のプロンプトでは、この一行がこうなる。

レシートの各項目を禁止リスト(アルコール、接待、私物)と
照合すること。禁止項目が見つかった場合:
1. open_compliance_case を呼び出す。
   引数: レシート番号と違反内容の説明。
2. respond を呼び出す。
   引数:
   - decision: REJECT_DISALLOWED
   - evidence: 検出された禁止項目
   - user_message: 却下理由の説明
禁止項目が検出された場合、submit_expense を呼んではならない。

どのツールを、どの順序で、どのパラメータで呼ぶか。呼んではいけないツールは何か。テストが要求する精度まで、プロンプトが自動的に詳細化される。 論文ではこの工程を「コンパイル」と呼んでいる。

テストに出る問題だけ解けるようにならないのか

テストに通るようにプロンプトを書き直すなら、テストに出る問題だけ解けるようになって、実際の場面では使えないのではないか。

機械学習では、訓練データに含まれるパターンだけを丸暗記して未知のデータに対応できなくなる「過学習」という問題がある。同じことが起きないのか。だがここでプロンプトを書き直しているのは LLM だ。テスト「アルコールがあったら却下する」が失敗したとき、LLM は「アルコール→却下」とだけ書くのではなく、「レシートの全項目を禁止リストと照合する」という汎用的な指示を書く。自然言語の意図を理解して書き直すので、個別のテストケースへの丸暗記にはなりにくいようだ。

実験でもこれは確認されている。テストに含まれていないケースでも 97% 正しく動作した(4 種類のエージェントでの実験結果)。

現時点の限界──複雑な仕様では成功率が下がる

この手法はまだ概念実証(「動くことを示す」段階の実験)だ。実験で使われたエージェントは 10〜14 の分岐ステップを持つ規模で、著者自身も「50 以上の分岐ステップへのスケーリングは未検証」と認めている。

実験データにも限界が見える。初回の仕様では約 92% の成功率だったが、仕様が変わってテストを更新したとき(たとえば「高額経費にマネージャー承認を追加」)、成功率は 58% に低下した。著者は、仕様の曖昧な記述がテスト間の矛盾を生んだことが原因だと分析している。ボトルネックは生成の仕組みではなく、仕様の品質にあるようだ。

プロンプトに「テストを書く」という選択肢

この研究が示しているのは、プロンプトにもテストを書けるという事実だ。仕様が複雑になると全テストを通せないケースもあり、万能ではない。だが、プロンプトの品質を「動かして確認する」以外の方法で検証できるというのは、現時点でも実用的な価値がある。

数千行のプロンプトを手作業で保守している現場にとって、「変更がどこを壊したか」をテストで検出できるだけでも大きい。自動生成までいかなくても、プロンプトにテストを書くという発想自体に意味がある。


論文情報