← 기사 목록
日本語https://zenn.dev/topics/llm/feed

AIエージェントの記憶を要約で壊さない:TiDB Cloudで作るBreadcrumb Memory

추출된 키워드

40
Breadcrumb Memory·5TiDB Cloud·5AIエージェント·5Decide·4Recall·4Extract·4Retrieve·4exact value·4query_hints·4source_range·4summary·4compact·4長期記憶·4agent_memories·3full-text search·3Mastra·3mem9·3vector search·3embedding·3superseded·3active·3recall surface·3raw_events·3Source Hit@k·3Observational Memory·3MemGPT·3SQL filter·3source recall·3PostgreSQL·2Claude Code·2observer agent·2Codex·2VEC_COSINE_DISTANCE·2pgvector·2FTS·2Cloudflare D1·2Vectorize·2SQLite·2RAG·2local vector index·2

원문

26,083
AIエージェントの記憶を要約で壊さない:TiDB Cloudで作るBreadcrumb Memory

AIエージェントの記憶を要約で壊さない:TiDB Cloudで作るBreadcrumb Memory

Compactは記憶ではなく地図である:AIエージェントのためのBreadcrumb Memory設計

AIエージェントに長期記憶を持たせるとき、「過去の会話を要約して保存すればよい」と考えるのは自然なことです。実際、多くの開発者がそのように実装しています。

しかし、こんな経験はないでしょうか。エージェントに「前に決めた正規表現、もう一回出して」と頼んだら、それっぽいけれど微妙に違う値が返ってきた。エスケープが1つ抜けている。ドメインが1つ足りない。テストは通らない。なのにエージェントだけは自信満々に答えている。

これはバグというより、summaryから正規表現を再生成した結果です。

TL;DR

  • compactは記憶そのものではなく、原文へ戻るための索引として扱う
  • 正規表現・SQL・ファイルパスなどのexact valueはsummaryから再生成しない
  • source_range
    /
    query_hints
    /
    status
    /
    Source Hit@k
    を持たせると、長期開発エージェントの記憶が壊れにくくなる

長い会話をそのままプロンプトに入れ続けるとコンテキストウィンドウが詰まります。そこで古い会話をcompactして短いsummaryに変換する、という設計は自然ですし、実際にかなり有効です。Claude CodeやCodexをはじめ、多くのAIエージェントにこうした機能が実装されています。ただ、コードを書くエージェントを数日以上使い続けると、この設計の危うさが見えてきます。

たとえば、認証まわりのコードをエージェントと一緒に数日かけてリファクタリングしているとします。初日にこう指示しました。

メールアドレスのバリデーションは、学校用アカウントだけ許可したい。
ただし、teacher.example.ed.jp と student.example.ed.jp の両方を通して。
古い alumni.example.ed.jp はもう通さないで。

エージェントは正規表現を修正し、テストも追加しました。数日後、こう聞きます。

この前決めた、認証のメールアドレス正規表現の条件をもう一回出して。

単純なcompactしかしていないエージェントは、ここで危険な状態になります。記憶には、おそらく次のようなsummaryだけが残っています。

ユーザーは認証ロジックを修正した。
学校用メールアドレスだけを許可する方針になった。

これでは正確な条件を復元できません。

teacher.example.ed.jp
student.example.ed.jp
は通すのか。
alumni.example.ed.jp
はどうするのか。実際に書いた正規表現は何だったのか。その後のテストで何を確認したのか。これらの情報はsummaryから消えています。

エージェントは「それっぽい正規表現」を作って返してしまいます。しかしそれは、前に決めたものではありません。これは単なる物忘れではなく、要約によって根拠に戻れなくなった状態です。

本記事では、AIエージェントのcompactを「記憶そのもの」ではなく、記憶へ戻るための地図として設計する方法を考えます。この設計をBreadcrumb Memoryと呼ぶことにします。

