0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft Agent Framework のメモリー機能を拡張し、Azure AI Search で本格的な長期記憶基盤を構築する

0
Last updated at Posted at 2026-05-29

現在いろいろなエンジニアの方と Microsoft Agent Framework Workshop をやっているのですが、そのなかで記憶に関するディスカッションがかなり活発に行われています。その中でのフィードバックを受けて、今回の様な実装案を考えました。

Microsoft Agent Framework には組み込みの MemoryContextProviderMem0ContextProvider が用意されていますが、実際に使ってみるとあれがない、これがないとなることが多いです。そこで Azure AI Search を組み合わせ、エージェント専用の人間の記憶に近づけたカスタムメモリーレイヤーを設計・実装してみました。

その中で、今回記憶の検索に特化した新しいランキング方針 RankingProfile.IMPORTANTRankingProfile.RECENT を実装しました。

なぜカスタムメモリーが必要か

AI エージェントに「前回の会話で伝えた好み」を覚えさせたい場合、Mem0 のような汎用メモリーライブラリを使う選択肢があります。しかし、実運用で以下の要件が出てくると、汎用ライブラリでは制御しきれなくなります。

  • 記憶の重要度鮮度検索順位を変えたい
  • 古い記憶を自動的にフェードアウトさせ、エージェントに渡す候補から外したい(忘却
  • スコアリングプロファイル、日付フィルター、カテゴリ絞り込み、セマンティックランカーなど Azure AI Search の便利機能をフルに使いたい
  • 記憶の追加・更新・削除の判断ロジックを透明化したい

本記事では、Microsoft Agent Framework v1.4.0 の ContextProvider を継承し、Azure AI Search のインデックスを記憶ストアとして直接制御するアーキテクチャを紹介します。

アーキテクチャの全体像

シーケンス図

設計のポイントは 3 つです。

  1. 記憶のライフサイクルをトップレベルフィールドで管理する
    importanceretention_scoreaccess_countupdated_at を JSON ペイロードに閉じ込めず、Azure AI Search のフィルター・ソート・スコアリングプロファイルでそのまま使えるようにします。
  2. 検索方式とランキング方針を分ける
    SearchMode は keyword / vector / hybrid / semantic の候補取得方式だけを表し、記憶の検索に最適化した RankingProfileIMPORTANTRECENT のランキング方針を表します。
  3. LLM が使えなくてもフォールバックで動く
    記憶抽出と統合判断にルールベースのフォールバックを持たせ、LLM が未設定でも構造を確認できます。

クラス構成

クラス 責務
AzureAISearchMemoryContextProvider ContextProvider を継承。before_run() で記憶検索・注入、after_run() で記憶保存を実行
AzureAISearchMemoryStore Azure AI Search インデックスの作成、ドキュメント Upsert、検索、状態更新、忘却処理
AzureOpenAIMemoryIntelligence Azure OpenAI の JSON Completion と Embedding を呼び出す薄いヘルパー
MemoryExtractor 会話メッセージから記憶候補 (MemoryCandidate) を抽出
MemoryConsolidator 新規候補と既存記憶を比較し、ADD / UPDATE / DELETE / NONE を決定
HumanMemoryPolicy 時間減衰、想起強化、アーカイブ判定のポリシー

データモデル

MemoryRecord: Azure AI Search に保存する 1 件の記憶

@dataclass
class MemoryRecord:
    id: str                          # UUID
    user_id: str                     # ユーザー単位の分離
    agent_id: str                    # エージェント単位の分離
    memory: str                      # 記憶本文
    category: str = "general"        # preference / profile / plan など
    importance: float = 0.5          # 0.0–1.0(忘却しにくさ)
    confidence: float = 0.7          # 抽出時の確信度
    status: MemoryStatus = "active"  # active / archived / deleted
    retention_score: float = 1.0     # 保持スコア(減衰で下がる)
    access_count: int = 0            # 想起回数
    created_at: datetime             # 作成日時
    updated_at: datetime             # 更新日時
    last_accessed_at: datetime | None  # 最後に想起された日時
    expires_at: datetime | None      # 期限(一時的な記憶用)
    embedding: list[float]           # ベクトル検索用

importanceretention_score をトップレベルフィールドにすることで、Azure AI Search のスコアリングプロファイルから直接参照できます。Mem0 が使うような payload JSON に閉じ込めると、フィルターやソートで活用できなくなります。

MemoryStatus: 記憶のライフサイクル状態

状態 意味 検索対象
active 通常の検索対象 常に含む
archived 古い / 低保持スコア include_archived=True で含む
deleted 明示削除または矛盾により無効化 含まない

検索方式とランキング方針: SearchMode / RankingProfile

利用シナリオに応じて検索手法を変えられる設計にしました。IMPORTANTRECENT が今回新規で開発したものです。

class SearchMode(StrEnum):
    KEYWORD = "keyword"      # テキスト一致
    VECTOR = "vector"        # 意味的類似
    HYBRID = "hybrid"        # キーワード + ベクトル
    SEMANTIC = "semantic"    # Hybrid + セマンティックランカー

class RankingProfile(StrEnum):
    NONE = "none"            # スコアリングプロファイルなし
    RECENT = "recent"        # 最近更新を優先
    IMPORTANT = "important"  # 重要度・保持スコア・参照回数を重視

スコアリングプロファイルとの対応

RankingProfile 内部で使うプロファイル ブーストする値
IMPORTANT memory-priority importance (boost=8)、retention_score (boost=4)、access_count (boost=4, logarithmic)
RECENT memory-recency updated_at (boost=8, Freshness 90 日)
NONE なし Azure AI Search の Relevance Score のみ

呼び出し側のコードはシンプルです。

options = MemorySearchOptions(
    mode=SearchMode.HYBRID,
    ranking_profile=RankingProfile.IMPORTANT,
    top=5,
    min_retention_score=0.1,
)
results = provider.search_memories("大阪旅行の好み", options=options)

参考:RAGOps Studio におけるスコアリングプロファイルの管理

ちなみに、RAGOps Studio を使うとスコアリングプロファイルが UI で管理できます👍

image.png

記憶抽出: MemoryExtractor

会話メッセージから「永続的に保存すべき記憶」を抽出します。記憶の抽出については以前も取り上げましたが、今回も同様にプロンプトで指定します。

LLM 抽出モード

Azure OpenAI の JSON モードで、以下のような構造を返却させます。

{
  "memories": [
    {
      "memory": "大阪旅行では駅近ホテルを最優先する",
      "category": "preference",
      "importance": 0.85,
      "confidence": 0.9
    }
  ]
}

抽出対象はユーザーの嗜好、制約、プロフィール、定期的なニーズです。一時的な事実やアシスタントだけが主張した内容は対象外とします。

ルールフォールバック

LLM が使えない場合は、日本語・英語のパターンマッチで永続的な発話を検出します。適当です。

durable_patterns = [
    (r"(好き|好み|優先|苦手|嫌い|必要|いつも|毎回)", "preference"),
    (r"(prefer|like|dislike|always|usually)", "preference"),
]

フォールバック時の既定値は importance=0.65confidence=0.55source="rule-fallback" です。

記憶統合: MemoryConsolidator

記憶の統合については、Foundry Agent Service においてもフルマネージドで機能実装していましたが、今回ここのロジックについても独自で実装します。LLM を使用して類似・重複するトピックをマージし、冗長な情報の保存を防ぎます。

LLM が新しい記憶候補と既存記憶を比較し、4 つのイベントから 1 つを選択します。具体的には意味的な類似度比較、矛盾検出、マージ文の自然言語生成を行います。

イベント 意味 処理
ADD 新規記憶として保存 新しい MemoryRecord を Upsert
UPDATE 既存記憶に統合 本文マージ後に既存レコードを Upsert
DELETE 既存記憶を無効化 statusdeleted に変更
NONE 既存記憶でカバー済み 何もしない

判断フロー(ルールフォールバック)

近傍なし → ADD
正規化テキスト完全一致 or スコア >= 0.92 → NONE
忘却・削除指示に見える → DELETE
スコア >= 0.78 かつカテゴリ一致 → UPDATE
その他 → ADD

UPDATE 時は importanceconfidencemax(existing, candidate) を使い、重要度を不用意に下げない設計にしています。

人間らしい記憶: HumanMemoryPolicy

エージェントの記憶が増え続けると、古い予定や弱い好みまで毎回参照してしまい応答品質を下げます。HumanMemoryPolicy は、思い出された記憶を残しやすくし、使われない記憶を徐々に見えにくくする仕組みです。

保持スコアの計算式(簡易版)

認知科学的にはもっと究めることができるようですが、今回デモ用途として簡易的に実装しています。保持スコアは「この記憶をまだエージェントに見せる価値があるか」を 0.0〜1.0 で表す数値です。スコアが下がると検索フィルターで除外され、閾値を下回ると archived 状態に遷移します。

式は 3 つの独立した要素の組み合わせです。

$$
\text{retention_score} = \text{decay} \times \text{importance_anchor} + \text{access_rehearsal}
$$

前半の積が「時間経過と重要度で決まる基本的な残り具合」を表し、後半の加算項が「実際に使われた実績による底上げ」を表します。

各要素の定義:

$$
\text{decay} = 0.5^{\frac{\text{age_days}}{\text{half_life_days}}}
$$

$$
\text{importance_anchor} = 0.35 + 0.65 \times \text{importance}
$$

$$
\text{access_rehearsal} = \min(\text{access_count} \times 0.04,\ 0.35)
$$

式の意味としては、

  1. 記憶が作られてからの経過日数で基本的な「新鮮さ」が決まります(decay)
  2. その新鮮さに重要度の重みを掛けて、「時間と重要度を踏まえた基本値」が出ます(decay × anchor)
  3. 最後に、実際に使われた実績分を足して最終スコアにします(+ rehearsal)

想起による強化

before_run() の検索で使われた記憶は touch_results() により以下が更新されます。

record.last_accessed_at = now
record.access_count += 1
record.retention_score = policy.retention_score(record)
record.status = policy.status_for(record)

よく使う記憶は残りやすく、使わない記憶は自然にフェードアウトする。Ebbinghaus の忘却曲線を簡略化したモデルです。この辺、カスタムしがいがあって面白いですね。

ContextProvider への接続

Agent Framework の ContextProvider を継承し、before_run()after_run() をオーバーライドします。

from agent_framework import Agent
from agent_framework._sessions import ContextProvider

class AzureAISearchMemoryContextProvider(ContextProvider):
    async def before_run(self, *, agent, session, context, state):
        # 1. ユーザー入力からクエリ構築
        # 2. Embedding 生成
        # 3. Azure AI Search で関連記憶を検索
        # 4. touch_results() で想起強化
        # 5. context.extend_messages() で記憶をコンテキストに注入

    async def after_run(self, *, agent, session, context, state):
        # 1. 入力 + 応答メッセージを収集
        # 2. MemoryExtractor で記憶候補を抽出
        # 3. MemoryConsolidator で ADD/UPDATE/DELETE/NONE 判断
        # 4. AzureAISearchMemoryStore で反映
        # 5. (オプション) 忘却 dry-run を state に保存

エージェントへの接続は context_providers に渡すだけです。

agent = Agent(
    client=create_azure_openai_chat_client(),
    name="TravelAgent",
    instructions="関連する記憶がある場合だけ利用して回答してください。",
    context_providers=[custom_memory_provider],
)
result = await agent.run("大阪旅行の計画を立てて", session=session)

コンテキスト注入フォーマット

検索結果はシステムメッセージとしてエージェントに渡されます。

## Relevant memories
Use these memories only when they help answer the user.
1. [preference; importance=0.85; retention=0.91] 大阪旅行では駅近ホテルを最優先する
2. [preference; importance=0.70; retention=0.88] ベジタリアン対応の店を優先して探す

Azure AI Search インデックス設計

フィールド設計

フィールド 属性 用途
id String Key UUID
user_id / agent_id String Filterable, Sortable データ分離
memory / summary String Searchable テキスト検索対象
category String Searchable, Filterable, Facetable カテゴリ別絞り込み
importance Double Filterable, Sortable, Facetable スコアリングプロファイルで利用
retention_score Double Filterable, Sortable 保持スコアフィルター
access_count Int32 Filterable, Sortable 想起回数
created_at / updated_at DateTimeOffset Filterable, Sortable Freshness スコアリング
status String Filterable, Facetable 検索可視性制御
embedding Collection(Edm.Single) Vector HNSW ベクトル検索

Semantic Configuration

content_fields: memory, summary
keyword_fields: category

データ分離

検索フィルターには常に以下が含まれます。記憶はユーザーごとに混ぜずに管理する必要がありますのでこのようなフィルタが必要になります。

user_id eq '<user_id>' and agent_id eq '<agent_id>'

これはアプリケーションレベルの分離です。マルチテナント本番環境では、テナント ID の追加、インデックス分離、RBAC、監査ログの検討が必要です。

RankingProfile.IMPORTANT vs RankingProfile.RECENT

同じクエリでも、検索モードによって上位結果が変わります😎

# 古いが重要度が高く、何度も参照された基本方針
record_a = MemoryRecord(
    memory="大阪旅行のホテルの基本方針はたこ焼き屋が徒歩圏内であること。",
    importance=1.0, access_count=20, days_old=75,
)

# 新しいが重要度の低い一時候補
record_b = MemoryRecord(
    memory="大阪旅行のホテルの基本方針はたこ焼き屋が徒歩圏内であること。",
    importance=0.05, access_count=0, days_old=0,
)
モード 上位に来る記憶 理由
IMPORTANT record_a importance=1.0access_count=20 のブーストが大きい
RECENT record_b updated_at が直近で Freshness スコアが最大

使い分け:

  • 長期的な好みや強い制約を優先したい → ranking_profile=RankingProfile.IMPORTANT
  • 直近の予定や最近追加された条件を優先したい → ranking_profile=RankingProfile.RECENT
  • 通常の候補取得方式 → mode=SearchMode.HYBRID

忘却: dry-run とメンテナンス

apply_forgetting()retention_score asc で弱い記憶を取得し、新しいスコアと状態を計算します。

# 候補の確認だけ(既定)
preview = store.apply_forgetting(policy, dry_run=True, limit=25)

# 実際にインデックスを更新(定期メンテナンスジョブで実行)
store.apply_forgetting(policy, dry_run=False, limit=100)

物理削除はしません。deleted 状態のドキュメントを一定期間後にパージする場合は、別途バッチ処理として設計します。

汎用記憶との比較

観点 汎用記憶 この実装
スキーマ制御 ライブラリが管理 自分で設計(フィルター・ソート・プロファイル自由)
検索順位の調整 ライブラリ内部のロジック Azure AI Search のスコアリングプロファイル
記憶の状態管理 限定的 active / archived / deleted を明示的に遷移
忘却 手動削除 時間減衰 + 想起強化 + アーカイブ
抽出・統合の透明性 ブラックボックス MemoryExtractor / MemoryConsolidator として分離
フォールバック なし LLM 未設定時のルールベース処理
セマンティックランカー 非対応 SearchMode.SEMANTIC で利用可能

現行の制約

以下も今後実装してもいいかも。

制約 改善案
LLM 判断は非決定的 JSON Schema バリデーション、Few-Shot、評価データセットの追加
フォールバック Embedding は簡易的 本番では Azure OpenAI Embedding を必須にする
グラフ構造はない エンティティ / Fact インデックス追加、または Graph DB 連携
評価ハーネスがない LoCoMo / LongMemEval 風の小型 Eval ノートブックを追加

まとめ

今回はこんな感じで記憶を Azure AI Search で実装してみました。

  1. 記憶抽出 — どの発話を記憶として残すか
  2. 統合判断 — 追加、更新、削除、スキップをどう分けるか
  3. 検索と順位付け — Azure AI Search のスコアリングプロファイルに記憶の優先度を伝える
  4. 忘却と保持 — 使われた記憶を強め、古い記憶を弱める

Azure AI Search の強みは、キーワード、ベクトル、ハイブリッド、セマンティック検索等を用途に応じて組み合わせられる点にあります。正確な語句一致を重視したい場面ではキーワード検索、言い換えや曖昧な表現を拾いたい場面ではベクトル検索、両方を活かしたい場面ではハイブリッド検索が使えます。また、これらは Agentic に組み合わせることもできます。

さらにスコアリングプロファイルを使うことで、本文の一致度だけでなく、重要度、保持スコア、更新日時、アクセス回数のような非ベクトルフィールドも順位付けに反映できます。これはエージェントの記憶検索と相性がよく、「意味的に近い」だけでなく、「今このユーザーに渡す価値が高い」記憶を選びやすくなります。

そしてスコアリングプロファイルはキーワード検索だけでなく、ベクトルクエリやハイブリッドクエリでも利用できます。ただ、ブースト対象は非ベクトルフィールドですので、ベクトル検索で意味的に近い候補を見つけ、スコアリングプロファイルで重要度や鮮度を反映して並べ替える、という役割分担ができます。

この柔軟性により、Azure AI Search は単なる類似検索ではなく、記憶ストアとしても使えるマルチロールな検索エンジンになります。エージェントのメモリーレイヤーでは、記憶本文、重要度、保持スコア、アクセス回数、更新日時をトップレベルフィールドとして持たせることで、説明可能で制御しやすい記憶検索を実現できます。

GitHub

↑ はMicrosoft Agent Framework Workshop シリーズとして作成しています。今後以下に統合予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?