Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Jun 21, 2026 - 日記

Prompt Iterator Pattern: Coding Agent時代のLoop設計としてのプロンプトイテレータ

Prompt Iterator Pattern: Coding Agent時代のLoop設計としてのプロンプトイテレータ

最近のLLMエージェントは「自律的に動けること」が重視されがちです。タスクを渡すと、調査し、計画し、実装し、検証し、次の作業まで自分で選ぶ。うまく動くと、とても便利です。

一方で、自由を与えすぎると壊れ方も大きくなります。同じ作業を繰り返す。優先順位がおかしくなる。網羅性が保証できない。あとから「なぜその順番で作業したのか」を追えない。こうした問題は、エージェントを長く回すほど目立ちます。

そこで発想を逆にします。LLMに「次に何をするか」まで決めさせません。外部プログラムが状態を読み、次の作業単位を選び、プロンプトを生成し、終了条件を判定します。LLMは、渡された1回分の作業を実行するだけにします。

この設計を、ここでは Prompt Iterator Pattern、またはプロンプトイテレータ方式と呼びます。

最近のCoding Agentまわりでは、個々のプロンプトをどう書くかよりも、「エージェントにプロンプトを渡し続けるループをどう設計するか」が議論されています。Peter Steinberger氏の「coding agentsにプロンプトを書くのではなく、エージェントにプロンプトするループを設計すべきだ」 という投稿は、その流れを端的に表しています。

Prompt Iterator Patternは、このLoop設計の一つです。単に良いプロンプトを1回書くのではなく、次のプロンプトを生成する仕組みそのものを設計します。

LLMエージェントは巨大な while(true) になりがち

多くのLLMエージェント設計は、暗黙的に次のようなループを持っています。

  • 現在の状態を読む
  • 次にやることを決める
  • 作業する
  • 結果を見る
  • 進捗を更新する
  • 終わったか判断する
  • まだなら次へ進む

これ自体は自然です。問題は、これらをひとつの巨大なプロンプトやエージェント内部の判断に押し込めてしまうことです。

LLMに以下を全部任せると、制御が難しくなります。

  • 状態管理
  • タスク選択
  • 優先順位付け
  • 網羅性の担保
  • 進捗管理
  • 終了判定
  • 実作業
  • エラー時の判断

これは、アプリケーション全体をひとつの巨大な while true と巨大な if 文で書くようなものです。最初は動きます。しかし、あとから観測しづらく、止めづらく、修正しづらくなります。

Prompt Iterator Patternとは何か

Prompt Iterator Patternは、この巨大なループを分解する設計です。

役割を3つに分けます。

役割 責務
Iterator 状態を読み、次の仕事を選び、プロンプトを出力する
LLM 渡されたプロンプトの作業だけを実行する
Runner IteratorとLLMをつなぎ、ループを回す

ポイントは、LLMを「意思決定者」ではなく「作業者」に寄せることです。

LLMに任せる範囲を狭くします。その代わり、システム全体としての制御性を上げます。

これはUnixの「ひとつのプログラムはひとつの仕事をうまくやる」という考え方を、LLMエージェントに持ち込む設計です。Iteratorは次のプロンプトを作る。LLM Runnerはプロンプトを実行する。Result Ingestorは結果を状態に戻す。それぞれが小さな責務を持ちます。

この考え方は、以前書いたAgentic CLI Design とも相性が良いです。CLIを人間向けUIではなく、エージェントが呼び出すプロトコルとして設計する、という発想の延長にあります。

基本モデル

流れはシンプルです。

flowchart LR
    State[(State)]
    Iterator[Iterator Command]
    Prompt[Prompt]
    LLM[LLM Runner]
    Result[Result]
    Ingestor[Result Ingestor]

    State --> Iterator
    Iterator --> Prompt
    Prompt --> LLM
    LLM --> Result
    Result --> Ingestor
    Ingestor --> State

状態は外部に置きます。たとえば、JSON、SQLite、Git上のファイル、issue tracker、レビューキューなどです。

Iteratorは状態を読んで、次に実行すべき作業を1つ選びます。そして、その作業に必要なプロンプトを標準出力に出します。

LLM Runnerは、そのプロンプトを読んで実行します。結果はファイル、ログ、パッチ、コメント、JSONなどとして残します。

Result Ingestorは、LLMの結果を読み取り、状態を更新します。成功、失敗、再試行、保留、人間承認待ちなどを記録します。

最小インターフェイス

この方式の最小形は、ただのCLIループです。

次の例は、Iteratorが次のプロンプトを生成し、LLM Runnerがそれを実行し、Ingestorが結果を状態に戻す構成です。前提は、prompt-iterator が標準出力にプロンプトを書き、終了コードで継続可否を返すことです。確認観点は、プロンプト生成・LLM実行・状態更新がそれぞれ独立して観測できることです。

1
2
3
4
while prompt-iterator > prompt.md; do
  llm-runner < prompt.md
  result-ingestor
done

これだけです。

重要なのは、ループの判断がLLMの内側にないことです。prompt-iterator が「次があるか」を決めます。LLMは、渡された prompt.md の作業だけを実行します。

