AI代理开始直接读取数据库
MCP(Model Context Protocol)是一项开放标准,旨在统一AI工具连接外部数据和系统的方式。Anthropic于2024年11月25日发布了MCP,并于2025年12月9日将其捐赠给Linux Foundation旗下的Agentic AI Foundation。接入面向数据库的MCP服务器后,AI代理可以查询表列表和列,并直接执行SQL。
这带来了设计视角上的变化。人类开发者可以查阅公司内部Wiki或询问同事,但代理看到的大多是数据库目录(catalog)——表名、列名与类型、约束、注释。只有让模式本身成为文档,代理才不会连接错误的表或弄错单位。
本文以PostgreSQL为基准,整理五条对人有益、对代理更为重要的原则。
1. 名称不要缩写,按含义命名
代理会靠猜测来理解usr_amt_cd这样的缩写。而猜测可能出错。
-- 应避免的命名:缩写、没有单位、无意义的标志
CREATE TABLE ord (
id BIGINT PRIMARY KEY,
usr BIGINT,
amt NUMERIC,
flg CHAR(1),
dt TIMESTAMP
);
-- 推荐:含义、单位、时区体现在名称和类型中
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL,
amount_krw NUMERIC(12,0) NOT NULL,
is_refunded BOOLEAN NOT NULL DEFAULT false,
ordered_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
- 把单位和货币写进名称:
amount_krw、duration_ms、weight_g - 布尔值以
is_、has_开头:用is_refunded代替flg - 同一概念在所有表中使用同一名称:如果有的地方叫
user_id,有的地方叫member_no,代理就无法知道两者是否是同一个东西 - 时间使用
TIMESTAMPTZ:没有时区的值,无论是人还是代理,理解都会出现分歧
2. 用COMMENT ON把含义留在目录中
PostgreSQL的COMMENT ON会把说明存储在目录(pg_description)中。与代码注释或Wiki不同,它随数据库一起存在,因此查询模式的工具可以通过同一途径一并读取。
COMMENT ON TABLE orders IS '客户订单。在支付完成时创建,即使取消也不删除行(以is_refunded标记)。';
COMMENT ON COLUMN orders.amount_krw IS '支付金额(韩元,含增值税)。折扣后的金额。';
COMMENT ON COLUMN orders.is_refunded IS '是否全额退款。部分退款请参见refunds表。';
存储的说明可以这样查询。
SELECT a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS data_type,
col_description(a.attrelid, a.attnum) AS description
FROM pg_attribute a
WHERE a.attrelid = 'orders'::regclass
AND a.attnum > 0
AND NOT a.attisdropped;
应该写些什么呢?
- 值的单位和包含范围(是否含增值税、折扣前还是折扣后)
- 状态值的含义及其转换规则
- 是否为软删除,以及"真正有效的行"的条件
- 与其他表的关系中需要注意的地方(如部分退款在另一张表中等)
3. 用约束表达规则——代理看不到应用代码
如果"该列的值只能是pending、paid、shipped之一"这条规则只存在于应用代码中,那么对代理来说,这条规则并不存在。用约束声明后,它可以从目录中读取,而且即使有人试图写入错误的值,数据库也会拒绝,成为最后一道防线。
CREATE TABLE customers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE shipments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders (id),
status TEXT NOT NULL
CHECK (status IN ('pending', 'paid', 'shipped', 'delivered')),
shipped_at TIMESTAMPTZ,
CHECK (status <> 'shipped' OR shipped_at IS NOT NULL)
);
- 明确声明外键,代理就不必猜测JOIN路径,而可以从目录中确认
- CHECK能体现允许的值以及列与列之间的规则
- NOT NULL、UNIQUE传达"始终存在""只有一个"这样的事实
4. 用视图整理要呈现给代理的部分
如果让代理每次都从原始表计算"月销售额",那么每次提问时,是否包含退款、如何处理时区都可能不一样。把达成共识的定义固定为一个视图并附上说明,代理只需使用这个视图即可。
CREATE VIEW monthly_revenue AS
SELECT date_trunc('month', ordered_at AT TIME ZONE 'Asia/Seoul') AS month_kst,
sum(amount_krw) AS revenue_krw,
count(*) AS order_count
FROM orders
WHERE NOT is_refunded
GROUP BY 1;
COMMENT ON VIEW monthly_revenue IS '月销售额(韩元)。按韩国时间划分月份,不含全额退款的订单。';
5. 权限最小化——边界靠GRANT,其余只是辅助手段
为代理使用的账号创建单独的角色,只授予所需模式的读取权限。
CREATE ROLE ai_reader LOGIN; -- 密码和认证方式按运营环境的策略设置
GRANT USAGE ON SCHEMA analytics TO ai_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO ai_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO ai_reader;
-- 用于防止失误的默认值(并非安全边界)
ALTER ROLE ai_reader SET default_transaction_read_only = on;
ALTER ROLE ai_reader SET statement_timeout = '15s';
有一点需要注意。default_transaction_read_only和statement_timeout是用户可以在会话中重新更改的设置。它们能减少误发写入查询、繁重查询长时间运行的情况,但并不能阻止这些情况。真正的边界是通过GRANT授予的权限。
- 个人信息列最好直接从代理专用模式的视图中排除,这样更安全
- 如果需要按行划分范围,请使用
ALTER TABLE ... ENABLE ROW LEVEL SECURITY和策略(CREATE POLICY) - 不连接生产环境的主库,而是连接只读副本(备库),可以减轻繁重的分析查询对服务的影响
使用Erdmates设计
在Erdmates中为表和列写好说明后,导出为PostgreSQL DDL时会一并生成COMMENT ON TABLE、COMMENT ON COLUMN语句(MySQL则为表的COMMENT)。反过来,导入现有数据库的SQL时,也会读取COMMENT ON语句,将其还原为ERD中的说明。在ERD中通过连线确认外键关系的同时打磨好名称和说明,人和代理看到的就是同一份文档。
总结
在代理直接操作数据库的时代,模式设计并不是什么新技术,而是更严格地遵守老原则。体现含义的名称、留在目录中的说明、以约束声明的规则、承载共识定义的视图、最小权限。具备这五点的模式能让代理少犯错,即使犯了错,数据库也会将其拦下。