ただし、これはまったく新しい記憶の仕組みを提案する記事ではありません。Breadcrumb Memoryは、Observational MemoryやMemGPT系のsource recallの考え方を、長期開発・研究支援エージェント向けに再整理した設計パターンです。本記事の貢献は次の3点に絞っています。

  • exact valueをsummaryから復元しないという運用ルールとその理由の具体化
  • (summaryとは別に、将来の検索に引っかかるための語彙を意図的に残す)
    query_hints
    をrecall surfaceとして明示的に設計すること
  • (「答えられたか」ではなく「根拠へ戻れたか」を測る)
    Source Hit@k
    を評価指標の中心に置くこと

これらを、schema・recall policy・状態管理・評価指標まで含めて一体的に設計することが本記事の主眼です。

本記事でのTiDB Cloudの位置づけ

Breadcrumb Memoryの設計は特定のデータベースに依存しません。PostgreSQL + pgvector + FTS、TiDB Cloud、Cloudflare D1 + Vectorize、SQLite + local vector indexなど、複数の基盤で実装できます。

ただし、この設計を実装していくと、SQL filter・vector search・full-text searchを同じクエリ内で扱えるデータベースが自然と欲しくなります。本記事では、その3つを同じ基盤で扱いやすい例としてTiDB Cloudを使います。「TiDBだからBreadcrumb Memoryが成立する」のではなく、「Breadcrumb Memoryを実装するとSQL + vectorをまとめて扱える基盤が必要になる」という順番です。

想定しているユースケース

想定しているのは、数日から数週間続く開発・研究・実験支援エージェントです。コードのリファクタリング、研究実験のログ管理、論文や調査メモの継続整理、小規模なシステム開発支援といった場面が典型的なケースです。

とくに相性が良いのは、長期継続するコーディング支援エージェントです。認証ロジック、DBスキーマ、API仕様、テスト条件、デプロイ設定を継続的に修正していると、過去の会話にはファイルパス、関数名、DB migration、API endpoint、正規表現、環境変数、ポート番号、テスト条件、一度却下した設計案、後から上書きされた方針といった情報が大量に出てきます。

これらはsummaryから再生成すると壊れやすい情報です。普段は軽いBreadcrumbだけを使い、必要なときだけ

source_range
から原文やツール結果へ戻る。そういう設計が効いてきます。

逆に、短いFAQチャットボットや単発のRAGであれば、Breadcrumb Memoryは少し重い設計になります。これは汎用チャットボットのための仕組みというより、長期継続する開発・研究・実験支援エージェントのための記憶設計です。

エージェントメモリで本当に必要なのは「思い出し方」

RAGでは多くの場合、ユーザーの質問に関連する文書を検索してLLMに渡します。一方、AIエージェントのメモリはもう少し複雑です。過去の会話、ユーザーの好み、コードの変更履歴、ツール実行結果、失敗した試行、途中で決まった方針など、エージェントはそれらをまたいで動きます。

ここで重要なのは「たくさん保存すること」ではありません。本当に必要なのは、何を記憶するか、どの粒度で記憶するか、いつ思い出すか、summaryだけで足りるか、原文やツール出力に戻るべきか、古い記憶と新しい記憶が矛盾したらどうするか、といった判断です。

エージェントメモリは、保存機能というより思い出し方の設計です。

この考え方は、実装に落とす段階で効いてきます。メモリを「何でも保存する箱」として作るとすぐにノイズだらけになります。一方で「短く要約する機能」として割り切ると、今度は根拠に戻れません。必要なのはその中間にある設計、つまり短く持ちながら、必要なときには正しい原文へ戻れる構造です。

Compactは記憶ではなく地図である

普通のcompactは、過去の会話を短くまとめます。先ほどの認証ロジックの話なら、おそらく次のようになります。

「ユーザーは認証ロジックを修正した。学校用メールアドレスだけを許可する方針になった」

短くはなっています。しかし、地図としては弱い。どの会話で決めたのか、どのコード変更に対応しているのか、どのテストが根拠なのか、正確な正規表現はどこにあるのか。肝心の手がかりが残っていません。

Breadcrumb Memoryでは、compactの結果を次のように持ちます。

