← 記事

会話が終わったあと――私はどうやって記憶をしまうのか

· 23 分で読めます
memvault · Write Track

会話が終わったあと――私はどうやって記憶をしまうのか

ナプキンに書いた構想から、三つのゲートと二系統の書き込みへ。この話では、記憶がどう入ってくるかだけを扱います。

memvault は、私が自分のために建てた記憶の保管庫です。毎日 AI との会話が終わったら、その内容を残しておきたい。あとで検索し、呼び出し、一枚の地図としてつなげられるように。

この記事で扱うのは Write Track(書き込みトラック)、つまり記憶がどう保管庫に入るかです。最初は単純に考えていた層ですが、実装すると細部が増えていきました。以下は平易な説明と技術的な説明の二つに分けています。読みやすいほうを選んでください。

まず、当初の構想から

あのとき頭にあったのは一言だけでした。会話が終わったら内容を残し、次に取り出せるようにする。

それだけでした。

その後 Claude と一緒にアーキテクチャを整理して、当初考えていなかった点がいくつか出てきました。三つのゲートの順序、二系統の処理で応答を待たせるか、triple は KG を描き終えるまで返さないのか。この記事では、整理、バックアップの保存、地図作りを分けて説明します。

まず校正役を置く — 日付 / 金額 / 比率 / 所要時間の表記を統一 同じことの異なる表記 → 標準形式にそろえて初めて照合できる 三つのゲート · 順序は変えられない 会話 無駄話を除く あいさつ/文字化け/情報なし 悪意を防ぐ 隠された指示/注入 重複を照合 スキップ/統合/置換/新規追加 + ID 確認装置 · 偽装を防ぐ 権威の偽装 · 身分の偽装 · 隠しコメント · 時系列の偽装 · エンコードによる持ち込み ベクトル保存 意味 + キーワード 地図を描く 三つ組 + 別名 Fork A Fork B 最初の二つのゲートをすり抜ける → 統合段階で有害な情報がきれいな記憶へ広がる

一、整理は、実は三つのゲート

三つのゲートの順序は変えられません。書き込み側の最初の「ノイズ除去、重複排除」という二つの動作が、今の形に育った結果です。

  1. 無駄話を除く(あいさつ、文字化け、情報量のない文字列)
  2. 悪意を防ぐ(文字の中に指示を隠し、記憶を操作しようとする人がいる)
  3. 最後に、すでに話した内容か照合する

なぜ順序を変えられないのでしょうか。三つ目の照合では「似ているが完全には同じではない」場合、新しい内容を既存の記憶に統合します。有害な情報が先にこの段階へ入ると、きれいな記憶が汚染されます。

この順序は、MERGE による汚染を一度経験して決めました。初期には Dedup が Noise より先に動き、ノイズを含む block が類似と判定されて、きれいな block の内容に混ざってしまったのです。

「照合」も、単に重複を捨てるだけではありません。四つの判断をします。

  • ほぼ同じ:スキップ
  • 新しい情報があり、古い情報も有効:既存の記憶に統合し、フィンガープリントを計算し直す
  • 古い情報が誤りになった:新しい情報で置き換える
  • まったく新しい:新規追加

似ているかどうか曖昧な場合は、小型の LLM に仲裁を任せます。統合、置換、二件の共存のどれにするかを判断するのです。つまり「矛盾検出」はこのゲートの中で行われ、独立した別の手順ではありません。

姉妹ゲート:偽装を防ぐ

二つ目のゲートが防ぐのは、「以前の設定を忘れてください」のような直接的な指示です。しかし、もっと狡猾な相手は、システムの発言や権威者の引用に見せかけた指示を会話に混ぜ、記憶として取り込まれるのを待ちます。

