手順

AIエージェントへのタスクの出し方:一度に一機能、機能リスト、完了基準

更新日: · Skillaria.Top

AIエージェントには一度に一つのタスクを渡します。作業中の機能は常に一つだけで、その機能には観察可能な振る舞いと実行可能な検証コマンドがあります。途中で見つかった問題はエージェントが直すのではなくバックログに記録します。全機能の状態は機械可読なリスト(例:feature_list.json)に保存され、「完了」への遷移は検証スクリプトだけが行えます。

エージェントが何でも一度にやろうとする理由

会員登録機能を頼むと、数時間後には差分に12ファイルの変更と800行の新規コードが並んでいます。ユーザーモデル、メール認証、メール送信サービスの接続、パスワードのハッシュ化、作り直したミドルウェア、移動されたテストです。どれも強く絡み合い、どのシナリオも最後まで通りません。次のセッションはこのコードを開いても、どこから手を付ければいいか分かりません。

原因は「モデルが悪い」ことではありません。人間には「今日はここまで」という感覚や疲労があり、それが作業の範囲を自然に制限します。エージェントにはそのどちらもありません。「ついでにエラー処理も直しておきます」と書くのは数十トークンで済みますが、プロジェクトにとってのコストは現実のものです。注意は分散し、結合度は上がり、完成したものは残りません。

単純な計算もあります。C を有効な文脈の容量、k をエージェントが同時に抱えるタスク数とします。各タスクには約 C/k の推論リソースしか割り当てられません。その値が一つのタスクを完了するのに必要な量を下回ると、どのタスクも完了しなくなります。だからAIエージェントへのタスクの出し方の正しい答えは、プロンプトの言い回しではなく制約から始まります。作業中のタスクは一つだけです。

一機能ルール(WIP=1)

WIP は work in progress、つまり作業中のタスク数です。エージェントに対してはこれを1にします。どの瞬間も有効な機能は正確に一つだけで、次の機能は現在の機能が検証を通過してから始めます。このルールはプロジェクトの指示ファイル(AGENTS.md または CLAUDE.md)に書き、すべてのセッションでエージェントが見るようにします。

## 作業ルール
- どの時点でも `active` の状態にある機能は「ちょうど一つ」です。
- 次の機能は、現在の機能が `passing` に移ってからのみ始めます。
- 「ついでに」のリファクタリング、名前変更、ディレクトリの再編、無関係なバグ修正は禁止です。問題を見つけたら `BACKLOG.md` に書いて先に進みます。
- 有用であっても、有効な機能のスコープ外の変更は違反とみなします。

最後の項目は見た目以上に重要です。エージェントは「ついで」の作業を本当に有益だと信じていますし、実際に有益なことも多いのです。しかし、スコープ外の有益な修正も二つのタスクを一つの差分に混ぜてしまい、後から別々に巻き戻したり検証したりできなくなります。

BACKLOG.md:「ついで」の行き先

「ついで修正」の衝動は単に禁止するだけでは足りず、正当な逃げ道が必要です。その逃げ道が BACKLOG.md です。エージェントは見つけた問題を一、二行でそこに書き、現在の機能に戻ります。情報は失われず、スコープも広がりません。バックログがなければ、一機能ルールは遅かれ早かれ回避されます。「バグを黙って残す」ほうが違反よりも悪いと、エージェントには思えてしまうからです。

# BACKLOG.md
- [ ] middleware/errors.py: /auth と /orders でエラーの形式が違う
- [ ] tests/: ユーザーのフィクスチャが3つのファイルで重複している
- [ ] 登録時のメール認証は必要か? — 確認すること

完了基準:「完了」とはコマンドのこと

問題の後半は、エージェントが自分でタスクの完了を決めてしまうことです。「コードは正常に見える」「関数は存在する」「コンパイルエラーはない」は、いずれも印象であって事実ではありません。エージェントにとってのタスクの完了基準は、合格か不合格かが一意に定まる実行可能なコマンドであるべきです。

