메모리
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 트레이트
메모리 백엔드를 위한 핵심 트레이트:
#[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?)이 됩니다:
- Global entries (
project_id = None): 모든 프로젝트 컨텍스트 및 전역 전용 검색에서 볼 수 있습니다. - Project entries (
project_id = Some(id)): 해당 특정 프로젝트 내에서 검색할 때만 볼 수 있습니다. - Project search (
project_id = Some(id)): 전역 항목 + 해당 프로젝트의 항목을 반환합니다. - Global search (
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 또는 아카이빙을 구현하세요 |
| 전략적으로 임베딩 | 원시 대화가 아닌 요약을 저장하세요 |
세션과의 비교
| 기능 | 세션 상태 | 메모리 |
|---|---|---|
| 지속성 | 세션 수명 | 영구적 |
| 범위 | 단일 세션 | 세션 간 |
| 검색 | 키-값 조회 | 의미론적 검색 |
| 사용 사례 | 현재 컨텍스트 | 장기 기억 |
이전: ← Guardrails | 다음: Studio →