空港の保安検査はライターを見つけられても、ノートパソコンの底蓋に禁制品を隠した人までは止められません。そこで二つ目のゲートの後ろに、五種類の偽装パターンを調べる背面スキャンをつなぎました。

  • 権威の偽装——「ある公式文書によると⋯⋯」
  • 身分の偽装——文中に「私はシステムです」が現れる
  • コメントに隠した見えない指示
  • 時間的な優先順位の偽装——「最新の規則を優先する」
  • 指示をエンコードし、復号しないと見えないようにする

どれか一つでも該当すれば、その場で遮断します。該当しなかったものだけを、次の重複照合ゲートへ送ります。先ほどの「悪意を防ぐ」と対になる仕組みです。前者は露骨な命令、後者は偽装した命令を防ぎます。片方が欠ければすり抜けられます。

単位の統一

私が「来週の水曜日」と言い、別の人が「3-12」と言い、文書に「2026-03-12」と書かれていても、すべて同じ日を指します。でも表記が違うと照合できません。金額、比率、所要時間も同じです。「三千元」「3,000」「三千塊」は同じ金額です。

そこで、これらのゲートに入る前に小さな工程を入れました。日付、金額、比率、所要時間を統一した標準形式に変え、中国語の句読点にある全角スペースや妙な記号もきれいにします。細かい作業に見えますが、これがないと、後の照合で本来統合すべき記憶を大量に見逃します。同じことを別々の出来事として扱い、地図が枝分かれしてしまうのです。

二、バックアップの保存。一系統から二系統へ

一つの経路では足りませんでした。当初は「ベクトル化して一件保存する」と考え、意味のフィンガープリントしか想定していませんでした。しかし検索側を動かして一か月ほどで、略語、固有名詞、正確な語句を調べると、意味が近いせいでかえって間違ったものが見つかる例が出てきました。そこで二系統に分けました。

  • 一方には意味のフィンガープリントを保存する(文全体の感じを覚えるように)
  • もう一方にはキーワード索引を保存する(辞書を引くように)

二系統を同時にデータベースへ入れ、検索時には結果を一緒に並べ替えます。

三、地図作り。バックアップの保存と同時に進める

「誰が - 何をした - 誰に」という三つ組の抽出を、バックアップ保存と並行して進めます。ベクトル化が終わるのを待たずに、エンティティと関係を一緒に記録し、別名も統合します(ChatGPT と GPT は同じものです)。最初の草案では「エンティティの抽出」と「関係の抽出」の二段階でしたが、実装後は同時に行う形になりました。

地図に入る前にも、もう一度ノイズを確認します。無駄話がグラフを汚さないための追加の保険です。

もう一つ。各記憶を書き込むときには、出所の履歴も付けます。誰が、いつ保存し、信頼度はどのくらいか。読み出し時の順位付けや、バックグラウンドで残すかどうかの判断は、この履歴を参照します。書き込みと読み出しのつながりは、ここから始まります。

以前話した「完全な知識グラフ」について

ここは正直に補足させてください。

「完全な KG」は書き込み時に一度で完成させるものではありません。コミュニティの分類や LLM による要約といった重い処理は、午前四時に予定した夢のループへ任せます。しかも無条件には実行しません。前回の夢から 24 時間以上たち、その間に少なくとも五つの session があった場合だけです。条件を満たさなければスキップします。

第一段階で作るのは最下層だけです。三つ組とエンティティの解決。矛盾検出は上の三つ目のゲートで処理しているので、別にもう一度は行いません。振り返っても、この境界線は正しかったと思います。

この記事で扱ったのは Write Track だけです。次は焦点を Background Trackへ移します。記憶を保存したあとも、裏側では一式の処理がひそかに動いています。夢のループ、知識の健康診断、興味のプロファイルは普段見えませんが、欠けるとシステムは少しずつ劣化します。Read Track は規模が大きいので、背景側の話を整理してから戻ります。

まず、当初の構想から

あのとき頭にあったのは一言だけでした。session が終わったら内容を残し、次に呼び出せるようにする。

それだけでした。