{
  "id": "mem_auth_email_policy_20260512",
  "kind": "observation",
  "summary": "認証ロジックでは、teacher.example.ed.jp と student.example.ed.jp を許可し、alumni.example.ed.jp は除外する方針になった。",
  "source_range": {
    "thread_id": "thread_refactor_auth",
    "start_index": 128,
    "end_index": 146
  },
  "tags": [
    "auth",
    "email-validation",
    "regex",
    "refactoring"
  ],
  "query_hints": [
    "メールアドレス 正規表現",
    "teacher.example.ed.jp",
    "student.example.ed.jp",
    "alumni.example.ed.jp",
    "auth email validation regex"
  ],
  "retrieve_when": "認証ロジック、メールアドレス条件、正規表現、過去に決めたバリデーション仕様について聞かれたとき",
  "importance": 0.91,
  "status": "active"
}

ここで保存しているのは、単なるsummaryではなく、

summary
は「だいたい何の話か」を示し、
source_range
は原文へ戻る場所を示します。
tags
は構造化された絞り込みに使い、
query_hints
は将来検索されそうな語彙を明示的に残します。
retrieve_when
は使うべき場面、
status
は古い記憶や置き換えられた記憶を扱うための状態です。

つまり、ここでのcompactは記憶の圧縮ではなく、索引の作成です。

Mastraのドキュメントにも同じ発想があります。通常、Observational Memoryではメッセージをobservationへ圧縮する一方、retrieval modeではobservation groupを元メッセージにリンクし、正確な文言・ツール出力・時系列が必要なときにrecall toolでsource messagesをたどれるとされています。要するに「summaryで全部を抱え込まない」という発想です。

Breadcrumb Memoryの位置づけ

Breadcrumb Memoryは、既存のメモリ機構を置き換えるものではなく、Mastra Observational Memoryのような「observation + source recall」の考え方を、自作のデータ基盤に落とすならどうなるかを考えた設計パターンです。schema、検索条件、更新ロジック、評価指標まで含めて、ひとまとめに設計するための枠組みとして整理しています。

既存の仕組みとの位置づけを整理すると次のようになります。

仕組み位置づけ
Mastra Observational Memorybackground agentで履歴をobservationへ圧縮し、必要ならsource messagesへrecallするメモリ機構
mem9TiDB Cloud上で動くmanaged memory API。persistent memory、hybrid retrieval、shared memory spacesを重視
Breadcrumb Memoryobservation + source recallの考え方を、DB schema・query_hints・status管理・評価指標まで含めて長期開発・研究支援向けに再整理した設計パターン

managed APIで素早く始めたいならmem9のような仕組みが向いています。対して、memory recordのschema、query_hintsの作り方、source_rangeの粒度、active/superseded/conflictedの扱い、評価指標まで自分で制御したいなら、Breadcrumb Memoryを自作する価値があります。

地図を広げ、目的地を決め、現地に飛び、必要な宝だけ持ち帰る

Breadcrumb Memoryの検索パイプラインは、Retrieve → Decide → Recall → Extractの4段階に分けます。地図のメタファーで言うなら、次の流れです。Retrieveで地図を広げ、Decideで目的地を決め、Recallで現地に飛び、Extractで必要な宝だけ持ち帰る。

この4段階に分けることで、「いつも全部の原文を読む」でも「summaryだけで無理やり答える」でもない、中間の設計に落とし込めます。

Retrieve: 地図を広げる

最初に取得するのは原文ではありません。まずは

summary
tags
query_hints
を対象にしたvector searchとSQL filterを組み合わせて、軽量なBreadcrumbだけを検索します。

この段階では最大10件程度の候補を取って、summaryとquery_hintsをDecideに渡します。取得するのはsummary、tags、query_hints、retrieve_when、created_at、importance、statusといった項目です。正規表現のコード全文や過去の会話全文はまだ取りません。必要なのは地図だけで、まずは輪郭を掴みます。

ユーザーが「前に認証まわりで決めたことって何だっけ?」と聞いた場合、まずは

auth
email-validation
regex
認証ロジック
のようなタグやquery_hintsに引っかかるBreadcrumbを取ります。この段階で必要なのは「どの記憶が関係ありそうか」だけです。正規表現そのものを復元する必要は、まだありません。