CLI契約を決める

Prompt Iterator Patternでは、CLIの契約を明確にします。ここが曖昧だと、またLLM側に判断が戻ってしまいます。

最低限、次の契約にすると扱いやすいです。

要素 契約
stdout 次にLLMへ渡すプロンプト
stderr 人間向けログ
exit 0 継続。次のプロンプトを出力した
exit 1 完了。もう処理する仕事がない
exit 2+ エラー。人間または上位Runnerが対応する

この分離が大事です。

stdout は機械が読む出力です。ここにはプロンプトだけを出します。余計な進捗ログやデバッグログを混ぜません。

stderr は人間が読む出力です。どの状態を読んだか、なぜそのタスクを選んだか、何件残っているか、といった情報を出します。

終了コードは、Runnerの制御に使います。LLMに「続けるべきですか?」と聞く必要はありません。

このあたりは、POSIX系CLIで長く使われてきた標準出力・標準エラー出力・終了ステータスの考え方をそのまま使えます。細かい設計では、POSIX Utility Conventions のような既存のCLI慣習も参考になります。

Iteratorは何をするのか

Iteratorの責務は「次の仕事を選ぶ」ことです。

もう少し分解すると、次の処理をします。

  1. 状態を読む
  2. 未処理、失敗、保留などの候補を集める
  3. 優先順位や依存関係を見て1つ選ぶ
  4. LLMに渡すプロンプトを生成する
  5. 継続・完了・エラーを終了コードで返す

Iteratorは、LLMよりも決定的に動くべきです。

たとえば、ファイルレビューなら「未レビューのファイル一覧」を状態として持ちます。Iteratorはその中から次の1ファイルを選びます。LLMには「このファイルをこのルールでレビューしてください」とだけ渡します。

LLMは、全体の未レビュー件数を知る必要はありません。次にどのファイルへ進むかも決めません。

Result Ingestorは何をするのか

Result Ingestorは、LLMの出力を状態に戻す部品です。

ここを省略すると、LLMの作業結果がログに流れて終わります。次のループで何をすべきか判断できません。

Ingestorの仕事は、たとえば次のようなものです。

  • LLMが生成した結果ファイルを読む
  • 成功・失敗・要再試行を判定する
  • 対象タスクを完了済みにする
  • 指摘事項やパッチを保存する
  • エラー内容を記録する
  • 人間承認待ちにする
  • 次回のIteratorが読める形で状態を更新する

LLMの出力形式は、できるだけ構造化すると扱いやすいです。Markdownでもよいですが、Ingestorが読む部分はJSONやYAMLに寄せると安定します。

抽象例: レビュー対象を順番に返すCLI

具体例として、レビュー対象を順番に返すCLIを考えます。

ここでは review-gauntlet という名前のCLIを例にします。これは特定のツール名というより、「レビュー対象を順番に返すCLI」の抽象例だと思ってください。前提は、CLI側が未レビュー項目を状態として持ち、ready が次のレビュー用プロンプトを出すことです。確認観点は、LLMがレビュー順を決めず、CLIが返した対象だけをレビューすることです。

1
2
3
4
while review-gauntlet ready > prompt.md; do
  opencode run < prompt.md
  review-gauntlet ingest
done

この例では、責務が分かれています。

  • review-gauntlet ready
    • 次にレビューすべき対象を選ぶ
    • レビュー観点を含むプロンプトを生成する
    • 対象がなければ exit 1 で終了する
  • opencode run
    • 渡されたプロンプトを実行する
    • レビュー結果を出力する
  • review-gauntlet ingest
    • レビュー結果を読み取る
    • 対象を完了済みにする
    • 指摘や未解決事項を状態に保存する

この形にすると、LLMが途中で「次は別の観点を見よう」「このファイルは飛ばそう」と勝手に決めにくくなります。必要ならIterator側のロジックを変更します。

プロンプトは「1回分の仕事」にする

Prompt Iterator Patternで生成するプロンプトは、小さいほど安定します。

悪いプロンプトは、次のようなものです。

リポジトリ全体を確認し、重要な問題を探し、必要なら修正し、完了まで進めてください。

これはLLMに任せすぎです。何を優先するか、どこまで見るか、いつ終えるかが曖昧です。

良いプロンプトは、次のように範囲を切ります。

  • 対象ファイル
  • 適用するルール
  • 出力形式
  • 禁止事項
  • 完了条件
  • 状態更新に必要なメタデータ

たとえば、1ファイルずつレビューするなら、Iteratorは次のようなプロンプトを生成します。

以下は、IteratorがLLMに渡す1回分のレビュー指示の例です。前提は、対象ファイルとレビュー観点がIterator側で決定済みであることです。確認観点は、LLMの出力からIngestorが判定結果と指摘事項を取り出せることです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
あなたはコードレビュー担当です。

対象:
- src/example.ts

レビュー観点:
- 入力検証が不足していないか
- 例外時に状態が壊れないか
- ログに秘密情報が出ないか

