☰
Easy Data x AI:Agent安全操作数据库的中间层架构与实践
2026/10/15 11:01:00 网站建设 项目流程

去年年初我们团队在做一个 Agent 客服项目,模型对答如流,业务方很高兴,但在演示“帮我把上周的订单金额汇总一下”这个需求时,没有一个人敢把生成好的 SQL 真扔进生产库去跑。原因很简单:模型写出来的 SQL 语法看着没问题,可它一旦猜错了字段名,或者多了一个不带条件的 DELETE,后果就不是“演示翻车”这么简单了。

后来我花了两个星期,把“让 Agent 用数据库”这件事做成了一层很薄的中间服务,命名为Easy Data x AI。它的目标很直接:让 Agent 能安全、稳定地通过自然语言操作数据库,包括查询、受控变更、数据分析,而不是只停留在“会写 SQL 但没人敢让它执行”的尴尬状态。这篇文章我会把这套方案的架构思路、关键实现、并发与安全设计,以及我在实测中踩过的坑完整拆开讲,适合正在做 Agent 应用、想给模型接入数据库的开发者,也适合数据分析师了解 AI 查数背后的工程约束。

1. Agent 和数据库之间到底差在哪

1.1 模型会写 SQL,但你不敢让它直接执行

现在主流大模型写 SQL 的能力已经相当能打,给它一个清晰的表结构,它能在多数场景下生成正确的查询语句。但“正确”和“可执行”是两回事。我在调研阶段做过一轮压力测试:用同一个模型连接同一个测试库,让它完成 20 个查询任务,表名、字段名全部写在系统提示词里,结果仍然有 3 个 SQL 引用了不存在的字段,2 个查询没有限定 LIMIT,还有一个聚合查询直接把JOIN写反了。这些错误在静态检查里很难发现,一旦直接执行,轻则查询报错,重则把两张关联表搞成笛卡尔积,几分钟内把数据库负载拉高。

更现实的风险是,Agent 并不是只生成一次 SQL。它在对话中可能根据用户的追问反复改写语句,如果每一次改写都直接落库,那么出错的概率会叠加。到这一步我意识到,必须把“SQL 生成”和“SQL 执行”彻底拆开,在中间加一层可控的代理逻辑,而不是让 Agent 拿着连接串直接怼数据库。

1.2 你以为的限制条件,模型其实看不到

很多人觉得只要在提示词里写一句“你只能执行 SELECT”,模型就会遵守。实测下来不可靠。模型对权限没有感知,它不知道当前连接的数据库账号只有只读权限,也不知道某些表属于敏感业务数据。就算模型本身不怀好意,它也可能在生成增量查询时顺手把整张表的数据拉出来,这在数据量大的表上会直接造成慢查询。

更隐蔽的问题出在上下文上。对话轮次一长,模型会遗忘表结构的细节,尤其是当数据库有几十张表、每张表有几十个字段时,系统提示词里的结构说明很容易被后面的对话内容淹没。一旦模型开始“猜字段”,生成出来的 SQL 就会在语法正确的前提下查询一个完全不存在的结果集。所以“Agent 直连数据库”这条路,从一开始就存在先天的可维护性问题,必须在中间加工程约束。

1.3 中间层要解决的不只是安全问题

既然不能直连,那中间层到底要做什么?我的设计定位是三层职责:第一,把数据库的表结构、字段语义、约束条件转换成 Agent 容易理解的形式,降低幻觉率;第二,拦截所有不安全的 SQL,包括非查询语句、无 WHERE 条件的更新删除、跨表全量扫描;第三,提供可观测性,让每一次数据库操作都能被记录和审计。简单说,就是让 Agent 变成数据库的一个“谨慎的临时工”,而不是一个拥有 root 权限的管理员。

2. Easy Data x AI 的整体设计:把数据库能力工具化、服务化

2.1 两条技术路线,我为什么选了服务化

接入方式上一般有两种选择。第一种是让 Agent 框架直接加载数据库驱动,在 Agent 进程内执行 SQL。这种方式实现最快,适合本地小工具,但问题是 Agent 进程通常是无状态的,扩缩容频繁,连接管理很麻烦,更别提在上下文里塞连接串带来的安全隐患。第二种是把数据库能力封装成一个独立的数据服务,通过 function calling 或 HTTP 接口暴露给 Agent。我选了后者。

