Speicher
Langfristiger semantischer Speicher für KI-Agenten mit adk-memory.
Übersicht
Das Speichersystem stellt eine persistente, durchsuchbare Speicherung für Agentenunterhaltungen bereit. Im Gegensatz zum Sitzungsstatus (der flüchtig ist) bleibt der Speicher über Sitzungen hinweg erhalten und ermöglicht es Agenten, relevanten Kontext aus vergangenen Interaktionen abzurufen.
Installation
[dependencies]
adk-memory = "2.1.0"
Grundkonzepte
MemoryEntry
Ein einzelner Speichereintrag 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 zentrale 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(())
}
Speicherisolierung
Speicherinhalte werden isoliert nach:
- app_name: Verschiedene Anwendungen verfügen über separate Speicherbereiche
- user_id: Die Speicherinhalte jedes Benutzers sind privat
- project_id (optional): Einträge können auf ein Projekt innerhalb eines Benutzers begrenzt 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
};
Projektspezifischer Speicher
Speicherinhalte können auf ein Projekt innerhalb eines Benutzers begrenzt werden. Der Isolationsschlüssel wird zu (app_name, user_id, project_id?):
- Globale Einträge (
project_id = None): in allen Projektkontexten und bei Suchen, die nur globale Einträge berücksichtigen, sichtbar. - Projekteinträge (
project_id = Some(id)): nur bei der Suche innerhalb dieses bestimmten Projekts sichtbar. - Projektsuche (
project_id = Some(id)): liefert globale Einträge plus Einträge für dieses Projekt. - Globale Suche (
project_id = None): liefert nur globale Einträge.
Speichern projektspezifischer 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 Projektbereich
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?;
Löschen projektspezifischer Einträge
// 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 Projektbereich
MemoryServiceAdapter verbindet MemoryService mit adk_core::Memory. Verwenden Sie with_project_id(), um alle Vorgänge 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
Projektbezeichner werden bei allen Schreibvorgängen 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
Matrix der Suchsemantik
SearchRequest.project_id | Gibt globale Einträge zurück | Gibt Projekt-Einträge zurück |
|---|---|---|
None | ✅ passende Abfrage | ❌ keine |
Some("A") | ✅ passende Abfrage | ✅ nur Einträge des Projekts „A“, die der Abfrage entsprechen |
Matrix zur Löschsemantik
| Vorgang | Geltungsbereich |
|---|---|
delete_entries (kein Projekt) | Nur globale Einträge, die der Abfrage entsprechen |
delete_entries_in_project("A") | Nur Einträge des Projekts „A“, die der Abfrage entsprechen |
delete_project("A") | Alle Einträge für das Projekt „A“ |
delete_user | Alle Einträge (global + alle Projekte) |
Suchverhalten
InMemoryMemoryService verwendet eine wortbasierte Übereinstimmung:
- Die Abfrage wird in Wörter (Kleinschreibung) zerlegt
- Der Inhalt jeder Erinnerung wird zerlegt
- Erinnerungen mit beliebigen ü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. eine 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 ist in LlmAgentBuilder integriert:
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 jedem Durchlauf werden relevante Erinnerungen gesucht
- Übereinstimmende Erinnerungen werden in den Kontext eingefügt
- Nach jeder Sitzung wird die Unterhaltung als Erinnerungen gespeichert
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 Vorgehensweisen
| Vorgehensweise | Beschreibung |
|---|---|
| Vektordatenbank in der Produktion verwenden | InMemory ist nur für Entwicklungstests vorgesehen |
| Nach Benutzer begrenzen | user_id aus Datenschutzgründen immer einschließen |
| Ergebnisse begrenzen | Begrenze die zurückgegebenen Erinnerungen, um einen Kontextüberlauf zu vermeiden |
| Alte Erinnerungen bereinigen | Implementiere TTL oder archiviere veraltete Daten |
| Strategisch einbetten | Speichere Zusammenfassungen statt roher Konversationen |
Vergleich mit Sessions
| Funktion | Sitzungsstatus | Speicher |
|---|---|---|
| Persistenz | Lebensdauer der Sitzung | Permanent |
| Geltungsbereich | Einzelne Sitzung | Sitzungsübergreifend |
| Suche | Schlüssel-Wert-Suche | Semantische Suche |
| Anwendungsfall | Aktueller Kontext | Langzeitgedächtnis |
Zurück: ← Guardrails | Weiter: Studio →