Progressive Export — LLMアプリのためのハーネスエンジニアリング

LLM CLI はエージェント型アプリの新しい REPL である。対話的に探索し、繰り返しや壊れやすい処理を skill に書き出し、安定した能力を MCP ツールに昇格させ、最後に信頼できるツール群の上で動く小さなアプリランタイムに置き換える。
llm
agents
engineering
公開

2026年5月27日

English version here

LLMアプリは、アプリケーション開発の中心を変える。LLMアプリ以前、開発者はユーザーのリクエストを満たす正確な API 呼び出しシーケンスを自分で書く必要があった。アプリケーションロジックの大部分は、開発者が書いた制御フローの中にあった:

flowchart LR
  request["User request"] --> apiA["Call API A"]
  apiA --> transform["Transform output"]
  transform --> apiB["Call API B"]
  apiB --> errors["Handle errors"]
  errors --> result["Return result"]

  classDef user fill:#eef2ff,stroke:#4f46e5,color:#111827;
  classDef code fill:#f8fafc,stroke:#64748b,color:#111827;
  class request user;
  class apiA,transform,apiB,errors,result code;

LLMアプリでは、開発者は固定のシーケンスではなく、利用可能な能力(capability)の集合を提供するようになる。どのツールを呼ぶか、コンピュータが実行した出力をどう解釈するか、作業をどう順序付けるかは LLM が決める:

flowchart LR
  request["User request"] --> intent["LLM understands intent"]
  intent --> choose["LLM chooses tools / apps / MCPs"]
  choose --> sequence["LLM sequences calls"]
  sequence --> execute["Computer executes calls"]
  execute --> interpret["LLM interprets outputs"]
  interpret --> result["Result"]

  classDef user fill:#eef2ff,stroke:#4f46e5,color:#111827;
  classDef llm fill:#ecfeff,stroke:#0891b2,color:#111827;
  classDef system fill:#f0fdf4,stroke:#16a34a,color:#111827;
  class request user;
  class intent,choose,sequence,interpret llm;
  class execute,result system;

これはアプリケーション・アーキテクチャをなくすわけではない。アーキテクチャの意味を変えるのだ。重要な設計上の問いはこうなる:LLM にどの能力面(capability surface)の上で動いてもらうか、どの部分を LLM の中に残すか、どの部分を信頼できる外部ツールへ書き出す(export する)か。

REPL のアナロジー

LLM CLI は、エージェント型アプリケーションのための新しい REPL として解釈することもできる。

かつてのワークフローでは、開発者は本番コードを書く前に ipython やシェルなどの REPL で API を理解した。REPL は次のことを発見する場所だった:

  • どの API が必要か
  • API がどう振る舞うか
  • 入力と出力がどんな形か
  • どんなエラーが起きるか
  • どんなエラーハンドリングが必要か
  • どの呼び出しシーケンスが要件を満たすか

その探索の後で、開発者はスクリプトを書き、関数やモジュールを抽出した。

nbdev はこのワークフローをより明示的にした。探索はノートブックの中で実行、そして記録され、その後にライブラリ関数が実装される。テストはライブラリ関数実装直後に追加され(TDD)、安定した関数は nbdev-export で再利用可能なモジュールへ書き出された。

LLM CLI は LLMアプリに対して同じ役割を果たすと考えることが出来る。何を恒久的なインフラにすべきかを決める前に、タスクを探索する場所だ。

Progressive Export(漸進的な書き出し)

実践的な開発パスはこうなる:

flowchart LR
  explore["Interactive<br/>LLM CLI exploration"] --> skills["LLM CLI<br/>with skills"]
  skills --> implement["LLM implements<br/>tools"]
  implement --> export["LLM exports<br/>tools to MCP"]
  export --> mcp["LLM CLI<br/>with MCP tools"]
  mcp --> app["App runtime<br/>with LLM calls"]
  app --> agent["Automated app<br/>or personal agent"]

  classDef exploration fill:#eef2ff,stroke:#4f46e5,color:#111827;
  classDef skill fill:#fff7ed,stroke:#ea580c,color:#111827;
  classDef tool fill:#f0fdf4,stroke:#16a34a,color:#111827;
  classDef appc fill:#f8fafc,stroke:#64748b,color:#111827;
  class explore exploration;
  class skills skill;
  class implement,export,mcp tool;
  class app,agent appc;

最初の段階は緩く、対話的であるべきだ。LLM にタスク全体をやらせてみる。これによってタスクの本当の形が見えてくる:LLM が得意なこと、間違えること、繰り返されるツール呼び出し、出力を構造化すべき箇所、決定論的なコードの方が安全な箇所。

次の段階は skill だ。skill は LLM が従える再利用可能な手順を記録する。ワークフローがまだ探索的で、人間との対話が中心の間はこれが有効だ。車輪の再発明を減らすが、LLM は依然として手順を読み、推論し、コマンドを呼び、出力を解釈しなければならない。skillは外部LLMコールを含むため遅くコスト高ではあるが、自然言語で書かれた手順書なので、初期実装の手間が少ない。