TiDB Cloudでは、このRetrieveを次のように書けます。

status = 'active'
project_id
のようなSQLフィルタと、embeddingによる意味検索を1つのクエリで扱えます。
SELECT id, summary, source_thread_id, source_start_index, source_end_index, status
FROM agent_memories
WHERE user_id = ?
  AND project_id = ?
  AND status = 'active'
ORDER BY VEC_COSINE_DISTANCE(embedding, ?) ASC
LIMIT 10;

これは単なるvector searchではなく、古い記憶を

superseded
のまま残しつつ、通常の回答ではactiveな記憶だけを優先できる点が重要です。

Decide: 目的地を決める

次に、summaryだけで答えられるのか、原文やツール出力に戻るべきなのかを判断します。「前に認証まわりで何を変えたんだっけ?」という質問なら、方針レベルのsummaryで足りるかもしれません。

しかし「この前決めたメールアドレス正規表現を正確に出して」と聞かれたら、summaryから正規表現を再生成してはいけません。正規表現、SQL、ファイルパス、ポート番号、API仕様、テスト条件といった情報は、summaryから再生成すると壊れやすいものです。必要なら、現地に飛ぶべきです。

ただし、

exactValuePatterns
のような文字列マッチだけに頼るのは危険です。ユーザーが「昨日のあれ、どうなった?」のように曖昧に聞いた場合、表面上はexact valueを要求していなくても、実際には原文やツール出力に戻らないと答えられないことがあります。

Decideフェーズは、少なくとも次のような段階判定にすることを推奨します。まず質問を粗く分類してexact valueが要りそうかを推定し、判定が曖昧なら保守的に

source_range
側へ倒します。summaryだけで足りる場合でも、必要ならchunk indexまで上げます。

分類は最初から複雑にしなくても、たとえば次の3分類で十分であるはずです。

判定次の動き
summary_ok
前に認証まわりで何を変えた?Breadcrumbのsummaryで答える
exact_value_needed
正規表現を正確に出して!SQLをそのまま見せて!
source_range
へ戻る
ambiguous
昨日のあれ、どうなった?
chunk_index
以上に上げる

軽量LLMに投げるなら、プロンプトはこの程度で足ります。

ユーザーの質問を次の3つに分類してください。
summary_ok: 方針や概要だけで答えられる
exact_value_needed: 正規表現、SQL、パス、数値、設定値など正確な値が必要
ambiguous: 判断できない、または文脈が足りない

出力は classification と reason だけにしてください。

実運用では

summary_only
chunk_index
source_range
llm_extract
のように段階的に上げる方が安定します。大事なのはDecideを一発判定にしないことです。最初の判定は保守的にし、情報が足りなければ後段のtool callで補う方が、曖昧な質問に強くなります。

Recall: 現地に飛ぶ

詳細が必要な場合だけ、以下のようなインターフェースで、エージェントはToolを呼び出します。

type RecallMemorySourceInput = {
  sourceThreadId: string;
  sourceStartIndex: number;
  sourceEndIndex: number;
  query: string;
  maxTokens: number;
};

type RecalledEvidence = {
  sourceThreadId: string;
  sourceStartIndex: number;
  sourceEndIndex: number;
  matchedEventIds: string[];
  extractedText: string;
  reason: string;
};

このToolが

source_range
の全原文をそのまま返すわけではない点が重要です。
source_range
はプロンプトに直接入れる範囲ではなく、再検索・抽出する対象範囲です。

必要なexact valueが複数の

source_range
にまたがる場合、1回のrecallでは足りません。たとえば、DBスキーマは3日前に変更され、対応するAPI仕様は昨日決まった、というケースでは、両方のsourceを見ないと正確な回答を組み立てられません。このときは
recallMemorySource
を複数回、必要なら並列で呼び出します。

検索は単発ではなくマルチホップです。1つの

source_range
だけ見て答えを閉じず、必要なら複数の
source_range
をまたいで根拠を集める設計にします。

Extract: 必要な宝だけ持ち帰る

