メモリ
AI エージェント向けの長期的なセマンティックメモリ (adk-memory を使用)。
概要
メモリシステムは、エージェントの会話を永続的に検索可能なストレージとして提供します。セッションの状態 (一時的なもの) とは異なり、メモリはセッションをまたいで永続化され、エージェントが過去のやり取りから関連するコンテキストを呼び出すことを可能にします。
インストール
[dependencies]
adk-memory = "2.0.0"
コアコンセプト
MemoryEntry
コンテンツ、作成者、タイムスタンプを持つ単一のメモリレコード:
use adk_memory::MemoryEntry;
use adk_core::Content;
use chrono::Utc;
let entry = MemoryEntry {
content: Content::new("user").with_text("I prefer dark mode"),
author: "user".to_string(),
timestamp: Utc::now(),
};
MemoryService Trait
メモリバックエンドのコアとなる trait:
#[async_trait]
pub trait MemoryService: Send + Sync {
/// Store session memories for a user
async fn add_session(
&self,
app_name: &str,
user_id: &str,
session_id: &str,
entries: Vec<MemoryEntry>,
) -> Result<()>;
/// Search memories by query
async fn search(&self, req: SearchRequest) -> Result<SearchResponse>;
}
SearchRequest
メモリ検索のクエリパラメータ:
use adk_memory::SearchRequest;
let request = SearchRequest {
query: "user preferences".to_string(),
user_id: "user-123".to_string(),
app_name: "my_app".to_string(),
limit: None,
min_score: None,
project_id: None, // None = global only, Some("id") = global + project
};
InMemoryMemoryService
開発およびテスト用のシンプルなインメモリ実装:
use adk_memory::{InMemoryMemoryService, MemoryService, MemoryEntry, SearchRequest};
use adk_core::Content;
use chrono::Utc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let memory = InMemoryMemoryService::new();
// Store memories from a session
let entries = vec![
MemoryEntry {
content: Content::new("user").with_text("I like Rust programming"),
author: "user".to_string(),
timestamp: Utc::now(),
},
MemoryEntry {
content: Content::new("assistant").with_text("Rust is great for systems programming"),
author: "assistant".to_string(),
timestamp: Utc::now(),
},
];
memory.add_session("my_app", "user-123", "session-1", entries).await?;
// Search memories
let request = SearchRequest {
query: "Rust".to_string(),
user_id: "user-123".to_string(),
app_name: "my_app".to_string(),
limit: None,
min_score: None,
project_id: None,
};
let response = memory.search(request).await?;
println!("Found {} memories", response.memories.len());
Ok(())
}
メモリの分離
メモリは以下によって分離されます:
- app_name: 異なるアプリケーションは別々のメモリ空間を持ちます
- user_id: 各ユーザーのメモリはプライベートです
- project_id (オプション): エントリはユーザー内のプロジェクトにスコープ設定できます
// User A's memories
memory.add_session("app", "user-a", "sess-1", entries_a).await?;
// User B's memories (separate)
memory.add_session("app", "user-b", "sess-1", entries_b).await?;
// Search only returns user-a's memories
let request = SearchRequest {
query: "topic".to_string(),
user_id: "user-a".to_string(),
app_name: "app".to_string(),
limit: None,
min_score: None,
project_id: None, // None = global entries only
};
プロジェクトスコープのメモリ
メモリはユーザー内のプロジェクトにスコープ設定できます。分離キーは (app_name, user_id, project_id?) になります:
- グローバルエントリ (
project_id = None): すべてのプロジェクトコンテキストおよびグローバルのみの検索で表示されます。 - プロジェクトエントリ (
project_id = Some(id)): その特定のプロジェクト内での検索時にのみ表示されます。 - プロジェクト検索 (
project_id = Some(id)): グローバルエントリ + そのプロジェクトのエントリを返します。 - グローバル検索 (
project_id = None): グローバルエントリのみを返します。
プロジェクトスコープのエントリの保存
use adk_memory::{InMemoryMemoryService, MemoryService, MemoryEntry};
use adk_core::Content;
use chrono::Utc;
let service = InMemoryMemoryService::new();
let entry = MemoryEntry {
content: Content::new("user").with_text("Project uses microservices"),
author: "user".to_string(),
timestamp: Utc::now(),
};
// Global entry (no project scope)
service.add_session("app", "user-1", "sess-1", vec![entry.clone()]).await?;
// Project-scoped entry
service.add_session_to_project("app", "user-1", "sess-2", "my-project", vec![entry.clone()]).await?;
// Single entry to a project
service.add_entry_to_project("app", "user-1", "my-project", entry).await?;
プロジェクトスコープでの検索
use adk_memory::SearchRequest;
// Global-only search — returns only global entries
let global = service.search(SearchRequest {
query: "microservices".into(),
user_id: "user-1".into(),
app_name: "app".into(),
limit: None,
min_score: None,
project_id: None,
}).await?;
// Project search — returns global + project entries
let project = service.search(SearchRequest {
query: "microservices".into(),
user_id: "user-1".into(),
app_name: "app".into(),
limit: None,
min_score: None,
project_id: Some("my-project".into()),
}).await?;
プロジェクトスコープでの削除
// Delete entries matching a query within a project only
service.delete_entries_in_project("app", "user-1", "my-project", "microservices").await?;
// Delete ALL entries for a project
service.delete_project("app", "user-1", "my-project").await?;
// Global delete — only removes global entries, project entries are unaffected
service.delete_entries("app", "user-1", "microservices").await?;
// GDPR delete_user — removes everything (global + all projects)
service.delete_user("app", "user-1").await?;
プロジェクトスコープでの MemoryServiceAdapter
MemoryServiceAdapter は MemoryService を adk_core::Memory に橋渡しします。すべての操作をスコープ設定するには with_project_id() を使用します:
use adk_memory::{InMemoryMemoryService, MemoryServiceAdapter};
use adk_core::Memory;
use std::sync::Arc;
let service = Arc::new(InMemoryMemoryService::new());
// Adapter without project — operates on global entries
let global_adapter = MemoryServiceAdapter::new(service.clone(), "app", "user-1");
// Adapter with project — all search/add/delete operations scoped to the project
let project_adapter = MemoryServiceAdapter::new(service.clone(), "app", "user-1")
.with_project_id("my-project");
// Core Memory trait also supports ad-hoc project access
global_adapter.search_in_project("query", "other-project").await?;
global_adapter.add_to_project(entry, "other-project").await?;
プロジェクト ID の検証
プロジェクト識別子は、すべての書き込み操作で検証されます:
- 空であってはなりません
- 256 文字を超えてはなりません
use adk_memory::validate_project_id;
validate_project_id("my-project")?; // Ok
validate_project_id("")?; // Err: must not be empty
validate_project_id(&"x".repeat(257))?; // Err: exceeds 256 chars
検索セマンティクスのマトリックス
SearchRequest.project_id | グローバルエントリを返す | プロジェクトエントリを返す |
|---|---|---|
None | ✅ クエリに一致 | ❌ なし |
Some("A") | ✅ クエリに一致 | ✅ プロジェクト「A」のエントリのみクエリに一致 |
削除セマンティクス行列
| 操作 | スコープ |
|---|---|
delete_entries (プロジェクトなし) | クエリに一致するグローバルエントリのみ |
delete_entries_in_project("A") | プロジェクト「A」のクエリに一致するエントリのみ |
delete_project("A") | プロジェクト「A」のすべてのエントリ |
delete_user | すべてのエントリ (グローバル + すべてのプロジェクト) |
検索動作
InMemoryMemoryService は単語ベースのマッチングを使用します。
- クエリは単語(小文字)にトークン化されます
- 各メモリのコンテンツはトークン化されます
- 一致する単語を含むメモリが返されます
// Query: "rust programming"
// Matches memories containing "rust" OR "programming"
カスタムメモリバックエンド
カスタムストレージ(例:ベクトルデータベース)のために MemoryService を実装します。
use adk_memory::{MemoryService, MemoryEntry, SearchRequest, SearchResponse};
use adk_core::Result;
use async_trait::async_trait;
pub struct VectorMemoryService {
// Your vector DB client
}
#[async_trait]
impl MemoryService for VectorMemoryService {
async fn add_session(
&self,
app_name: &str,
user_id: &str,
session_id: &str,
entries: Vec<MemoryEntry>,
) -> Result<()> {
// 1. Generate embeddings for each entry
// 2. Store in vector database with metadata
Ok(())
}
async fn search(&self, req: SearchRequest) -> Result<SearchResponse> {
// 1. Generate embedding for query
// 2. Perform similarity search
// 3. Return top-k results
Ok(SearchResponse { memories: vec![] })
}
}
Agent との統合
メモリは LlmAgentBuilder と統合されます。
use adk_agent::LlmAgentBuilder;
use adk_memory::InMemoryMemoryService;
use std::sync::Arc;
let memory = Arc::new(InMemoryMemoryService::new());
let agent = LlmAgentBuilder::new("assistant")
.model(model)
.instruction("You are a helpful assistant with memory.")
.memory(memory)
.build()?;
メモリが設定されている場合:
- 各ターン前に、関連するメモリが検索されます
- 一致するメモリがコンテキストに注入されます
- 各セッション後、会話はメモリとして保存されます
アーキテクチャ
┌─────────────────────────────────────────────────────────────┐
│ Agent Request │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Memory Search │
│ │
│ SearchRequest { query, user_id, app_name, project_id } │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MemoryService │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐ │ │
│ │ │InMemory │ │ SQLite │ │Postgres│ │ Redis │ │ │
│ │ │(dev) │ │ │ │pgvector│ │ │ │ │
│ │ └─────────┘ └──────────┘ └────────┘ └──────────┘ │ │
│ │ ┌─────────┐ ┌──────────┐ │ │
│ │ │MongoDB │ │ Neo4j │ │ │
│ │ └─────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ SearchResponse { memories: Vec<MemoryEntry> } │
│ (filtered by project scope) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Context Injection │
│ │
│ Relevant memories added to agent context │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent Execution │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Memory Storage │
│ │
│ Session conversation stored for future recall │
│ (global or project-scoped) │
└─────────────────────────────────────────────────────────────┘
ベストプラクティス
| 実践 | 説明 |
|---|---|
| 本番環境でのベクターDBの使用 | InMemory は開発/テスト専用です |
| ユーザーによるスコープ設定 | プライバシー保護のため、常に user_id を含めます |
| 結果の制限 | コンテキストのオーバーフローを避けるため、返される記憶を制限します |
| 古い記憶のクリーンアップ | 古いデータに対して TTL またはアーカイブを実装します |
| 戦略的な埋め込み | 生の会話ではなく、要約を保存します |
セッションとの比較
| 特徴 | セッション状態 | メモリ |
|---|---|---|
| 永続性 | セッションの寿命 | 永続的 |
| スコープ | 単一セッション | セッション間 |
| 検索 | キーと値のルックアップ | セマンティック検索 |
| ユースケース | 現在のコンテキスト | 長期的な想起 |