深度分析:传统数据库设计假设与 agent 自主行为相冲突,需要重新思考系统架构。
engineering, databases, and systems. always building.
在你曾做过的每一个数据库架构设计的基础之上,都存在一个隐含的契约。你可能从来没有把它写下来。没人写过。它只是...存在了。
这个契约大概是这样的:调用者是由人类编写的应用,运行确定性代码,发出可预测的查询,在部署前经过开发者审查。写入是有意的。连接很短暂。当出问题时,人类会注意到。数据库可以相对简单而高效,因为应用层足够聪明和谨慎。
四十年来,这个契约一直有效。它影响了我们如何设计 schema、调整连接池大小、授予权限,以及思考故障模式的方式。它之所以有效,是因为这个假设是正确的。
它不再正确。AI 智能体系统同时在每一层违反了这个契约。
在本文中,我逐一分析哪些假设正在失效、为什么它们很重要,以及如何应对——附带具体的模式和代码。让我们开始吧…
在你使用智能体之前部署的任何应用中,到达数据库的查询都由人类编写。
开发者编写了 SQL
开发者对其进行了代码审查
开发者测试并部署了它。
这个假设如此根深蒂固,以至于工具会自动反映它:Postgres 查询规划器围绕观察到的查询模式构建统计信息,缓存层会在重复查询时预热,连接池则围绕已知复杂度的预期并发查询数调整。
AI 智能体工作方式不同;它们通过推理来确定查询。不同的推理路径会对同一个表产生不同的查询。
处理客户分析任务的 AI 智能体可能会发出一个跨越五个表的前所未有的 JOIN,在思考结果时保持连接打开,然后发出完全不同的后续查询。你的索引只覆盖了预期的查询路径。你的连接池是根据观察到的峰值大小调整的。当 AI 智能体可以根据需要构建任意查询时,这两者都不再适用。
语句超时是你的第一道防线。花费 30 秒的人类编写查询是一个 bug,某人会发现。耗时 30 秒的 AI 智能体查询可能只是一个没人关注的推理循环。
因此,要在角色级别设置超时,而不仅仅是应用级别。
CREATE ROLE agent_worker;
ALTER ROLE agent_worker SET statement_timeout = '5s';
ALTER ROLE agent_worker SET idle_in_transaction_session_timeout = '10s';
idle_in_transaction_session_timeout 特别重要。AI 智能体在推理中途暂停时可能会持有打开的事务,这是一个合理的场景。
数据库架构中最危险的假设是:每次写入发生前都经过了人类审查。这在你的整个职业生涯中基本都是真的,但现在不再是。
AI 智能体会自主地进行写操作。它们根据对任务的当前理解进行写入,而这种理解可能是错误的。当工具返回意外结果时,AI 智能体会在循环中进行写入。当瞬时网络错误使 AI 智能体"认为"首次尝试失败时,它们会在重试时进行写入。AI 智能体甚至可以在你收到 Slack 通知说出现问题的时间内写入数千行数据。
这是一个真实发生过的故障模式——AI 智能体调用遗留 API,收到 HTTP 200 以及空结果集。API 无声地失败了,因为下游数据库连接池已耗尽。AI 智能体将"无数据"理解为"没有问题",于是继续用不完整的数据处理 500 笔交易。没有异常被抛出。没有告警被触发。日志显示每条记录都是 "decision: approved"。
这里的核心修复是设计写入路径时,假定调用者可能出错、可能会重试、可能不会监控结果。
永远不要让 AI 智能体硬删除任何东西。对 AI 智能体可以写入的任何表都应该使用软删除作为基线
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE orders ADD COLUMN deleted_by TEXT; -- 'agent:customer-support-v2', 'user:abc123'
ALTER TABLE orders ADD COLUMN delete_reason TEXT;
-- Agents query this view; they never see deleted rows and can't accidentally undelete
CREATE VIEW active_orders AS
SELECT * FROM orders WHERE deleted_at IS NULL;
deleted_by 列比看起来要重要得多。当你调试两小时前发生的事情时,"显示 AI 智能体 X 删除的所有东西" 会是你想运行的查询。
对于风险更高的操作——财务记录、库存变化、用户状态修改——可以考虑更进一步,使表成为只追加的。AI 智能体从不发出 UPDATE 或 DELETE。它只发出带有新状态和原因说明的 INSERT:
CREATE TABLE order_state_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id),
previous_status TEXT,
new_status TEXT NOT NULL,
changed_by TEXT NOT NULL,
changed_at TIMESTAMPTZ DEFAULT now(),
reason TEXT,
idempotency_key TEXT UNIQUE
);
这是在表级别应用的事件溯源模式。为最敏感的实体使用单个只追加日志表,可以提供完整的审计跟踪,并让"撤销"变成一个投影查询。
AI 智能体进行重试,这是刻意设计的。每个编排框架都遵循至少一次交付的语义。如果某个步骤失败,它会再次运行。你的写入路径必须考虑这一点。
幂等性密钥是 AI 智能体在每次写入时包含的一个稳定标识符。数据库通过唯一约束在静默中拒绝重复。AI 智能体无论如何都会收到成功的响应。运行操作两次的结果与运行一次相同。
-- The agent generates this key from
-- task_id + operation_type + target_id
-- It is deterministic for the same logical
-- operation, so retries produce the same key
ALTER TABLE order_state_log
ADD CONSTRAINT uq_idempotency_key UNIQUE (idempotency_key);
实际上,AI 智能体像这样构造密钥:
import hashlib
def make_idempotency_key(task_id: str,
operation: str, target_id: str) -> str:
raw = f"{task_id}:{operation}:{target_id}"
return hashlib.sha256(raw.encode()).hexdigest()[:32]
任务 ID 来自编排层,在相同逻辑任务的重试中保持稳定。这意味着 AI 智能体可以根据需要进行任意次重试,而你的数据库每个逻辑操作只会看到一次写入。
传统的连接池大小调整遵循一个简单直观的心理模型。你的应用处理 N 个并发请求。每个请求在短时间内需要一个数据库连接。你将池大小设置为略高于预期的并发峰值,留一些余量,就完成了。
AI 智能体以三种方式打破了这个模型。
多步推理任务可能发出查询、暂停以使用 LLM 处理结果、再发出另一个查询、再次暂停,如此反复。每次暂停都会保持连接打开。每个任务的连接时间不再是 "查询执行时间"——而是 "查询执行时间 + LLM 推理时间 × 推理步骤"。
单个高级 AI 智能体任务通常会产生多个子 AI 智能体来并行工作。一个任务会变成五个并发的数据库会话。当并发的 AI 智能体工作流在长 IO 等待期间持有 db.session,直到 Postgres 耗尽连接槽时,这会导致连接耗尽。
在开发环境中,你有三个 AI 智能体。在生产环境中,你有三十个。没有人更新连接池配置。
解决方案是为 AI 智能体工作负载创建专用连接池,其大小应独立于面向用户的事务应用流量进行调整
# 经验法则:(num_agent_workers * avg_concurrent_steps * 0.5)
# 0.5 系数考虑了大多数 AI 智能体步骤涉及 LLM 时间而非数据库时间的事实
agent_engine = create_engine(
DATABASE_URL,
pool_size=10, # AI 智能体的基础连接池
max_overflow=5, # 额外容量
pool_timeout=3, # 快速失败而非排队
pool_recycle=300, # 每 5 分钟回收一次连接
pool_pre_ping=True, # 在使用前验证连接
connect_args={
"options": "-c statement_timeout=5000 -c idle_in_transaction_session_timeout=10000"
}
)
pool_timeout=3 是刻意设置的。当 AI 智能体在 3 秒内无法获得连接时,应该快速失败并进行指数退避重试,而不是无限期地排队等待。在饱和的连接池中排队的请求会导致级联故障。
对于运行多个并发 AI 智能体的系统,在 AI 智能体和 Postgres 之间添加 PgBouncer。PgBouncer 以事务池模式运行,这意味着它在每个事务完成后立即将连接返回到池中,而不是为整个会话保持连接。这大幅提升了您在 AI 智能体工作负载中的有效连接容量。
# pgbouncer.ini
[databases]
mydb = host=postgres_host dbname=mydb
[pgbouncer]
pool_mode = transaction # critical: release connection after each transaction
max_client_conn = 500 # clients (agents) can connect up to this number
default_pool_size = 20 # actual postgres connections (much smaller)
reserve_pool_size = 5 # emergency capacity
reserve_pool_timeout = 1.0 # fail fast if reserve is also exhausted
在事务池模式下,20 个实际的 Postgres 连接可以服务 500 个 AI 智能体连接,因为每个 AI 智能体仅在单个事务期间保持 Postgres 连接,而不是整个多步骤任务的持续时间。
在人工操作的系统中,缓慢或不正确的查询会迅速显现。仪表板加载缓慢。API 超时。工程师运行 EXPLAIN ANALYZE 并找到问题。反馈循环很紧密。
AI 智能体打破了这个反馈循环。获得缓慢查询结果的 AI 智能体只是使用该结果。获得空结果集的 AI 智能体不知道数据是否真的不存在,或者查询是否错误。它继续执行其任务,可能基于错误的读取做出决策。
这是与应用程序错误不同的一类故障。异常是可观察的。语义上错误但返回行的查询则不然。
缓解方法是在数据库访问层中构建 AI 智能体特定的可观测性。标准的慢查询日志是不够的。您需要知道哪个 AI 智能体、哪个任务和哪个推理步骤产生了查询。在 Postgres 中最实用的方法是查询注释。
from sqlalchemy import text, event
from sqlalchemy.engine import Engine
@event.listens_for(Engine, "before_cursor_execute")
def add_agent_context_comment(conn, cursor, statement, parameters, context, executemany):
agent_ctx = getattr(conn.info, "agent_context", None)
if agent_ctx:
statement = f"/* agent_id={agent_ctx['agent_id']}, task_id={agent_ctx['task_id']}, step={agent_ctx['step']} */ {statement}"
return statement, parameters
# Usage: set context on the connection before executing
with engine.connect() as conn:
conn.info["agent_context"] = {
"agent_id": "fulfillment-v3",
"task_id": "task-abc-123",
"step": "check-inventory"
}
conn.execute(text("SELECT ..."))
这些注释出现在 pg_stat_activity、pg_stat_statements 和您的慢查询日志中。在慢查询日志中标记为 agent_id=fulfillment-v3、task_id=task-abc-123、step=check-inventory 的查询可以立即采取行动。没有这个,您就在做考古工作。
构建一个监控视图,将按 AI 智能体分组的查询呈现出来:
-- pg_stat_statements with agent context extracted from query text
SELECT
(regexp_match(query, 'agent_id=([^,]+)'))[1] AS agent_id,
(regexp_match(query, 'task_id=([^,]+)'))[1] AS task_id,
count(*) AS call_count,
round(mean_exec_time::numeric, 2) AS avg_ms,
round(total_exec_time::numeric, 2) AS total_ms
FROM pg_stat_statements
WHERE query LIKE '%agent_id=%'
GROUP BY 1, 2
ORDER BY total_ms DESC;
当您看到单个 AI 智能体类型占总数据库时间的 60% 时,您就知道要查看哪里。
这是大多数团队在坏掉之前从不考虑的假设。您的 schema 是为开发者人体工程学设计的 - 命名为对工程师有意义,为查询便利而结构化,可空列"意味着某些东西"仅当您阅读原始迁移注释时。
当一个 AI 智能体可以看到您的 schema 时 - 通过 Text-to-SQL、通过工具定义、通过包装您的数据库的 MCP 服务器 - schema 就成为了与语言模型的契约。列名、表结构和可空性现在影响 LLM 是否生成正确的查询或听起来很有自信的胡言乱语。
考虑这两个列定义之间的区别:
-- What most schemas look like
CREATE TABLE orders (
id UUID PRIMARY KEY,
usr_id UUID, -- which user?
stat_cd INT, -- what does 2 mean? what does 7 mean?
flg_1 BOOLEAN, -- ???
upd_ts TIMESTAMPTZ -- updated at? but by whom?
);
-- What a schema legible to an agent looks like
CREATE TABLE orders (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers(id),
fulfillment_status TEXT NOT NULL CHECK (
fulfillment_status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')
),
requires_signature BOOLEAN NOT NULL DEFAULT false,
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
第二个 schema 几乎自动生成正确的 LLM 查询。第一个 schema 需要大量的提示工程来补偿本应在 schema 级别完成的工作。
对于您无法重命名的 schema(遗留系统、高迁移成本的表),构建一个面向 AI 智能体的视图层:
-- The raw table retains its legacy names
-- Agents query this view; they never touch the underlying table directly
CREATE VIEW agent_orders AS
SELECT
id,
usr_id AS customer_id,
CASE stat_cd
WHEN 1 THEN 'pending'
WHEN 2 THEN 'processing'
WHEN 5 THEN 'shipped'
WHEN 7 THEN 'delivered'
WHEN 9 THEN 'cancelled'
END AS fulfillment_status,
flg_1 AS requires_signature,
upd_ts AS last_modified_at
FROM orders
WHERE deleted_at IS NULL; -- agents only ever see active rows
编写列注释就像编写文档字符串一样 - 因为对于 Text-to-SQL AI 智能体,它们就是:
COMMENT ON COLUMN agent_orders.fulfillment_status IS
'Current state of the order in the fulfillment pipeline. '
'Use this to filter orders that need action: pending and processing orders are active. '
'Cancelled orders should never be modified.';
COMMENT ON COLUMN agent_orders.requires_signature IS
'True if the delivery requires an adult signature. '
'When true, the shipping agent must schedule a delivery window.';
还有一个故障模式值得单独处理,因为它贯穿了上述所有假设:行为不当的 AI 智能体的影响半径由它被授予的访问权限决定。
传统应用程序共享一个数据库角色,或最多为不同的服务拥有几个角色。假设是应用程序代码是护栏。如果代码只允许用户更新自己的记录,数据库角色无需强制执行 - 应用程序层处理了它。
AI 智能体使这个假设变得危险。推理进入不正确状态的 AI 智能体可能会发出应用程序开发者从未预期的查询。AI 智能体不是已知的、有限的代码路径集 - 它是一个具有数据库连接访问权限的通用推理器。应用程序层护栏不会像它们约束确定性代码那样约束它。
修复是按 AI 智能体类型的角色访问,具有在数据库级别定义的最小必需权限:
-- Each agent type gets its own role
CREATE ROLE agent_fulfillment;
CREATE ROLE agent_customer_support;
CREATE ROLE agent_analytics;
-- agent_analytics: read-only, only the tables it needs
GRANT SELECT ON agent_orders TO agent_analytics;
GRANT SELECT ON customers TO agent_analytics;
-- Explicitly: no access to payments, credentials, PII tables
-- agent_customer_support: can update order status, cannot touch financials
GRANT SELECT ON agent_orders TO agent_customer_support;
GRANT INSERT ON order_state_log TO agent_customer_support;
-- Does not have UPDATE on orders -- changes go through the event log
-- agent_fulfillment: can read and update shipping-related fields only
GRANT SELECT, UPDATE (fulfillment_status, shipped_at, tracking_number)
ON orders TO agent_fulfillment;
在访问设计审查中要问的问题不是"这个 AI 智能体需要什么?"而是"如果这个 AI 智能体的推理出错,或其凭证被泄露,最坏的情况是什么?"在数据库级别减少那个影响半径,它在那里无法被绕过。
综合这些,这就是一个已经内化这些故障模式的团队的数据层的样子。其中没有什么奇特的。所有这些都存在于经过实战测试的数据库工具中。
每个 AI 智能体类型都有自己的数据库角色,具有最小必需权限,在数据库级别通过角色级超时强制执行。AI 智能体通过专用连接池连接,大小针对 AI 智能体工作负载模式,与面向人类的流量分离。PgBouncer 在 AI 智能体和 Postgres 之间以事务池模式运行。
代理可以写入的表使用软删除,包含一个 deleted_by 列来捕获代理身份。高风险的写入路径使用仅附加的事件日志表,并带有幂等密钥约束。每次写入都携带一个代理 ID 和任务 ID,以便审计追踪始终可以追溯。
代理可以访问的模式对象以可读性命名,而不是为了遗留方便。维护的视图层将旧的列名转换为有意义的列名。列注释作为文档字符串编写。代理被授予对视图的访问权限,而不是直接访问底层表。
代理发出的每个查询都包含一条注释,含有代理 ID、任务 ID 和推理步骤。一个监控仪表板汇总这些数据,使得值班工程师能够实时看到"代理 X 在过去一小时内消耗了数据库时间的 40%"。
断路器被定义如下:每个任务的最大写入次数在编排层强制执行,每个语句影响的最大行数通过语句复杂性检查强制执行,最大任务持续时间通过杀死停滞代理会话的监控进程强制执行。
这些都不是新技术。软删除、仅附加日志、最小特权角色、行级安全性、幂等密钥、查询标记——这些模式已经存在多年了。代理强制的转变是这些模式从"我们一直想要实现的最佳实践"变为"承重基础设施"。代理不会给你推迟实施它们的奢侈。
数据库不是为这个调用方设计的。但使其安全的工具已经存在。
传统数据库架构建立在 AI 智能体工作负载系统地违反的假设上:确定性调用方、有意的写入、短连接、明显的故障和作为开发者合约的模式。
这些假设之所以成立,是因为人类总是在某个环节中。AI 智能体移除了这种保证。结果是,长期被视为可选最佳实践的模式——软删除、仅附加日志、幂等密钥、最小特权角色、查询标记——成为了承重基础设施。
这一切都不需要新技术。它需要将数据库视为一个防御层,假设调用方可能出错、可能重试,以及可能不会监看结果。
如果你觉得这有帮助和有趣,
在 HackerNews 上分享
订阅我的 RSS 源,在我发布新内容时立即获得通知。
Principal Engineer II at Razorpay - building Agent Studio,Google Cloud Memorystore & Dataproc 前 staff engineer,DiceDB 创建者,Amazon Fast Data 前员工,Unacademy 前工程总监。我通过 YouTube 上无废话的工程视频和我的课程来激发工程好奇心。
Applied AI Masterclass
System Design Masterclass
System Design for Beginners
Applied AI Masterclass
System Design Masterclass
System Design for Beginners
Arpit's Newsletter 由 145,000 名工程师阅读
每周关于真实系统设计、分布式系统或深入研究某些超聪明算法的文章。
本网站上列出的课程由
Relog Deeptech Pvt. Ltd. 203, Sagar Apartment, Camp Road, Mangilal Plot, Amravati, Maharashtra, 444602 GSTIN: 27AALCR5165R1ZF
提供
© Arpit Bhayani, 2025