메모리
adk-memory을 사용하는 AI 에이전트를 위한 장기 의미 메모리입니다.
개요
메모리 시스템은 에이전트 대화를 위한 영구적이고 검색 가능한 저장소를 제공합니다. 일시적인 세션 상태와 달리 메모리는 세션 간에 유지되며, 에이전트가 이전 상호작용의 관련 컨텍스트를 떠올릴 수 있도록 합니다.
설치
[dependencies]
adk-memory = "2.1.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 트레이트
메모리 백엔드를 위한 핵심 트레이트입니다.
#[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![] })
}
}
에이전트와의 통합
메모리는 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 또는 보관 기능 구현 |
| 전략적으로 임베딩 | 원시 대화가 아닌 요약 저장 |
세션과의 비교
| 기능 | 세션 상태 | 메모리 |
|---|---|---|
| 지속성 | 세션 수명 | 영구적 |
| 범위 | 단일 세션 | 세션 간 |
| 검색 | 키-값 조회 | 의미 검색 |
| 사용 사례 | 현재 컨텍스트 | 장기 기억 |