服务化带来的第一个好处是连接管理可以收敛。Agent 不需要知道数据库地址和账号,它只知道自己有几个“工具”可以调用,比如query_database、execute_change。这些工具统一走数据服务层,连接池、超时、审计全部在服务层完成。第二个好处是安全策略可以集中配置,新增一个数据库或者调整某个库的权限,不用改 Agent 代码。第三个好处是便于并发控制,当多个 Agent 同时访问数据库时,可以在服务层统一限流。

2.2 最小可用架构拆解

整个链路分为四层:

  • Agent 层:负责理解用户意图,维护对话状态,决定何时调用数据库工具
  • Function 层:Agent 框架里的工具函数定义,描述每个工具能做什么、参数是什么
  • 数据服务层:核心执行逻辑,负责参数校验、SQL 重写、执行、格式化结果
  • 数据库层:真正的数据存储,可以是一体化实例,也可以是读写分离架构

我用一个最小的 Python FastAPI 服务来承载数据服务层,内部模块包括:元数据缓存、SQL 校验器、执行器、审计日志器。Agent 层用的是通用框架,只在提示词里配置好工具说明。

2.3 数据源注册与元数据注入

这一步是整个方案里最容易忽略但回报最高的部分。Agent 生成 SQL 的质量,很大程度上取决于它是否真正“了解”表结构。我不会直接把数据库表结构清单一股脑丢给模型,而是做一层元数据语义化:除了字段名和类型,还会加入字段的业务含义、取值范围、枚举值、常用查询模式。

举个例子,订单表里的status字段,如果只告诉模型它是string类型,模型可能会触发ORDER BY status之类的操作。但如果我在元数据里补充说明:“status 是订单状态,取值范围 pending / paid / shipped / cancelled,常用筛选条件”,模型生成的 WHERE 条件就会准确得多。这个元数据会被缓存在数据服务层,定期刷新,Agent 每次需要查询时通过工具参数传入需要关注的表名。

2.4 权限模型:只读查询、受控变更、危险操作隔离

数据服务层必须要有比数据库账号更细粒度的权限控制。我的做法是把操作分成三类:

  • 只读查询:SELECT、SHOW、DESCRIBE,默认放行,但强制加 LIMIT
  • 受控变更:INSERT、UPDATE、DELETE,必须有明确条件且经过二次确认
  • 危险操作:DROP、ALTER、TRUNCATE,默认禁止,需要审批流程手动放行

这三类操作在数据服务层用不同的函数暴露给 Agent。Agent 可以自由调用查询工具,但变更类工具调用后不会立即执行,而是返回一个“变更预览”,等用户确认后才真正落库。这个设计让 Agent 在演示场景下可以安全地展示能力,而真正发生数据变更时仍由人来兜底。

3. 自然语言到 SQL 的完整链路:从生成、校验到执行

3.1 给 Agent 一份“数据库说明书”而不是裸表结构

系统提示词里我维护了一段结构化说明,包含数据库的用途、每张表的业务含义、关键字段的枚举值,以及几条常见查询的示例。这段说明不能太长,否则会挤压对话上下文,所以要挑最重要的内容。我的做法是分两个层级:一个全局摘要,包含数据库所有表的一句话介绍;一个按需详情,只有当 Agent 需要操作某张表时,才通过工具调用获取该表的完整字段信息。

这样一个好处是 Agent 不会在对话一开始就被几十张表的结构淹没,而是按需加载,生成 SQL 时能聚焦在当前业务上下文里。还有一个细节:字段注释尽可能写得像人话。比如created_at注释是“订单创建时间,格式为本地时区”,而不仅仅是“timestamp”。模型对这类语义化描述的吸收效果远好于纯字段名。

3.2 SQL 校验层:规则拦截和 AST 校验双重保险

校验层是我花时间最多的地方。第一层是规则拦截,用正则和关键字列表把明显危险的语句直接挡掉。比如检测到DROP、TRUNCATE、ALTER等关键字,直接返回“操作不允许”;检测到没有 WHERE 条件的UPDATE或DELETE,直接拒绝。这一层不能解决所有问题,但能挡住最容易出事故的类别。

