毎回同じ自己紹介をAIにしているのを、やめたかった

Claude / ChatGPT / Gemini / Claude Code を行き来していると、毎回似たような前置きを打っていることに気付きます。 「自分は saka2jp、PeopleX所属でAI面接プロダクトに関わっている。技術ブログを書いていて、応答は日本語・結論先出しで、Prisma upsertは禁止…」。 AI別にプロフィールを書き直し、ChatGPTで決めた方針がClaudeで失われ、Claude Codeのセッションを跨ぐと「今何のプロジェクトに取り組んでるか」を毎回最初から説明する。 この「文脈の再説明コスト」と「AI間の文脈分断」は、生産性のボトルネックである以前に、たぶん5年後の自分への裏切りなんですよね(積み上がるはずだったデジタル資産が、各プラットフォームの会話履歴に閉じ込められて散逸する)。

そこで連休を使って、AIプラットフォーム非依存な"自分専用Context Engine"~/context-engine/ に建てました。 ゴールは1つ — あらゆるAIが同じMCPサーバーを叩き、同じ「自分像」と「現在の状況」と「過去の学び」にアクセスできる状態を作る。 この記事ではその設計と実装、運用してみての学びをまとめます。

設計が満たすべき4条件

作る前に「これを満たさないと意味がない」という条件を4つ言語化しました。実装の途中で迷ったらこの4条件に戻る、というアンカーです。

条件 意味 本設計での解
マルチAIアクセス可能性 あらゆるAIから同じ文脈を参照できる MCP(Model Context Protocol)を採用、stdio + Cloudflare Tunnel経由HTTPSの二口で同じサーバーを公開
ローカルAIによる自動整理 機密データを外に出さずに情報を整理・更新 Ollama (Qwen3-14B) でセッショントランスクリプトを抽出、distill処理はすべてローカル
ハルシネーション防止の最小文脈 関連箇所だけを精度高く渡す BM25 + Vector + RRF Hybrid Retrievalで topK のみ返却、未検証情報は confidence: raw で隔離
構造化データとしての蓄積 プレーンテキストでなく、機械可読な構造 Markdown + YAML frontmatter を SSoT に、5年後も他ツールに乗り換え可能な形で保存

どれも「言われてみればそう」ですが、特に2つ目(ローカルAIによる整理)と4つ目(プレーンテキストSSoT)は、SaaS型メモリサービスを採用すると同時に放棄せざるを得ない条件です。ここで 「自前で組む」 以外の解が消えました。

「文脈」を3層に解体する

最初に書いた設計書では「context」「文脈」「メモリ」「プロフィール」が3つの異なる意味で混在していました。grilling sessionで自分の語彙を炙り出した結果、 取り出され方が違うなら、層を分けるべき という結論に到達。最終的に以下の3層に明示分離しました。

Layer 役割 取り出し方 具体物
Identity ほぼ不変の自分自身(誰か、何者か) 常に注入(話題に関係なく毎回) personal/identity/canonical.md 1ファイル → 全scopeに投影
Project State 進行中の取り組みの状態(何を、いつまでに、誰と) 現在性重視(active状態 / 直近touch) 各Vault {scope}/projects/{slug}/ ディレクトリ
Knowledge 積み上がる学び・価値観・事実 トピック類似でHybrid検索 各Vault {scope}/knowledge/{values|facts}/

このレイヤリングが効くのは、 同じ「自分に関する情報」でも、AIに渡すロジックがまったく違う からです。 「あなたは誰?」には Identity を全部渡す。「今何やってる?」には Project State の active 上位を返す。「Mem0のベンチマーク覚えてる?」には Knowledge を意味類似で検索する。 ここを1つのテーブル/コレクションに混ぜると、結局AI側で「全部読んでから関係するやつだけ使って」という地獄になります。

3-Vault × 3層マトリクス

3層に 3-Vault スコープ境界(personal / work / public)を直交させます。 漏洩経路を物理ディレクトリで分けつつ、Identity だけは「投影」で全scopeに到達させる構造です。

flowchart LR
    subgraph Canonical[personal/identity/canonical.md]
      C1[★ 唯一のIdentity SSoT<br/>visibilityで投影先を制御]
    end
    subgraph Personal[vault/personal/]
      P1[projects/]
      P2[knowledge/]
      P3[inbox/]
    end
    subgraph Work[vault/work/]
      W1[projects/]
      W2[knowledge/]
      W3[people/]
    end
    subgraph Public[vault/public/]
      U1[projects/]
      U2[knowledge/]
      U3[identity/_profile.md<br/>※投影出力 / SSoTではない]
    end
    C1 -- projection --> P1
    C1 -- projection --> W1
    C1 -- projection --> U3
    style C1 fill:#1e293b,stroke:#3b82f6,color:#ededed
3-Vault × 3層マトリクス。Identity SSoT は canonical.md ただ1つ、それを各scopeのidentityCardに動的投影する

