網誌
架構

發佈日期 更新日期

Arag 及 Anshu

AI 代理應用程式的數據庫設計模式

以 Markdown 閱讀

適合代理的數據庫結構描述會把任務、操作和審計事件與業務數據列分開記錄,利用約束條件執行租戶範圍和冪等性、協調並行 worker,並保留足夠的歷史,讓系統在逾時後復原,而不會把模型對話記錄當成證據。

數據庫無需理解提示;它需要令不安全的狀態轉換和重複副作用難以發生。

從任務和操作開始

任務是用戶層級的工作流程,操作則是該任務嘗試執行的一項外部或內部副作用。兩者應分開,因為一項任務可能多次重試同一操作,也可能在操作之間等待。

CREATE TABLE agent_tasks (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL,
  workflow_version text NOT NULL,
  state text NOT NULL,
  state_version bigint NOT NULL DEFAULT 0,
  current_step text,
  wake_at timestamptz,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (tenant_id, id)
);

CREATE TABLE agent_operations (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL,
  task_id uuid NOT NULL,
  idempotency_key text NOT NULL,
  action text NOT NULL,
  input_hash text NOT NULL,
  state text NOT NULL,
  provider_reference text,
  result jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (tenant_id, idempotency_key),
  FOREIGN KEY (tenant_id, task_id) REFERENCES agent_tasks (tenant_id, id)
);

複合外鍵可防止操作指向另一租戶的任務。唯一冪等鍵則防止兩個 worker 重複建立同一個邏輯操作。傳回現有結果前,請先比較已保存的輸入雜湊,避免同一個鍵在不知情的情況下授權不同參數。

使用約束條件執行模型無權協商的規則

在數據庫中執行唯一性、外鍵、不可為 null 的擁有權、合法 enum 值和版本檢查。應用程式驗證仍可改善錯誤訊息,但最終約束能保護每一個寫入者,包括遷移指令碼或新產生的工具。

就租戶隔離而言,應確保每張由租戶擁有的子數據表都能透過租戶範圍鍵連接。多租戶代理基礎設施指南涵蓋數據列、結構描述和數據庫隔離選項。

同時記錄目前狀態和只可附加的事件

目前狀態數據表令讀取和排程保持簡單;只可附加的事件數據表則解釋狀態如何改變。

在同一個 transaction 中寫入狀態更新和事件:

UPDATE agent_tasks
SET state = $1, state_version = state_version + 1, updated_at = now()
WHERE tenant_id = $2 AND id = $3 AND state_version = $4;

INSERT INTO agent_events
  (tenant_id, task_id, event_type, actor_id, policy_version, payload)
VALUES ($1, $2, $3, $4, $5, $6);

提交前,要求更新影響一個數據列。若沒有更新任何數據列,代表另一個 worker 已推進任務,過時的 worker 必須重新載入,而不能覆寫狀態。

不要在事件負載中放置密鑰、完整提示或大小不受限制的供應商回應。請儲存經遮蔽的事實或參照,並使用獨立的保留政策。

使用 outbox 可靠地傳送下游事件

當數據庫更新必須發佈事件或把工作加入佇列時,請在同一個 transaction 中寫入 outbox 數據列。Relay 傳送待處理的數據列,再把它標記為已傳送。如果 Relay 因遺失確認而重複發佈,消費者仍必須具備冪等性。

這可消除數據庫已提交、但程序在通知佇列前當機的缺口,也令操作具備可查詢的傳送狀態,無需依賴日誌文字。

使用 lease 或版本檢查協調 worker

短暫的狀態轉換應使用樂觀版本;worker 需要暫時擁有任務時則使用 lease。Lease 包含擁有者、取得時間和到期時間,在工作期間持續續期,並在完成時要求呼叫者是擁有者或持有較新的有效 lease。

不要在模型思考或等待供應商回應期間一直開啟數據庫 transaction。應先保留操作並提交、執行呼叫,再對賬結果和更新狀態。持久狀態指南說明這項設計如何支援全新的 worker。

讓 JSON 保持靈活,同時明確定義可查詢的事實

JSONB 適合儲存模型專用元數據、供應商負載片段,以及隨工作流程而異的步驟局部值。租戶擁有權、狀態、操作身份、排程、保留規則,以及政策或索引使用的欄位則應使用數據欄。

如果每次授權查詢或排程器都會使用某個 JSON 路徑,便應把它提升為具備類型的數據欄。數據庫 introspection 對程式開發代理有用,但簡單的結構描述並不等於權限;應把工具限制在已批准的陳述式或應用程式 API,而不是讓產生的 SQL 取得管理員連線。

連結追蹤數據,但不要把數據庫當成遙測儲存庫

在任務和操作中保存 trace 和 span 識別碼,詳細的模型與工具遙測則保留在可觀察性系統中。這樣,調查人員可從錯誤的業務數據列找到產生它的執行過程,而不會在 Postgres 重複儲存大型追蹤數據。

OpenTelemetry 的 GenAI 語意慣例定義了常見的代理和工具屬性。這套結構描述仍在演進,因此應固定 instrumentation 輸出的版本。

在重試和並行情況下驗證結構描述

讓兩個 worker 同時處理相同任務;使用相同和互相衝突的參數重用一個冪等鍵;在業務 transaction 提交後、佇列確認前令程序當機;在測試環境中刪除一個租戶,並確認其任務、操作、產物和憑證會遵循預定政策。

下方提示會把這些保證對應至目前的結構描述。它要求提供查詢路徑和約束條件,因為「我們在代理中處理」並不是數據庫的不變條件。