🤖

AIの「中の人」になってみる

に公開

登壇してお話しました

.NETラボ 勉強会 2026年8月で,AIの「中の人」になってみるというタイトルでお話ししました.

アーカイブもあります.
資料はこちらです.

https://speakerdeck.com/htkym/ai-no-naka-no-hito-ni-na-te-miru

GitHub Copilot CLI のプロバイダーを自作アプリケーションへ向け,モデルを human として,人間が応答する仕組みにしました.私がLLMとなって遊んでみました.
AI エージェントの入力を人間がそのまま読んでみると,依頼文だけではなく,ツール定義,履歴,実行結果など,多くの情報を踏まえて次の行動を決めていることが分かります.

https://github.com/Htkym/youarellm

youarellm は,GitHub Copilot CLI の BYOK で使える,ローカルの human OpenAI 互換プロバイダーです.

今回のお話は 【永久0円】人間LLMのすすめ というブログ記事を参考にしました.
非常に面白い記事ですので,ぜひお読みください...!!

https://qiita.com/Syuparn/items/0001f93221d4d7556271

この記事では,アプリケーションの実装,デモ,トークンの測定,そこから感想とともに考えた迷わないためにしたほうが良さそうなことをまとめます.

内容

youarellm で AI の「中の人」になる

今回作った youarellm は,OpenAI 互換 API のリクエストを受け取り,人間が応答を返せるようにした Blazor アプリケーションです.
Copilot CLI の接続先をローカルの youarellm に変え,human をモデルとして指定しました.

Copilot CLI
  -> POST /v1/chat/completions または /v1/responses
  -> youarellm のオペレーター UI
  -> 人間がメッセージまたは tool call を返す
  -> Copilot CLI

「人間が代わりに考えるだけ」なので,この実験中のモデル利用料は発生しません.
その代わり,人間が実際にエージェントと同じ入力を読んでます.

まずはBYOKでGitHub Copilot CLIをローカルの youarellm に接続する方法を紹介します.
設定値があるのですが,わかりにくいので分けて整理しました.

役割 今回の例
Provider どこへ,どの接続契約で送るか openai type とローカルの base URL
API どの JSON とストリーミング形式で会話するか Chat Completions / Responses
Model 応答を生成するもの human

openai provider type は,OpenAI 社のサービスだけを意味する名前ではありません.
OpenAI 互換の HTTP API を使う接続方式なので,base URL を変えれば OpenAI,Ollama,youarellm のような接続先を選べます.

Chat Completions と Responses を受け取る

youarellm では,Chat Completions と Responses の両方を実装しています.
Copilot CLI では通常 Chat Completions 形式が使われますが,COPILOT_PROVIDER_WIRE_API=responses を設定すると Responses 形式を選べます.

観点 Chat Completions Responses
Endpoint /v1/chat/completions /v1/responses
入力 messages instructionsinput
出力 choices[].message 型付きの output[]
ツール結果 role: "tool" function_call_output
会話継続 クライアントが履歴を送る 履歴送信に加えて previous_response_id などを使える

Chat Completions は,指示,過去の会話,今回の依頼,ツール定義を 1 つのリクエストとして送る形式です.
Responses は input の要素に型があるため,メッセージ,function call,function call の結果などを受信側で判別しやすい形になってます.

どちらの形式でも,受け取るのは会話文だけではありません.

system instructions   立ち位置やルール
custom instructions   プロジェクト固有の約束
tool definitions      使える道具の一覧と説明
messages              これまでのやり取り
tool results          道具を実行した結果

人間が読むと情報量の多さに辟易します...
エージェントはこの一式をもとにして,次に返す文章やツール呼び出しを選びます.

tool call は「実行してほしい」という返答

よくよく考えたらわかることですが,AI モデルが勝手にローカル PC 上のコマンドを実行しているわけではないです.
今回LLM役をやってみてなるほどなぁと思ったので,ツール呼び出しの仕組みをまとめておきます.
プロバイダーは「このツールを,この引数で実行してほしい」という tool call を返し,Copilot CLI がそれを自分の環境で実行します.

