ブログ
アーキテクチャ

公開日: 更新日

AragとAnshu

AIエージェント・アプリケーションのデータベース設計パターン

Markdownで読む

エージェント対応のデータベーススキーマは、タスク、操作、監査イベントをビジネス上のrowと分けて記録します。constraintでテナントscopeと冪等性を強制し、並行workerを調整し、モデルのtranscriptを証拠として扱わずタイムアウト後に復旧できるだけの履歴を保持します。

データベースがプロンプトを理解する必要はありません。危険な状態遷移や効果の重複が起きにくい構造にする必要があります。

タスクと操作から始める

タスクはユーザー単位のワークフローです。操作は、そのタスクが試みた一つの外部または内部の効果です。一つのタスクが同じ操作を数回再試行したり、操作間で待機したりするため、両者を分けます。

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)
);

複合foreign keyにより、操作が別テナントのタスクを参照することを防ぎます。一意の冪等性キーにより、二つのworkerが同じ論理操作を二重作成することを防ぎます。既存の結果を返す前に保存済みinput hashを比較し、同じキーで異なるparameterが暗黙に認可されないようにします。

モデルと交渉してはいけないルールにはconstraintを使う

一意性、foreign key、非nullの所有権、有効なenum値、version checkをデータベースで強制します。アプリケーション側の検証もエラー改善に役立ちますが、最終constraintはmigration scriptや新しく生成されたツールを含むすべてのwriterを保護します。

テナント分離では、テナント所有のすべての子tableをテナントscope付きkeyからjoinできるようにします。マルチテナントのエージェントインフラ・ガイドでは、row、schema、database単位の分離を解説しています。

現在状態とappend-only eventを記録する

現在状態のtableは読み取りとscheduleを簡単にし、append-only event tableは状態が変化した過程を説明します。

状態更新とeventを一つの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);

commit前にupdateが1行へ作用したことを確認します。0行なら、別のworkerがすでにタスクを進めています。古いworkerは上書きせず、再読込する必要があります。

event payloadにシークレット、完全なプロンプト、無制限のプロバイダーresponseを含めないでください。redact済みの事実または参照を、別の保持ポリシーとともに保存します。

確実な下流配信のためにoutboxを追加する

データベース更新からイベント公開または作業enqueueが必要な場合、同じtransactionでoutbox rowを書き込みます。relayが保留中のrowを送信し、配信済みにします。確認を受け取れずrelayが2回publishする可能性があるため、consumer側にも冪等性が必要です。

これにより、データベースがcommitした後、queueへ通知する前にプロセスがクラッシュする隙間を閉じます。また、ログ行へ依存せず、照会可能な配信状態を操作へ提供できます。

leaseまたはversion checkでworkerを調整する

短い状態遷移にはoptimistic versionを使います。workerがタスクを一時的に所有する必要がある場合はleaseを使います。leaseにはowner、取得時刻、失効期限を含めます。作業中は更新し、完了にはownerまたは新しい有効なleaseを必要とします。

モデルの思考中やプロバイダーの応答待ちに、データベースtransactionを開いたままにしてはいけません。操作を予約してcommitし、呼び出しを行い、照合して更新します。永続状態ガイドでは、これが新しいworkerによる再開を支える仕組みを説明しています。

JSONを柔軟に使い、照会する事実は明示する

JSONBは、モデル固有metadata、プロバイダーpayloadの一部、ワークフローごとに異なる手順ローカルの値に適しています。columnはテナント所有権、状態、操作ID、schedule、保持、ポリシーやindexで使うフィールドに適しています。

すべての認可queryやschedulerに現れるJSON pathは、型付きcolumnへ昇格させます。データベースのintrospectionはコーディングエージェントに役立ちますが、スキーマが単純でも権限にはなりません。生成SQLへ管理者接続を渡すのではなく、ツールを承認済みstatementまたはアプリケーションAPIへ制限します。

データベースをテレメトリー保存先にせず、トレースを結び付ける

タスクと操作にtrace IDとspan IDを保存します。詳細なモデルとツールのテレメトリーはobservability systemに置きます。これにより、大きなトレースをPostgresへ複製せず、問題のあるビジネスrowから作成元の実行へ移動できます。

OpenTelemetryのGenAIセマンティック規約は、一般的なエージェントとツールのattributeを定義しています。このschemaは進化するものとして扱い、instrumentationが出力するversionを固定します。

再試行と並行実行に対してスキーマを検証する

二つのworkerで同じタスクを実行します。同じparameterと競合するparameterで冪等性キーを再利用します。ビジネスtransactionのcommit後、queueのacknowledgement前にクラッシュさせます。テスト環境でテナントを削除し、所有するタスク、操作、成果物、認証情報が意図したポリシーに従うことを確認します。

以下のプロンプトは、これらの保証を現在のスキーマへ対応付けます。「エージェント側で処理している」はデータベースの不変条件ではないため、query経路とconstraintを求めます。