F01: ユーザー登録
verification: pytest tests/e2e/test_auth.py::test_register_flow
state: active

弱い基準と強い基準の違いは例で見ると分かりやすくなります。

弱い基準強い基準
コードがコンパイルできるpytest tests/e2e/test_cart.py -q が通る
構文エラーがない/cart への curl が 200 を返す
関数 addToCart が存在する商品追加後、GET /cart にその商品と正しい合計が表示される
正しく見えるnpx playwright test cart.spec.ts が通る

良い検証コマンドは決定的です(同じコードなら同じ結果になります)。人の手を借りずに実行でき、ユーザーや API クライアントが見るのと同じ外側から振る舞いを確認します。そのようなコマンドを書けないなら、タスクの定義が曖昧だというサインです。

分解:機能はどのくらいの大きさにすべきか

「一度に一機能」の次によく聞かれるのは、何を機能と呼ぶかです。広すぎるタスクは一つのセッションでは終わらず、狭すぎるタスクは起動、文脈の読み込み、検証の負担に埋もれます。

  • 広すぎる:「カートを実装する」。1週間の作業で、シナリオは数十に及びます。
  • ちょうどよい:「ユーザーは商品をカートに追加し、リストで確認できる」。観察可能な振る舞いが一つで、検証シナリオも一つです。
  • 狭すぎる:「CartItem モデルに quantity フィールドを作る」。これは実装の一ステップであって振る舞いではなく、単独で検証するのは意味がありません。

大きさの目安は、一機能=一セッション=意味のある一コミット=エンドツーエンド(E2E)シナリオ一つ、です。セッションに収まらないなら、さらに分けます。その機能に独立したシナリオを書けないなら、小さすぎるので隣の機能と統合します。

例:「カート」タスクの分解

広い要件を、それぞれ一つの振る舞いと独自の検証コマンドを持つ原子的な単位に分けます。

  1. 商品をカートに追加し、リストに表示されることを確認する。
  2. 商品の数量を変更すると、合計が再計算される。
  3. カートから商品を削除する。
  4. ゲストのカートはログイン後も保持される。
  5. 在庫のない商品は追加できず、分かりやすいエラーが出る。

分解の最終チェックは、どの単位も「ついでに」別の単位に触れる必要がないことです。項目4のために項目1を書き直す必要があるなら、順序や境界の選び方が良くありません。

機能リスト:feature_list.json

一機能ルールはセッションの中の順序を保ちます。しかしセッションをまたいで、何が完了したかは誰が覚えているのでしょうか。チャットに「認証とカタログは完成、カートはほぼ完成、ただしバグあり」と書いても、新しいセッションには何も伝わりません。「ほぼ」が何を意味するのか、どのテストが通ったのか、決済を何が止めているのかが分からないからです。毎回の新しいセッションは、同じ車を運転する新しいドライバーのようなものです。メモが前任者の頭の中だけにあったなら、新しいドライバーは何も知りません。

そこで状態を外に出し、機械が読めるファイルにします。このコースでは .agent/feature_list.json を使います。

[
  {
    "id": "F01",
    "behavior": "ユーザーはメールアドレスとパスワードで登録し、そのままログインする",
    "verification": "pytest tests/e2e/test_auth.py::test_register_flow",
    "state": "passing",
    "evidence": "a3f9c12: 1 passed in 2.31s"
  },
  {
    "id": "F02",
    "behavior": "ユーザーは商品をカートに追加し、リストで確認できる",
    "verification": "pytest tests/e2e/test_cart.py::test_add_item -q",
    "state": "active"
  },
  {
    "id": "F03",
    "behavior": "カード決済による注文の支払い",
    "verification": "pytest tests/e2e/test_payment.py -q",
    "state": "blocked",
    "blocker": "決済ゲートウェイのテストキーがない"
  }
]

各項目には必須の三つのフィールドがあります。外から観察できる振る舞いの説明、人手を介さず実行できる検証コマンド、そして状態です。任意の四つ目のフィールドは evidence で、コミットのハッシュと検証の出力を記録します。これは passing への遷移が実際に行われたことの証拠です。