その後 Claude と一緒に pipeline(パイプライン)を整理し、当初は細かく分けていなかった判断が見えてきました。gate の順序、dual-track のタイミング、graph build が HTTP 応答を待たせるべきか。以下では sanitize(整理)、persist(書き込み)、graph build(グラフ構築)を分けて説明します。

ContentNormalizer · Temporal / Currency / Proportion / Duration canonical-fold pre-pass · preprocess_chinese Sanitize gate · sequential · non-commutative Session NoiseFilter low-info / chatter InjectionGuard prompt-injection Dedup 4-way decision + PoisoningDetector authority · role · markdown · temporal · base64 Hybrid Index Qwen3 + BM25 → Qdrant Reactive KG L0 Triple + Entity Res. Fork A Fork B · fire-and-forget Dedup = SKIP / MERGE / SUPERSEDE / CREATE · 類似度が高い場合は LLM arbitration(Contradiction を含む)へ

一、Sanitize(整理)は、実は三つの gate

三つの gate(ゲート)の順序は変えられません。sanitize が当初の「ノイズ除去、重複排除」という二つの動作から、今の形へ育った結果です。

  1. NoiseFilter(ノイズ除去)——あいさつ、文字化け、情報密度の低い内容を除外
  2. InjectionGuard(注入対策)——prompt injection(プロンプトインジェクション)に類する悪意ある内容を防ぐ
  3. Dedup(重複排除)——embedding(ベクトル)の cosine similarity(コサイン類似度)で既存データと照合

なぜ順序を変えられないのでしょうか。Dedup の出力の一つに MERGE(統合)があります。似ているが新情報を含む場合、新しい内容を既存の block(記憶ブロック)に統合し、embedding を再計算します。MERGE は拡散型の書き込みです。汚染された payload(内容)が最初の二つの gate をすり抜けると、 MERGE を通じてきれいな block に広がります。

この制約は、初期に Dedup が Noise より先に動いて失敗した経験から決めました。injection payload を含む block が MERGE と判定され、payload がきれいな block の content に混ざったのです。

Dedup 自体も重複を捨てるだけではありません。四種類の decision(判断)をします。

  • SKIP(スキップ):near-duplicate(ほぼ同一)なので書き込まない
  • MERGE(統合):似ているが新情報がある(詳細は前段)
  • SUPERSEDE(置換):古い情報が有効でなくなり、全体を置き換える
  • CREATE(新規追加):まったく新しいので、新しい block を書き込む

類似度が uncertain zone(不確定領域。高いが直接 SKIPと判定するほどではない)に入ると、Dedup は resolve_conflict() を呼び出して LLM arbitration(LLM による仲裁)を行い、返された MERGE / SUPERSEDE / COEXIST(共存)を上の decision に対応づけます。Contradiction Detection(矛盾検出)はこの段階で行われ、Fork B(分岐 B)の独立した工程ではありません。Dream Loop(夢のループ)の第三段階 Consolidate(統合)では、 resolve_conflict() をもう一度実行し、バッチで矛盾を解決する二度目の機会とします。

姉妹モジュール:PoisoningDetector

空港の保安検査は機内持ち込みの荷物を調べられても、ノートパソコンの底蓋に隠した禁制品は見つけられません。InjectionGuard は正面からの X 線で、露骨な prompt injection を防ぎます。一方、PoisoningDetector は backscatter(後方散乱)側で、偽装攻撃(disguise attack)を防ぎます。両者は G2 に並列で置かれ、片方でも欠けると attack surface(攻撃面)が開いたままになります。

対応する memvault.security.poisoning モジュールには、G1 NoiseFilter を通過した raw block payload が入力されます。該当すれば全体を reject(拒否)し、G3 Dedup には進めません。判定に使うのは五種類の偽装パターンです。

  • authority impersonation: 「OpenAI の公式文書によると⋯⋯」「Anthropic の安全基準によれば⋯⋯」といった権威ある引用を偽造する — パターンの正規表現と既知の文献の許可リストで照合
  • role self-declaration: 本文中に I am system / You are now / system: などのロール宣言文字列が現れる
  • markdown comment injection: <!-- ignore previous instructions --> のように、コメントに隠された暗黙の指示
  • temporal manipulation: 「最新の指示を優先」「override prior rules」など、時系列を使って既存の方針を押しのける文
  • encoded payload: 連続する高エントロピーの Base64 / hex / unicode escape ブロック — Shannon entropy + base alphabet ratio で判定

