挙動契約 (Behavior Contracts)¶
対象: CPersona 2.5.x。 このページの記述は現行リリースラインのソースと 照合済みです。ここに書かれた挙動は契約です。呼び出し側は依存してよく、 変更する場合は pre-release ladder とリリースノートを通ります (リリースライフサイクル 参照)。告知なく 変わることはありません。
翻訳について: 正本は英語版です。記述が食い違う場合は英語版が優先です (右上の言語切替から参照できます)。
このページは、ツール名だけからは取り違えやすい挙動を集めたものです。 いくつかは実運用のオペレーターが外側から CPersona を計測して発見しました。 挙動が意外に見えるものには、その場で理由を併記しています。
1. recall の返却順: 末尾が最良¶
recall は内部で候補を「良い順」に並べ、limit で切り、そのスライスを
反転します。応答はスコアの昇順で並んでおり、最後の要素が最強のマッチ
です。
これは意図的な設計です。LLM は文脈の末尾に最も強く注意を向ける ("lost in the middle") ため、最強の記憶を注入点の手前側に置いています。
帰結:
- 評価するとき: recall の応答に対して hit@k を測るなら、末尾から 数えてください。先頭から数えると結果が反転します。
recall_with_contextは契約が異なります。 渡した会話履歴と想起した記憶を マージし、スコア順ではなく時系列順で返します。
ここでの時系列は、タイムスタンプの文字列ではなく、それが指す時刻です。 どの UTC オフセットで書かれていても、マージのどちら側が書いたものであれ、 他のすべてのスタンプに対して正しい位置に入ります。オフセットなしの表記は UTC として読まれ、同じ扱いになります。
タイムスタンプが無い、または解釈できないメッセージは時刻を指しません。 それらは時刻を持つすべてのメッセージより前に、マージされた順序のまま 置かれます。末尾は、本当に最新のものだけのための位置です。
2. confidence スコアリングは融合モードを上書きする¶
CPERSONA_CONFIDENCE_ENABLED (既定 false) は、メタデータを足すだけの
スイッチではありません。on にすると:
- 結果集合が confidence スコアで並べ直され、
- 品質ゲートが融合スコアではなく confidence を見るようになります。
融合モード (CPERSONA_RECALL_MODE=rrf|rsf|cascade) はどの候補が結果集合に
入るかは引き続き決めますが、返ってくる順序はもう決めません。
1,545 文書のコーパスに 394 クエリで実測しました。confidence を on にすると
rsf と rrf は 394 クエリ全てで同一の行を同一の順序で返し、off では一致率が
10% 未満でした。
ランキングとゲートのシグナル優先順位は confidence > rsf > cosine > rrf
です。スコアの付いた行では match_reason.signal が、その行で実際にどの分岐が
効いたかを報告します。
スコアの付かない行は、このキー自体を持ちません。cascade recall が埋める
FTS / keyword 行と、confidence off のときの注入プロフィール行がそれです。
confidence on では、プロフィール行も他の行と同様にスコアが付き match_reason
を持ちます。match_reason は「全行にあるフィールド」ではなく「あるかないか」
として扱ってください。
なお confidence はマッチ強度ではありません。コサイン類似度・時間減衰・ resolved 状態・想起回数をブレンドした別の量です。完全一致の行が言い換えの行 よりこの尺度で低く出ることは、正当に起こり得ます。
3. エピソード境界ペナルティ¶
エピソードが存在するとき、最新のエピソード境界より古い記憶には減衰係数が 掛かります:
factor = max(exp(-RATE × hours_before_boundary), FLOOR)
| つまみ | 環境変数 | 既定値 |
|---|---|---|
| 有効化 | CPERSONA_EPISODE_PENALTY_ENABLED |
true |
| レート | CPERSONA_EPISODE_DECAY_RATE |
0.01 |
| 下限 | CPERSONA_EPISODE_DECAY_FLOOR |
0.5 |
- 境界は最新エピソードの
created_atで、クエリと同じ分離軸 (agent / project / channel) にスコープされます。無関係なバケットの エピソードが、あなたの境界を動かすことはありません。 - 境界以降の記憶 (現在のセッション) は影響を受けません (係数 1.0)。
- 既定値では係数は約 69 時間 (
ln 2 / 0.01) で下限に達します。つまり 約 3 日より古いものは一律に半減します。この機構は現在のセッションへの 緩い優先であって、細かい新しさランキングではありません。直近数日の 内部での順序付けは、この分解能の外です。緩やかなカーブが欲しければRATE=0.002でランプを約 2 週間に伸ばせます。
一括インポートの落とし穴。 境界は単に「最も新しいエピソード行」です。
過去の会話を archive_episode で後から流し込むと、インポート時刻が境界に
なり、既存の記憶が全てペナルティ領域に落ちます。エピソードを後から流し込まない
か、インポート中は CPERSONA_EPISODE_PENALTY_ENABLED=false にしてください。
4. ベクトル走査ウィンドウ (CPERSONA_MAX_MEMORIES)¶
CPERSONA_MAX_MEMORIES (既定 10000) は保存上限ではありません。
ベクトル検索の走査ウィンドウです。ベクトル検索は直近 N 行だけを対象に
します (記憶とエピソードはそれぞれ別にウィンドウ適用されます)。ウィンドウより
古い行はベクトル検索からは見えません。ただしウィンドウ制限のない FTS 経路と
キーワード経路からは到達できます。
このウィンドウは同時に「新しさの事前分布」でもあります。 最も新しい行だけを 残すことで、最近の記憶それぞれに対して、コーパス全体ではなく N 行という候補集合を 与えています。これは精度のコストではなく、精度そのものです。
237,654 件の保存済み文書での実測では、ウィンドウを 10,000 から 200,000 に広げると、 答えがウィンドウより古い側にある場合は NDCG@10 が 4.93 向上し、ウィンドウ内に ある場合は 20.19 低下しました (結果の切り詰めは一切発生していません)。
この低下は順位の押し出しです。ベクトル検索は融合層に上位 limit 件だけを渡します。
10,000 件中 3 位だった最近の答えが 200,000 件中 30 位になると、それはリストの下位に
落ちるのではなくリストから外れ、票そのものが消えます。したがってこの値を
上げるのは単なる緩和ではありません。 事前分布を取り除くことで到達距離を
伸ばしています。
CPERSONA_VECTOR_REACH (既定 0) はこの 2 つを分離します。効果を持たせるには
CPERSONA_MAX_MEMORIES より大きく設定する必要があります。同値以下では何も
変わらず、追加の処理も走りません。
大きくすると、ウィンドウと reach の間にある行が第 2 のリストとしてランク付けされ、 もう 1 本のランク済みリストとして融合層に渡されます。しきい値も打ち切りも同点処理も、 1 本目と同じです。ウィンドウの幅は変わらないので、 いま結果に入っている行はいまと同じ票を保ち、古い行は追加されうるだけです。
適用範囲には 2 つの制限があります。融合モード限定
(CPERSONA_RECALL_MODE=rrf または rsf。cascade はリストを融合せず段を連結
するので、この設定を無視します) と、ローカルのベクトル検索限定
(CPERSONA_VECTOR_SEARCH_MODE=remote ではサービス側が自身のウィンドウで
ランク付けします) です。
rsf では遠方リストは第 4 のチャネルとして融合されます。合計がアクティブな
チャネル数で割られる関係上、cosine スケールの min_score に対して融合スコア
全体が下がります。この設定の事前登録された測定は rrf に対するもので、rsf
については何の主張もしていません。
CPERSONA_VECTOR_FAR_LIMIT (既定 0) は、その第 2 のリストのうち何行を融合層に
渡すかを制限します。既定では上限は応答の limit で、これは reach だけを設定した
ときにできるリストそのものです。0 より大きくすると、limit とその値の小さい方
まで、同じリストの先頭から切り詰められます。決めるのは遠方の何行が投票できるか
だけで、行のスコア計算には一切関与しません。
ウィンドウを上げることは、同じ動作で到達範囲を伸ばし、新しさの優遇を消します。 実測では、古い答えで 5 点を得る代わりに、最近の答えで NDCG@10 を 20 点失いました (到達範囲・新しさ・far の票)。これは値段の付いたつまみであって、大きなコーパスに 対するサポートされた答えではありません。既定値は far の票に値段が付くまで 動きません。
I/O のコスト目安です。768 次元 float32 の埋め込みは 1 行あたり約 3 KB なので、
10,000 行のウィンドウでは最悪ケースで 1 回の recall あたり約 60 MB を読みます
(記憶 + エピソード)。reach を有効にした場合のコストも同じ形で効きます。1 回の
recall が現在より CPERSONA_VECTOR_REACH − CPERSONA_MAX_MEMORIES 行だけ多く
埋め込みを読みます。
連続配置ベクトルインデックスが構築済みなら、それはインデックスの高速経路です。 無ければチャンク分割されたテーブル走査になります。後者のレイテンシは、237,654 行の コーパスに対する reach 200,000 の実測で既定の約 2 倍でした (いずれの場合も キーワード経路が下限として効きます)。メモリ使用量はどちらの数値に対しても、 走査が保持するチャンクとマップするインデックスファイル以上には増えません。
アーカイブや間引きの定期処理は不要です。長期的なモデルは物理削除をせず、古い行はウィンドウと減衰で沈ませるというものです。
5. 重複排除の意味論: upsert ではなく skip¶
store は 2 通りの重複排除を行いますが、両者のスコープは異なります:
msg_id重複排除。 既存のmsg_idを持つstoreはスキップされ ます (result: "skipped"、既存行の id をエコー)。この判定は agent と project には及びますが、channelには及びません。同じmsg_idを別の channel に 書いても、最初の channel の行に対してスキップされます。- 内容重複排除。 同一の内容文字列も同様にスキップされ、agent / project /
channel にスコープされます。ユニーク索引が裏打ちしますが、その範囲は厳密な
バケット (
agent_id, project_id, channel, content) に限られる一方、先に走る 判定はグローバルプールも見ます。したがって異なる project バケットへ並行に 書き込む 2 者は、どちらも着地しえます。
決定的な帰結: upsert は存在しません。 同じ msg_id で変更された内容を
再保存しても、保存済みの行は更新されません。スキップされます。保存済みの
記憶を変更するには update_memory (自動で再埋め込み) か、delete_memory +
store を使ってください。
裏を返せば、依存してよい保証でもあります。変更のない内容の再投入は構造上 無害なので、コーパスを素朴に丸ごと再投入しても安全です。この意味論の上で 文書索引を回す方法は コーパス索引パターン を参照してください。
ハンドラまで到達した store は result を持ちます。stored (行を書いた)、
skipped (重複ヒット、または永続化が一時停止中。異常ではありません)、rejected
(拒否、reason 付き) のいずれかです。
その上位に 1 層あり、そこは汎用の形で答えます。ACL を設定している場合、
クライアントに許可されていない呼び出しは
{ok: false, error: "permission_denied", tool, client_id} を返し、result を
持ちません。まず ok is false で分岐し、その後に result を見てください。
6. autocut は類似度スケールのシグナルでしか発火しない¶
autocut (スコア差が最大の箇所で切り落とす仕組み) は、スコアの差が関連性の 切れ目を表しているという前提に立ちます。これが成り立つのは類似度スケールの シグナルだけです:
- 発火するのは、confidence スコアリング下、または全行がシグナルを持つ 均質な生コサインのリストです。
- 意図的に不活性なのは、
rsfとrrfの順序付け下です。ランク融合の スコアは構造上双曲的に減衰し、その差は関連性の切れ目ではなく retriever の 重なりを表します。融合順の結果では、混入の抑制は融合側の品質ゲートが 担います。
したがって既定構成 (confidence off、rrf または rsf) では、
CPERSONA_AUTOCUT_MIN_RESULTS をいじっても recall 件数は変わりません。
融合モードでゲートを動かすつまみは set_recall_precision です。
recall のチューニング を参照してください。
7. プロフィール行はスコアを持たない¶
update_profile の行は注入行として recall 応答に付加され、スコアリングには
参加しません。行は最大 1 件です。profiles は (agent_id, user_id) で一意で
あり、どの書き込み経路も user_id を '' に束縛するため、2 回目の
update_profile は蓄積されず 1 件目を置き換えます。
- 行が 50 件未満のプールでは、confidence の on / off にかかわらずどのスコア
分岐よりも前に落とされます。ここでのプールは、その recall の隔離スコープに
おける memories + episodes の合計件数です。したがって小さなコーパスや狭く
スコープされたコーパスでは、recall の他の設定がどうであれプロフィール行は
返りません。30 件のプールで、どの保存行も答えないクエリ (=
limitは何も 切り落とさない) を confidence on で実測: 応答は空、同じ実行内の 50 件の 対照ではプロフィールが返りました。 - この閾値を超えたうえで confidence off (既定) の場合、プロフィール行は
スコアを持たず最後尾に並び、スコア付きの結果だけで
limitが埋まっていると 切り落とされます。実コーパスにrsf/limit=10で実測: 生き残ったプロフィール行は 0 件でした。 - この閾値を超えたうえで confidence on の場合、プロフィール行は高い confidence スコアを受け取り、安定して上位に現れます。
confidence を有効にして運用しているのでない限り、プロフィールを「常に必ず
注入されるチャネル」として扱わないでください。常に必ず存在してほしい事実に
対する正しい機構は、確率的な recall ではなく決定的注入 (あなたの CLAUDE.md
やシステムプロンプト) です。
recall に頼らないという選択
を参照してください。
8. gate_fallback の応答は低信頼¶
recall の応答が gate_fallback: true を伴う場合 (伴わないときは欠落)、
全ての候補が品質ゲートを下回ったため、空の結果の代わりにゲート未満の
字句マッチを返したという意味です。これらの行は低信頼として扱ってください。
識別子やハッシュの検索で、完全一致がクエリ文と意味的に遠い場合に典型的です。
この救済経路は confidence スコアリング有効時にしか存在しません。 返される行に
印を付けるのは、CPERSONA_CONFIDENCE_ENABLED が有効なときだけ走る backfill です。
したがって既定の構成では gate_fallback は決して現れません。全候補がゲートを
下回った recall は、単に空を返します。
9. lock_memory は保護する、押し上げはしない¶
lock_memory は行を削除と編集から保護します。ランキングには影響しません。
ロックした記憶でも recall に負けることはあります。要件が「決して失われては
ならない」ならロックしてください。要件が「常に文脈に載っていてほしい」なら、
決定的注入を使ってください (プロフィールに関する注意は §7 を参照)。
10. 応答の形: 成功と失敗の見分け方¶
v2.5.2 以降、規則は統一されています: ok is false で分岐し、error を
伴う応答は ok の有無にかかわらず失敗として扱ってください。
同リリースで 3 点が変わりました。いずれも、それまでの形では失敗が成功として 読めてしまったためです。
storeは結果をresultで報告します —stored/skipped/rejected— 常に true のokではなくなりました。以前は拒否された書き込みが 成功と同じ形に見えていました (§5 参照)。check_healthはstatusという単一の判定を返します。 従来のhealthyブール値はありません。- ハンドラが返すツールレベルの失敗は、すべて
ok: falseを伴います。 以前は多くがerrorだけを返し、分岐できるokがありませんでした。説明は 従来どおりerrorで運ばれます。ただしstoreだけはreasonに入ります。
次の 2 つの形は、この規則の外側にあります。以前からそうでした。
- 最外層の MCP ディスパッチは、未知のツール名や、ハンドラから漏れた例外に
対して、
okを持たない素のerrorで応答します。この層は他の Cloto サーバーと 共有するライブラリから vendoring されているため、揃えるにはローカルの修正では なく上流の変更が要ります。 - 成功した読み取り (
get_contents/list_memories/list_episodes/get_profile) も、okを持たないペイロードを返します。
どちらも上の規則で覆われます。規則を「ok を確認する」ではなく「error を伴う
応答は失敗として扱う」と述べているのは、そのためです。