なぜ Markdown の箇条書きではなく JSON なのか。読むのはエージェントだけでなく、スクリプトも読むからです。スケジューラは not_started の中から次の機能を選び、検証器は検証コマンドを取り出し、セッションのレポートが自動的に組み立てられ、状態の分布からプロジェクトの健全性の指標が得られます。機能リストはハーネスの背骨であって、メモ書きではありません。

四つの状態と許可された遷移

遷移いつ
not_started → active機能に着手するとき。ほかに active の機能がないこと
active → passing検証スクリプトが終了コード 0 で完了したとき
active → blocked外部のブロッカーがあるとき。理由を blocker に記録する
blocked → activeブロッカーが解消したとき

ほかの遷移はありません。特に、検証を迂回して passing に入る道はなく、「ほぼ完了」という状態もありません。

passing ゲート:エージェントは要求できるが、宣言はできない

この仕組み全体の中心ルールは、エージェントが state フィールドを手で編集しないことです。エージェントは検証を実行できますが、結果を自分で宣言することはできません。宣言するのはスクリプトです。「エージェントが報告した」と「スクリプトが確認した」の違いは、テキストと事実の違いです。このルールも指示ファイルに書きます。

## 機能リストのルール
- ファイル: `.agent/feature_list.json`。スコープに関する唯一の正しい情報源です。
- `active` の状態にある機能はちょうど一つです。
- `state` を手で編集しないこと。変更するのは `scripts/verify.sh` だけです。
- 機能を `passing` にするには `./scripts/verify.sh F02` を実行し、終了コード 0 を得ます。
- 機能がブロックされたら `blocked` に移し、必ず `blocker` を書きます。
- 新しい機能は `not_started` にのみ追加し、必ず検証コマンドを付けます。

スクリプト自体は二段階で検証します。まずプロジェクト全体の実行(機能がほかの部分を壊していないこと)、次にその機能の検証コマンドです。ローカルで成功しても、全体に回帰があれば成功とはみなしません。

#!/usr/bin/env bash
# passing を設定する権限を持つ唯一のコンポーネント。
set -uo pipefail
FID="$1"
FILE=".agent/feature_list.json"

CMD=$(jq -r --arg id "$FID" '.[] | select(.id==$id) | .verification' "$FILE")
if [ -z "$CMD" ] || [ "$CMD" = "null" ]; then
  echo "FAIL: $FID の検証コマンドがありません"; exit 2
fi

echo "== 全体チェック =="
make check || { echo "FAIL: 全体チェックが赤いため、$FID は passing にできません"; exit 1; }

echo "== 機能チェック: $CMD =="
if OUT=$(eval "$CMD" 2>&1); then
  SHA=$(git rev-parse --short HEAD)
  LAST=$(printf '%s\n' "$OUT" | tail -n 1)
  TMP=$(mktemp)
  jq --arg id "$FID" --arg ev "$SHA: $LAST" \
    'map(if .id==$id then .state="passing" | .evidence=$ev else . end)' \
    "$FILE" > "$TMP" && mv "$TMP" "$FILE"
  echo "PASS: $FID"
else
  printf '%s\n' "$OUT"
  echo "FAIL: $FID"; exit 1
fi

テキストに書いた「state を編集するな」は、依然としてお願いにすぎません。制約にするには、書き込みを機械的に締める必要があります。例えば、evidence をコミット履歴と照合するCIチェックを置く、あるいはツール側でエージェントの権限を制限します。原理はハーネス全体と同じです。重要なルールは仕組みで裏打ちします。

エージェントがタスクを最後までやり切っているかの測り方

魅力的に見える指標はセッションあたりのコード量ですが、実際には逆効果です。大きな差分は、エージェントがいくつもの方向に散らばった兆候であることが多いのです。数えるべきなのは行数ではなく、passing になった機能の数です。

