我给自己写了一个数据库巡检 agent:每天早上连一次业务数据库,检查数据有没有按时进来、有没有断档、数值是否越界、作业有没有失败、备份是不是太旧,然后让本地小模型给每个问题写“可能原因”和“建议排查”,出一份 HTML 报告。
它碰的是生产库,所以第一件要想清楚的事不是“它能发现什么”,而是“它不能做什么”。我给它设了三层护栏,越往外越硬。这篇讲每一层挡什么、为什么光靠代码不够。
先划定一条线:模型不写 SQL
很多“数据库 agent”的做法是让模型根据问题生成 SQL 再执行。巡检不需要这么做。我把检查做成固定的几种类型:数据新鲜度、时间断档、数值范围、空值比例、增长量、作业失败、备份时间,外加一个人工写好的自定义查询。每种类型的 SQL 由代码按模板生成,配置文件里只填表名、列名和阈值。
模型只看到查询结果和规则判出来的问题,从头到尾接触不到数据库连接。这一步把最大的风险面直接拿掉了,后面三层护栏防的是我自己写配置时手滑。
第一层:名称校验,不拼接任意字符串
表名和列名没法用参数化查询传,只能拼进 SQL。所以配置里的每个名称都先过一遍白名单式校验,合格的才加上方括号拼进去;阈值、时间这类值一律走参数。
import re
_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$")
def ident(name: str) -> str:
"""dbo.Table 或 Table → [dbo].[Table];不合格直接报错"""
parts = name.split(".")
if not 1 <= len(parts) <= 3 or not all(_IDENT.match(p) for p in parts):
raise ValueError(f"非法名称: {name!r}")
return ".".join(f"[{p}]" for p in parts)
sql = f"SELECT MAX({ident(col)}) FROM {ident(table)}"
示例里的规则故意定得很窄:只允许字母、数字、下划线。中文表名、带空格的名称会被拒,遇到了再单独放开,而不是一开始就把口子开大。
第二层:语句闸门,只放行单条查询
自定义查询是人写的 SQL,所以执行前还要过一道闸门:只允许一条语句,并且以 SELECT 或 WITH 开头。写这一层时我踩到两个容易忽略的点:
SELECT开头也可能写数据。SELECT ... INTO 新表会建表并写入。WITH开头不代表只读。在 SQL Server 里,WITH cte AS (...) DELETE FROM cte是合法语句,前面的 CTE 只是铺垫。
所以闸门除了看开头,还要去掉注释和字符串之后检查有没有分号分隔的第二条语句,以及有没有写入类关键字(示意):
_WRITE = re.compile(
r"\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|"
r"EXEC|EXECUTE|GRANT|REVOKE|INTO|BACKUP|RESTORE|DBCC|SHUTDOWN)\b", re.I)
def gate(sql: str) -> str:
s = strip_comments_and_strings(sql).strip().rstrip(";")
if ";" in s:
raise ValueError("只允许单条语句")
if not re.match(r"^(SELECT|WITH)\b", s, re.I):
raise ValueError("只允许 SELECT / WITH 开头")
if _WRITE.search(s):
raise ValueError("包含写入或执行类关键字")
return sql
这个闸门会误伤:列名里刚好叫 Create_Time 不受影响(有单词边界),但字段叫 Into 就会被拒。我接受这种误伤,宁可拒错,不可放错。
第三层:账号权限,真正的保险
前两层都是我写的代码,代码就会有漏洞:关键字清单总会漏掉什么,正则总有没想到的写法。所以真正兜底的是数据库自己:
- 专用账号,只给
db_datareader。即使前两层全部失守,数据库也会拒绝任何写入。 - 需要查作业和备份历史时,只给 msdb 里那几张历史表的 SELECT 权限,不给整库。
- 连接串带上
ApplicationIntent=ReadOnly。如果库有可读副本,查询会被路由到副本上,不打扰主库。 - 设置查询超时。只读也可能出事:一条全表扫描跑在业务高峰,照样能把库拖慢。所以巡检安排在早上业务量低的时候跑,并且应该给每条查询设上超时。
我的总结是:代码护栏防手滑,账号权限防万一。如果只能做一层,做第三层。
上线前的两个习惯
- 先只检查配置,不执行。巡检程序有一个
--check模式,只校验配置、不跑真正的检查。名称写错、查询过不了闸门,在这一步就能发现。 - 需要找人帮忙时,导出脱敏案例。表名列名换成 T1、C1,检查编号换成 K1,对照表只留在本机。这样可以把“报告哪里不对”拿出去讨论,而不带出任何业务信息。
坦白说一句:这套东西目前只在模拟连接上跑通了测试,还没接过真实的库。正因为如此,第三层的账号权限我打算在第一次连接之前就配好,而不是等出了问题再补。
小结
让 agent 只读,靠的不是在提示词里写“请不要修改数据”,而是让它在结构上做不到:模型不碰连接,名称有校验,语句有闸门,账号没有写权限。至于模型写出来的说明怎么检查,是另一半的问题,见《小模型写的说明,怎么放心放进报告》。