source_range
が長い場合、それを全部プロンプトに入れてしまうとcompactした意味がありません。raw eventsに戻った後は、今回のqueryに必要な部分だけを抽出します。
source_range → raw_events → chunk分割 → query照合 → ranking → 必要ならLLM抽出 → evidence snippets

通常時は軽いBreadcrumbだけで済ませ、正確性が必要なときだけ原文へ戻ります。戻ったとしても、原文を全部持ち帰るのではなく、必要な部分だけを持ち帰る。これがBreadcrumb Memoryの中心です。

exact valueはsummaryから再生成しない

Breadcrumb Memoryで特に重要なのが、exact valueをsummaryから再生成しないことです。ここでいうexact valueとは、正規表現、ポート番号、ファイルパス、環境変数名、関数名、SQL、API endpoint、許可ドメイン、テスト条件、ハイパーパラメータといった情報を指します。

これらは、summaryに書かれているように見えても信用しすぎてはいけません。LLMはsummaryを作るとき、細部を自然に丸めます。正規表現のエスケープを落とす、ポート番号の桁を間違える、ファイルパスの大文字小文字を変える。そうしたズレが、そのまま「それっぽいが前に決めたものとは違う値」として返ってきます。

たとえば、summaryに「学校用メールアドレスだけを許可する正規表現に変更した」と残っていたとします。このsummaryから、あとでLLMに正規表現を再生成させるべきではありません。重要なのは「学校用メールアドレスだけ」という雰囲気ではなく、teacher.example.ed.jpは許可する、student.example.ed.jpは許可する、alumni.example.ed.jpは許可しない、という正確な条件だからです。

そのためBreadcrumb Memoryでは次のルールを置きます。exact valueはsummaryから復元しない。exact valueが必要な質問では、必ずsource_rangeからrecallする。

summaryは地図です。正規表現やSQLやファイルパスは現地にある実物です。地図を見て、実物を想像で描き直してはいけません。

query_hintsはrecall surfaceである

Breadcrumb Memoryで次に重要なのが

query_hints
です。人間がすべて手で書く必要はありません。compact時に、observer agentへ次のような指示を与えます。
あなたは会話を長期記憶に変換するobserverです。
summaryだけでなく、将来検索されそうな語句をquery_hintsとして出力してください。

query_hintsには、次の種類の語句を含めてください。
- ユーザーが後で使いそうな自然言語
- 技術用語
- 関数名・変数名・定数名
- ファイル名
- 略称
- 固有名詞
- エラー文や設定値の一部

ただし、query_hintsには長文を入れすぎないでください。
検索で引っかかるための短い語句を中心にしてください。

たとえば、認証ロジックの変更に関するmemoryなら次のようになります。

{
  "summary": "認証ロジックでは、teacher.example.ed.jp と student.example.ed.jp を許可し、alumni.example.ed.jp は除外する方針になった。",
  "query_hints": [
    "メールアドレス 正規表現",
    "認証ロジック",
    "学校用メールアドレス",
    "teacher.example.ed.jp",
    "student.example.ed.jp",
    "alumni.example.ed.jp",
    "validateEmail",
    "AUTH_ALLOWED_DOMAINS",
    "regex",
    "email-validation",
    "auth.ts",
    "validate-email.ts"
  ]
}

ポイントは、summaryとは別に「将来検索されるための表面」を作ることです。summaryは人間が読んで意味を理解するためのもので、query_hintsはあとでmemory retrievalに引っかけるためのものです。そのため、embedding対象もsummaryだけにしません。

embedding_text
summary + retrieve_when + tags + query_hints
のような単純な連結で十分です。

意味検索ではsummaryが効き、キーワード検索や再検索ではquery_hintsが効きます。query_hintsは人間向けの飾りではなく、将来の検索品質を上げるためのrecall surfaceです。

テーブル設計

最小構成では、raw_eventsとagent_memoriesの2つのテーブルを用意します。