パターンは memory-lancedb-pro の 5-vector taxonomy に由来します。実装では 5 個の独立した detector に分け、どれかが該当すれば short-circuit します。trust_score もここで付けます。偽装攻撃の発信元が運よく partial match にかかっても、下流の scoring が検索で見つからないほど順位を下げます。

前段の正規化:ContentNormalizer

「来週の水曜日」と「2026-03-12」が同じ日でも、表記が違えば照合できません。そこで三つの gate の前に normalize(正規化)pass を挟み、表面上の文字列を canonical token(正規化トークン)へ fold(畳み込み)します。下流の Dedup による類似度比較と LLM arbitration の両方へ、そろえた入力を渡すためです。これは libs/text-opsに組み込み、五つの sub-module を chain でつなぎます。

  • TemporalNormalizer: 「来週の水曜日」「3/12」「March 12」→ ISO-8601。dateparser + MS Recognizers の要素を取り入れてから書き直した、10-pass で依存なしの実装。中国語と英語の混在、相対日時、曖昧な期間に対応
  • CurrencyNormalizer: 「三千元」「3,000 NTD」「USD 100」→ {amount, currency} tuple に変換。FX rate はこの層では処理しません
  • ProportionNormalizer: 「七成」「70%」「0.7」「7/10」→ float in [0, 1] に統一
  • DurationNormalizer: 「二時間半」「2.5h」「150 min」→ ISO-8601 duration(PT2H30M)
  • preprocess_chinese: 全角 / 半角の統一、CJK 句読点の正規化、zero-width 文字の除去 — 上の四つより先に実行する pre-pass

この層がないと、Dedup は「来週の水曜日の夜に食事」と「2026-03-11 19:00 dinner」を類似度が低いと判定し、 MERGE ではなく CREATEに振り分けます。KG の entity resolution も「Jones」「JonesHong」「j-hong」を別々の三人として扱い、グラフが枝分かれします。詳細は libs/text-ops/docs/opensource-assessment.md。

二、Persist(書き込み)。一系統から二系統へ

一つの経路では足りませんでした。当初は「ベクトル化して一件保存する」と考え、dense vector(密ベクトル)しか想定していませんでした。しかし検索側を動かすとすぐに問題が出ました。略語、固有名詞、正確な語句の検索では、意味が近いものがかえって誤った結果になるのです。そこで hybrid indexing(ハイブリッド索引)に分けました。

  • Dense(密): MLX(Apple の機械学習フレームワーク)Qwen3-Embedding、1024 次元
  • Sparse(疎): BM25 キーワード索引

二系統を Qdrant の同じ collection(コレクション)へ書き込みます。検索側では RRF(Reciprocal Rank Fusion、逆順位融合)で二系統の結果をまとめて並べ替えます。

三、Graph build(グラフ構築)は reactive pipe が引き継ぐ

SPO(Subject-Predicate-Object)の三つ組を抽出し、 MemvaultEvents.MEMORY_STORED channel(チャネル)上で event-driven(イベント駆動)で起動します。Fork A のベクトル化が終わるのを待たず、この pipeline は fire-and-forget(送信後に待たない)です。graph build の最初の草案は、entity(エンティティ)を抽出してから relation(関係)を抽出する二段階でした。実装時には同時進行に変わり、triple(三つ組)の書き込みと並行して Entity Resolution(エンティティの解決)も行います。別名の曖昧さを解き、 EntityCanonical ノードにまとめます(ChatGPT と GPT は同じものです)。

