コンテンツにスキップ

運用 Runbook

対象: CPersona 2.5.x。 このページは運用に関する正式リファレンスです。 バックアップ、劣化検知、recall のチューニング、CJK の指針、コーパス索引の パターンを扱います。ここが依拠している挙動の事実は契約です。 挙動契約 を参照してください。

翻訳について: 正本は英語版です。記述が食い違う場合は英語版が優先です (右上の言語切替から参照できます)。


バックアップとリストア

データベースは WAL モードで動作する単一の SQLite ファイル (CPERSONA_DB_PATH) です。稼働中の WAL データベースを素の cp でコピーすると、チェックポイントを またいで壊れたコピーができ得ます。 スクリプト化しないでください。

推奨順:

  1. オンライン物理バックアップ (第一選択 — サーバー稼働中でも安全):
sqlite3 "$CPERSONA_DB_PATH" ".backup 'cpersona-backup.db'"
# または
sqlite3 "$CPERSONA_DB_PATH" "VACUUM INTO 'cpersona-backup.db'"

どちらも並行書き込み下で一貫したスナップショットを作ります。

  1. オフラインコピー: サーバーを停止し、.db を -wal と -shm の 兄弟ファイルごとコピーします。

  2. 論理バックアップ (低頻度の補完として推奨): export_memories は スキーマバージョンに依存しない JSONL を書き出し、import_memories は 冪等です (重複はスキップ)。したがってリストア訓練を安全に反復できます。 月次程度が妥当な頻度です。

.db はインスタンスの全体ではありません。 データベースの外に状態があり、 上のどのバックアップ手段もそれらに触れません:

  • <CPERSONA_DB_PATH>.calibration.json — エージェント別のベクトル閾値・ ゲート状態・スコアリング版、そしてエージェント別の set_recall_precision の beta。

    これ抜きで復元しても、測定値のほうは自力で復旧します。既定の CPERSONA_CALIBRATE_ON_MODEL_CHANGE=true では、sidecar が無い起動は フォールバックではなく再較正を行い、それが起きなかった場合は deep_check が never_calibrated を報告します。

    beta は復旧しません。 beta は測定値ではなくポリシーの入力であり、 述べられた選好を再導出する仕組みは存在しません。precision を調整していた 運用者は、何も告げられないままその調整を失い、再較正は全ゲートを既定の beta で測ります。sidecar をデータベースと並べてコピーするか、復元後に set_recall_precision を再適用してください。 - ~/.cpersona/operating-context.toml (または CPERSONA_OPERATING_CONTEXT_PATH) — 全クライアントに配られる運用者の指示。 - CPERSONA_ACL_FILE を設定しているなら、その ACL ファイル。これ抜きで復元 すると、誰が接続できるかが変わります。 - 外部ベクトル索引 — CPERSONA_VECTOR_SEARCH_MODE=remote かつ CPERSONA_STORE_BLOB=false の構成の場合。この構成では埋め込みがリモート索引 にしか存在しないので、データベースと上記 3 ファイルを全て戻しても、ベクトル腕 がゼロの状態で復元されます。check_health は no_local_vector_fallback と して報告します。索引も同じ頻度でバックアップするか、STORE_BLOB=true のまま にして .db を自己完結に保ってください。 - <CPERSONA_DB_PATH>.memories.vecindex と .episodes.vecindex (連続ベクトル 索引) は含めません。 データベースから導出される派生物で、リストアするもの ではなく作り直すものです。バックアップに含めても容量の無駄で、古い索引が 戻ってきても次の build で正されます。 連続ベクトル索引 を参照してください。

稼働中のデータベースはクラウド同期フォルダの外に置いてください (Dropbox、Drive など)。同期クライアントは SQLite の WAL ファイルと相性が 悪いためです。同期するのはバックアップの方にしてください。

埋め込みサーバー停止の検知