制約:
- 対象ファイル以外はレビューしない
- 修正は提案のみ。ファイルは変更しない
- 判断できない場合は「未確認」と書く

出力形式:
```json
{
  "target": "src/example.ts",
  "status": "pass | fail | needs_human",
  "findings": [
    {
      "severity": "low | medium | high",
      "line": 123,
      "message": "説明",
      "suggestion": "修正案"
    }
  ]
}
```

このように、LLMの自由度を「対象作業の中」に閉じ込めます。

LLMは賢い実行者として使います。プロジェクト全体の進行管理者にはしません。

どこに状態を置くか

状態の置き場所は、用途によって変わります。

小さな用途ならJSONファイルで十分です。

以下は、Iteratorが読む状態ファイルの例です。前提は、各タスクが pendingdonefailed などの状態を持つことです。確認観点は、Iteratorがこの状態だけを見て次のタスクを決められることです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "tasks": [
    {
      "id": "review-src-example",
      "target": "src/example.ts",
      "status": "pending",
      "attempts": 0
    },
    {
      "id": "review-src-api",
      "target": "src/api.ts",
      "status": "done",
      "attempts": 1
    },
    {
      "id": "review-src-auth",
      "target": "src/auth.ts",
      "status": "failed",
      "attempts": 2,
      "last_error": "LLM output was not valid JSON"
    }
  ]
}

もう少し大きくなると、SQLiteが便利です。複数プロセスから扱うなら、ロックやトランザクションも考えやすくなります。

GitHub Issues、Linear、Notion、独自DBなどを状態ストアにしてもよいです。大事なのは、Iteratorが「次に何をするか」を再現可能な形で決められることです。

向いている用途

Prompt Iterator Patternは、網羅性や進捗管理が必要なタスクに向いています。

たとえば、次のような用途です。

  • files × rules の網羅レビュー
  • issueの順次処理
  • テスト失敗の修正ループ
  • 章ごとのドキュメント生成
  • データ変換バッチ
  • lint / typecheck / test を見ながらの修正
  • 人間承認待ちタスクの再開
  • 大量のプロンプト評価
  • 移行作業のチェックリスト消化

特に「全部見たと言えること」が大事なタスクに向いています。

LLMに自由探索させると、抜け漏れを証明しづらくなります。Iteratorが対象リストを持っていれば、何件中何件が完了したかを明確にできます。

向いていない用途

逆に、探索的なタスクには向かない場合があります。

たとえば、次のような作業です。

  • まだ目的が曖昧な調査
  • 企画やアイデア出し
  • 解法自体を発見したい問題
  • 人間と対話しながら方向性を決める作業
  • 強い創造性が必要な初期設計

この場合は、LLMにある程度の自由度を渡したほうがよいです。

Prompt Iterator Patternは、LLMの能力を下げる設計ではありません。LLMに任せるべき範囲と、プログラムで制御すべき範囲を分ける設計です。

実装時のチェックリスト

実装するときは、次の点を確認すると安定します。

Iterator

  • 状態を外部ファイルまたはDBから読んでいる
  • 次のタスク選択が決定的である
  • stdout にプロンプトだけを出している
  • stderr に人間向けログを出している
  • タスクがあれば exit 0
  • タスクがなければ exit 1
  • 異常時は exit 2+

Prompt

  • 1回分の作業に絞っている
  • 対象が明確である
  • 完了条件が明確である
  • 出力形式が明確である
  • 禁止事項が明確である
  • 状態更新に必要なIDを含んでいる

LLM Runner

  • 標準入力からプロンプトを読める
  • 結果をファイルまたは標準出力に残せる
  • 失敗時のログを保存できる
  • タイムアウトやリトライの扱いが決まっている

Result Ingestor

  • LLMの出力を検証している
  • 成功・失敗・保留を状態に反映している
  • 不正な出力を失敗として記録できる
  • 再試行回数を管理している
  • 人間承認待ちを表現できる

関連する考え方

Prompt Iterator Patternは、単独の発明というより、エージェント時代のCLI設計を組み合わせたパターンです。

関連する記事としては、次のあたりが近いです。

Prompt Iterator Patternは、これらのうち「反復実行」と「次の作業選択」に焦点を当てたものです。プロンプトを管理するだけでなく、プロンプトを順番に生成し、LLMを1ステップずつ動かします。

自律性を下げると、制御性が上がる

LLMエージェントの設計では、つい「もっと自律的にできないか」と考えたくなります。

しかし、実運用では逆が効く場面も多いです。

LLMに任せる範囲を狭くする。次に何をするかは外部プログラムが決める。状態と終了条件をプログラム側に置く。これだけで、エージェントはかなり扱いやすくなります。

Prompt Iterator Patternの本質は、LLMエージェントの自律性を下げることで、システム全体の制御性を上げることです。

LLMを万能の自律エージェントとして扱うのではなく、強力な1ステップ実行器として扱う。その周りを、小さくて観測可能なCLIで組み立てる。

このほうが、Unix的で、テストしやすく、止めやすく、再開しやすいです。LLMエージェントをプロダクションに近い場所で使うなら、この分離はかなり実用的な設計パターンになります。