第二层是 AST 校验。我把生成的 SQL 用解析器解析成语法树,然后判断语法树的类型:如果是 SELECT,检查是否包含 LIMIT,如果没有就用代码强制追加;如果是 UPDATE 或 DELETE,检查 WHERE 子句是否存在且不是恒真条件,比如WHERE 1=1。AST 校验还能识别出嵌套子查询、多语句拼接等问题。规则拦截只负责粗筛,AST 校验负责精筛。

提示:SQL 解析器本身也可能有解析不了的方言,建议在服务里加一个“解析失败则拒绝执行”的兜底逻辑,不要默认放行。

3.3 执行层设计:超时、LIMIT 强制、结果格式化

即使 SQL 通过了校验,执行时也必须有硬性保护。我强制所有查询类 SQL 的返回行数上限,默认 100 行,防止 Agent 把几百万行数据直接拉到内存里。超时设置默认 5 秒,超过就终止查询并返回“查询超时,请简化条件”。这个配置看起来简单,实际在慢查询上能省掉很多事。

结果格式化也很重要。模型返回的是对话文本,但数据库查询结果是一条条记录,如果不做格式化,模型可能会自己编造统计结果。我的做法是:数据服务层把查询结果转成紧凑的 JSON 数组,同时附上字段名和行数统计,然后作为工具调用的返回值交回 Agent。Agent 再基于这些结构化结果生成文本回答。这样至少能保证模型看到的数字是真实查询来的,而不是它自己猜的。

3.4 兜底与纠错机制:SQL 错了怎么办

再好的校验也不可能 100% 发现语义错误。所以我在执行层加了“失败反馈”机制:当 SQL 执行报错或结果明显为空时,数据服务层会把错误信息和修正建议返回给 Agent,让它在下一轮生成时自动修正。

比如明明筛选时间字段用的是date类型,但模型写成了字符串比较导致报错,服务层返回的反馈会包含“字段类型不匹配,请检查字段名和类型”。Agent 看到反馈后会自动改写 SQL。实测中,这个循环能让 SQL 的最终通过率从 70% 提升到 95% 左右。不过也要注意设置最大修正轮数,比如 3 次,超出后直接告诉用户“当前条件无法查询”,避免 Agent 陷入死循环。

4. 并发与安全:让 Agent 在多任务下不捅娄子

4.1 数据库连接池:不是越大越好

Agent 调用数据库的场景和别人工系统有一个明显区别:Agent 可能在同一瞬间收到来自多个用户的多轮对话请求,每个请求又可能触发多个 SQL 工具调用。如果每个工具调用都新建数据库连接,连接数会很快被打满。很多人在这个环节第一反应是调大连接池 size,但连接池不是越大越好,尤其在高并发场景下,连接数超过数据库实例能承载的范围,反而会引发连接风暴。

我在这套服务里使用的是标准连接池配置,初始连接数 10,最大连接数 30,连接空闲回收时间 300 秒,并加了等待获取连接的超时时间,默认 10 秒。一旦连接池满,新的请求会排队而不是无限创建连接。这样做的结果是数据库侧不会突然冒出几百个连接,慢查询的影响也被限制在队列里,而不是直接把实例拖垮。

4.2 读写分离与资源隔离

为了让 Agent 的查询不影响业务系统的写入性能,我强烈建议把 Agent 的查询流量指向从库或独立的只读副本。尤其是分析型查询,往往需要扫全表,放在主库上代价很高。Easy Data x AI 的数据源注册里,每个数据库都可以配置多个连接地址,服务层会根据工具的类型自动路由:只读查询走向只读地址,受控变更走向主库地址。

如果条件允许,再给数据服务层单独分配一台实例或者独立的数据库账号,让 Agent 产生的慢查询最多影响 Agent 系统,不会波及其他业务。资源隔离的核心原则是:让 AI 可能造成的影响面最小化。

4.3 审计日志:每个 SQL 都要可追溯

Agent 把数据库用起来之后,审计就变得非常重要。我在这套服务里为每个 SQL 请求生成了唯一请求 ID,并把用户请求、Agent 产生的工具调用参数、最终执行的 SQL、执行结果状态、耗时、执行账号全部写入审计日志表。日志同样走独立存储,避免和业务库混在一起。这个设计在实际使用中帮我们解决过不少问题:一旦有人问“某个数据为什么被改了”,可以通过请求 ID 快速定位到是哪一轮对话、哪个 Agent 发起的变更。