ベクトル検索は最も強い検索層ですが、誰も見張っていなければ、ネットワーク境界で 何も告げずに劣化します。検知面は 3 つあり、呼び出し側のエージェントには 1 番目を見るよう指示してください。

  1. recall 応答の advisory (v2.4.33 以降、主たる検知面)。状態機械が実際の 埋め込み失敗を観測し、劣化した recall には advisory = {degraded, severity, reason, evidence, runbook, advisory_scope} が付きます。「ベクトル層が落ちている。FTS + keyword のみで応答中」という 意味です。

    このフィールドをユーザーに提示し、その runbook に従うようエージェントに 指示してください (通常は「埋め込みサーバーを起動または向け直してから再度 recall する」)。runbook が短縮されている場合は「もう伝えた」という意味です。 その「誰に」を述べるのが advisory_scope で、プロセスが自分専用なら session、HTTP トランスポートでは状態がサーバー全体のものなので process になります。共有サーバーでは、fault は自分のセッションが見たと仮定せず 完全版 runbook を繰り返します (bug-251)。

    CPERSONA_DEGRADED_ADVISORY=false は advisory を黙らせます。埋め込み バックエンド無しでの運用を運用者が受け入れたことを記録する用途で設定して ください。これはサポートされる fallback であって、推奨構成ではありません。 設計: DEGRADED_ADVISORY_DESIGN。 2. store 応答の embedded。 全ての書き込みが、自分の埋め込みが永続化された かどうかを報告します。サーバー停止中に行われた書き込みは embedded: false で返り、それらの行は修復可能です (次項)。 3. check_health(agent_id, fix=true) は、埋め込みが NULL のまま保存された 行を検出し、サーバー復帰後に再埋め込みします。エンドポイントに到達できない 場合、次元チェックは今も失敗ではなくスキップされますが、エンドポイント 自体は見張られるようになりました。構成済みのバックエンドが応答しない時、 embedding_backend が、失敗した呼び出し自身の証跡を添えて warn を報告 します。 4. not_probed はそのままの意味で読んでください。 check_health(fix=false) はネットワーク呼び出しを行わないので、生存を検査できません。沈黙から健全性を 推測させる代わりに、embedding_backend が not_probed を返し、検査していない ことを明示します。

    fix=true の実行でも、probe が埋め込みクライアントの 5 分間キャッシュから 応答された場合には、同じものを返します。次元はエンドポイントに要求が届かない まま返るので、そこからは何も分かりません。2 つのどちらであったかは、finding の reason に書かれます。recall が既に latch した fault は、そこでも報告されます。

    バックエンド未構成の場合、このチェックは意図的に沈黙します。それはサポート された構成であり、提起するのは advisory の面だからです。

recall のチューニング

つまみを、手を伸ばすべき順に:

  1. set_recall_precision(agent_id, precision) — 主たるポリシーつまみで、 既定の融合モードでは事実上唯一のものです。融合品質ゲートの動作点 β を動かし ます。strict=2.0 (混入が減り、取りこぼしが増える)、balanced=1.0 (既定)、 lenient=0.5 (取りこぼしが減り、混入が増える)。即時反映で、再起動は不要です。
  2. calibrate_threshold(agent_id) — コーパスからゲート位置を導出し直します (ラベル不要)。agent_id 付きで (かつ融合ゲート有効で) 呼ぶと、ベクトル 閾値と融合ゲートの両方を較正します。付けずに呼ぶと、ベクトル閾値のみを 較正します。表示される値はベクトル側の閾値ですが、融合モードで実際に結果を 切っているのは融合ゲートです。コーパスが大きく変わった後、一括再構築の後、 埋め込みモデルを変えた後に再実行してください。
  3. CPERSONA_AUTOCUT_MIN_RESULTS — autocut は類似度スケールのシグナルに 対して発火します。confidence スコアリング下、あるいは生の cosine だけで 構成された均質なリスト (cascade が作るのがそれで、confidence の on/off は 問いません) に対してです。rsf / rrf では意図的に不活性なので (契約 §6)、 既定構成ではこのつまみは何もしません。それを決めているのは融合モードであって、 confidence フラグではありません。
  4. CPERSONA_FUSED_GATE_ENABLED=false — 最後の手段。ゲートが無いとフィルタ はプール規模のヒューリスティック (_adaptive_min_score) にフォールバック しますが、それでも弱い一致は弾かれます。開けっ放しになるのではなく、粗い ゲートになるということです。失うのはこのコーパスに対して測定された動作点 です。

precision の選び方。 トレードオフは用途ごとに非対称です。索引的なコーパス (recall が、無関係な行を捨てられる AI に供給される) では取りこぼしの方が 混入より悪いので、数日 lenient で運用し、混入が実コストになった場合にのみ balanced に戻してください。混入に敏感な文脈 (狭い窓に recall を注入する) では balanced / strict を選んでください。

recall に頼らないという選択

確率的検索は負けることがあります。文脈に無いこと自体が害になる事実 (現在の 最優先の決定、常時有効な安全ルール) は、記憶ではなく決定的に注入される面 (CLAUDE.md、システムプロンプト、エージェントが常に読む索引ファイル) に置いて ください。

