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アプリは、アプリケーション開発の中心を変える。LLMアプリ以前、開発者はユーザーのリクエストを満たす正確な API 呼び出しシーケンスを自分で書く必要があった。アプリケーションロジックの大部分は、開発者が書いた制御フローの中にあった:
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を実装させる仕組みを作るかだと思う(要調査)。