コンテンツにスキップ

recall プレビュー階層の設計 (v2.5.0)

翻訳について: 正本は英語版です。日本語版が古い場合は英語版を参照してください。

ステータス: 2026-07-15 承認 (設計レビュー)、2.5.0 pre-release ラインで出荷済み。 対象範囲: recall / recall_with_context の MCP 応答、および新規ツール get_contents。

問題

recall 系ツールはヒットした全行の本文を丸ごと返します。Claude Code 環境での 実測 (トークン棚卸し、2026-07-11): 記憶 1 件あたり ~815 トークン、エピソード 1 件 あたり ~1,040 トークン、limit=10 の 1 呼び出しあたり ≈9–10k トークン。 ×5.2 呼び出し/セッションで ≈ 23k トークン/セッションとなり、recall 側では 単独で最大のコンテキストコストです。

そのテキストの大半は読まれません。呼び出し側がふつう必要とするのは、リスト全体に 対する関連度の判断と、そのうち 1〜2 行の全文だけです。

設計原則 (v2.5.0 の limit 再配置と共通)

整形と上限は境界層の仕事であり、ライブラリは完全なデータを返す。 先行する 変更が、エージェントから見える limit の上限を MCP の JSON Schema へ移し、 ライブラリ側はスキャン窓へのクランプだけを行うようにしました。ライブラリを直接 呼ぶ側が、コンテキスト窓を守るために存在する上限に縛られないようにするためです。 プレビュー階層は、同じ移動をペイロードの形に対して行うものです。

  • do_recall / do_recall_with_context (ライブラリ) — 本文は全文のまま、変更なし。
  • MCP ツールのラッパー (server.py) — 呼び出し側がオプトアウトしない限り、本文を プレビュー階層まで切り詰める。

この配置により、ベンチハーネス (ライブラリの直接呼び出し) と、将来の in-process な 利用側はいずれも、この減量に対して構造的に免疫を持ちます。プレビューを見るのは MCP 経路の利用側だけです。

応答の契約

recall が返す各メッセージには、全文取得のための安定したハンドルが付きます:

{
  "ref": "mem:123",            // or "ep:45" — always present on DB-backed rows
  "content": "<pure prefix, at most CPERSONA_RECALL_PREVIEW_CHARS chars>",
  "content_truncated": true,    // only present when trimmed
  "content_len": 1893,          // full length, only present when trimmed
  "source": {...}, "timestamp": "...", "id": "<msg_id>", "confidence": {...}
}

契約の詳細です。いずれも意味を担っています。

  • ref は新規で、常に存在します (記憶・エピソードとも)。2.5.0 より前は応答に msg_id しか出ておらず (エピソードには存在しません)、追って全文を取りにいく ために行を指す手段がありませんでした。種別を接頭辞にしている (mem: / ep:) のは、 両テーブルが 1 つの AUTOINCREMENT id 空間を共有しているためです (bug-040/041 の 衝突クラス)。
  • プレビューは純粋な前方部分であり、省略記号のマーカーは付きません。 exclude_contents の重複排除契約 (_content_excluded) は、正規化した上での 双方向の前方一致です。したがって、プレビューを後続の呼び出しに渡し返しても、 保存されている全文と一致します。マーカーを埋め込むと、これが誰にも気づかれない まま壊れます。
  • マーカーは切り詰めた時だけ付きます。 短い行は content_truncated も content_len も持ちません (ペイロード減量の一貫性)。
  • recall_with_context がエコーバックする会話エントリは、一律に切り詰められます (呼び出し側はすでにその全文を持っているため)。これらに ref は付きません。

抜粋 (excerpt、2.6)

前方部分が「この行は関連するか?」に答えられるのは、レコードが早い位置でそう 語っている場合だけです。LongMemEval で、質問ごとに同じ 10 行を返したうえで 回答リーダーを当てて測ったところ、プレビューの先頭 500 文字は 500 問中 260 問に 答え、全文レコードは 351 問に答えました。行は届いていたのに、答えは切り口の 向こう側にありました。そこで、プレビューが切り落とす行には、一致した部分も 併せて載せます:

{
  "ref": "mem:123",
  "content": "<pure prefix, unchanged>",
  "content_truncated": true, "content_len": 1893,
  "excerpt": "<matching passages, at most CPERSONA_RECALL_EXCERPT_CHARS, joined by ' … '>",
  "excerpt_basis": "blocks"
}
  • 作り方。 レコードのブロックを、再構成の出口と同じ順序で順位付けし (字面の重なりと、問いのベクトルとの近さを融合。近さは、すべてのブロックが保存済みの int8 ベクトルを持つ時はそのコサイン、そうでない時は符号ビットの Hamming 距離)、 それぞれをそれを支配する範囲 (そのブロックが 属する文と、それを限定する隣の部分 —「ただし」のように前を覆す文) まで広げ、重ならずに上限に収まる限り、その順に 取っていきます。表示はテキスト順なので、抜粋は前から素直に読めます。接している範囲は 1 つの範囲として示し、間に本文がある範囲だけを ' … ' で区切ります (2.6.4)。同じ ベンチマークで 800 文字の抜粋は 500 問中 341 問に答え — 全文レコードの 56% の 長さで、リーダーの実行ごとのノイズの範囲内です — 500 文字では 318 問で、同じ サイズでのプレビューの 260 問を上回りました。
  • excerpt_basis はその作り方を示します。 blocks: レコードの現在の ブロック集合。lexical: 現在のブロック集合がないため、読み取り時にレコードを 分割し、共有語のみで順位付けした。start: レコードが 1 ブロックなので、抜粋は その先頭。
  • 追加のみ。 content はこれまでどおり純粋な前方部分のままなので、 exclude_contents も content を読む利用側もいっさい変化を見ません。抜粋は、 full_content=true のとき (全行が全文)、プレビューが全文で見せる行、そして ライブラリ層では存在しません: do_recall が計算するのは MCP 境界が求めたときだけです。
  • 決定的。 モデルは呼びません。クエリベクトルは recall がすでに埋め込んだものです。

将来のリリースで抜粋を既定の content にし、前方部分を引退させるかもしれません。 その変更は、プレビュー自体と同様、別の判断です。

全文へのアクセス (2 つの経路)

  1. full_content: true — 両方の recall 系ツールに追加された真偽値パラメータ (既定は false) で、信頼できる利用側のためのオプトアウトです。

    bug-211 以降は、1 応答あたり 200,000 文字の予算で上限が付いています。それを 超えた行はプレビュー階層に戻り (関連度の高い行から全文で残します、bug-214)、 応答には full_content_budget_chars が付きます。

    2.5.0 より前のサーバーは未知のパラメータを無視するので、利用側は自分の コネクタが更新されるより前にこれを採用できます (前方互換な移行)。 2. get_contents(agent_id, refs) — 最大 20 件の ref をまとめて全文の行に 解決する新規ツールです。

    読み取りは id をキーとし (出どころは recall)、agent_id の所有述語を伴います。 他のエージェントの ref は missing に入り、漏洩になることはありません。不正な 形式の ref も missing に入ります (fail-soft。1 件の不正な ref がバッチ全体を 中断させてはなりません)。

    20 件という上限があるのは、全文の行 1 件が ~800 トークンに相当するからです。 これより大きなバッチは、プレビューが塞いだコンテキスト爆発の穴を開け直します。

設定

CPERSONA_RECALL_PREVIEW_CHARS — 既定は 500 で、設計レビューで決定しました。 長い行はトークンの 60% 以上を落とせる一方、プレビューは関連度の判断には十分なまま です。0 は切り詰めを完全に無効化します。

利用側への影響と移行

利用側 影響 移行
ベンチハーネス (LMEB) なし — ライブラリの直接呼び出しであり、検索の指標は id ベース なし
ClotoCore カーネル (build_chat_messages、Discord ブリッジ) recall の本文をそのまま LLM プロンプトへ注入するため、プレビューでは品質が落ちる フェーズ 1: full_content: true を渡す (応答が bug-211 の 200,000 文字予算に収まっている限り挙動は同一 — 超えると行はプレビューに降格し、カーネルは残りを get_contents で取得する必要があります。コネクタの bump より先に入れても安全)。フェーズ 2 (任意): カーネル側のコンテキスト減量のためにプレビュー + get_contents を採用する
Claude Code セッション 最大の受益者 (~15k トークン/セッションの見込み) なし — 既定でプレビュー、必要に応じて get_contents

バージョニング

MCP プロトコルとしては追加のみ (新しいフィールド、新しい任意パラメータ、新しい ツール) ですが、既定の本文の形が変わります。これは意図的な破壊的挙動変更であり、 2.5.0 の「破壊的だが内部にとどまる」安定化の軸に分類されます。

利用側は 2.5.0 の alpha ラインの間に、stable なコネクタが出るより前に移行します。 DB 層では何も置き換えません。スキーマ変更はなく、SCHEMA_VERSION は 13 のままです。