有用な切り分けは、問われずに発火すべきものは決定的注入へ、問われたときに 見つかればよいものは記憶へです。ここから系が 2 つ出てきます。

  • 決定の更新は追記でなく上書きで行う。 決定が変わったら古い行を update_memory してください (自動で再埋め込みされます)。古くなった決定が recall に勝つのを止める最も確実な方法は、それが検索空間に存在しないことです。
  • lock_memory は消失からは守りますが、順位で負けることからは守りません (契約 §9)。

日本語 / CJK コーパス

  • CPERSONA_RECALL_MODE=rsf を設定してください。FTS5 は CJK のトークン化が 苦手ですが、rsf はマージの際に keyword チャネルのスコアの大きさを保持し、 それが補償するシグナルになります。これ以外に CJK 固有の設定はありません。
  • jina-v5-nano — 本プロジェクトが動かしているモデルであり、参照実装の埋め込み サーバーが既定でダウンロードするモデル — の日本語における既知の特性 (長期の 本番運用で確認済み): クエリと記憶の間に固有名詞や識別子のアンカーが 1 つ 以上重なるときは強い一方、共有語彙のない純粋な概念一致には弱いです。 具体的なアンカー語を含めてクエリを書くこと (「キーワードアンカリング」) は、 現行モデルに対する正しい適応であって、後ろめたく思うべき回避策ではありま せん。
  • モデルスロットは差し替え可能です (CEmbedding プロバイダ)。埋め込みモデルを 入れ替える場合は、コーパス全体を再埋め込みしてから calibrate_threshold を 実行してください。サーバーは次元が変わったときは自動で再較正しますが、 同一次元のモデル入れ替えでは手動の再較正が必要です。

コーパス索引と同期のパターン

CPersona の第一の設計中心は、会話から積み上がる記憶です。正本の Markdown ファイルに対する検索索引として使うことも可能ですが、正しいパターンは 2 つの 事実に規定されます。CPersona が受動的なサーバーであること (ファイル監視は なく、取り込みは常に呼び出し側が駆動する) と、重複排除が upsert ではなく skip であること (契約 §5) です。

パターン A — 索引を使い捨ての射影として作り直す (まずはこちらを推奨)。

  1. 索引に専用の agent_id を与えます (例: md-index)。索引チャンクを会話 エージェントの記憶に混ぜないでください。分けておけば、エピソード境界の機構も 絡んできません。
  2. 元ドキュメントが変わったら delete_agent_data(agent_id="md-index") を実行し、 全チャンクを再 store します。
  3. 再構築のたびに calibrate_threshold を再実行してください。 set_recall_precision を使っているなら、それも再適用します。 delete_agent_data は、その agent の行と一緒に較正状態も破棄するためです。 再構築が夜間バッチなら、再較正もバッチの一部にします。
  4. コスト: 数千チャンクの再埋め込みは CPU で数分〜数十分です (環境依存)。 得られるのは、差分ロジックが不要になることと、再構築が終わった時点で索引が ソースと一致することです。

    再構築の最中は一致しません。delete_agent_data は再 store が始まる 前にコミットするので、その窓の中で発行された recall は、部分的な索引か、 何も無い索引を見ます。誰も問い合わせていない時間に再構築するか、第 2 の agent_id の下で構築して、完成後に読み手を切り替えてください。

