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.

5 phút đọc
Tập này đang được chuẩn bị, quay lại sau nhé.

flare-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