ローカルLLMでTool Callを安定させるHermes Adapter設計メモ

なぜこのアプローチを試しているのか

  • ローカル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 CLItool call の実行、sandbox、approval、stdout/stderr capture
OpenAI-compatible proxyResponses API と Chat Completions API の変換
Hermes adapterHermes 専用 prompt、tool parser、tool pruning、deterministic routing
llama-serverGGUF model の推論
internal read-only toolsURL 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 は次の変換を担当する。

  1. OpenAI 互換の tools schema を受け取る
  2. Hermes 向けの <tools> prompt を組み立てる
  3. Hermes が返した <tool_call>{...}</tool_call> を parse する
  4. OpenAI 互換の message.tool_calls に戻す
  5. 必要に応じて 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 に限定する。

  • pwd
  • date
  • cat
  • ls
  • rg
  • 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_urlsearch_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 に寄せる設計である。