ローカルLLMでTool Callを安定させるHermes Adapter設計メモ
Posted:
なぜこのアプローチを試しているのか
- ローカルLLM、日本語は結構返してくれるようになった
- tool searchの段階で結構あやしい
- ローカルLLMの性能に期待せずに、途中でインターセプトしてcodexなどから使えないか?と考えた
- 2026年時点ではまだうまくできてない(かなりのユースケースに対応するコードが必要そう)
ローカルLLMでTool Callを安定させるHermes Adapter設計メモ
小型のローカル LLM を coding agent や terminal helper に使う場合、通常の chat 品質よりも tool call の安定性が先に問題になる。
特に 8B 級の model では、次のような失敗が起きやすい。
- tool call すべき場面で自然文のコマンド例を返す
- tool name や arguments の JSON が崩れる
- tool 数が多すぎて選択を誤る
- tool output に混じった環境 warning を実行結果と誤解する
- 「実行しないで」と言われたのに実行系 tool を選びかける
このメモは、Hermes 3 Llama 3.1 8B Q5_K_M を例に、ローカル LLM と Codex CLI の間に小さい adapter を置いて tool call を安定させる設計をまとめる。
基本構成
想定した流れは次のようになる。
flowchart LR
CLI[Codex CLI] --> Proxy[OpenAI-compatible proxy]
Proxy --> Adapter[Hermes tool adapter]
Adapter --> Llama[llama-server]
Llama --> Model[Hermes 3 Llama 3.1 8B GGUF]
Adapter --> InternalTools[adapter internal read-only tools]
InternalTools --> Fetch[fetch_url]
InternalTools --> Search[search_web]
InternalTools --> Research[optional research helper]
CLI --> ToolExecutor[Codex tool executor]
ToolExecutor --> Shell[exec_command / write_stdin]
役割を分けると、
| 層 | 役割 |
|---|---|
| Codex CLI | tool call の実行、sandbox、approval、stdout/stderr capture |
| OpenAI-compatible proxy | Responses API と Chat Completions API の変換 |
| Hermes adapter | Hermes 専用 prompt、tool parser、tool pruning、deterministic routing |
| llama-server | GGUF model の推論 |
| internal read-only tools | URL fetch や web search のように adapter 内で完結する軽い read-only 処理 |
重要なのは、adapter が shell を直接実行しないこと。shell 実行の安全境界は Codex CLI 側に残し、adapter は「どの tool call を返すか」を助ける層に留める。
なぜAdapterが必要か
Hermes は <tools> と <tool_call> を使う専用の function calling 形式を持つ。一方、Codex CLI から proxy へ来る request は OpenAI 互換の tool schema であり、そのままローカル model に渡しても期待通りの tool_calls にならないことがある。
adapter は次の変換を担当する。
- OpenAI 互換の
toolsschema を受け取る - Hermes 向けの
<tools>prompt を組み立てる - Hermes が返した
<tool_call>{...}</tool_call>を parse する - OpenAI 互換の
message.tool_callsに戻す - 必要に応じて final answer の重複や malformed tool call を補正する
この形にすると、model 固有の癖を adapter に閉じ込められる。proxy や Codex CLI の側からは、通常の OpenAI-compatible endpoint に見える。
Tool数を減らす
Codex CLI からは shell、stdin、plan、MCP resource、image、sub-agent など多くの tool schema が届くことがある。hosted model なら扱えても、8B 級 local model には多すぎる。
adapter 側では、turn ごとに model へ見せる tool を絞る。
| ユーザー意図 | modelへ見せるtoolの例 |
|---|---|
| shell 調査 | exec_command, write_stdin |
| 明示的な計画更新 | plan 系 |
| MCP resource 一覧の明示要求 | resource 系 |
| 画像ファイルの明示 | image 表示系 |
| sub-agent の明示 | sub-agent 系 |
通常 turn では shell 系と最小限の補助 tool だけを前に出す。MCP resource listing は、ユーザーが resource 一覧を明示した場合だけ見せる。これにより、MCP tool と resource API の混同を減らせる。
Deterministic Router
小型 model に「これは shell 実行依頼か」を毎回判断させると不安定になる。そのため、明示的な read-only command は adapter が deterministic に tool call へ変換する。
対象にするのは、まず副作用が小さい command に限定する。
pwddatecatlsrg- bounded な
journalctl - 状態確認だけの
systemctl
逆に、次のようなものは upstream model に投げる前に抑止する。
- file mutation
- package install
- service restart
- redirect / pipe / heredoc
- shell substitution
- unbounded log read
- destructive git command
- infrastructure apply 系 command
この設計のポイントは、read-only allowlist を安易に広げないこと。read-only に見える command でも、credential、plugin、remote API、cluster state に触るものは副作用や情報漏洩の境界が曖昧になる。
Output Relay
tool result を再度 LLM に解釈させると、warning や log の一部を誤読することがある。特にユーザーが「出力だけ」「最後の行だけ」「そのまま返して」と指定している場合は、adapter が deterministic に結果を返す方が安定する。 (ただしユーザー側が「出力だけ」というフレーズを覚えて対応する必要がある点がネック)
扱いやすい selector は次のようなものだった。
- stdout だけ
- stderr だけ
- exit code だけ
- 最初の行
- 最後の行
- JSON の特定 key
- UUID のような明確な marker
これは model の能力を上げる工夫ではなく、model に判断させない場所を増やす工夫である。
Warning Filter
terminal tool output には、実行対象 command の stdout ではない環境 warning が混じることがある。これを LLM に渡すと、「ファイルが読めなかった」「コマンドが失敗した」と誤解することがある。
対策は allowlist 方式にする。
- 既知の無害 warning だけを分離する
- 完全削除ではなく、adapter log や metadata には残す
- unknown warning は隠さない
安全側に倒すなら、「重要かもしれない warning は model に見せる」が基本である。見せないのは、原因と影響を確認済みの既知 warning だけにする。
Internal Read-Only Web Tools
Codex CLI が知らない tool call を adapter が返しても、Codex CLI は実行できない。そこで fetch_url や search_web のような小さい read-only web tool は、adapter 内部で実行してよい。
この場合は SSRF 対策が必須になる。
- HTTP / HTTPS のみに限定する
- localhost、private IP、link-local、metadata IP を拒否する
- timeout を短くする
- max bytes を決める
- redirect の扱いを明示する
- response body をそのまま長文で model に戻さない
外部検索や高レベルな調査 helper は、通常 regression から分ける。network、quota、credential、desktop keyring などに依存すると、adapter の parser 品質とは別の理由で不安定になるためだ。
Context Compression
8B 級 model では、長い過去会話や長い tool result をそのまま入れると tool selection が悪化しやすい。ただし、雑な要約を tool selection 前に入れると、path、flag、URL、negative intent などが消えて逆効果になる。
そのため、ここで使うのは「汎用要約」ではなく loss-aware な context compression として扱う。
保持すべきもの:
- 最新 user turn
- command candidate
- path
- URL
- service name
- error line
- exit code
- negative intent
- required argument
縮めてよいもの:
- 古い背景説明
- 長い web page
- 長い log の繰り返し
- 長い tool output の低関連部分
実装としては、まず exact fact preservation と BM25-lite のような query-aware extraction が扱いやすい。TextRank や LexRank は背景説明の圧縮には使えるが、tool selection 直前の主役にはしにくい。
実装方針のまとめ
ローカル LLM を tool calling に使う場合、model に全部を任せるより、adapter で次を引き受けた方が安定する。
- model 固有 prompt と parser
- tool schema pruning
- 明示 read-only command の deterministic routing
- non-read-only command の deterministic suppression
- warning filter
- output-only relay
- internal read-only web tool
- malformed / duplicate の補正
- context compression
これは local model を hosted model と同等にする設計ではない。小型 model が得意な判断に集中できるよう、周辺の決定を deterministic に寄せる設計である。