Speicher
Langzeit-Semantikspeicher für KI-Agenten unter Verwendung von adk-memory.
Übersicht
Das Speichersystem bietet persistenten, durchsuchbaren Speicher für Agentenkonversationen. Im Gegensatz zum Sitzungsstatus (der vergänglich ist) bleibt der Speicher über Sitzungen hinweg bestehen und ermöglicht es Agenten, relevante Kontexte aus früheren Interaktionen abzurufen.
Installation
[dependencies]
adk-memory = "2.0.0"
Kernkonzepte
MemoryEntry
Ein einzelner Speicherdatensatz mit Inhalt, Autor und Zeitstempel:
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
Das Kern-Trait für Speicher-Backends:
#[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
Abfrageparameter für die Speichersuche:
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
Einfache In-Memory-Implementierung für Entwicklung und Tests:
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(())
}
Speicherisolation
Speicher sind isoliert durch:
- app_name: Verschiedene Anwendungen haben separate Speicherbereiche
- user_id: Die Speicher jedes Benutzers sind privat
- project_id (optional): Einträge können auf ein Projekt innerhalb eines Benutzers beschränkt werden
// 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
};
Projektbezogener Speicher
Speicher können auf ein Projekt innerhalb eines Benutzers beschränkt werden. Der Isolationsschlüssel wird (app_name, user_id, project_id?):
- Globale Einträge (
project_id = None): sichtbar in allen Projektkontexten und bei rein globalen Suchen. - Projekteinträge (
project_id = Some(id)): nur sichtbar bei der Suche innerhalb dieses spezifischen Projekts. - Projektsuche (
project_id = Some(id)): gibt globale Einträge + Einträge für dieses Projekt zurück. - Globale Suche (
project_id = None): gibt nur globale Einträge zurück.
Speichern projektbezogener Einträge
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?;
Suchen mit Projektumfang
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?;
Projektbezogene Löschung
// 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 mit Projektumfang
Die MemoryServiceAdapter überbrückt MemoryService zu adk_core::Memory. Verwenden Sie with_project_id(), um alle Operationen zu begrenzen:
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?;
Validierung der Projekt-ID
Projekt-Identifikatoren werden bei allen Schreiboperationen validiert:
- Darf nicht leer sein
- Darf 256 Zeichen nicht überschreiten
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
Suchsemantik-Matrix
SearchRequest.project_id | Gibt globale Einträge zurück | Gibt Projekteinträge zurück |
|---|---|---|
None | ✅ passende Abfrage | ❌ keine |
Some("A") | ✅ passende Abfrage | ✅ nur Einträge des Projekts "A", die der Abfrage entsprechen |
Löschsemantik-Matrix
| Operation | Umfang |
|---|---|
delete_entries (kein Projekt) | Globale Einträge, die nur der Abfrage entsprechen |
delete_entries_in_project("A") | Einträge von Projekt "A", die nur der Abfrage entsprechen |
delete_project("A") | Alle Einträge für Projekt "A" |
delete_user | Alle Einträge (global + alle Projekte) |
Suchverhalten
Das InMemoryMemoryService verwendet wortbasierte Übereinstimmung:
- Abfrage wird in Wörter (Kleinbuchstaben) tokenisiert
- Der Inhalt jedes Speichers wird tokenisiert
- Speicher mit übereinstimmenden Wörtern werden zurückgegeben
// Query: "rust programming"
// Matches memories containing "rust" OR "programming"
Benutzerdefiniertes Speicher-Backend
Implementieren Sie MemoryService für benutzerdefinierten Speicher (z.B. Vektordatenbank):
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![] })
}
}
Integration mit Agents
Der Speicher integriert sich mit 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()?;
Wenn der Speicher konfiguriert ist:
- Vor jeder Runde werden relevante Speicher durchsucht
- Übereinstimmende Speicher werden in den Kontext eingefügt
- Nach jeder Sitzung wird die Konversation als Speicher abgelegt
Architektur
┌─────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────┘
Bewährte Verfahren
| Praxis | Beschreibung |
|---|---|
| Vektor-DB in Produktion verwenden | InMemory ist nur für Entwicklung/Tests |
| Umfang nach Benutzer | Fügen Sie user_id immer aus Datenschutzgründen hinzu |
| Ergebnisse begrenzen | Zurückgegebene Erinnerungen begrenzen, um Kontextüberlauf zu vermeiden |
| Alte Erinnerungen bereinigen | Implementierung von TTL oder Archivierung für veraltete Daten |
| Strategisch einbetten | Zusammenfassungen speichern, nicht Rohkonversationen |
Vergleich mit Sessions
| Merkmal | Sitzungsstatus | Speicher |
|---|---|---|
| Persistenz | Sitzungslebensdauer | Permanent |
| Umfang | Einzelne Sitzung | Sitzungsübergreifend |
| Suche | Schlüssel-Wert-Suche | Semantische Suche |
| Anwendungsfall | Aktueller Kontext | Langzeitgedächtnis |
Zurück: ← Guardrails | Weiter: Studio →