youarellm                         Copilot CLI
────────                         ───────────
function: powershell
arguments: {"command":"..."}  ->  コマンドを実行
                                      |
                      tool result  <-

実行結果は,次のリクエストで tool result として人間側へ戻ります.
この往復を実際に手で返してみると,エージェントが操作しているように見えても,役割が分かれています.

また,ツールの名前と引数はスキーマに厳密に従う必要があります.
たとえば read_filereadFile と返しただけでも,クライアントは期待したツールとして実行できません.
Chat Completions では,arguments が JSON オブジェクトではなく,JSON を文字列化した値になる点も注意ですね.

{
  "name": "read_file",
  "arguments": "{\"path\":\"src/GreetingService.cs\"}"
}

デモ

デモでは,Aspire の WithTerminal を使ってみました.
ちょうど,Aspire 13.5 の新機能として出てきて,ラッキーでした.ありがとうございます.
デモは,Copilotとyouarellmの間で,私が人間LLMとして応答する流れを見せるものでした.

Aspire の WithTerminal()

WithTerminal() は,Aspire のリソースに対話式ターミナルを公開するための API です.
付与したリソースは Aspire Dashboard から直接操作でき,aspire terminal attach <resource> を使えばローカルのターミナルからも接続できます.
今回のデモでは,Copilot CLI の対話を Dashboard 内で見せるために使いました.(おかげ画面切り替えが楽になりました)

#pragma warning disable ASPIRETERMINAL001 // experimental API なので,つけとかないと警告が出ます

var interactiveTool = builder.AddExecutable("interactive-tool", "my-command", ".")
    .WithTerminal(); // このメソッドを呼ぶだけ

人間が操作すると,画面を直接見られない,進捗をどう伝えるか,次にどのコマンドを実行するかなど,モデルが普段引き受けている判断を一つずつ考えることになります.
ツールの説明や実行結果が,次の行動を選ぶための重要な手がかりになっていることを実感しました.

/fleet に近い形で,親タスクを複数の worker に分けられるシミュレーション機能もアプリに実装しています.
これをやってみると,親に渡るのは worker が返した結果だけで,worker が読んだソース全文やログは自動では届きません.

Specification worker:
  evidence: README.md L12 ...

Implementation worker:
  evidence: GreetingService.cs L5 ...

そのため,worker の返答には,結論だけでなく根拠と確認範囲を残す必要があります.
Copilot CLI のサブエージェントのログとか見ているとわかるですが,実際にこういう返答をしていたりします.
サブエージェントはこういう風に実装されてるんだなぁと思いました.

履歴は「記憶」ではなく毎回渡される状態

Chat Completions API はステートレスです.
サーバーが前回までの会話を覚えているのではなく,クライアントが過去のメッセージやツールの実行結果を組み立て,毎回のリクエストとして送ります.

POST /v1/chat/completions
  system instructions
  + 過去のメッセージ
  + tool results
  + 今回の依頼
  + tool definitions

Responses API には previous_response_id や conversation を使った会話継続の手段もあります.
ただし,何を引き継ぐかを設計する必要があることは同じです.たとえば previous_response_id を使っても,前回の instructions が自動で引き継がれるわけではありません.

長い履歴をそのまま積み上げるだけではなく,次の作業に必要な状態を書き直す方がいいなと思いました.
(少なくとも人間には積み上げ式は向いてないと思いました)

残したい情報 理由
決めたことと根拠 同じ判断をやり直さないため
未確認のこと 推測を事実として扱わないため
変更したファイルや識別子 次の操作対象を特定するため
テスト結果や失敗内容 確認済みの範囲を判断するため

多分,/compact のような,履歴を要約して渡す機能はこんな感じになってるじゃないでしょうか?

測ってみると,トークンは直感どおりには減らない

GitHub-hosted モデルとの比較

最初に,youarellmhuman と GitHub-hosted の GPT-5.6 Terra で,「こんにちは」と送ったときの initial context を比べました.
短い依頼でも,背景にある instructions やツール定義が大きな割合を占めていました.

