はじめに
先日、DatabricksからMemCurrentというエージェント向けメモリアーキテクチャが発表されました。
あわせて、Agent Bricks上でAgent Memoryという機能もBetaで公開されています。
私自身の理解を深めるためにAgent Memoryを最小構成で動かし、SDKで直接触りながら「記憶がどこに、どう保存され、どう検索されるのか」を確かめてみました。
Agent Memory・Agent Bricks CLIはいずれもBeta機能です(2026年10月時点)。内部構造は今後変わる可能性が高いため、本記事の内容は執筆時点の観察結果としてご覧ください。
Free Editionでは、メモリストア・セッションストアの作成ができませんでした。今回はAWS上の製品版ワークスペースを利用しています。
本記事で検証したのはメモリストア(派生記憶の保存・検索)の挙動のみです。MemCurrentの構成要素であるdreamingパイプライン(セッションから記憶を蒸留・整理する処理)は検証していません。また次回。
MemCurrentとは
MemCurrentは、Databricks Researchが発表した本番エージェント向けの永続メモリアーキテクチャです。Agent Bricks MemoryとGenieのメモリ機能の基盤になっています。
主な特徴は以下の通りです:
- 2層のメモリ: セッションの生ログを持つ「エピソード記憶」と、そこから蒸留した学びを持つ「派生記憶」の2層で構成される
- 3つのアクセス方式: セッション開始時に自動でコンテキストを注入する「プラグイン」、エージェントが必要に応じて呼ぶ「ツール」、バックグラウンドで記憶を整理する「dreamingパイプライン」を組み合わせる
- 品質とコストの両立: Databricksの社内ベンチマークでは、メモリなしと比べて合格率が約3倍(22.3%→68.4%)、トークンは31%減と報告されている
記事内にもありますが、アーキテクチャとしては以下の構成です。
また、プロダクトとしてのAgent Memoryは、これを実装したものです。
このうち本記事で扱うのは、派生記憶を保存するメモリストアと、その検索です。
dreamingパイプラインは今回の検証対象外で、また別の機会に詳細を深掘りしたいと思います。
では、(Claude君と)実際の機能の詳細を確認していきます。
Step0. Agent Bricks CLIで最小構成を作る
まずはドキュメントのquickstartをベースに、メモリとセッション付きのエージェントを作ってみます。
(pipをuvを使うように変更しています。--profileの指定は環境に合わせて変更してください。)
uv tool install databricks-agentbricks
databricks auth login --profile DEFAULT
agentbricks login --profile DEFAULT
agentbricks init --framework langgraph my-agent
cd my-agent
agentbricks sessions bind my-mem-agent-sessions
agentbricks memory bind my-mem-agent-memory
agentbricks deploy
生成されるagent.tomlには、メモリとセッションのストア名が記録されます。
schema_version = 1
[agent]
framework = "langgraph"
server = "agentbricks"
[memory_store]
name = "my-mem-agent-memory"
[session_store]
name = "my-mem-agent-session"
[tracing]
experiment_name = "/Shared/agentbricks_traces/my-agent"
agentbricks deployでデプロイすると関連するリソースが作成され、サンプルのエージェントが利用できるようになります。
注意点?①:agentbricks devではメモリが使えない
ローカルでagentbricks devを動かす場合、指定したセッションストアやメモリストアは利用できません。
エージェントでメモリを試すならagentbricks deployした後のアプリで行う必要があります。
注意点?②:my-agent-memoryという名前でメモリストアが作れない
メモリストアをmy-agent-memoryでCLIから作成しようとしたところ、作った覚えがないのですがにALREADY_EXISTSで失敗しました。
agentbricks memory stores create --name my-agent-memory
# => error(ALREADY_EXISTS)
別の名前にすると普通に作成できます。
原因が確認できていないのですが、現時点では上記の名前を避けた方がよさそうです。(おま環の可能性大ですが)
Step1. SDKでメモリを直接触ってみる
エージェント経由だと何が起きているのか分かりにくいので、今回はAgentKit SDK(databricks_agentkit)でメモリストアを直接操作してみます。
メモリのエントリは、誰の記憶かを表すactor_id、ファイルパス風のキーpath、本文のcontent、検索用のdescriptionを持ちます。まずストアを作って、英語・日本語のエントリを入れてみます。
from databricks.sdk import WorkspaceClient
from databricks_agentkit import AgentKitClient
client = AgentKitClient(WorkspaceClient())
store = client.memory_stores.create("memcurrent-lab-memory")
store.add(
actor_id="user-a",
path="/preferences/communication.md",
content="Prefers email over phone. Timezone: JST.",
description="User A communication preferences",
)
store.add(
actor_id="user-a",
path="/preferences/report-style.md",
content="レポートは結論を先に書き、表は3列以内にしてほしい。",
description="ユーザーAのレポート書式の好み",
)
# ほか、略語(SDP)、月次売上の集計手順、user-b の好み などを投入
投入したエントリ一覧(長いので折り畳み)
| actor_id | path | content |
|---|---|---|
| user-a | /preferences/communication.md | Prefers email over phone. Timezone: JST. |
| user-a | /preferences/report-style.md | レポートは結論を先に書き、表は3列以内にしてほしい。 |
| user-a | /domain/glossary/sdp.md | 社内でSDPと言った場合はSpark Declarative Pipelinesを指す。 |
| user-a | /workflows/monthly-sales.md | 月次売上はmain.sales.ordersをorder_dateで月集計する。キャンセル(status='C')は除外。 |
| user-b | /preferences/communication.md | Prefers Slack. Never send email. |
| team:process-mgmt | /domain/glossary/kpi.md | 稼働率は計画停止を除いた時間で割る。 |
同じpathへのaddは上書きではなくエラー
好みが変わった想定で、同じactor_id・pathにもう一度addしてみます。
store.add(
actor_id="user-a",
path="/preferences/communication.md",
content="Prefers Teams chat now. Timezone: JST.",
description="User A communication preferences (updated)",
)
結果はエラーでした。
{
"error_type": "AgentCliError",
"error": "A managed memory entry already exists for this actor, session, and path (existing entry id: f9fbef22-...); update the existing entry instead."
}
エントリはactor_id・session_id・pathの組で一意になっていて、addはupsertではないようです。書き換えたい場合は、既存エントリを取得してupdateします。
target = next(m for m in store.list(actor_id="user-a", path_prefix="/preferences/communication"))
target.update(content="Prefers Teams chat now. Timezone: JST.")
ドキュメントのツール例はaddを呼ぶだけなので、そのままだとエージェントが同じpathに書き直そうとした2回目でツールがエラーを返します。「既存ならupdate」への分岐を入れておく必要があります。
なお、メモリストアはLakebaseのプロジェクトprojects/databricks-internal-custom-agentsにデータベースとして作成されます。
その中のmemory.memory_entriesテーブルにエントリデータが記録されています。
メモリの詳細を確認したい場合はLakebaseを直接利用する、という方法もあると思います。
(Betaなので将来的にこの構造は変わるかもしれませんが)
列としては埋め込みのベクトルデータやtsvectorの列がありました。
後述しますが、Lakebase Searchの仕組を使って検索できるようにしているものと思います。
なお、埋め込みモデルに何を利用しているかは未確認です。(たぶんQwen3-Embedding-0.6Bだと思うのですが)
Step2. 検索の処理を深掘りする
次にメモリに対して検索を動かして挙動を確かめてみました。
results = store.search(actor_id="user-a", query="communication preferences", limit=10)
for r in results:
print(r.memory.path, r.score)
/preferences/communication.md 0.03278688524590164
/workflows/monthly-sales.md 0.016129032258064516
/preferences/report-style.md 0.015873015873015872
/domain/glossary/sdp.md 0.015625
スコアが分数になっている
試しに分数に直してみます。
| スコア | 分数 |
|---|---|
| 0.03279 | 2/61 = 1/61 + 1/61 |
| 0.01639 | 1/61 |
| 0.01613 | 1/62 |
| 0.01587 | 1/63 |
| 0.01563 | 1/64 |
これは Reciprocal Rank Fusion(RRF) の式そのものです。RRFは複数の検索結果を順位で融合する手法で、各リストでの順位をrとしてΣ 1/(k + r)でスコアを出します。k=60はよく使われる定数です。
つまりは、ハイブリッド検索(ベクトル検索+キーワード検索)をしているのではと類推できます。
この見方で、いろいろなクエリの結果を整理したのが下の表です。(スコアの分解はスクリプトで自動化しました。)
| クエリ | 1位 | スコア | キーワード側 | もう片方 |
|---|---|---|---|---|
| communication preferences | communication.md | 1/61 + 1/61 | ✅ | ✅ |
| how should I contact this person | communication.md | 1/61 | ❌ | ✅ |
| レポート 書式 | report-style.md | 1/61 | ❌ | ✅ |
| 報告書のフォーマット | report-style.md | 1/61 | ❌ | ✅ |
| SDPとは | sdp.md | 1/61 | ❌ | ✅ |
| 集計手順 | monthly-sales.md | 1/61 | ❌ | ✅ |
| banana smoothie recipe(無関係) | communication.md | 1/61 | ❌ | 全件を返す |
キーワードが一致しない同義語のクエリでも、正解が毎回1位に来ています。もう片方のリストは意味的な近さで並べている、つまりベクトル検索だと考えられます。
一方で、次の点も見えてきます。
- 日本語ではキーワード側がほぼ効いていない:「レポート」も「SDP」も本文に含まれているのに、キーワード側はヒットしていません
- 関連性のしきい値がない:無関係なクエリでも、ベクトル側が全件を順位付けして返してきます
日本語はmemory_entiresのcontent_tsvecotr列を見るに、日本語部分の分かち書きがされてなかったのでそんな気はしていました。
まだ未実施ですが、lakebase_tokenizerをインストールすればこの挙動は改善するかもしれません。(後でやってみるつもりです)
Step3. Laksebase Searchかどうか確認
高確率でLakebase Searchを使っていると思うのですが、裏付けを取ってみました。
拡張とインデックス
まず、Lakebaseのdatabricks-internal-custom-agentsプロジェクトに入っている拡張を確認します。
SELECT extname, extversion FROM pg_extension
WHERE extname IN ('lakebase_vector', 'lakebase_text', 'vector', 'lakebase_tokenizer');
lakebase_textとlakebase_vectorが見つかりました。これはLakebase Searchの拡張です。
Lakebase Searchは、lakebase_textでBM25の全文検索を、lakebase_vectorでANN(近似最近傍)のベクトル検索を提供し、両者をRRFで融合するハイブリッド検索の書き方がドキュメントで紹介されています。Step2で見たスコアの形とぴったり合います。
全文検索用の列の定義
続いて、全文検索用の列の定義を見てみます。
SELECT table_name, column_name, data_type, generation_expression
FROM information_schema.columns
WHERE data_type = 'tsvector' OR generation_expression IS NOT NULL;
content_tsvectorという生成列が、次の式で定義されていました。
to_tsvector('english'::regconfig,
((COALESCE(content, ''::text) || ' '::text) || COALESCE(description, ''::text)))
ts_vectorはLakebaseのキーワード検索に使われる列と思うので、Lakebaseのキーワード検索も対応していることがわかります。
Step4. updateは検索にすぐ反映されるか
最後に、updateした内容がすぐ検索に反映されるかを確かめました。最初に試したときは、update直後にキーワード検索で引っかからない現象が一度起きたので、改めて時系列で計測しています。
検証用のエントリをwalrus Gustavという内容で追加し、penguin Ingridにupdateして、前後で検索します。
store.add(actor_id="user-a", path="/probe/update-test.md",
content="The office mascot is a walrus named Gustav.",
description="probe entry for update test")
# 検索 → update → 直後に検索
[ 8.3s] P2_after_add_old_word 'walrus Gustav' -> ✅ keyword hit (1/61 + 1/61)
[ 9.0s] P2_after_update_new_word_0s 'penguin Ingrid' -> ✅ keyword hit (1/61 + 1/61)
[ 9.1s] P2_after_update_old_word_0s 'walrus Gustav' -> ❌ vector only (1/62)
[ 10.6s] P2_after_update_desc_0s 'probe entry' -> ✅ keyword hit (1/61 + 1/63)
update直後から新しい内容でキーワード検索に当たり、古い内容では当たらなくなっています。ベクトル側でも、古い内容の語で検索すると1位から落ちているので、埋め込みも即時に更新されているようです。
分かったことのまとめ
推測を多々含みますが、検証で分かったことを整理しておきます。
-
同じactor・session・pathへのaddは拒否される。書き換えは
updateで行う - 検索はLakebase Searchのハイブリッド(BM25 + ベクトル、RRF k=60)
-
キーワード検索の対象は
contentとdescriptionだけ。pathは対象外 -
日本語は分かち書きされない。
lakebase_tokenizerを別途入れれば対応するかも?(要検証)
まとめ
Databricks Agent Memory(MemCurrent)を最小構成で動かし、SDKで直接触りながら、メモリの保存先と検索の仕組みを確かめてみました。
検索スコアの形と内部の生成列の定義から、Agent Memoryの検索がLakebase Searchのハイブリッド検索で実装されていることが推測できます。仕組みが分かると内部動作や日本語の課題などもわかるので、対策を検討できます。
今回検証したのはメモリストアの保存と検索だけで、MemCurrentの構成要素であるdreamingパイプライン(セッションから記憶を蒸留・整理する処理)は検証していません。
MemCurrentの効果の多くはdreamingパイプラインによるものとされているので、そちらの挙動も今後確かめていきたいと思います。

