アーキテクチャ¶
対象: CPersona 2.5.x。 このページは各部品がどう噛み合っているか、そして なぜそうなっているかを説明します。呼び出し側から見える保証は 挙動契約 に置き、ここでは繰り返さずリンクします。
翻訳について: 正本は英語版です。日本語版が古い場合は英語版を参照してください。
構成要素¶
┌─────────────────────────────────────┐
│ MCP Host │
│ (Claude Desktop / Claude Code) │
└──────────────┬──────────────────────┘
│ MCP (JSON-RPC)
┌──────────────▼──────────────────────┐
│ cpersona │
│ (server.py) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ store │ │ recall │ ... │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────▼────────────▼─────────────┐ │
│ │ SQLite DB │ │
│ │ │ │
│ │ memories (content + embed) │ │
│ │ episodes (summaries) │ │
│ │ profiles (attributes) │ │
│ │ memories_fts (FTS5 index) │ │
│ │ episodes_fts (FTS5 index) │ │
│ │ pending_memory_tasks (queue) │ │
│ └───────────────────────────────┘ │
│ │
└──────────────┬──────────────────────┘
│ HTTP
┌──────────────▼──────────────────────┐
│ Embedding Server │
│ (jina-v5-nano ONNX, 768d) │
└─────────────────────────────────────┘
この形から出てくる帰結が 2 つあり、はっきり述べておく価値があります:
- 埋め込みサーバーが唯一の外部依存であり、それは任意であり、HTTP 越しに 到達します — つまりネットワーク境界で壊れうる唯一の部分でもあります。だから 劣化には 専用の検知面 があり、「答えが悪くなったことに誰かが気づく」に委ねていません。
- それ以外はほぼファイル 1 つです。 監視すべきデーモンも、用意すべき
サービスもなく、コーパスはコピーできる
.dbです。ただし小さなファイルが 3 つ データベースの外にあり、ホストを移すときに忘れられるのはこれらです: 較正 sidecar<CPERSONA_DB_PATH>.calibration.json(エージェント別の閾値と ゲート状態)、運用者の~/.cpersona/operating-context.toml、そして使っている なら ACL ファイル。.dbだけを復元すると記憶はすべて戻りますが、調整が 黙って失われます — バックアップと復元 を参照してください。
ストレージ¶
WAL モードの SQLite データベース 1 つ (CPERSONA_DB_PATH)、現在の
schema v13 で、起動時に自動で前進マイグレーションされます。データ用テーブルは
memories / episodes / profiles / pending_memory_tasks の 4 つ、加えて
記録用の schema_version テーブルと、トリガーで同期される FTS5 仮想テーブルが
2 つあります。
FTS5 索引は trigram トークナイザを使います。この選択こそが、CPersona が 日本語やその他の分かち書きしない文字体系で機能する理由です: 単語境界ベースの トークナイザは日本語の一文を巨大な 1 トークンとして索引してしまいますが、 trigram は語の始まりがどこであれ部分文字列でマッチします。識別子やエラー文字列 — ベクトル検索が日常的に取り逃すもの — でキーワードチャネルが働くのも同じ理由です。
WAL は稼働中に -wal サイドカーを持つため、動作中のデータベースを素の cp で
コピーするとチェックポイントをまたいで壊れたコピーができることがあります —
安全な方法は バックアップ runbook にあります。
検索¶
融合段に入る retriever は 3 本です: ベクトル検索、memories に対する FTS5、 episodes に対する FTS5。
| Retriever | 方式 | 得意なもの |
|---|---|---|
| Vector | 保存済み埋め込みのコサイン類似度 | 意味 — 言い換え、同義語、「X についてのあれ」 |
| FTS5 (memories) | SQLite 全文検索、trigram トークナイザ | 完全な語: 名前、識別子、エラー文字列、CJK の部分文字列 |
| FTS5 (episodes) | 同じものをエピソードの要約とキーワードに対して | ある話題がどのセッションで話されたかを見つける |
キーワード (LIKE) は 4 本目の retriever ではありません。 memories
チャネルの内側にフォールバックとして置かれ、FTS が無効か MATCH が 0 行の
ときにだけ走ります — つまり FTS memories と並んで融合に入ることはなく、
その代わりを務めます。
rrf モードでのパイプライン:
Query → ┌── Vector search (コサイン類似度) ──────────────┐
├── FTS5 over memories (keyword LIKE fallback) ─┼── 融合 → 品質ゲート → limit → 反転
└── FTS5 over episodes ────────────────────────┘
4 つの段階には個別に注意を払う価値があります。いずれも呼び出し側から見える帰結を 持つからです:
- 融合 (
CPERSONA_RECALL_MODE)。rrfは順位のみで融合します — 頑健で スケール非依存、そしてスコアの大きさを捨てます。rsfは各チャネルの生スコアを クエリ単位で正規化して加算するため、bm25 の大きさが融合後も残ります。その 大きさは 日本語コーパス における 識別シグナルであり、そこでrsfが推奨される理由です。cascadeは チャネルを順に埋める方式でレガシーです。 - 品質ゲートが「そもそも返してよいほど良いか」を決めます。閾値は
calibrate_thresholdがコーパス自身から導出し、実際に回すつまみはset_recall_precisionです。全候補がゲートを下回った場合、応答は空で 返ります — 例外は confidence スコアリング有効時で、そのときだけゲート未満の 字句マッチがgate_fallbackの印つきで返ります。この印は既定構成 (confidence 無効) では到達不能なので、 探しに行く前に知っておく価値があります。 - confidence スコアリング (
CPERSONA_CONFIDENCE_ENABLED、既定は無効) は メタデータのスイッチではありません: 有効時は結果集合が confidence スコアで 並べ直され、ゲートも融合スコアではなくそのスコアを見ます (契約 §2)。 confidence はコサイン類似度・動的な時間減衰・解決済みかどうか・想起回数を 混ぜた量なので、マッチの強さではありません。完全一致の行が言い換えの行より 下に来ることは、この尺度では正当に起こりえます。 - 最後の反転。 結果は
limitで切られてから反転されるので、応答は 悪い順から良い順に並び、末尾の要素が最良のマッチになります (契約 §1)。 これは意図的です: LLM は文脈の末尾に最も強く注意を向けるため、最良の記憶を 注入点のいちばん近くに置いています。
結果を形づくる境界が 2 つ、融合の両側にあります。CPERSONA_MAX_MEMORIES は
ベクトル検索の 走査ウィンドウ
であって保存件数の上限ではなく、融合が見られる範囲を縛ります。エピソード境界
ペナルティ は反対側で働き、
直近の archive_episode より古い記憶の融合済みスコアに、ゲートの前で係数を
掛けます。
3 つの記憶タイプ¶
- 宣言的記憶 (
store/recall) — 個別の事実・決定・ルール。日常の単位です。 - エピソード記憶 (
archive_episode) — セッション要約。宣言的記憶と並んで 検索対象になり、1 件アーカイブするたびに、それ以前に書かれたものを古びさせる 境界も動きます。古いコーパスが今日の答えを溺れさせないのはこの働きによります。 - プロフィール (
update_profile) — ユーザーやプロジェクトについて蓄積された 属性。スコープ内の行数 (記憶 + エピソードの合計 = ゲートが管轄するプール) が 50 以上の場合に限って recall 応答へ付加され (それ未満ではゲートが落とします)、 プレビュー切り詰めの対象外で、かつ スコアを持ちません — そのため既定構成では末尾に並び、limitで切られることがあります。
分離軸¶
行は 3 つの軸で分離されます。軸は入れ子ではなく合成されます。読み取りの意味論は 意図的に統一されていません — 軸ごとに答えている問いが違うからです:
| 軸 | 省略 (None) |
空 ('') |
値 X |
|---|---|---|---|
agent_id |
フィルタなし — 意図的なエージェント横断走査 | '' に完全一致 |
X に完全一致。エージェント間で行を共有しません |
project_id |
フィルタなし | グローバルプールのみ | X に加えてグローバルプール |
channel |
フィルタなし | フィルタなし | X に加えてチャネル無指定の行 |
この非対称性こそが要点です。agent_id は和を取らないハードな分離であり —
エージェント間で行を共有しません — かつ '' の束縛は範囲を広げるのではなく
狭めます。軸を決めずに述語を組み立てた内部コードは、空エージェントの
バケットを指すのであって、他エージェントの行に届くことはありません。
このバケットは「どの書き込みも生まない値」ではなく実在の宛先です —
store は空の agent_id を受け付けます。ツールスキーマの required は
「キーが存在すること」であって「空でないこと」ではないからです。軸の省略はその逆で、意図的なエージェント横断走査
です。 一覧系ツールはそう解釈します — agent_id を渡さない list_memories
呼び出しは、データベース内の全エージェントの行を返します。project_id は
グローバルプールと和を取るので、共有
文脈を各プロジェクトに複製せずに全プロジェクトへ届けられます。channel は
「未設定 = すべて」として扱うため、ブリッジにチャネルを足しても、それ以前に
書かれた記憶が隠れることはありません。
LLM 非依存¶
CPersona は生成モデルを呼びません。要約も抽出も書き換えも判定もしません — それらは
すべて呼び出し側のエージェントが行い、結果を CPersona に渡します
(archive_episode はあなたが計算した要約を受け取り、update_profile はあなたが
計算したプロフィールを受け取ります)。
これは意図的な取引です: エージェント側のロジックを少し多く書く代わりに、記憶が API コストも、隠れたレイテンシも、非決定性も持ち込まない。同時に、CPersona の 答えは再現できます — 同じコーパスと同じクエリは同じ行を返す — これが ベンチマーク を測定可能にしている条件でもあります。
バックグラウンドタスクキュー¶
pending_memory_tasks は DB に永続化された作業キューで、起動時にワーカーが
これを掃き出し、固定間隔 (CPERSONA_TASK_RETRY_DELAY) で再試行します。メモリ上ではなくデータベースに
あるため、クラッシュや再起動でも作業は失われず再開されます。
現行ラインでは、ここに投入するものはありません。 このキューはサーバー側での
エピソード要約生成のために存在しましたが、その機能は v2.4.10 より前に削除され
ました — 今日の archive_episode は事前計算された要約を要求して行を直接書き、
プロフィール更新も同期的です。残っているのは掃き出し側です: 古い版が残した行や、
処理途中で中断された実行の行は、今も正しく完了されます。get_queue_status は
深さと再試行を報告し、健全な新しめのインスタンスでは空のキューを報告します —
それが期待される読み取りであって、異常の兆候ではありません。
トランスポート¶
既定は stdio で、MCP クライアントがプロセスを所有し、ネットワークは介在
しません。CPERSONA_TRANSPORT=streamable-http は代わりに HTTP で複数クライアントへ
提供します — この時点でサーバーは認証について判断を迫ります: CPERSONA_AUTH_TOKEN
を設定するか、ACL ファイルを構成するか、CPERSONA_ALLOW_UNAUTHENTICATED_HTTP=true
で「認証なしでよい」と明示するかです。v2.5.3 はこの拒否を無条件にしました。
それ以前の版は bind アドレスから判断していましたが、それは到達可能性の境界では
ありません。要件およびクライアント別 ACL は
リモート HTTP トランスポート
で扱います。