パターン B — 外部の内容ハッシュ台帳による差分更新。

  1. 各チャンクを、安定した msg_id (例: path#heading) と、source.id に入れた 元ドキュメントとともに store します。こうすると recall の source_id 引数 (前方一致) が、ドキュメント単位のフィルタとしても使えます。
  2. 呼び出し側で キー → 内容ハッシュ の台帳を保持し、ハッシュが変わった チャンクだけを処理します。
  3. 変更されたチャンクは必ず update_memory (または delete_memory + store) を通してください。 同じ msg_id で変更後の内容を再保存しても、 何も告げられずスキップされます。変更のないチャンクは何も考えずに再投入して 構いません。完全一致の重複排除が、それを無害だと保証します。

再構築時間が実際に痛くなるほどコーパスが大きくなるまでは、A を選んでください。 壊れる台帳がなく、drift するモードもありません。

スケール

成長にアーカイブや間引きの定期処理は不要です。ベクトル層は新しい順のウィンドウ (CPERSONA_MAX_MEMORIES、既定 10,000。大きなコーパスでは環境変数で上げて ください。契約 §4) を走査します。FTS と keyword のチャネルは全履歴に届き、古い行は削除されるのでは なく、ウィンドウと減衰によって沈んでいきます。

連続ベクトル索引

ローカルのベクトル走査は、窓の中の埋め込みを SQLite から 1 行ずつ読み出します。 走査の時間の大半は、計算ではなくこの読み出しです。連続ベクトル索引は、同じ 埋め込みを計算が望む並びで持つデータベース脇のファイルで、走査はそれを一度に 読みます。行も、スコアも、順序も同じで、変わるのはレイテンシだけです。基準機 では 100,000 行で、ベクトル側が 604 ms から 77 ms になりました。

索引はテーブルごとに 1 つです。memories と episodes は別々に走査されるので、 ファイルも別で、build も検査もそれぞれ行います。索引の無い episodes テーブルは、 その 5 倍の行数を持つ索引済み memories テーブルよりクエリあたりの費用が高いので、 片方を build する運用なら両方 build してください。

派生物です。 正本はデータベースだけで、索引はその射影です。キャッシュと 同じ扱いをします。バックアップしない、修復しない、いつ消しても安全。何かおかしく 見えたら、ファイルを消して build し直してください。

build は運用者の操作です。 稼働中のサーバーは、索引を作りも更新もしません。 入口は cron や systemd timer から呼ぶためのコマンドです。

python -m cpersona.vector_index --db "$CPERSONA_DB_PATH" build
python -m cpersona.vector_index --db "$CPERSONA_DB_PATH" --table episodes build
python -m cpersona.vector_index --db "$CPERSONA_DB_PATH" status
python -m cpersona.vector_index --db "$CPERSONA_DB_PATH" --table episodes status

--table でファイルを選びます (既定は memories)。

build はデータベースを読むだけで (書き込みはしません)、索引をアトミックに 置き換えます。サーバーは次のクエリで新しいファイルを拾うので、再起動は不要です。 build できれば exit 0、辞退したら exit 1 で理由を印字します。理由は、埋め込み 済みの行が無いか、2 種類の埋め込み幅が同時に存在するか (モデル変更の途中の 一時的な状態。再埋め込みが終わってから build し直します) のいずれかです。

status は、索引の有無と使用可否、保持行数、build 以降に書かれた行数を報告 します。索引が無ければ exit 1、ファイルはあるが使えなければ exit 2 です。 どちらも --json を付けると機械可読な 1 行になります。

build と build の間に起きること。 索引は build 時点の最大行 id を覚えて います。それ以降に書かれた行は索引に無く、失われもしません。毎クエリがそれらを データベースから厳密に読み (走査が常にそうしてきたとおり)、索引側の行と合流 させます。

だから build が遅れても、答えは変わりません。変わるのは、各クエリのうち行単位で 読まれる部分が育ち、レイテンシが索引なしの数字へ戻っていくことだけです。再 build の頻度は性能の設定であって、正しさの設定ではありません。既定は毎晩、成長の速い コーパスなら毎時が目安で、遅れ具合は status でいつでも見られます。

索引が使われない時。 索引が置き換える走査はそのまま残っていて、fallback です。ファイルが無い、ファイル自身の整合性検査に落ちる、クエリベクトルの幅が 索引と合わない、索引が持つ行がその後のメンテナンス修復で埋め込みを失った — これらの時、recall は走査に戻ります (遅いが、正しい)。

いずれも運用者が health を読む場所に報告されます。check_health は、索引が 無ければ vector_index_absent (カバーできる行数つき)、ファイルが信頼できなければ vector_index_unusable を出します (テーブルごとに 1 行、それぞれ table を 名乗ります)。サーバーは、走査に戻したクエリごとに warning をログへ出します。

こうしないと、1 週間使えないままだった索引が「なぜか速くならない」としか 見えません。報告は、その失敗を防ぐためにあります。

索引の設定項目はありません。パスは CPERSONA_DB_PATH から導出され、索引が担う 走査窓は、走査と同じ CPERSONA_MAX_MEMORIES です。

メンテナンスの頻度

  • 月次: check_health(agent_id, fix=true) で決定的な整合性チェックと 自動修復、deep_check(agent_id, fix=true) で意味的なパス、加えて export_memories による論理バックアップ。
  • コーパスの大変動の後 (一括インポート、再構築、モデル変更): calibrate_threshold(agent_id)。
  • ベクトル索引を使っているなら timer で: python -m cpersona.vector_index build (既定は毎晩)。build が遅れた時の代償 (レイテンシであって、正しさではない) は 連続ベクトル索引 を参照してください。
  • バージョンアップ: スキーマは自身で前方マイグレーションします。較正は スコアリング関数と埋め込み次元に対して fingerprint されており、どちらかが 変わると初回起動時にサーバーが再較正します (v2.5.2 以降)。