講座で紹介されている便利な指標は VCR(Verified Completion Rate、検証済み完了率)です。検証済みのタスク数を有効化したタスク数で割ったものです。ルールは単純で、VCR が1未満のあいだは新しいタスクを有効化しません。これはハーネスがエージェントにかける逆方向の圧力であり、未完了の作業を感覚ではなく目に見える数字にする方法でもあります。

エージェントにタスクを出すときのよくある間違い

  • 検証コマンドのないタスク。「会員登録を作って」だけでシナリオがないと、エージェントはどこで止めるかを自分で決め、たいてい間違えます。
  • 一つの依頼に複数の機能。「カートと決済を作って」は有効なタスクが二つあることになり、どちらも最後までやり切れません。
  • 状態がチャットにだけある。会話に書かれた進捗はセッションの終わりとともに消えます。ファイルに置く必要があります。
  • 検証の代わりに自己申告。「エージェントがテストは通ったと言った」は、スクリプトの終了コードと同じではありません。
  • バックログがない。「ついでは禁止」なのに、見つけたことを書く場所がないと、エージェントはそのルールを回避します。

一機能では足りなくなったとき

「一つの機能、一つのセッション、一つの検証」というやり方は、一人のエージェントが一つのループで動く場合にうまく機能します。タスクが増え、独立した分岐が現れると、複数のループからなるシステムに広げます。計画ノード、実装ノード、検証ノードがあり、その間をつなぐ辺は並列にも条件付きにもなり、検証に失敗したら実装に戻ります。しかしそこでも、各実装ノードは同じルールに従います。一つのタスク、実行可能な完了基準、そしてエージェントの頭の外にある状態です。グラフは規律を取り消すものではなく、その上に構築されるものです。

同じ解説には重要な警告もあります。ループは与えられた指標を忠実に最適化します。完了基準の選び方が悪いと、例えば「問題が解決された」の代わりに「チケットが閉じられた」を基準にすると、エージェントはタスクではなく基準を満たすことを学んでしまいます。だからこそ、振る舞いの定義と検証コマンドの書き方には、思う以上の時間をかける価値があります。

まとめ

AIエージェントにタスクを出すなら、一度に一つ、外から観察できる振る舞いと実行可能な検証コマンドを添えて、途中で見つけたことはバックログに書かせ、完了への遷移はエージェントではなくスクリプトが行う機能リストで管理します。エージェントにブレーキをかけるのはハーネスの役目で、エージェント自身はそれを自発的にかけません。WIP=1、feature_list.json、verify.sh は、講座の実践プロジェクトで一通り扱っています。

よくある質問

小さいタスクなら、一度に複数渡してもいいですか?

機能リストに複数並べることはできますが、作業中のタスクは一つだけにします。エージェントは現在の機能が検証を通過してから次を取り上げます。こうすれば、どのタスクも最後までやり切れます。

自動検証を書けないタスクはどうすればいいですか?

多くの場合、タスクが「操作」として書かれていて「振る舞い」になっていないことが原因です。ユーザーや API クライアントが観察できる形に言い換えてください。見た目の部分にはブラウザでの E2E テストが使えます。

feature_list.json は人間とエージェントのどちらが書くべきですか?

最初のリストは一緒に作るのが効率的です。エージェントが分解案を出し、人間が修正します。その後、エージェントは機能を追加できますが、not_started の状態で、検証コマンドを付けることが条件です。

feature_list.json はトラッカーのタスクと何が違うのですか?

トラッカーは人間のためのもので、機能リストは機械が読むためのものです。各項目には実行可能なコマンドと、スクリプトだけが変更できる厳密な状態があります。両者を同期させることはできますが、エージェントにはリポジトリ内のファイルが必要です。

一機能ルールは作業を遅くしませんか?

セッションは短くなりますが、最後までやり切れるタスクの割合が増えます。以前は、中途半端な三つの機能が混ざったコードを解きほぐすのに費やしていた時間が節約されます。

動画でこのテーマを学ぶ

このテーマはコース「Harness Engineering: инженерия надёжности AI-агентов」で実践とともに詳しく解説しています。

コースへ