审计日志的数据结构包含四个核心字段:request_id、agent_id、sql_text、execution_status。其余像user_ip、session_id、execution_duration_ms都作为附加字段存进去。整体上不复杂,但没有它,Agent 数据库能力上线后就是一笔糊涂账。

4.4 限流与排队:防止 Agent 一次发一排 SQL

单个 Agent 对话中可能为了完成一个复杂任务发起多次数据库调用,如果不做限流,一个小任务就能产生十几个并发 SQL。我的做法是给每个 Agent 会话设置令牌桶限流:允许每秒钟最多调用 5 次数据库工具,超出后排队等待;同时给单个会话设置每分钟最多执行 20 条 SQL 的硬上限。如果用户确实需要大量数据扫描,应该走离线任务通道,而不是在对话中实时执行。

限流参数需要结合实际业务调整。我一开始给的是每会话每秒 2 次,结果 Agent 在做多表 join 时频繁等待,体验很糟;后来调整到 5 次,并且允许短时突发 8 次,在稳定性和体验之间找到了平衡点。

5. 实测过程中踩得最狠的几个坑

5.1 模型生成的 SQL 看起来很对,但一跑就慌

第一个坑出现在表名和字段名的幻觉问题上。我没有把完整的字段清单以结构化文本形式传给模型,只给了一张表的业务描述,结果模型根据业务常识“猜”了一个看似合理的字段名。例如订单表的实际字段是order_total,模型却生成了total_amount,语法完全正确,执行却报字段不存在。这个问题靠 SQL 校验器发现不了,因为校验器只查语法,不查字段名是否存在。

我后来在数据服务层加了一个“元数据对齐”步骤:在 SQL 执行前,用数据库的实时元数据与 SQL 中的表名和字段名做一次比对,发现不存在的字段名就直接拦截,并返回可选的近似字段名列表。这一步把字段幻觉从 20% 降到了 5% 以下,但更关键的一点是,元数据比对必须在执行前做,而不是等报错后再让 Agent 改。

5.2 连接泄漏:看起来是小问题,数据库直接被打满

我的服务刚上线时,出现过一次数据库连接数告警。排查下来发现,部分 SQL 执行超时后,连接没有被正确归还到连接池。原因很直接:我在执行器里写了conn = get_conn(),但在异常分支里没有finally: conn.close()。一次超时不算什么,但 Agent 高并发场景下每分钟产生几十个超时请求,连接很快就泄漏光了。修复方式就是把所有连接获取放进with上下文管理器里,保证无论正常还是异常都归还连接。

这个坑说到底是基本功,但在 Agent 调用链里特别容易出现,因为工具调用不是简单的函数调用,中间还可能经过消息队列、模型重试、日志记录。任何一环出了异常,连接都可能残留。

5.3 时区与默认值问题:测试数据永远和你以为的不一样

某一次的统计任务里,Agent 生成的 SQL 用了WHERE created_at >= '2025-01-01',结果查出来的数据和业务方人工查询差了 8 小时。原因是表里的时间字段存储的是 UTC,而业务方期望的是本地时间。模型不知道这一点,它只会按常识写 SQL。这个问题的解法不是让模型更聪明,而是在元数据字段说明里主动写清楚“created_at 是 UTC 时间,查询时请使用转换函数”。只要字段说明足够明确,模型生成的 SQL 就会自动加转换。

5.4 事务没提交或没回滚,锁表之后插不进数据

在受控变更功能里,我一开始把变更操作放在事务里执行,但 Agent 在一次多步操作中可能先更新订单表,再更新用户表,其中第二步失败了,事务没有自动回滚,导致第一步的更新一直持有锁,其他写入请求全部卡住。后来我明确规定:每个变更工具必须是单条 SQL 的事务,也就是只提交一条 SQL,执行成功立即 COMMIT,失败立即 ROLLBACK。不要让 Agent 在一个工具调用里执行多条写 SQL,更不要让 Agent 自己控制事务边界。如果需要多步变更,那应该走到人工审批流程,而不是交给模型编排。

5.5 一次误删数据的启发:受控变更必须走审批流

