flare-agent Bài 4: Memory — KV, D1 và Vectorize Adapters
Xây dựng @flare-agent/memory — abstract hóa Cloudflare KV, D1, Vectorize về cùng interface để agent có short-term memory, long-term storage và RAG capability.
Nghe bài viết này dưới dạng podcastflare-agent Bài 4: Memory — KV, D1 và Vectorize Adapters
Series: Build Your Own AI Agent Framework trên Cloudflare Bài: 4 / 7 —
@flare-agent/memory
Ba tầng memory
Một agent cần nhiều loại memory khác nhau:
| Tầng | Storage | Dùng cho | TTL |
|---|---|---|---|
| Short-term | KV | Conversation history trong session | 24h |
| Long-term | D1 (SQLite) | User data, progress, structured facts | Vĩnh viễn |
| Semantic | Vectorize | RAG — tìm kiếm theo nghĩa | Vĩnh viễn |
Cấu trúc package
packages/memory/
src/
adapters/
kv.ts # KVMemoryAdapter
d1.ts # D1MemoryAdapter
vectorize.ts # VectorizeAdapter
noop.ts # NoopAdapter (stateless/testing)
MemoryManager.ts # Combine adapters
index.ts
migrations/
001_init.sql # D1 schema
KV Adapter — Short-term memory
// src/adapters/kv.ts
import type { MemoryAdapter, Message } from '@flare-agent/types';
const SESSION_TTL = 60 * 60 * 24; // 24 giờ
const MAX_MESSAGES = 50; // giới hạn để không overflow context window
export class KVMemoryAdapter implements MemoryAdapter {
constructor(private kv: KVNamespace) {}
async getMessages(sessionId: string): Promise<Message[]> {
const raw = await this.kv.get(`session:${sessionId}`);
if (!raw) return [];
return JSON.parse(raw) as Message[];
}
async addMessage(sessionId: string, message: Message): Promise<void> {
const messages = await this.getMessages(sessionId);
messages.push(message);
// Trim nếu quá dài — giữ lại N messages gần nhất
const trimmed = messages.slice(-MAX_MESSAGES);
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(trimmed),
{ expirationTtl: SESSION_TTL }
);
}
async clearSession(sessionId: string): Promise<void> {
await this.kv.delete(`session:${sessionId}`);
}
}
D1 Adapter — Long-term memory
Migration SQL
-- migrations/001_init.sql
CREATE TABLE IF NOT EXISTS agent_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
tool_call_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_session_messages
ON agent_messages(session_id, created_at);
CREATE TABLE IF NOT EXISTS agent_sessions (
id TEXT PRIMARY KEY,
user_id TEXT,
metadata TEXT, -- JSON
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
D1 Adapter
// src/adapters/d1.ts
import type { MemoryAdapter, Message } from '@flare-agent/types';
export class D1MemoryAdapter implements MemoryAdapter {
constructor(private db: D1Database) {}
async getMessages(sessionId: string): Promise<Message[]> {
const { results } = await this.db
.prepare(
`SELECT role, content, tool_call_id
FROM agent_messages
WHERE session_id = ?
ORDER BY created_at ASC`
)
.bind(sessionId)
.all<{ role: string; content: string; tool_call_id: string | null }>();
return results.map((row) => ({
role: row.role as Message['role'],
content: row.content,
...(row.tool_call_id && { toolCallId: row.tool_call_id }),
}));
}
async addMessage(sessionId: string, message: Message): Promise<void> {
await this.db
.prepare(
`INSERT INTO agent_messages (session_id, role, content, tool_call_id)
VALUES (?, ?, ?, ?)`
)
.bind(
sessionId,
message.role,
message.content,
message.toolCallId ?? null
)
.run();
}
async clearSession(sessionId: string): Promise<void> {
await this.db
.prepare('DELETE FROM agent_messages WHERE session_id = ?')
.bind(sessionId)
.run();
}
}
Vectorize Adapter — Semantic memory (RAG)
// src/adapters/vectorize.ts
import type { VectorAdapter, VectorItem, VectorMatch } from '@flare-agent/types';
export class VectorizeAdapter implements VectorAdapter {
constructor(
private vectorize: VectorizeIndex,
private ai: Ai // dùng Workers AI để tạo embeddings
) {}
// Tạo embedding từ text
private async embed(text: string): Promise<number[]> {
const response = await this.ai.run(
'@cf/baai/bge-base-en-v1.5',
{ text: [text] }
) as any;
return response.data[0];
}
async insert(items: VectorItem[]): Promise<void> {
await this.vectorize.insert(
items.map((item) => ({
id: item.id,
values: item.values,
metadata: item.metadata,
}))
);
}
// Insert với auto-embed từ text
async insertText(
id: string,
text: string,
metadata?: Record<string, unknown>
): Promise<void> {
const values = await this.embed(text);
await this.insert([{ id, values, metadata: { ...metadata, text } }]);
}
async query(vector: number[], topK = 5): Promise<VectorMatch[]> {
const results = await this.vectorize.query(vector, {
topK,
returnMetadata: true,
});
return results.matches.map((m) => ({
id: m.id,
score: m.score,
metadata: m.metadata,
}));
}
// Query bằng text — tự động embed
async queryByText(text: string, topK = 5): Promise<VectorMatch[]> {
const vector = await this.embed(text);
return this.query(vector, topK);
}
async delete(ids: string[]): Promise<void> {
await this.vectorize.deleteByIds(ids);
}
}
Memory Manager — Combine tất cả
// src/MemoryManager.ts
import type { Message } from '@flare-agent/types';
import type { KVMemoryAdapter } from './adapters/kv';
import type { D1MemoryAdapter } from './adapters/d1';
import type { VectorizeAdapter } from './adapters/vectorize';
interface MemoryManagerConfig {
shortTerm?: KVMemoryAdapter;
longTerm?: D1MemoryAdapter;
vector?: VectorizeAdapter;
}
export class MemoryManager {
constructor(private config: MemoryManagerConfig) {}
// Conversation history — ưu tiên long-term nếu có, fallback KV
async getMessages(sessionId: string): Promise<Message[]> {
if (this.config.longTerm) {
return this.config.longTerm.getMessages(sessionId);
}
if (this.config.shortTerm) {
return this.config.shortTerm.getMessages(sessionId);
}
return [];
}
async addMessage(sessionId: string, message: Message): Promise<void> {
// Lưu vào cả hai nếu có — KV cho fast access, D1 cho persist
await Promise.all([
this.config.shortTerm?.addMessage(sessionId, message),
this.config.longTerm?.addMessage(sessionId, message),
]);
}
// RAG: lưu knowledge vào vector store
async remember(text: string, metadata?: Record<string, unknown>): Promise<void> {
await this.config.vector?.insertText(
crypto.randomUUID(),
text,
metadata
);
}
// RAG: recall relevant context
async recall(query: string, topK = 5): Promise<string[]> {
if (!this.config.vector) return [];
const matches = await this.config.vector.queryByText(query, topK);
return matches
.map((m) => m.metadata?.text as string)
.filter(Boolean);
}
}
Sử dụng
// Trong Worker
const memory = new MemoryManager({
shortTerm: new KVMemoryAdapter(env.KV),
longTerm: new D1MemoryAdapter(env.DB),
vector: new VectorizeAdapter(env.VECTORIZE, env.AI),
});
// Lấy messages
const history = await memory.getMessages(sessionId);
// Thêm message
await memory.addMessage(sessionId, { role: 'user', content: 'Hello' });
// RAG
await memory.remember('Người dùng thích học từ vựng về business', { userId });
const context = await memory.recall('business vocabulary');
Checklist
- Tạo
packages/memory/ - Chạy migration:
wrangler d1 execute DB --file=migrations/001_init.sql - Tạo KV namespace:
wrangler kv namespace create AGENT_KV - Test KVAdapter với Miniflare locally
Bài tiếp theo: Bài 5 — Core: Agent Loop & Tool Registry
Chưa có bình luận
Để lại bình luận
Bình luận sẽ được phê duyệt trước khi hiển thị.