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 podcast

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

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ị.

Nhận bài viết mới

Mình sẽ gửi email khi có bài mới. Không spam.