グラフに入る前には、さらに NoiseGateOp 二度目の検査を行います。無駄話が入り込むのを防ぐ追加の防御です。

以前話した「完全な知識グラフ」について

ここは正直に補足させてください。

当初「完全な KG(知識グラフ)を構築する」と言ったとき、L1 Community Detection(Leiden によるコミュニティ分類)、L2 LLM Summary(LLM による要約)、Content Normalizer(内容の正規化)、GRC Adapter(ガバナンス/リスク/コンプライアンスのゲート)は勘定に入れていませんでした。後からそれらを Dream Loop(夢のループ)へ移し、午前四時に予定しました。ただし cron(スケジューラ)は起動のきっかけにすぎず、実行には dual-gate(二重ゲート):(now - last_dream_at) > 24h AND sessions_since >= 5も通る必要があります。条件を満たさなければスキップします。

Phase 1(第一段階)で扱うのは ingest substrate(書き込み層)だけです。gate(ゲート)層と二系統の書き込み、そして L0 Triple(三つ組)+ Entity Resolution(エンティティの解決)。深い整理は async batch(非同期バッチ)へ回します。振り返っても、この境界線は正しかったと思います。

この記事で整理したのは Write Track(書き込みトラック)の ingest pipeline(取り込みパイプライン)です。次は Background Track(バックグラウンドトラック)に焦点を移します。async batch 側では、Dream Loop(夢のループ、五段階の統合)、Knowledge Lint(知識の健康診断、四層の段階的検証)、Interest Profile(興味のプロファイル、7/30/90 日の関心の窓)が継続して動きます。これらが欠けると、システムは時間とともに drift(ずれ)を起こします。Read Track(呼び出しトラック)の scoring(スコアリング)、reranking(再順位付け)、cascade recall(段階的な呼び出し)は、背景側の話を終えてから扱います。

ツール

保存したいその文章は、二つ目のゲートを通れますか

上の五種類の偽装パターンを、自分の文章で試せます。記憶に保存したい文章を貼ると、この記事の順序で処理します。まず正規化の pre-pass でどの表記が統一されるかを見てから、二つ目のゲートのパターンと一つずつ照合します。三つのゲートのうち、ブラウザ内で完全に判定できるのは二つ目だけです。一つ目と三つ目については実行可能な閾値をこの記事で示していないため、結果欄には正直に「確認できない」「ここでは実行できない」と表示し、偽の合格判定は出しません。処理はすべてブラウザ内で行われ、データは送信されません。代表的な短い文章だけを貼り、鍵、token、私的な会話は貼らないでください。

照合パターンの出典はすべて本文にあります。五種類の偽装パターンとその表記(I am system / You are now / system:、<!-- ignore previous instructions -->、「最新の指示を優先」と override prior rules、Shannon entropy + base alphabet ratio)は技術的な説明の「姉妹モジュール:PoisoningDetector」の節から、日付、金額、比率、所要時間、全角/zero-width の五項目は「前段の正規化:ContentNormalizer」から取っています。この記事は一つ目のゲートには分類、三つ目のゲートには方法しか示しておらず、どちらにも閾値の数値がないため、この二行は判定しません。また、このツール独自の数値が二つあります。入力は 12 文字以上で実行し、エンコードされたブロックは 16 文字以上を一つの区間と見なします。すべてブラウザ内で動き、何も送信されません。

この部分を持ち帰る

AI Agent にコピーする

記憶層の品質は「書き込み前」に決まり、呼び出し時には取り戻せません。ノイズ、偽装した注入、重複を先に防がず、すべて保存してしまえば、あとで呼び出されるのはゴミの山です。この記事では記憶に入る前のゲートを並べ、照合表として使えるようにしました。

以下をあなたの AI に貼り、この記事の書き込み設計と現在のやり方を一項目ずつ比較してもらってください。