その次の段階は Tool/MCP だ。ある能力が安定し、繰り返し使われ、信頼性/コスト/速度が重要になったら、型付きの外部ツールへ昇格させるべきだ。その時点で、LLM は手順全体をコンテキストに抱え続ける必要がなくなる。ツールを選び、構造化された引数を渡すだけでよい。

最終的なアプリは小さくできる。信頼できない処理や冗長な処理の多くは、すでに信頼できる外部ツールへ書き出されているからだ。アプリは対話的な LLM CLI を自前の LLM 呼び出し”LOOP”に置き換えるが、探索中に発見したタスク知識、MCP の境界、スキーマ、検証ルールはそのまま引き継ぐことが出来る。

アプリはそれらのツールを包む薄いハーネス(ループ)になる:UI、スケジューリング、永続化、権限、可観測性、検証、そして残りの LLM 呼び出し。

OpenClaw や Agent-Sin のようなパーソナルエージェントなら、パスはこうなる:

flowchart LR
  human["Human + LLM CLI<br/>explore a task"] --> skill["Repeated procedure<br/>becomes a skill"]
  skill --> mcp["Stable capability<br/>becomes MCP"]
  mcp --> intent["App calls LLM<br/>for intent / routing"]
  intent --> execution["App calls MCP tools<br/>for execution"]
  execution --> channel["Scheduler / chat channel<br/>runs without CLI"]

  classDef exploration fill:#eef2ff,stroke:#4f46e5,color:#111827;
  classDef skillc fill:#fff7ed,stroke:#ea580c,color:#111827;
  classDef tool fill:#f0fdf4,stroke:#16a34a,color:#111827;
  classDef appc fill:#f8fafc,stroke:#64748b,color:#111827;
  class human exploration;
  class skill skillc;
  class mcp,execution tool;
  class intent,channel appc;

このパスは厳密には直線的ではない。skill や MCP ツールを作ると新しいエッジケースが見つかることが多いし、アプリを運用すると新しい故障モードが見えてくる。それらの観察は LLM CLI での探索にループバックさせるべきだ。ループはこうだ:

flowchart LR
  explore["Explore"] --> export["Export"]
  export --> operate["Operate"]
  operate --> observe["Observe"]
  observe --> explore

  classDef loop fill:#f8fafc,stroke:#64748b,color:#111827;
  class explore,export,operate,observe loop;

何が書き出されるのか

鍵となる動きは、仕事を LLM の外へ書き出すことだ。

最初は interactiveにLLM対話 がすべてをやってもいい:

flowchart LR
  request["Understand request"] --> apis["Choose APIs"]
  apis --> sequence["Decide sequence"]
  sequence --> parse["Parse output"]
  parse --> errors["Handle errors"]
  errors --> summary["Summarize result"]

  classDef llm fill:#ecfeff,stroke:#0891b2,color:#111827;
  class request,apis,sequence,parse,errors,summary llm;

ワークフローが成熟するにつれて、繰り返される部分と壊れやすい部分が外へ移っていく:

flowchart TB
  llm["LLM<br/>understand request<br/>route<br/>judge<br/>summarize"]
  skill["Skill<br/>remembered procedure<br/>for interactive use"]
  mcp["MCP<br/>typed reliable<br/>execution boundary"]
  app["App<br/>automation<br/>persistence<br/>verification<br/>UX"]

  llm --> skill
  llm --> mcp
  mcp --> app

  classDef llmc fill:#ecfeff,stroke:#0891b2,color:#111827;
  classDef skillc fill:#fff7ed,stroke:#ea580c,color:#111827;
  classDef tool fill:#f0fdf4,stroke:#16a34a,color:#111827;
  classDef appc fill:#f8fafc,stroke:#64748b,color:#111827;
  class llm llmc;
  class skill skillc;
  class mcp tool;
  class app appc;

これがアプリケーション側から見たハーネスエンジニアリングだ。目標は LLM を排除することではない。LLM に最小限の有用な役割だけを残し、実行・検証・永続化・統合を決定論的なコンポーネントへ移すことだ。

痛みが出てから書き出す

この進行は「すべてを MCP にせよ」というルールではない。対話的な LLM CLI ワークフローがうまく動いていて、使用頻度が低く、失敗のコストが安いなら、そのままでいい。

書き出すのは圧力があるときだけだ:

  • 反復:同じ手順が頻繁に使われる
  • コスト:LLM が手順の再読・再発見にトークンを使いすぎる
  • 信頼性:もうミスが許容できない
  • 自動化:人間が見ていなくても動く必要がある
  • 共有:複数のエージェント・アプリ・人が同じ能力を必要とする
  • セキュリティ:シークレット、認証、権限、副作用により強い境界が必要

これはワークフローを YAGNI に沿わせ続ける。skill が中間段階としてしばしば適切なのは、作るのが安く、MCP がメンテナンスコストに見合うかを証明するのに役立つからだ。

