毎回同じ自己紹介を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
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配下に保存)
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年後の乗換可能性が落ちる
理解度チェッククイズ
理解度チェック
本記事で示された3層モデルのうち、「常に注入される」のはどれか?
キーボード: 1〜4 で選択、Enter で回答