最让我印象深刻的一次事故不是删除,而是批量 UPDATE 把一列值统一改错了。Agent 在对话里被用户绕了几句,生成了一个没有足够精度 WHERE 条件的 UPDATE,执行层虽然检查了“有 WHERE 条件”,但没检查条件是否足够精准。数据改完之后用户才发现不对,幸好测试环境还有备份,但这件事推动了整个流程改变:从“校验通过就执行”改成了“校验通过后,还要返回受影响行数和变更预览,用户确认后才执行”。现在的受控变更工具已经不再返回“执行成功”,而是返回一条变更审批任务,人点了确认才真正写库。

6. 让 Agent 把数据库用起来的下一步玩法

6.1 从查询到数据库运维助手

把数据库能力工具化之后,Agent 能做的就不只是查数了。我们可以给 Agent 开放慢查询分析工具,让它读取慢查询日志,结合表结构提出索引建议;也可以开放表结构展示工具,让它回答“这个表有哪些字段、有什么约束条件”这类问题。这些场景的底层还是同一个数据服务层,只是工具函数从query_database扩展出了analyze_slow_queries、suggest_index、show_table_schema等。

这一点对中小团队特别有价值,很多团队的 DBA 资源不足,或者没有专职 DBA,Agent 可以充当一个“辅助值班人员”的角色,帮开发者快速定位慢查询和数据库配置问题。但最终执行变更还是需要人点击确认,这跟我前面说的设计原则保持一致。

6.2 跨库同步与数据归档类 Agent

数据领域经常需要做库表同步、数据归档、离线分析数据导出。这些任务流程固定但操作繁琐,非常适合 Agent 编排。我最近在试验一个数据归档 Agent:它每天会根据规则扫描订单等大表,把超过一定时间的数据归档到备份库,并在原表保留最近数据。整个流程由 Agent 拆成“查询待归档数据量 - 生成归档批次 - 执行变更并校验结果”三个步骤,每一步都通过数据服务层的工具完成,执行结果会反馈给 Agent 做下一步判断。

跨库同步的难点在于 Agent 必须感知两边的结构差异。我的做法是在元数据注册里把同步关系表也暴露给 Agent,例如“订单归档表是订单表的归档结构,多一个 archive_time 字段”。这样 Agent 生成的迁移 SQL 就能自动带上归档时间字段,而不是简单地把原表数据搬过去。

6.3 把同样的思路嵌入内部报表和工单系统

Easy Data x AI 的服务化设计让它不仅服务 Agent,也可以嵌入内部报表平台和工单系统。内部运营人员需要查数据时,面对的不再是复杂的 SQL 编辑器,而是一个自然语言输入框,输入“上周各渠道的新增用户数”,系统自动生成 SQL 并在限定权限范围内返回结果。这里的数据服务层和权限模型完全复用,唯一变的是前端交互层。

还有一类常见场景是工单自动回复。客服工单里用户如果询问“物流什么时候能到”,Agent 可以查询订单表和物流表,结合当前时间给出预测。这个场景很能体现“让 Agent 把数据库用起来”的价值:模型不再是空谈,而是真正基于业务数据回答用户。

6.4 边界与底线:什么场景不建议让 Agent 直接写库

功能做得越多,越要清楚边界。我现在的原则是:Agent 可以写测试库,生产库的写操作必须走变更审批;Agent 可以读大多数业务表,但包含敏感信息的表要么不在元数据里注册,要么在服务层强制脱敏。尤其是身份信息、支付信息相关的字段,一旦注册到元数据里,Agent 就会把它当作普通字段去查询,这很危险。正确做法是在元数据描述里不出现这些字段,让模型根本不知道它的存在。

还有一类不建议让 Agent 直接操作的是需要强一致性的多步事务。Agent 的调度不具备补偿能力,一旦中途失败,很难自动处理部分成功、部分失败的状态。这类场景宁可让 Agent 生成一个计划给人审,也不要让它直接执行一堆写操作。

我在实际落地这层服务后最深的感受是:让 Agent 使用数据库,真正的难点从来不在模型会不会写 SQL,而在于你有没有想清楚执行边界、反馈机制和安全护栏。把“能查到的”和“能改的”分开,把每一步操作都变成可审计、可回滚、可确认的流程,Agent 才能真正变成一个靠谱的数据库助手。上面这套思路里最值得你直接复制的,可能就是那句“先让 Agent 成为只读分析工具,再逐步放开受控变更”,稳扎稳打,数据库会比你想的更愿意向 AI 敞开大门。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询