LLM に残るもの

LLM には、曖昧さが有用な仕事を残すべきだ:

  • オープンエンドなユーザー意図の理解
  • ルールがまだ安定していないときのツール間のルーティング
  • 結果の要約と説明
  • 決定論的なルールがないトレードオフの判断
  • ユーザー固有の好みへの適応
  • まだ変化し続けている例外パターンの処理

一貫性が柔軟性より価値を持つ仕事は、決定論的なコンポーネントが引き受けるべきだ:

  • 繰り返される API 呼び出し
  • パースとバリデーション
  • 認証とシークレットの扱い
  • 永続化
  • 検証
  • リトライと失敗の分類
  • レイテンシに敏感な処理、高頻度の処理

これが「LLM に最小限の有用な役割だけを残す」ことの実践的な意味だ。

Skill vs MCP

skill は、探索中や人間がループに入っている間は優れている。作るのが安く、編集しやすく、人がワークフローを自然に記述する形に近い。

しかし skill は本番アプリでの利用にはコストが高くつきうる。LLM が skill を読んで推論するたびにトークンを消費しうるし、コマンド構築のミスや解釈の揺れの余地も残る。

能力を頻繁に、無人で、あるいは複数のクライアント間で共有して動かす必要があるなら MCP の方がよい。MCP は LLM により小さく構造化されたインターフェースを与える:

tool_name(arguments) -> structured result

アプリにとって、これは opex と信頼性の問題だ。頻繁に使われる skill は継続的なトークンコストになりうる。頻繁に使われる MCP ツールは安定したサービス境界になる。

MCP は、その能力を誰が呼べるかも変える。skill は通常 LLM CLI セッションを前提とする:LLM が skill を読み、手順に従い、コマンドを呼ぶ。MCP は、LLM クライアントからも、別のエージェントからも、普通のアプリからも呼べるツール境界を公開する。能力が MCP になった時点で、それは対話的な LLM セッションの中だけの存在ではなくなる。

これが、最終プロダクトから対話的 CLI を消すことを可能にする鍵となるステップだ。アプリは Claude Code や Codex をユーザーインターフェースとして動かす必要はない。自前で LLM を呼び、同じ MCP ツールを公開し、構造化されたツール結果を検証し、自分の UI・チャットチャネル・スケジューラを通じてレスポンスを返せばよい。

ただし CLI を置き換えるということは、CLI が暗黙に提供していたハーネスをアプリが自前で持つということだ:

  • ツール呼び出しループ
  • コンテキスト注入
  • 権限と承認フロー
  • 構造化出力のバリデーション
  • トランスクリプトとツールのトレース
  • エラーハンドリングとリトライポリシー
  • サンドボックスやアクセス境界

つまり最終的なアプリは、生の LLM API 呼び出しではない。MCP ツールを囲む小さな LLM ランタイムだ。

それでも構造化出力は重要

仕事を MCP ツールへ移した後も、いくつかの LLM 呼び出しは残る。それらの呼び出しは可能な限り構造化出力を使うべきだ。Pydantic スタイルのスキーマ、型付きの結果、バリデーション、明示的な失敗状態はハーネスの一部だ。

同じ原則がすべての層に当てはまる:

  • 曖昧な自由テキストを減らす
  • 型付きの入出力を定義する
  • 結果を検証する
  • 失敗を可視化する
  • LLM の自由度は、それが有用な場所にだけ残す

可観測性もハーネスの一部だ。アプリは後でシステムを改善するのに十分な情報を記録すべきだ:ツールのトレース、バリデーションの失敗、リトライ、コスト、レイテンシ、ユーザーによる修正、そして LLM が人間の助けを必要としたケース。

元になったアイデア

このフレーミングは、ハーネスエンジニアリングに関する議論から3つのアイデアをつないでいる:

  • Tejas Kumar:モデルの周りに検証と決定論的なハンドラを足せば、プロンプトを変えずに信頼性を上げられる
  • 入江慎吾:有用なパーソナルエージェントは、LLM がやるべきこととプログラムがやるべきことを分離している
  • Newbee/西見:理解は外注できない——だからこそ最初の探索段階は無駄ではない

結論

LLM CLI はエージェント型アプリケーションの新しい REPL だ。まず対話的にタスクを探索し、繰り返される処理と壊れやすい処理を skill へ書き出し、安定した能力を MCP ツールへ昇格させ、最後に、信頼できるツール群の上で LLM を呼ぶ小さなアプリランタイムで対話的 CLI を置き換える。

より深い変化は、アプリケーション開発がもはや制御フローを直接書くことだけではなくなった、ということだ。LLM が有用な能力の上で安全に動けるようにするハーネスを発見し、形作り、固めていくことになった。最良の道は progressive export だ:LLM から始め、曖昧さが利益になるものは残し、それ以外は、圧力が本物になったときにだけ外へ移していく。次のステップはどうやって自動的にLLMにSKILLを実装させる仕組みを作るかだと思う(要調査)。