raw_events
は会話やツール実行結果の原文を保存するテーブル、
agent_memories
はcompact後のBreadcrumbを保存するテーブルです。
CREATE TABLE agent_memories (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  project_id TEXT,
  summary TEXT NOT NULL,
  source_thread_id TEXT NOT NULL,
  source_start_index INTEGER NOT NULL,
  source_end_index INTEGER NOT NULL,
  tags JSON,
  query_hints JSON,
  retrieve_when TEXT,
  importance REAL DEFAULT 0.5,
  status TEXT DEFAULT 'active',           -- active / superseded / archived / conflicted
  supersedes_memory_id TEXT,
  metadata JSON,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

実際にはここにembedding用のvector columnを追加します。embeddingの対象は

summary
だけにしません。
embedding_text
summary + retrieve_when + tags + query_hints
で十分です。summaryだけだと、あとから検索されそうな語彙が落ちてしまうためです。

たとえば、summaryには「認証ロジックを変更した」としか書かれていなくても、あとからユーザーは「validateEmailの条件って何だった?」や「AUTH_ALLOWED_DOMAINSってどこで決めた?」と聞いてくるかもしれません。そのとき効くのが

query_hints
です。

古い記憶をどう扱うか

長期記憶で厄介なのは、古い記憶が残り続けることです。最初は「alumni.example.ed.jpも許可する」と決めていたとします。しかし後から「alumni.example.ed.jpは許可しない」に方針が変わりました。

このとき古い記憶を完全に削除すると、なぜ方針が変わったのかを追えなくなります。一方で古い記憶をactiveのまま残すと、検索時に混ざって危険です。そこで削除ではなく

superseded
にします。
UPDATE agent_memories
SET
  status = 'superseded',
  updated_at = CURRENT_TIMESTAMP
WHERE id = 'mem_auth_allow_alumni_old';

新しい記憶は古い記憶を置き換えたものとして保存し、検索時は基本的にactiveな記憶を優先します。rerankでは新しさもスコアに入れます。

final_score =
  vector_score
  + keyword_score
  + tag_score
  + importance_score
  + recency_score
  - stale_penalty

これにより、古い記憶を履歴として残しつつ、現在の回答では新しい記憶を優先できます。SQLで状態管理できる基盤の強みが出る部分です。

source_rangeは長すぎても短すぎてもよくない

source_rangeは長すぎても短すぎても問題になります。100件以上のraw eventsを1つのmemoryに紐づけると、

recallMemorySource
の中でchunk分割・スコアリング・抽出を行うコストが増えます。逆に短すぎると文脈が失われます。

最初の実装では「1 observation = 20〜40 events程度」を目安にするとよいでしょう。理論的な最適値ではなく、初期実装のための経験則です。eventの粒度やtool callの大きさ、会話密度によって変わるため、event数だけでなくtoken数も境界条件に入れた方が安全だとおもいます。「40 events または 6,000 tokensを超えたら切る」としておくと、巨大なtool outputが1つのeventに入った場合にも対応しやすくなると思います。

source_rangeは「固定長 + トピック境界」のハイブリッドで切る

source_rangeの切り方にはいくつか候補があります。

方法良い点弱い点
一定ターンごとに切る安い、実装が簡単、安定するトピックの途中で切れる
LLMでトピック分割する意味のまとまりに近くなるコストが増える、境界が不安定
人間が明示的に区切る正確運用が重い
ルール + LLM補助現実的実装は少し増える

一番現実的なのは、固定長を基本にして、明らかなトピック境界だけを補助的に使う方法です。最初の実装では次のようにします。raw_eventsを最大40 eventsで仮に区切り、tool call・file edit・test result・ユーザーの明示的な話題変更を境界候補にします。10 events未満の断片は前後と結合し、60 eventsまたは一定token数を超える場合だけLLMに分割させ、よくrecallされる範囲だけ後から改善します。

source_rangeの粒度を最初から完璧にしようとしないことが重要です。基本は安いルールで切り、LLMを使うのは境界が曖昧なときや範囲が長くなりすぎたときだけです。

recallMemorySourceは毎回フル処理しない

次に問題になるのが

recallMemorySource
のレイテンシです。素朴に実装すると、毎回raw eventsを読んでchunkに分けてqueryと照合してrankしてLLMでevidenceを抽出する、という処理が走ります。これは重い処理です。特に長いthreadで毎回これをやると、メモリ機能そのものがボトルネックになります。

そこでrecallを次のように段階化します。

  • Level 0: Breadcrumb summaryだけで答える
  • Level 1: 事前に作ったchunk indexから探す
  • Level 2: source_range内のraw chunksだけ再検索する
  • Level 3: exact valueが必要なときだけLLMでevidence抽出する

毎回raw eventsから全部作り直すのではなく、保存時にsource_rangeごとのchunkとembeddingをある程度作っておきます。chunk単位のembeddingを用意してchunk indexを作ることで、recall時にはraw events全体ではなく事前に作ったsource_chunksを検索できるようになります。

実運用では、一定期間が経過したsupersededな記憶やimportanceが極端に低い記憶はarchivedに移し、本文はCold Storageに退避させるのが現実的でしょう。

active → superseded → archived → Cold Storage (S3など)

この層を入れておくと、ホットな検索対象を軽く保ちつつ、必要になれば古い原文も復元できます。

schemaとquery_hintsは「固定コア + プロジェクト拡張」にする

Breadcrumb Memoryのschemaやquery_hintsを、プロジェクトごとに全部チューニングするのは大変です。基本は固定コア + プロジェクト拡張にすることを推奨します。

固定コアはsummary、source_range、tags、query_hints、retrieve_when、importance、status、created_at、updated_atです。ここは変えません。プロジェクト固有の情報は

metadata
に逃がします。

コーディング支援なら

metadata
にrepo、files、symbols、test_filesを持たせます。
{
  "repo": "school-equipment-reservation",
  "files": ["src/auth.ts", "src/validate-email.ts"],
  "symbols": ["validateEmail", "AUTH_ALLOWED_DOMAINS"],
  "test_files": ["tests/auth.test.ts"]
}

リサーチ支援ならpaper_ids、concepts、citation_keysを持たせます。

{
  "paper_ids": ["arXiv:2512.13002"],
  "concepts": ["sedenion", "zero divisor", "G2-invariant reduction"],
  "citation_keys": ["Author2026Sedenion"]
}

schemaを毎回変えるのではなく、共通部分は固定し、プロジェクト固有の情報だけmetadataに入れます。query_hintsも同様で、共通のobserver promptを使ったうえで、プロジェクトごとに少しだけ追加します。

実運用では「全部を賢くする」のではなく「失敗しやすいところだけ賢くする」

この設計をプロダクションで使うなら、方針はかなりシンプルです。

source_rangeはまず固定長 + tool境界で切り、長すぎる範囲だけLLMで再分割します。recallは普段はBreadcrumbとchunk indexだけを使い、exact valueが必要なときだけsource_rangeへ戻ります。query_hintsは共通promptで生成し、プロジェクトごとの差分は少しだけ足します。schemaは共通カラムを固定し、プロジェクト固有情報はmetadata JSONへ逃がします。評価は全部を評価せず、exact value復元・古い記憶の除外・source hitを重点的に見ます。

Breadcrumb Memoryはすべてを完璧に記憶するための仕組みではありません。高コストな処理を必要な場面に限定するための設計です。地図を見るだけで済むなら現地には行かない。現地に行く必要があるときだけsource_rangeに戻る。そして現地にあるものを全部持ち帰るのではなく、必要なものだけ持ち帰る。このバランスを取れるかどうかが、実際に使えるエージェントメモリになるかどうかの分かれ目です。

評価方法

メモリ機能は、デモだけだと評価しづらいものです。「なんとなく覚えているように見える」だけでは、本当に良いメモリかどうかわかりません。

評価するなら、まずRaw only(過去ログをそのまま検索)、Summary compact(要約だけを保存)、Breadcrumb Memory(要約・source_range・tags・query_hints・source recallを保存)の3方式を並べて比較することをお勧めします。

評価タスクはコーディング支援エージェントらしく、次のような内容が適切です。

  • Q1. 前に決めた認証仕様を答えられるか
  • Q2. その仕様の根拠になった発言に戻れるか
  • Q3. 正規表現や関数名を正確に復元できるか
  • Q4. 似ているが関係ない古い記憶を拾わないか
  • Q5. 古い方針と新しい方針が矛盾したとき、新しい方を優先できるか
  • Q6. 原文復元を必要なときだけ呼べるか
  • Q7. source_rangeが長い場合でも、必要な部分だけ抽出できるか

指標は次のとおりです。

  • Memory Hit@k: 必要なmemory recordが上位k件に入ったか
  • Source Hit@k: 正しいsource_rangeに戻れたか
  • Wrong Memory Rate: 関係ない記憶を使ってしまった割合
  • Exact Value Accuracy: 正規表現やSQLやファイルパスを正確に復元できたか
  • Token Cost: 回答時に注入したmemory token量
  • Recall Latency: recall toolを呼んだ場合の追加レイテンシ
  • Human Inspectability: 人間がmemory recordを見て、なぜその記憶が使われたか理解できるか

この中で特に重要なのが

Source Hit@k
です。Summary compactは「それっぽい回答」を作ることはできます。しかし正しいsourceに戻れるとは限りません。Breadcrumb Memoryで確認したいのはまさにそこです。エージェントが記憶を語れるかではなく、記憶の根拠へ戻れるか。これを評価します。

実際に計測するなら、まずは20〜50問程度の小さなゴールデンデータを作るのが第一歩です。過去ログから質問と正解のmemory/source_rangeを人手でラベル付けし、それを基準にMemory Hit@kとSource Hit@kを評価します。評価では精度だけでなく、Token CostとRecall Latencyも同時に見ます。「当たるが遅い」「速いが曖昧」になっていないかを、コストとのトレードオフで判断するためです。

最小実装の流れ

全体の実装は次の流れになります。

  • raw_eventsに会話やツール結果を保存する
  • 固定長 + tool境界でsource_range候補を作る
  • 必要に応じて長すぎるsource_rangeだけLLMで再分割する
  • observationからBreadcrumb Memoryを作る
  • summary + tags + query_hintsをembeddingする
  • source_rangeごとのchunk indexを作る
  • データベースに保存する(本記事ではTiDB Cloudを使用)
  • 次の質問でmemoryを検索する
  • Breadcrumbだけで答えられるか判断する
  • 必要な場合だけrecallMemorySourceを呼ぶ
  • source_range内からevidenceを抽出する
  • evidenceを使って回答する

この構成にすると、通常時は軽く動きます。正規表現、SQL、ファイルパス、テスト条件のような正確性が必要な場面だけ、原文へ戻れます。

やらないこと

実装時に重要なのは、何を賢くするかより何を賢くしないかです。

  • exact valueをsummaryから再生成しない
  • source_rangeの粒度を最初から完璧にしようとしない
  • chunkの再分割に毎回LLMを使わない
  • importanceスコアを最初から細かくチューニングしない
  • 古い記憶を削除せず、supersededとarchivedで扱う
  • 原文全体をrecallせず、必要なevidenceだけ抽出する

この割り切りがあると、メモリ機能は「何でも覚える巨大な履歴」ではなく、必要な根拠へ戻るための軽い地図になります。

まとめ

AIエージェントのメモリで重要なのは、すべてを覚えることではありません。必要な記憶へ正しく戻れることです。そのために本記事では、compactを「記憶の圧縮」ではなく記憶への地図作りとして扱いました。

Compactは記憶ではなく地図である。

検索パイプラインはRetrieve(地図を広げる)、Decide(目的地を決める)、Recall(現地に飛ぶ)、Extract(必要な宝だけ持ち帰る)の4段階に分けました。この設計により、summaryだけでは失われる原文・根拠・時系列・ツール出力に戻れるようになります。

AI時代のデータ基盤は、単にデータを保存する場所ではなくなっていくはずです。これから重要になるのは、AIが何を、いつ、どの粒度で、どの根拠つきで思い出すかを設計できる基盤です。エージェントメモリで大事なのは記憶量ではありません。まず軽い地図を取り出し、必要なときだけ根拠へ戻り、必要な部分だけ読むこと。そこに尽きます。

参考資料