35 KB のシステムプロンプトがローカルモデルで壊れる理由
- プロンプトを移す前にコンテキスト予算の計算を行い、Claude では気にならない 35 KB のプロンプトが小さなローカルウィンドウでは 4 分の 1 を占める理由を理解する
- プロンプトが長すぎるときに Ollama が正確に何をするかを知る — 中央部を捨て、先頭の数トークンだけを残し、サーバーログにしか知らせない
- ローカルエージェントにおけるコンテキスト枯渇の 5 つの挙動シグナルを、1 時間を無駄にする前に見抜く
- 一枚岩のプロンプトを、収まる単一目的のユニットに再構成し、ユニット間の状態をディスク経由で受け渡す
- 重要なランタイムのつまみを調整する: OLLAMA_CONTEXT_LENGTH、KV キャッシュの量子化、flash attention、並列リクエストの乗数
2026 年 9 月 13〜14 日に Hacker News のトップページに載ったある投稿は、多くの人がこれから体験するであろう出来事を描いていました。Claude Opus で問題なく動いていた 35 KB のシステムプロンプトを、Ollama 経由でセルフホストした 27B モデルに向けたところ、3 分ほどでエージェントは既に読んだファイルを再び読み、同一のツール呼び出しを繰り返し、完了済みの作業を書き直し始めたのです。モデルに「不具合」があったわけではありません。プロンプトとセッション履歴が 65K トークンのウィンドウを超えてしまい、ランタイムが会話の一部を捨て始めただけでした。
このページは、その投稿者が「あればよかった」と思ったガイドです。テーマはプロンプトのサイズ対コンテキスト予算であり、モデル間のプロンプトの言い回しの問題(モデル間でプロンプトを移植するで扱います)とも、どのローカルモデルを選ぶかの問題(Ollama でモデルをローカル実行するで扱います)とも異なります。
誰も最初にやらない計算
Claude では、35 KB のシステムプロンプトはおよそ 9,000 トークンで、1,000,000 トークンのウィンドウの中に収まります。1 % 未満です。意識することはありません。ローカルランタイムでも同じプロンプトは同じ 9,000 トークンですが、ウィンドウはあなたが設定した値であり、デフォルトは小さいのです。
Ollama のデフォルトコンテキスト(現在の main ドキュメント) | マシン |
|---|---|
| 4K トークン | VRAM 24 GiB 未満 |
| 32K トークン | VRAM 24〜48 GiB |
| 256K トークン | VRAM 48 GiB 以上 |
4K では、9,000 トークンのシステムプロンプトはそもそも収まりません。32K では、ユーザーが一言も発する前にウィンドウの 28 % を占めます。HN の投稿者が設定した 65K では 14 % で、問題なさそうに聞こえますが、エージェントのセッションが実際に運ぶものを足すと話が変わります。ツール定義(数百から数千トークン)、エージェントが読むすべてのファイル、すべてのツール結果、そしてモデル自身の出力です。中程度のファイルを 3 つ読み、コマンドを 2 つ実行するコーディングエージェントは、会話の 20〜30K トークン地点にいるのが普通です。9K の固定コストがあると、使える作業メモリは数ターンで尽きます。
Claude ではこれを見えなくしている 2 つのクラウド側の習慣が、ローカルには存在しません。ウィンドウは 2 桁小さく、組み込みのコンパクションもありません。Claude Code は上限に近づくと会話を要約します(コンテキスト管理)が、素の Ollama エンドポイントはそうしません。一部のローカルエージェントハーネスは独自にコンパクションを行いますが、その下にあるランタイムは行いません。
移行前にプロンプトを計測する(OpenAI 互換のローカルエンドポイントならどれでも)
# Count what the model will actually see, from Ollama's own tokenizer.
# prompt_eval_count in the response is the tokenised prompt length.
curl -s http://localhost:11434/api/chat -d '{
"model": "qwen3:32b",
"messages": [{"role":"system","content":"'"$(cat system-prompt.md | sed 's/"/\\"/g')"'"},
{"role":"user","content":"ping"}],
"stream": false,
"options": {"num_predict": 1}
}' | jq '{prompt_tokens: .prompt_eval_count, cached: .prompt_eval_cached_count}'この数値を、実行時の num_ctx と比較し、そこからツールスキーマと現実的なトランスクリプト分を引いてください。残りがウィンドウのおよそ半分を下回るなら、そのセットアップにはプロンプトが大きすぎ、言い回しをどう工夫しても解決しません。
プロンプトが収まらないとき Ollama は何をするか
これが「少し長すぎる」を「なぜか馬鹿になった」に変えるメカニズムであり、散文のドキュメントにはどこにも書かれていません。ソースの中にあります。
リクエストのプロンプトがコンテキストより長く、切り詰めが有効な場合(/api/chat と /api/generate ではデフォルトで有効です)、Ollama の llama.cpp ベースのランナーは llm/llama_server.go で次の処理を行います。
- プロンプトの先頭
num_keepトークンを保持する。デフォルトのnum_keepは 4 で、「コンテキストシフト時の問題を避けるため」に設定されています。 - 生成のために残りのコンテキストのおよそ半分を空けておく目標長を計算する。
- トークンを中央部から捨てる。保持された先頭部分は残り、末尾も残り、その間のブロックが削除されます。
- 切り詰め前後のサイズとともに
truncating input promptをサーバー側にログ出力する。API レスポンスにはフラグが一切なく、クライアントライブラリには通常の補完として見えます。
プロジェクト自身のテストテーブルがその規模を具体的に示しています。4,096 トークンのコンテキストとデフォルトの num_keep では、長すぎるプロンプトは 2,050 トークンに切り詰められます。ウィンドウの半分は返答のために意図的に確保されます。つまり 4K ウィンドウで 9,000 トークンのシステムプロンプトは、冒頭の 4 トークン、次に穴、そして会話の末尾に収まる分だけ、という形でモデルに届きます。指示は消え、最後のユーザーメッセージだけが生き残ります。これこそが HN の投稿が描いた「目的を言い直す、ツール呼び出しを繰り返す」という挙動です。モデルは、ルールが無言で削除された会話に対して行動するよう求められているのです。
無言バージョンから抜け出す方法は 2 つあります。
- リクエストで
"truncate": falseを送る。Ollama は推測する代わりに、プロンプトが現在利用可能なコンテキストより長いというメッセージとともに HTTP 400 を返します。エージェントハーネスにとってはこれが正しい挙動です。大きな音で失敗し、それからコンパクションするか再起動します。 num_keepをシステムプロンプトの長さまで上げて、先頭部分がシフトを生き延びるようにする。これでルールは保たれますが、会話は依然として中央部から削除されます。応急処置であって修正ではありません。
チャットエンドポイントには、この上にもう一段穏やかな層があります。メッセージリストが上限に対して長すぎるとき、Ollama のプロンプトビルダーはシステムメッセージと最新のターンを保ちつつ、古いメッセージから先に落とします(server/prompt.go を参照)。この方法でも中間のツール結果を失うことがあり、それがエージェントが既に読んだファイルを「忘れる」理由です。サーバーログを注視してください。OLLAMA_DEBUG=1 を付ければトリミングが見えます。
llama.cpp のサーバーも別のフラグの下で同じコンテキストシフトの挙動を持ち、vLLM はデフォルトで長すぎるプロンプトを拒否し、LM Studio はコンテキスト長をモデルごとの設定として公開していますが、小さいデフォルトのまま放置されがちです。身につけるべきパターンはこうです: 自分のランタイムが切り詰めるのかエラーを出すのかを知り、エラーの方を選ぶ。
5 つの失敗シグナル
HN 投稿の著者は、コンテキスト枯渇が外からどう見えるかのリストを残しており、それは小さなウィンドウのエージェントについて人々が報告する内容と一致します。これらのどれかが出たら「モデルが悪い」ではなく「ウィンドウが満杯」と捉えてください。
- 連続する同一のツール呼び出し。 モデルは前回の結果を視野に持っていないので、再び尋ねます。
- 既に読んだファイルを再び読む。 同じ原因で、読み取り結果がトリミングされました。
- タスクの途中で目的を自分の言葉で言い直す、ときにずれを伴って。システムプロンプトが切られ、モデルは会話の末尾からタスクを再構築しています。
- きれいな呼び出しが続いた後に現れるツール呼び出しのパース失敗。プロンプト内の例とスキーマが消えると、小さなモデルはデフォルトのフォーマットに戻ります。
- 実際に変更されたファイル数に対してターン数が多い。 この比率はハーネスで追跡するのに最も安価なメトリクスです。急上昇したら、止めて新しいコンテキストで再開してください。
OpenAI 互換クライアント経由で実行しているなら、毎ターン prompt_eval_count をログに出してください。会話が伸び続けているのにこの値が増えなくなったら、切り詰めが始まっています。
再構成: 単一目的プロンプトとディスク上の状態
35 KB のプロンプトは、たいてい製品一つ分のルールの塊です。ペルソナ、コーディング規約、ツールの作法、出力フォーマット、エッジケース、そして長い「決して X をするな」のリスト。1M トークンのモデルでは力技で通用します。32K のモデルでは通用せず、HN の著者がたどり着いた解決策は、最も鋭いコメント投稿者たちが独立に主張したものと同じでした。プロンプトを単一目的のユニットに分割し、それぞれを独自の短いセッションで実行する。
- 35 KB を通読し、各段落にそれが担う仕事をタグ付けする: 計画、編集、テスト、レビュー、コミットの記述。ほとんどの一枚岩は 5〜8 個の異なる仕事と、2 KB 未満の共有コア(ペルソナ、リポジトリの事実、安全ルール)で構成されている。
- 各仕事には共有コアと、その仕事固有のルールと例だけを持たせる。それぞれ 2〜4K トークンを目標にする。Ollama では、別々の Modelfile(`PARAMETER num_ctx` を明示的に設定し、仕事ごとに `SYSTEM` ブロック)として出荷するか、ハーネス内のエージェント別プロンプトファイルとして配置する。
- 各ユニットは終了時に短い構造化サマリー(何をしたか、何が残っているか、どのファイルか)をファイルに書き出す。次のユニットはそのファイルと、必要なリポジトリの一部だけを読む。セッションはそこで終了する。これは Claude Code のコンパクションのローカルモデル版を手作業で行うものだ。
- 小さなモデルは「Y をするな」より「X だけをせよ」にはるかによく従い、否定形のリストは一枚岩プロンプトが膨らむ場所でもある。`never touch tests` を `edit only files under src/` に変換する。
- すべてのツール結果は、トリミングされるまでコストを払い続けるコンテキストだ。ファイル全体を返すツールより、一部(1 つの関数、1 つのハンク)を返すツールを優先する。HN のあるコメント投稿者の `inspect_function` / `replace_function` のペアが典型例だ。
- ユニットを終え、引き継ぎを書き、セッションを終了する。失敗シグナルを待ってはいけない。その時点でプロンプトは既に切り詰められており、出力は信頼できない。
次の単一目的ユニットが最初に読む引き継ぎファイル
# HANDOFF — written by unit 2 (implement), read by unit 3 (test) Objective completed: renamed getUser -> fetchUser across src/ Files changed: src/api/user.ts, src/pages/profile.tsx, src/hooks/useUser.ts Not done: tests still reference getUser (tests/user.test.ts) Constraints carried forward: no new dependencies; keep TypeScript strict Next unit should: update tests, run `npm test`, write result to HANDOFF.md
これは、35 KB のプロンプトは「どの LLM でも混乱を招き焦点が定まらない」と述べたコメント投稿者への正直な答えでもあります。フロンティアモデルは肥大化を覆い隠し、ローカルモデルはそれを露わにします。目的ごとの分割は Claude でも通常は結果を改善し、Claude とローカルモデルの併用が安価なステップをローカルに振り分ける際に前提とする設計でもあります。
ランタイムを調整する
プロンプトが適切なサイズになったら、実際にどれだけのウィンドウを賄えるかは 4 つのつまみで決まります。すべて Ollama の FAQ とドキュメントからのものです。
| つまみ | 何をするか | 落とし穴 |
|---|---|---|
OLLAMA_CONTEXT_LENGTH=65536 ollama serve(またはリクエストごとの num_ctx、または Modelfile 内の PARAMETER num_ctx) | ウィンドウを設定する | コンテキストが大きいほどメモリを消費する。ドキュメントは率直にそう述べている。KV キャッシュはこれに比例して線形に増える |
OLLAMA_KV_CACHE_TYPE=q8_0 | KV キャッシュを量子化する。デフォルトの f16 のおよそ半分のメモリ。q4_0 はおよそ 4 分の 1 | flash attention が必要。品質コストは小さいが、q4_0 ではより大きい |
OLLAMA_FLASH_ATTENTION=1 | flash attention を有効にする。KV 量子化の前提条件で、長いコンテキストのメモリを削減する | すべてのバックエンド/モデルが対応しているわけではない。設定してサーバーログを確認する |
OLLAMA_NUM_PARALLEL(デフォルト 1) | モデルごとの同時リクエスト数 | 必要メモリは OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH に比例して増える。 64K の並列スロット 2 つは、128K のスロット 1 つ分の KV メモリを消費する |
固定メモリ予算での有用な手順はこうです: 小さいウィンドウで保持できる最大のモデルを選び、次に flash attention と q8_0 KV キャッシュを有効にし、それからサーバーログが CPU へのあふれを示すまでコンテキストを上げていく。GPU と CPU にまたがるコンテキスト分割は、トークン毎秒が崩壊する場所です。HN の複数のコメント投稿者がデュアル GPU 構成で FP8 の密な 27B〜32B モデルを非常に長いウィンドウで動かしたと報告していますが、単一の 128 GB ユニファイドメモリ機では、実用的な上限は著者が使った 65K でした。
Ollama はすべてのレスポンスで prompt_eval_cached_count を報告します。会話の一番最初にある安定したシステムプロンプトは、リクエスト間で KV キャッシュから再利用されるため、そのコストはターンごとではなくセッションごとに 1 回だけ支払われます。会話内で最初に変化した箇所以降はすべて再計算されます。固定の素材を先頭に、変動する素材を末尾に置く。Claude のプロンプトキャッシュと同じルールです(プロンプトキャッシュの経済学)。
移行チェックリスト
- システムプロンプトをローカルのトークナイザーでトークン化し、その数を書き留める。
-
num_ctxを明示的に設定する。デフォルトに頼らない。マシンの VRAM に依存する。 - 切り詰めかエラーかを決める。エージェントでは
"truncate": falseを送り、400 を処理する。 - プロンプトを目的ごとに分割する。共有コアは 2K トークン未満、各ユニットは 2〜4K。
- ユニット間の引き継ぎはファイル経由で行い、各ユニットの後でセッションを終了する。
- ターンごとに
prompt_eval_countをログに出し、頭打ちになったらアラートを出す。 - ウィンドウを上げる前に flash attention と
q8_0KV キャッシュを有効にする。 - 最初の 1 週間はサーバーログで
truncating input promptを監視する。
理解度チェック
0/5出典と参考資料
- Notes on gotchas while migrating 35 KB preprompts from Opus to self-hosted Ollama — このページのきっかけとなった 2026 年 9 月の投稿(128 GB Ryzen AI MAX+ 395、65K ウィンドウ、27B モデル、OpenCode ハーネス)と、その Hacker News での議論
- Ollama ドキュメント: コンテキスト長 — VRAM 段階別のデフォルトと
OLLAMA_CONTEXT_LENGTH - Ollama FAQ —
OLLAMA_KV_CACHE_TYPE、OLLAMA_FLASH_ATTENTION、OLLAMA_NUM_PARALLELとメモリルール - Ollama ソース:
llm/llama_server.go—truncating input promptのコンテキストシフトコードとそのテストテーブル。num_keep: 4のデフォルトはapi/types.go、truncateのデフォルト true はserver/routes.go - Ollama API リファレンス —
prompt_eval_count、prompt_eval_cached_count、options.num_ctx、keep_alive
次のステップ
- Claude とローカルモデルの併用 — 大きなプロンプトを Claude に残し、小さな単一目的のステップをローカルに送るハイブリッドパターン
- プライベートなローカル AI スタック — プロンプトが収まった後の、ランタイム、ハーネス、モデルの選択
- Claude Code のコンテキスト管理 — Claude ではコンパクションが何をしてくれるのかを知り、ローカルで手作業で再構築すべきものを把握する