英語は私の第一言語ではありません。私は中国語でこれを書き、AIの助けを借りて翻訳しました。原稿にはAI的な要素があるかもしれませんが、設計の判断、制作時の失敗、そしてそれらを原則へと蒸留した思考は私自身のものです。
Metaによる買収前、Manusのバックエンドリードを務めていました。過去2年間、AIエージェントを構築してきました — まずManusで、次に自分自身のオープンソースのエージェント実行環境(Pinix)とエージェント(agent-clip)で。道中、私を驚かせた結論に達しました:
単一の run(command="...") ツールと Unix風のコマンドは、タイプ済みの関数呼び出しのカタログを凌駕する。
以下に私が学んだことを示します。
なぜ *nix
Unixは50年前に設計上の決定をしました:すべてはテキストストリームです。 プログラムは複雑なバイナリ構造を交換したり、メモリオブジェクトを共有したりするわけではなく、テキストパイプを介して通信します。小さなツールはそれぞれ一つのことをよく行い、| で結合して強力なワークフローを作ります。プログラムは自己を --help で説明し、終了コードで成功や失敗を報告し、stderrを介してエラーを伝えます。
LLMsは50年後にはほとんど同じ決定を下しました:すべてはトークンです。 彼らはテキストを理解し、テキストのみを生成します。彼らの「思考」も「行動」もテキストであり、世界から受け取るフィードバックもテキストでなければなりません。
これら2つの決定は、半世紀離れた出発点にもかかわらず、同じインターフェースモデルへと収束します。人間の端末オペレーターのためのテキストベースのシステムとして設計された Unix は、cat、grep、pipe、exit codes、man pages を含むものとして、LLMsにとって単に「使える」存在ではなく、自然な適合です。ツールの利用に関して、LLMは本質的には端末オペレーターであり、人間より速く、訓練データには既に膨大なシェルコマンドとCLIパターンが含まれています。
これが nix Agent の核心哲学です:新しいツールインターフェースを発明しない。Unixが50年間証明してきたものをそのままLLMに渡す。
なぜ単一の run
単一ツール仮説
多くのエージェントフレームワークはLLMに独立したツールのカタログを提供します:
tools: [search_web, read_file, write_file, run_code, send_email, ...]
各呼び出しの前に、LLMはツール選択を行わねばなりません — どれを使うのか?パラメータは? ツールを増やすほど選択は難しくなり、正確性は低下します。認知的負荷は「どのツールか?」に費やされ、「何を達成するべきか?」には費やされません。
私のアプローチは、1つの run(command="...") ツール、すべての機能をCLIコマンドとして公開。
run(command="cat notes.md") run(command="cat log.txt | grep ERROR | wc -l") run(command="see screenshot.png") run(command="memory search 'deployment issue'") run(command="clip sandbox bash 'python3 analyze.py'")
LLMはどのコマンドを使うかを選ぶが、それは「15個の異なるスキーマのツールの中から選ぶ」こととは本質的に異なります。コマンド選択は統一された名前空間内での文字列組み合わせであり、関数選択は無関係なAPI間のコンテキスト切替です。
LLMsはすでにCLIを話す
なぜCLIコマンドは、構造化された関数呼び出しよりLLMsに適しているのか?
CLIはLLM訓練データで最も密度の高いツール利用パターンだからです。GitHub上には数十億行のコードが以下のようにあふれています:
```bash
README install instructions
pip install -r requirements.txt && python main.py
CI/CD build scripts
make build && make test && make deploy
Stack Overflow solutions
cat /var/log/syslog | grep "Out of memory" | tail -20 ```
私がLLMにCLIの使い方を教える必要はありません――すでに知っています。 この慣用性は確率的でモデル依存ですが、実際には主流モデルで非常に信頼性があります。
同じタスクの2つのアプローチを比較します:
``` Task: Read a log file, count the error lines
Function-calling approach (3 tool calls): 1. read_file(path="/var/log/app.log") → returns entire file 2. search_text(text=<entire file>, pattern="ERROR") → returns matching lines 3. count_lines(text=<matched lines>) → returns number
CLI approach (1 tool call): run(command="cat /var/log/app.log | grep ERROR | wc -l") → "42" ```
1つの呼び出しで3つを置換します。特別な最適化によるものではなく、Unixのパイプは元から組み合わせをサポートしているからです。
パイプと連鎖を機能させる
単一の run だけでは不十分です。run が同時に1つのコマンドしか実行できない場合、結合タスクには依然として複数回の呼び出しが必要です。そこで私はコマンドルーティング層にチェーンパーサー(parseChain)を作成し、4つのUnix演算子をサポートします:
| Pipe: stdout of previous command becomes stdin of next && And: execute next only if previous succeeded || Or: execute next only if previous failed ; Seq: execute next regardless of previous result
この仕組みにより、すべてのツール呼び出しが完結したワークフローになることが可能です:
```bash
1つのツール呼び出し:ダウンロード → 検査
curl -sL $URL -o data.csv && cat data.csv | head 5
1つのツール呼び出し:読み取り → フィルタ → ソート → 上位10
cat access.log | grep "500" | sort | head 10
1つのツール呼び出し:Aを試す、Bをフォールバック
cat config.yaml || echo "config not found, using defaults"
```
N個のコマンド × 4つの演算子 — 組成空間は飛躍的に拡大します。そしてLLMにとっては、すでに知っている文字列に過ぎません。
コマンドラインこそがLLMのネイティブなツールインターフェースである。
ヒューリスティック設計: CLIがエージェントを案内する
単一ツール+CLI は「何を使うか」を解決します。しかしエージェントはまだ「どう使うか」を知る必要があります。Googleは使えません。同僚にも聞けません。CLI自体をエージェントのナビゲーションシステムとして機能させるため、私は3つの段階的デザイン手法を用います。
技法1: Progressive --help の発見
設計のよいCLIツールは、ドキュメントを読み込む必要がない—なぜなら --help がすべてを教えてくれるからです。エージェントにも同じ原理を適用します。これを段階的開示として構造化します:エージェントはすべてのドキュメントを一度に読み込む必要はなく、進むにつれて必要な詳細をオンデマンドで発見します。
レベル0: ツールの説明 → コマンドリストの注入
run ツールの説明は、対話の開始時に動的に生成され、登録済みコマンドが1行要約付きでリストされます:
Available commands: cat — Read a text file. For images use 'see'. For binary use 'cat -b'. see — View an image (auto-attaches to vision) ls — List files in current topic write — Write file. Usage: write <path> [content] or stdin grep — Filter lines matching a pattern (supports -i, -v, -c) memory — Search or manage memory clip — Operate external environments (sandboxes, services) ...
エージェントはturn1から利用可能なものを知っていますが、すべてのコマンドのすべてのパラメータを覚える必要はありません—それは文脈の浪費です。
注: ここには設計上の問いがあります。全コマンドリストを注入する方法 vs. アクティブな発見のバランスです。コマンドが増えるとリスト自体が文脈予算を消費します。適切なバランスをまだ模索中です。アイデア歓迎。
レベル1: command(引数なし)→ 使用法
エージェントがコマンドに関心を示すと、それを呼び出すだけです。引数なし?コマンドは自分自身の使用法を返します:
``` → run(command="memory") [error] memory: usage: memory search|recent|store|facts|forget
→ run(command="clip") clip list — list available clips clip <name> — show clip details and commands clip <name> <command> [args...] — invoke a command clip <name> pull <remote-path> [name] — pull file from clip to local clip clip <name> push <local-path> <remote> — push local file to clip ```
今やエージェントは memory が5つのサブコマンドを持つこと、そして clip が list/pull/push をサポートしていることを知っています。ワンコール、ノイズなし。
レベル2: command subcommand(引数が欠如)→ 具体的なパラメータ
エージェントは memory search を使うことを決めますが、パラメータ形式が不明です。そこで深掘りします:
``` → run(command="memory search") [error] memory: usage: memory search <query> [-t topic_id] [-k keyword]
→ run(command="clip sandbox") Clip: sandbox Commands: clip sandbox bash <script> clip sandbox read <path> clip sandbox write <path> File transfer: clip sandbox pull <remote-path> [local-name] clip sandbox push <local-path> <remote-path>
段階的開示: 概要(注入) → 使用法(探究) → パラメータ(掘り下げ)。エージェントはオンデマンドに発見します。各レベルは次のステップに必要な情報だけを提供します。
これは、システムプロンプトに3,000語のツール文書を詰め込むのとは根本的に異なります。多くの場合、その情報は無関係であり、純粋な文脈の無駄です。段階的なヘルプはエージェントが「もっと必要か」を決めさせます。
またコマンド設計には要件が課されます:すべてのコマンドとサブコマンドは完全なヘルプ出力を持つべきです。 人間だけでなく、エージェントのためでもあります。良いヘルプメッセージはワンショット成功を意味します。欠如していると盲目的な推測になります。
技法2: エラーメッセージをナビゲーションに
エージェントはミスをします。重要なのはエラーを防ぐことではなく、どの方向へ導くべきかを示すエラーにすることです。
伝統的なCLIのエラーは、人間がGoogleできる前提で設計されています。エージェントはGoogleできません。したがって、すべてのエラーには「何が起きたのか」と「代わりにどうすべきか」を含める必要があります:
``` Traditional CLI: $ cat photo.png cat: binary file (standard output) → Human Googles "how to view image in terminal"
私の設計: [error] cat: binary image file (182KB). Use: see photo.png → エージェントは直接 see を呼び出し、1ステップで修正
さらに例:
``` [error] unknown command: foo Available: cat, ls, see, write, grep, memory, clip, ... → Agent immediately knows what commands exist
[error] not an image file: data.csv (use cat to read text files) → Agent switches from see to cat
[error] clip "sandbox" not found. Use 'clip list' to see available clips → Agent は最初にクリップをリストすることを知る ```
技法1(ヘルプ)は「何ができるか」を解決します。技法2(エラー)は「代わりに何をすべきか」を解決します。これらを組み合わせることで、エージェントのリカバリコストは最小、通常は右方向へ1~2手で解決します。
実例: 静かな stderr のコスト
しばらくの間、私のコードは外部サンドボックスを呼ぶ際にstderrを黙って捨てていました — stdoutが空でない場合、stderrを破棄していました。エージェントは pip install pymupdf を実行し、終了コードは127。stderrには bash: pip: command not found が含まれていましたが、エージェントには見えていませんでした。単に「失敗した」としか分からず、10個の異なるパッケージマネージャを盲信的に試しました:
pip install → 127 (存在しない) python3 -m pip → 1 (モジュールが見つからない) uv pip install → 1 (使い方が間違っている) pip3 install → 127 sudo apt install → 127 ... 5 回追加試行 ... uv run --with pymupdf python3 script.py → 0 ✓
計10回、推論それぞれ約5秒。もし最初の1回でstderrが見えていれば、1回の呼び出しで済んだはずです。
stderrはエージェントが最も必要とする情報であり、特にコマンドが失敗したときにこそ重要です。決して捨ててはいけません。
技法3: 一貫した出力フォーマット
最初の二つの技法は発見と修正を扱います。三つ目は、時間とともにシステムの使い方をエージェントが改良できるようにします。
私はすべてのツール結果に一貫したメタデータを付与します:
file1.txt file2.txt dir1/ [exit:0 | 12ms]
LLMは2つのシグナルを抽出します:
終了コード(UNIXの慣例、LLMは既に知っている):
exit:0— 成功exit:1— 一般的なエラーexit:127— コマンドが見つからない
所要時間(コスト認識):
12ms— 安価、自由に呼び出せる3.2s— 中程度45s— 高価、節度を持って使用
会話の中で [exit:N | Xs] を何度も見ると、エージェントはそのパターンを内在化します。エラーが exit:1 を示すとエラーを確認するようになり、長時間の所要で呼び出し回数を減らすようになります。
一貫した出力フォーマットは時間とともにエージェントを賢くします。一貫性がないと、すべての呼び出しが初回のもののように感じられます。
この3つの技法は段階的な進行を形成します:
--help → "What can I do?" → Proactive discovery Error Msg → "What should I do?" → Reactive correction Output Fmt → "How did it go?" → Continuous learning
二層アーキテクチャ: ヒューリスティック設計の技術
上の節ではCLIが意味レベルでエージェントを導く方法を説明しました。しかし実際に機能させるには、技術的な課題があります:コマンドの生の出力とLLMが見るべき情報は、しばしば異なるものです。
LLMの2つの厳しい制約
制約A: コンテキスト窓は有限で高価です。 1トークンもコストがかかり、注意力と推論速度にも影響します。10MBのファイルを文脈に詰め込むことは予算の無駄になるだけでなく、早い段階で会話を窓の外へ追い出します。エージェントは「忘れます」。
制約B: LLMはテキストしか処理できません。 バイナリデータはトークナイザーを通じて高エントロピーの無意味なトークンを生成します。文脈を無駄にするだけでなく、周囲の有効トークンへの注意力を乱し、推論品質を低下させます。
この2つの制約は、 rawなコマンド出力を直接LLMへ渡せないことを意味します — 処理用のプレゼンテーション層が必要です。しかしその処理はコマンド実行のロジックには影響を与えてはならず、パイプは壊れてしまいます。従って、2層構造が必要になります。
Execution layer vs. presentation layer
┌─────────────────────────────────────────────┐ │ Layer 2: LLM Presentation Layer │ ← Designed for LLM constraints │ Binary guard | Truncation+overflow | Meta │ ├─────────────────────────────────────────────┤ │ Layer 1: Unix Execution Layer │ ← Pure Unix semantics │ Command routing | pipe | chain | exit code │ └─────────────────────────────────────────────┘
例として cat bigfile.txt | grep error | head 10 が実行されるとき:
Inside Layer 1: cat output → [500KB raw text] → grep input grep output → [matching lines] → head input head output → [first 10 lines]
もしLayer 1で cat の出力を切り詰めれば → grep は最初の200行だけを検索し、結果が不完全になります。Layer 1に [exit:0] を追加すると、データとして grep に流れ、検索対象になります。
したがって Layer 1 は生データのまま、損失なし、メタデータフリーである必要があります。処理は Layer 2 のみで行われます — パイプ連鎖が完了し、最終結果がLLMに返される準備が整ったときです。
Layer 1はUnixの意味論を担い、Layer 2はLLMの認識を担います。分離はデザインの好みではなく、論理的必然性です。
Layer 2の4つのメカニズム
Mechanism A: Binary Guard (addressing Constraint B)
LLMへ返す前に、それがテキストかどうかをチェックします:
``` Null byte detected → binary UTF-8 validation failed → binary Control character ratio > 10% → binary
If image: [error] binary image (182KB). Use: see photo.png If other: [error] binary file (1.2MB). Use: cat -b file.bin ```
LLMは処理できないデータを受け取ることはありません。
Mechanism B: Overflow Mode (addressing Constraint A)
``` Output > 200 lines or > 50KB? → Truncate to first 200 lines (rune-safe, won't split UTF-8) → Write full output to /tmp/cmd-output/cmd-{n}.txt → Return to LLM:
[first 200 lines] --- output truncated (5000 lines, 245.3KB) --- Full output: /tmp/cmd-output/cmd-3.txt Explore: cat /tmp/cmd-output/cmd-3.txt | grep <pattern> cat /tmp/cmd-output/cmd-3.txt | tail 100 [exit:0 | 1.2s] ```
重要な点: LLMは既に grep、head、tail を使ってファイルをナビゲートする方法を知っています。Overflowモードは「大規模データ探索」を、LLMがすでに持つスキルへと変換します。
Mechanism C: Metadata Footer
actual output here [exit:0 | 1.2s]
終了コードと所要時間をLayer 2の最終行として付加します。成功/失敗とコスト認識の信号をエージェントに与えつつ、Layer 1のパイプデータを汚さずに済みます。
Mechanism D: stderr Attachment
``` When command fails with stderr: output + " [stderr] " + stderr
エージェントが何が失敗したのかを見られるようにし、盲目的なリトライを防ぎます。 ```
運用からの教訓: production からのストーリー
Story 1: 20回の thrashing を引き起こしたPNG
あるユーザーがアーキテクチャ図をアップロードしました。エージェントは cat でそれを読み取り、182KBのRAW PNGバイトを受け取りました。LLMのトークナイザはこれらのバイトを何千もの意味のないトークンに変換し、文脈に詰め込みました。エージェントは意味を理解できず、cat -f、cat --format、cat --type image などの読み取り方を試みましたが、毎回同じガラクタを受け取りました。20回の反復の後、処理は強制終了されました。
原因: cat に二進数検出がなく、Layer 2 にガードがなかったこと。 対策: isBinary() ガード+エラーガイダンス Use: see photo.png。 教訓: ツールの結果はエージェントの目そのものです。ゴミを返すとエージェントは盲目になります。
Story 2: 静かな stderr と10回の盲目リトライ
エージェントはPDFを読む必要がありました。pip install pymupdf を試し、終了コードは127。stderrには bash: pip: command not found が含まれていましたが、コードはそれを見ていませんでした。エージェントは「失敗した」としか認識せず、理由を理解できませんでした。長い試行錯誤が続きました:
pip install → 127 (存在しない) python3 -m pip → 1 (モジュールが見つからない) uv pip install → 1 (使い方が間違っている) pip3 install → 127 sudo apt install → 127 ... 5 more attempts ... uv run --with pymupdf python3 script.py → 0 ✓
10回の呼び出し、推論は各回約5秒。最初のときにstderrが見えていれば、1回の呼び出しで済んだはずです。
原因: InvokeClip が stdout が非空のときstderrを黙って捨てていました。対策: 失敗時には常にstderrを添付する。教訓: stderrはエージェントが最も必要とする情報であり、特にコマンドが失敗したときに重要です。
Story 3: Overflowモードの価値
エージェントは5,000行のログファイルを分析しました。切り詰めなければ、全文 (~200KB) が文脈に詰め込まれ、LLMの注意力が過負荷になり、応答品質が大きく低下し、以前の会話が文脈窓の外へと押し出されました。
Overflowモードでは:
``` [first 200 lines of log content]
--- output truncated (5000 lines, 198.5KB) --- Full output: /tmp/cmd-output/cmd-3.txt Explore: cat /tmp/cmd-output/cmd-3.txt | grep <pattern> cat /tmp/cmd-output/cmd-3.txt | tail 100 [exit:0 | 45ms] ```
最初の200行を見たエージェントはファイル構造を把握し、次に grep を使って問題を特定しました。結局3回の呼び出しで、コンテキストは2KB未満でした。
教訓: エージェントに「地図」を提供することは、全体の領域を渡すよりはるかに効果的です。
境界と制限
CLIは万能薬ではありません。次のような場面では型付きAPIの方が適している場合があります:
- 強く型付けされた相互作用: データベース問い合わせ、GraphQL API、構造化された入力/出力を必要とするケース。スキーマ検証の方が文字列解析より信頼性が高い。
- 高いセキュリティ要件: CLIの文字列連結には組み込みの注入リスクがあり、信頼できない入力の場合は型付きパラメータの方が安全です。agent-clipはサンドボックス分離でこれを緩和します。
- ネイティブなマルチモーダル: ピュアな音声/動画処理など、CLIのテキストパイプがボトルネックとなるケース。
さらに、「イテレーション制限なし」は「安全性なし」ではありません。安全は外部メカニズムで確保されます:
- サンドボックス分離: コマンドは BoxLite コンテナ内で実行され、脱出は不可能
- API予算: LLM呼び出しにはアカウントレベルの支出上限
- ユーザーキャンセル: フロントエンドにはキャンセルボタン、バックエンドは適切なシャットダウンをサポート
Unixの哲学を実行層に hand、LLMの認知的制約をプレゼンテーション層に hand、ヘルプ、エラーメッセージ、出力フォーマットを3つの進行的ヒューリスティックナビゲーション技法として使い分けます。
CLIはエージェントに必要な全てです。
ソースコード(Go): github.com/epiral/agent-clip
コアファイル: internal/tools.go(コマンドルーティング)、internal/chain.go(パイプ)、internal/loop.go(二層エージェントループ)、internal/fs.go(バイナリガード)、internal/clip.go(stderr処理)、internal/browser.go(視覚自動添付)、internal/memory.go(意味記憶)。
議論歓迎 — 特に同様のアプローチを試した方やCLIが崩れるケースを見つけた方は是非。コマンド発見の問題( injecting の分量 vs. エージェントの発見をどうバランスさせるか)は、私が現在も積極的に探っているテーマです。