youarellm / human GitHub-hosted / GPT-5.6 Terra
入力 50,998 tokens 43,380 tokens
主な内訳 system 11.0K
system tools 9.7K
MCP tools 30.1K
input 3
cache write 43,377
出力 1 token 15 tokens
実際の AI クレジット 0 10.86
Terra 換算の AI クレジット 約 12.75 10.86

youarellm 側の値は o200k_base によるローカル推定です.
GitHub-hosted の値は,プロバイダーが計測した input,cache,output をもとにした利用量です.
「こんにちは」に対して,50000 tokens と表示されてびっくりしましたが,GitHub-hosted でも大体同じくらいでTerra の料金で換算すると,両者 10 AI クレジットぐらいで安心しました.
(私の「こんにちは」に Terra 相当の価値があるのかはおいておいて...)
GitHub がどのように計測しているのかはわからないので,これくらいは誤差だとしてください...

ツール定義と結果のトークン

次に大きかったのは,ツール定義の固定費でした.
自作 fixture で 8 個のツールを定義したケースと,必要な 2 個だけに絞ったケースを比べると,入力全体で 295 tokens の差がありました.

条件 ツール数 ツール定義 入力全体
8 個をそのまま送る 8 425 tokens 642 tokens
必要な 2 個だけ送る 2 130 tokens 347 tokens
差分 -6 -295 tokens -295 tokens

同じツール定義が 20 ターン送られれば,単純合計で約 5,900 tokens の差になります.
今はプロンプトキャッッシュがあるので,毎ターンこのトークンを消費するわけではないとは思いますが,ツール定義は必要なものだけに絞る方がよさそうだとは思います.

一方で,文字数を削れば必ずトークンも減るわけではありません.

結果の渡し方 文字数 トークン
ソース全体 312 64
必要な範囲 + 行番号 262 66
必要な範囲,行番号なし 233 51

改行と行頭記号

書式についても同じでした.今回 o200k_base で測った範囲では,連続する改行は 1 token にまとまる一方で,行頭の記号やインデントはトークンを増やしました.
だからといって,すべてを 1 行に詰めるよりも,改行を境界として使い,箇条書きは並列関係を示す価値があるときに使う方がよさそうです.

再判断を減らすために渡すもの

エージェントの効率は,1 回の入力サイズだけでは決まらないです.
聞き返し,誤った前提での作業,やり直し,確認回数まで含めて,完了までの合計で考えるほうがよさそうでした.
下記のようなものがあると,次の行動を選ぶ判断が減らせます.

  • Goal: 何を完了させたいか
  • Evidence: 判断に使ってよい根拠は何か
  • Scope: 触ってよい範囲はどこか
  • Unknown: まだ分かっていないことは何か
  • Action: 次に何をするか
  • Done: どの状態なら完了か
  • Output: どの形式で結果を返すか

ツールも数だけの問題ではありません.
read_fileread_sourceinspect_file のように似た用途のツールが多いと,どれを使うべきか迷います.

返答も,状態を引き継げる形だといいのかなと思います.

evidence:     何を見たか
action:       何をしたか
verification: どう確かめたか
unknowns:     まだ分からないこと

これは出力を単に短くするためではなく,次に読む人や別のエージェントが状態を復元し,妥当な次の行動を選べるようにするためです.

まとめ

人間が AI エージェントの「中の人」になってみると,プロンプトやツール設計の意味みたいなものをちょっと理解できた気がします.

  • モデルは依頼文だけでなく,instructions,ツール定義,履歴,実行結果を受け取っている
  • tool call は実行そのものではなく,クライアントに実行を依頼する返答である
  • トークンを減らしても,検証に必要な根拠まで減らすと次の一手は軽くならない
  • 履歴を積むよりも,決定,根拠,未確認,次の行動を残した状態を渡す方がよい場面がある
  • 終了条件,根拠,範囲を明確にすると,再判断や手戻りを減らしやすい

「具体的に指示する」「前提と未確認を分ける」「結果に根拠を残す」という考え方は,LLM だけではなく,人間同士で作業を引き継ぐときにも役立ちます.
トークン数だけを見るのではなく,完了までに必要な判断の回数を減らせているかを見ていきたいです.

Discussion