Vault分離の境界は、リポジトリではなく MCPサーバーのscope強制 で担保します。 CWDから自動推定(~/peoplex/* → work、~/saka2jp/saka2blog → public、それ以外の ~/saka2jp/* → personal)し、外部からのBearer Tokenは事前にscopeが紐付いているため、 設定ミスで越境することが構造的に起こり得ない ようになっています。

Conversation Bookend — 会話の両端で読み書きする

Context Engine の中身ができても、「人間が忘れずに参照・更新する」運用は破綻します。 そこで 会話の両端でContext Engineと往復する パターンを採用しました。Claude Code なら hooks で完全自動化できます。

sequenceDiagram
    participant U as ユーザー
    participant CC as Claude Code
    participant CE as Context Engine MCP
    participant OL as Ollama (local)
    U->>CC: セッション開始
    CC->>CE: [READ] query_self(cwd, scope=auto)
    CE-->>CC: identityCard / currentProjects / relevantKnowledge
    CC->>U: 文脈注入済みで応答開始
    Note over U,CC: 通常の対話
    U->>CC: セッション終了 (Stop)
    CC->>OL: トランスクリプトを渡す
    OL-->>CC: 5カテゴリ抽出 (JSON)
    loop 各 insight
      CC->>CE: [WRITE] save_insight(category, content, confidence)
    end
    CE-->>CC: 振り分け完了 (vault配下に保存)
Conversation Bookend のシーケンス。SessionStart hook で READ、Stop hook で Ollama distill → WRITE が走る

Claude Code の ~/.claude/settings.json はこんな具合:

{
  "hooks": {
    "SessionStart": [{
      "command": "context-engine-cli inject-context",
      "description": "CWD / 直近編集ファイル / git log から3層文脈を自動注入"
    }],
    "Stop": [{
      "command": "context-engine-cli save-from-transcript",
      "description": "Ollamaに渡し、5カテゴリ抽出 → save_insight"
    }]
  }
}

これで「人間が忘れる」リスクが消えます。Claude.ai / ChatGPT は hooks 相当が無いので、Custom Instructions に「会話開始時に必ず query_self を呼ぶこと」を書き、Bearer Token認証付きのCloudflare TunnelでMCPに繋ぎます。 Gemini はMCP未対応なので、Bookend READ は諦めて月次のGoogle TakeoutでWRITE側だけ救済する、という割り切りにしました。

Hybrid Retrieval — BM25 × Vector × RRF

Knowledge層の検索が貧弱だと、上の構造はすべて無駄になります。 「Prisma upsert禁止」をキーワード検索で当てるのと、「上司との関わり」を意味類似で「砂田さんノート」に当てるのを、 1つのクエリで両立 する必要がありました。

検索器 実装 強み 弱み
BM25 SQLite FTS5 (tokenizer: unicode61) キーワード一致、固有名詞、コード片 同義語、概念類似、語形変化
Vector sqlite-vec + multilingual-e5-small (384次元) 意味類似、日本語の表記揺れ吸収 具体的な固有名詞のリコール、稀少語
RRF統合 RRF_score(d) = Σ 1 / (k + rank_i(d)), k=60 両者の順位を統合し、片方だけのhitも漏らさない 両方からhitしないクエリには弱い

チャンキング単位は H2ヘッダ境界 固定。frontmatterは除外し、embedding入力は passage: {docTitle} > {heading}\n{body} という形に正規化します(E5系モデルの推奨形式)。 FTS5 と vec0 仮想テーブルで 同じ rowid を共有 させているのが地味に効いていて、joinが1回で済むのでRRFの統合コストがほぼゼロです。

日本語の形態素解析は 意図的に入れていません。MeCabやSudachiを導入するとモデル配布のサイズが2倍になり、コンテナビルドも遅くなる。代わりに、 助詞・敬称(さん / について / は / を / が / の 等)でBM25クエリを軽量分割してOR検索 + Vectorの意味マッチで補完、という構成にしました。実測で十分なリコールが出ています。

# CLI から直接叩いた例(Hybridのスコアとランク表示込み)
$ pnpm query "上司との関わり" --scope=work

=== relevantKnowledge (2) ===
- [0.0328] (bm25 #N/A, vec #1) vault/work people — 砂田 滋弘 > マネジメント観
  /Users/jumpeisakatsu/context-engine/vault/work/people/砂田-滋弘.md
- [0.0312] (bm25 #2, vec #5) vault/work knowledge/value — 上司からのフィードバック...

searchMs: 14ms

ベンチマーク(M3 Pro 36GB)— 456 chunks(vault + saka2blog + zenn-contents込み)の初回index構築は約60秒、Embedder warmup後の検索latencyは warm 8〜18ms。 目標として置いた「query_self全体 100ms以内」は十分達成できています。

5カテゴリ Insight ルーティング

WRITE側の難しさは「LLMが自由作文すると、結局ノイズと幻覚が蓄積される」点にあります。 Bookend WRITE では Ollamaに固定5カテゴリの抽出だけを許可し、保存先・保存後のtypeも完全に決め打ちにしました。

Bookend category 保存先 保存後のtype
decision vault/{scope}/projects/{project}/decisions/ type: decision
value vault/{scope}/knowledge/values/ type: knowledge, category: value
knowledge vault/{scope}/knowledge/facts/ type: knowledge, category: fact
question vault/{scope}/inbox/questions/ type: question
identity-proposal vault/personal/identity/canonical.draft.md に追記 (canonical本体は人間がレビューしてマージ)

もう1つ重要なのが confidence 2値raw / validated)。Ollamaに「明言された確定情報なら validated、文脈推測・伝聞は raw」と指示します。

会話の発言 category confidence 理由
「料金プランは年契のみに決めた」 decision validated 明確な「決めた」
「料金プランは年契寄りで考えてるんだよね」 decision raw 確定していない
「いつもPrisma upsertは避けてる」 value validated 「いつも」で確定的
「Mem0は66.9%精度らしい」 knowledge raw 「らしい」で不確実
「Mem0の公式ベンチで66.9%確定」 knowledge validated 一次ソース明示

これにより週末レビューの対象は raw のみ に絞られ、量が manageable になります。 raw → validated 昇格は人間レビュー必須、AI単独では不可。当初設計では raw / validated / inferred の3値でしたが、運用区別が薄かったので2値に簡素化しました。「推論」「伝聞」は raw + tags: [他者情報] で表現します。

クロスAI接続 — 同じMCPを4種類のクライアントから叩く

最終的に以下の構成で4種類のAIから同じContext Engineに到達できるようになりました。

クライアント 接続方法 Bookend自動性
Claude Code stdio MCP (~/.claude.json で登録) 完全自動 (SessionStart / Stop hooks)
Claude.ai (Web/Mobile) Cloudflare Tunnel → HTTPS MCP / Bearer Token 半自動 (Custom Instructions で query_self 呼出を強制)
ChatGPT (Custom GPT) Cloudflare Tunnel → REST API + OpenAPI 3.1 spec / Bearer 半自動 (Custom GPT instructions)
Gemini MCP未対応 手動 (月次 Google Takeout → distill バッチ)

Cloudflare Tunnel経由のエンドポイントは Bearer Token必須で、 クライアント別にトークンを発行 しています(claude-web, chatgpt-gpt など)。 これで監査ログがクライアント単位で取れるし、片方のトークンが漏れても他方を生かしたままrotateできる。 ChatGPT向けには OpenAPI 3.1 spec を /openapi.json で配信し、Custom GPT の Actions タブから取り込むだけで query_self / save_insight が呼べる状態にしました。

7週間でここまでやった

設計合意 + 既存auto memoryの一回吸い上げ

4条件・3層モデル・3-Vault境界を確定。Claude Codeの ~/.claude/projects/.../memory/ から Identity/Project/Knowledge のseedを抽出してvaultに移植

query_self 3層返却 + BM25 (FTS5)

TypeScriptでMCPサーバー雛形、SQLite FTS5を unicode61 で構築。チャンキングはH2境界、scope自動推定込み

Vector検索 + RRF Hybrid

multilingual-e5-small (384次元) を ONNX runtime で動かし、sqlite-vec の vec0 に格納。RRFで統合、chokidar 増分インデックス

save_insight 5カテゴリルーティング + Ollama distill

Stop hookでトランスクリプトをOllamaに流し、5カテゴリ抽出。confidence事前仕分け、canonical.draft.md 経由のidentity-proposal (ADR-023) も追加

外部データ取り込み pipelines

n8nでQiita日次、Slack/Notion/GitHub週次、weekly-lintで raw → validated 昇格レビュー生成

Quartz v4 で vault/public/ → 静的サイト

schema.org JSON-LD 自動注入、Vercel deploy。ブログとは別の "公開可能な学びカタログ" として運用

HTTP MCP + REST + Cloudflare Tunnel + 月次Export救済

Bearer Token認証付きでClaude.aiとChatGPT接続。Gemini向けにmonthly-export-rescue.tsで取りこぼし救済

あえて踏まなかった選択

この手のプロジェクトは「全部やれそう」に見えて泥沼にハマるので、 スコープ外 をドキュメントに明示しています。

  • X (Twitter) の取り込み — 凍結リスク + 本人意向で不採用
  • Mac全体のローカルファイル取り込み — 範囲過大、当面 saka2blog と zenn-contents の2リポに限定
  • Apple Notes / Mail / Calendar / Messages / ブラウザ履歴 — プライバシー境界と運用負荷
  • Screenpipe / Pieces のような継続記録系 — 当面不採用、将来再検討
  • 純Vector検索のみ — キーワード弱、固有名詞のリコールが落ちる
  • OpenAI Embeddings API — API課金 + 機密データ送信、ローカル実行(multilingual-e5-small)で代替
  • Notion / Obsidian Sync を SSoT にする案 — プロプライエタリ依存、5年後の乗換可能性が落ちる

理解度チェッククイズ

理解度チェック

問題 0 / 50%
Q1

本記事で示された3層モデルのうち、「常に注入される」のはどれか?

キーボード: 1〜4 で選択、Enter で回答