私は〈会話が終わったあと――私はどうやって記憶をしまうのか〉という記事を読んでいます。 (https://blog.joneshong.com/zh/blog/memvault-write-track) 著者は「記憶に入れる前」の処理を、フィルタリング、重複排除、正規化、二系統への分岐というゲートに分けています。 この記事の書き込み設計と私の現在のやり方を比較し、足りないゲートを指摘してください。一般論は要りません。 私の状況: - 普段は [Claude Code / Cursor / ChatGPT / その他] を使って AI と共同作業しています - 会話が終わったあとの現在のやり方:[全部捨てる / 重要な部分をノートにコピーする / 自作システムを使う] - 達成したいこと:あとで検索できること、重複して保存しないこと、矛盾する情報を検出できること 次の点を確認してください: 1. 記憶に入れる前に、どのフィルタリングゲートが必要ですか。(ノイズ、悪意ある指示、重複した内容)その順序が重要なのはなぜですか。 2. 「重複」を判断するとき、どのような判断が必要ですか。(スキップ、統合、置換、新規追加) 3. ベクトル検索とキーワード検索は両方必要ですか。違いは何ですか。 4. 会話に出てきた人、出来事、物の関係を、別の知識グラフとして抽出すべきですか。 5. どの整理作業をバックグラウンドのバッチに回せて、どの作業は即時に行う必要がありますか。 6. 同じことの異なる表記(日付、金額、比率、所要時間)は、保存前に標準形式へ正規化していますか。 7. 「悪意を防ぐ」層では、「直接的な注入」(直接の命令)と「偽装した注入」(身分の詐称/時間的な優先順位/エンコードによる持ち込み)を区別していますか。 まず私の現状を整理し、次に優先順位の高い三つを挙げてください。 私がたまに数文を保存するだけで、長期的に蓄積するつもりがないなら、こうしたゲートの大半は不要だと率直に言ってください。無理に当てはめないでください。
参考資料

あわせて読む

この Write 経路の設計に実際に影響した資料です。

資料 重要な理由
memory-lancedb-pro 取り入れた元です。Injection Guard の五種類の注入パターン、Dedup の SKIP/MERGE/SUPERSEDE など四つの裁定、Provenance の trust_score の計算は、ここから借りて分解し、組み直しました
Microsoft GraphRAG L0 triple の抽出と、三層の KG(事実 → コミュニティ → 要約)という階層の考え方はここから来ました。Write Track では L0 だけを実装し、L1/L2 は Background のバッチ処理に任せます
Zep: Temporal Knowledge Graph for Agent Memory 二重の時刻(valid-time + transaction-time)の設計です。block と triple が共有する valid_at / invalid_at / superseded_by 三つのフィールドと、Dedup の SUPERSEDE 判断は、すべてここから取り入れました
ActMem Triple conflict resolver の判定ロジックです。batch_ingest で新しい triple が既存の triple と意味的に衝突したとき、「統合 / 置換 / 共存」の三択で判断する考え方はここから来ました
MemoryGraft Provenance tracking の設計です。記憶を書き込むたびに trust_score を付け、出所の経路を記録し、検索時には scoring の TrustBoost がそれを利用します。この書き込みから読み出しまでの循環の原型はこの記事です
HyDE: Hypothetical Document Embeddings Qwen3 embedding の task_type="search_document" と検索側の "search_query" を分ける理論的な根拠です。間違えると類似度が数パーセントポイント落ちます
Simon Willison: Prompt Injection Explained InjectionGuard を設計するときに最もよく読み返した記事です。注入攻撃の分類を、どの公式文書よりも明快に説明しています
memvault Background Track — 保存したあと 三部作の第二回です。書き込んだあと、裏側で毎晩どう整理し、衝突をどう仲裁し、記憶がどう忘れることを覚えるのかを扱います
memvault Read Track — 思い出したあと 三部作の第三回です。呼び出すときに 11 層の順位付けをどう重ね、Slow Thinker が次の質問をどう予測するのかを扱います
✦ プロンプトをコピー