1. 这个问题是怎么冒出来的
1.1 AI Agent 火了,数据焦虑也跟着火了
最近这段时间,团队里聊得最多的话题已经从"大模型能做什么"变成了"AI Agent 落地到业务里到底怎么接数据"。很多人一上来就兴奋地说:Agent 既然能理解自然语言,那直接让它写 SQL、连数据库、把活干了不就行了?听起来确实很丝滑——用户说一句"查一下上个月华南区所有销售额超过 50 万的订单",Agent 啪一下生成一条 SQL,啪一下执行,再啪一下把结果汇总成报表。整个链路仿佛没有任何多余的环节。
但真把这个方案往生产环境里一放,问题就全冒出来了。我自己在帮几个团队做 AI 项目架构评审时,几乎每次都有人提出类似的疑问:"为什么不直接让 Agent 连数据库?再多包一层 API 不是脱裤子放屁吗?"这个问题的背后,其实是很多研发同学对 AI Agent 架构边界感不够清晰。大家默认了"能干活"就等于"该干活",忽略了在数据访问这件事上,能力和权限是两回事。
先说结论:AI Agent 完全可以通过某种方式直接碰数据库,这在技术上行得通,但在架构上属于极其危险的偷懒。RESTful API 不是给 Agent 添麻烦,恰恰是给 Agent 装了一道"安全带"。没有这道安全带,Agent 的每一次数据操作都是一次裸奔。
1.2 "直接从数据库拿数据"的诱惑到底在哪
要理解为什么有人会倾向于直接执行 SQL,得先理解这个方案的"甜点"在哪里。
对开发者来说,最大的诱惑是开发效率。假设我要做一个内部数据分析 Agent,直接给 Agent 一个数据库连接串,把表结构告诉它,再让它根据用户的自然语言生成 SQL——这个链路用 Python 写可能三百行就搞定了。但如果你要包一层 RESTful API,你得定义接口、写 DTO(数据传输对象)、做鉴权、处理分页和错误码、设计限流策略……那工作量一下翻了好几倍。
还有一个更隐晦的原因是"灵活性的幻觉"。你永远不知道用户会问出什么样的问题,如果是固定接口,用户一问超出接口能力范围的问题,Agent 就傻了。但 SQL 是无限灵活的,任何问题理论上都能拼一条 SQL 出来。
这两个理由都成立,但它们只是短期视角上的成立。真正进入生产环境之后,这种灵活性和开发速度,会变成你最贵的账单。我们接下来把账一笔一笔算清楚。
2. 直接执行 SQL 的致命伤
2.1 安全维度:SQL 注入只是最表层的问题
谈到直接执行 SQL,很多人的第一反应是"SQL 注入"。这个认知没错,但远远不够。
常规的 SQL 注入是攻击者恶意构造输入,利用程序拼接 SQL 语句的漏洞去执行非预期的命令。在 AI Agent 的场景下,这个问题被放大成了一个新的维度:大模型本身就会"不经意间"构造恶意或非预期的 SQL。不需要有人恶意攻击,仅仅是 Agent 对用户问题的理解偏差,就可能生成一条删表语句。比如用户问"帮我清一下测试数据",Agent 可能生成DELETE FROM orders,看起来问题不大,但如果没有加 WHERE 条件,它清掉的是整个线上订单表。
我见过一个真实的复盘记录:某个团队接了一个内部数据 Agent,测试时一切正常,正式环境第一次跑就被用户一句"这个月的收入数据不对,帮我调整一下"给带沟里了。Agent 生成了一条带着UPDATE的 SQL,在执行时因为权限给得太宽,真的改了正式环境的账单数据。后来花了两个多星期才把数据恢复回来。这种事故,不是安全工程师黑客攻击,而是架构上没有把 Agent 的行为约束在安全边界内。
更麻烦的是,Agent 生成的 SQL 不一定能稳定复现,同样的用户输入在不同会话里可能生成逻辑不同的 SQL。这意味着你没法用传统的"SQL 审批流"来预先拦截,动态生成的语句永远在规则之外。
2.2 资源维度:一条慢查询能拖垮整个库
如果你觉得安全问题还能靠仔细调教提示词来缓解,那资源问题就是实打实的物理伤害。
数据库是一个共享的、有瓶颈的资源系统。连接数是有限的、锁是互斥的、IO 是有上限的。Agent 生成 SQL 时,并不会像 DBA 一样考虑这条 SQL 是否走索引、是否会锁表、是否会全表扫描。它唯一的目标是"产出符合用户问题的结果"。
举个例子:用户问"对比一下最近一年每个月的订单总量变化趋势",Agent 可能会生成一条对千万级订单表做GROUP BY month的聚合查询。这条 SQL 在测试库上一秒跑完,因为测试库只有十万条数据。但在生产库上,它可能扫掉几张表、持锁几分钟、把连接池打满,直接把在线交易事务拖死。
平时我们做慢查询优化,是一天看一次日志,发现问题然后改索引。AI Agent 则是每时每刻都在生成新的查询,你根本没有时间事后去优化——你只能做事前防护。防护的核心手段就是:Agent 不能直接接触数据库资源,它只能接触到预先定义好、经过性能评估的接口。
2.3 权限和审计的颗粒度问题
直接连数据库还有一个非常难解决的问题:权限怎么分?
传统数据库访问模型里,权限是基于角色的:运营角色只能查订单表,财务角色只能查账单表,管理员角色才能写数据。但 AI Agent 面对的是开放性极强的用户问题,它顶多能区分"管理员会话"和"普通用户会话",再细粒度的行级权限、列级权限,用数据库账号体系很难做到。
更深一层的问题是:Agent 操作数据库的行为,和用户本人的身份绑定了吗?你想要审计"是谁、在什么时间、通过哪个 Agent 会话、执行了什么数据操作",如果所有 Agent 共用一个数据库账号,你根本没法做归属追溯。万一出了数据泄露,你连是谁引发的都不知道。只有通过 API 层,在请求中注入用户身份、会话 ID,才能做到每次数据操作都可审计、可追踪。
3. RESTful API 为什么是更合理的边界
3.1 API 本质上是"业务语义的封装"
说完了直接执行 SQL 的坑,我们再来看 API 的价值。很多人把 API 理解成"技术接口",但在我看来,API 的真正价值是业务语义层的封装。
直接执行 SQL,等于把数据库表和列的结构暴露给 Agent。表结构是物理模型,它往往是历史包袱和工程妥协的结果,字段命名混乱、多表关联复杂、同一业务含义分散在多张表里。让 Agent 直接理解这些,就像让一个实习生去翻阅部门十年的纸质档案——不是不行,但错误率一定高。
而 API 把物理结构翻译成了业务语义:GET /api/v1/orders/summary?period=monthly®ion=...,语义清晰、参数明确、返回结构稳定。Agent 不需要知道orders表里status字段到底是1表示已支付还是2表示已支付,它只需要知道调用这个接口是查"订单汇总"的。
更重要的是,稳定可变的接口语义可以大幅降低 Agent 的规划难度。AI Agent 的规划(Planning)过程很大程度上依赖于对工具描述(Tool Description)的理解。API 的路径、参数、返回结构天然适合写成工具描述,而数据库表结构则极其冗长且晦涩。实际测试中,给 Agent 提供 10 个精心设计的 API,比给它看 30 张表结构图,任务执行正确率能高出一大截。
3.2 权限收敛:从"给你整个库"到"只给你需要的"
RESTful API 让你能做到"接口即权限"。
在设计 Agent 的数据访问层时,你可以按功能域拆接口:查询类接口、写操作类接口、管理类接口。每个接口在后端对应一个已经写死、经过评审的 SQL 或存储过程,权限控制落在"能不能调这个接口"这个层面,而不是"能不能执行任意 SQL"这个层面。
举个例子:Agent 只需要做"查询订单状态"这件事,你就只给它开GET /api/v1/orders/{id},不它给开DELETE /api/v1/orders/{id}。这样做完之后,就算 Agent 疯了,它也只能做查询,连删数据的 SQL 语句都拼不出来,因为根本没有执行 SQL 的通道。
这种"按能力开放"的模式,和人类团队的管理方式高度一致。你不会给每个员工都发一把能打开全公司所有门的钥匙,你只发他负责区域的门禁卡。AI Agent 也一样,而且它比人类员工更容易犯糊涂,更需要这种强制性的边界。
3.3 可观测、可回滚、可限流:生产落地必备三件套
API 层还是天然的治理抓手。我梳理一下生产环境里 API 层相对直连 SQL 的三个核心优势:
第一,可观测性。在 API 层,你可以记录每一次调用的参数、耗时、返回状态、调用方身份。这些日志不仅是排查问题的依据,还是优化 Agent 提示词的素材——通过分析 Agent 调了哪些接口、没调哪些接口,你能判断出工具编排是否合理。
第二,快速降级。假设某个查询接口因为数据量暴涨变慢了,在 API 层你可以在网关直接对特定接口做降级或者熔断,让 Agent 返回"当前暂时无法查询"的友好提示,而不是让底层数据库扛不住直接宕机。这在直连 SQL 的模式下几乎无法实现——你没办法对"某条动态生成的 SQL"做熔断。
第三,限流与配额。API 可以按调用方维度限制 QPS(每秒查询数),这样即便 Agent 进入了死循环、疯狂重复调用查询接口,也能被挡在网关层。直连 SQL 的话,你能做的只有断掉整个数据库连接,代价非常粗暴。
这里放一个对比表格,看得更直观一些:
| 对比维度 | 直接执行 SQL | RESTful API 中间层 |
|---|---|---|
| 权限精细度 | 按数据库账号粗粒度控制 | 接口级、按业务语义控制 |
| SQL 注入风险 | 高,存在不可控动态语句 | 低,SQL 不在外部传入 |
| 性能管控 | 难以对动态查询做限流 | 网关层可限流/熔断 |
| 审计追溯 | 无法关联用户身份与 Agent 会话 | 可在请求链路中注入全链路标识 |
| 接口演进 | 表结构变了 Agent 立即失效 | 接口不变则 Agent 不受影响 |
| 排查难度 | 动态 SQL 难复现、难定位 | 日志清晰、失败可复现 |
3.4 什么情况下直连 SQL 也可以接受
我不是一个"绝对不允许"主义者,毕竟架构上没有银弹。直连 SQL 在两类场景下是可以接受的:
一类是纯离线分析场景。比如让 Agent 写 SQL 去做数据探索、生成临时报表,目标库是一个隔离开的只读副本,有独立的账号、独立的连接池,就算 SQL 写烂了也影响不到线上业务。这时直连确实效率更高,Agent 的灵活性也能充分释放。
另一类是强可控实验环境。比如你在本地 Docker 里跑一个玩具数据库,Agent 怎么折腾都行,完全不影响任何业务。但这两类场景都有一个共同前提:数据库可以被任意折腾,且不会产生不可恢复的后果。你的生产环境,显然不满足这个前提。
所以核心判断标准就一句话:如果 Agent 的某次数据操作出错,后果是否在可接受范围内?如果答案是"不可接受",那就别直连 SQL,老老实实包 API。
4. 实操:给 AI Agent 设计数据访问层
4.1 设计原则:友好接口 + 明确权限 + 语义稳定的三大法则
如果现在你要给 Agent 设计一套 RESTful API 数据访问层,我建议你先预设三个设计原则。
第一个原则:接口的工具描述必须足够清晰。AI Agent 是通过工具描述(Tool Description)来理解接口的用途的。你需要给每个接口写好一段"人话描述",包含:这个接口是干什么的、参数是什么含义、返回什么格式、什么情况下会报错。这些描述直接影响 Agent 是否能正确选用接口。描述写得太技术化,比如"Query order by ID",Agent 理解得就会很浅;写成"查询订单详情,订单 ID 必传,返回订单状态、金额、物流信息",Agent 就知道什么时候该用它。
第二个原则:所有写操作必须走显式确认才执行。在 API 设计层面,写接口和读接口要物理分离。读接口可以让 Agent 自由调用,写接口必须设置门槛——比如强制要求 Agent 在调用时携带一个确认参数(例如confirm=true),或者在网关层对写接口做额外的权限校验。有些团队还要求 Agent 对写操作先调用一个"预检查"接口,也就是先查一下执行条件是否满足,然后再真正写。这些都是很有效的保护措施。
第三个原则:接口语义要稳定,不与数据库表结构绑定。接口的返回字段最好带有一个稳定版本号,因为 Agent 一旦在你的提示词中记住了"这个接口返回这个结构",你改了字段类型,Agent 就会开始编造数据。避免这个问题的方法就是接口语义层稳定,表结构再怎么调整,API 返回的总是一个稳定的业务视图。
4.2 一个最小可落地的实现示例
抽象地说原则没有用,我直接写一个最小示例。假设我要做一个"订单查询 Agent",后端接口设计方案如下:
接口 1:按订单 ID 查询订单详情
GET /api/v1/orders/{order_id}返回示例:
{ "order_id": "SO20250101001", "status": "paid", "total_amount": 1299.50, "customer_id": 10086, "items": [ {"product_name": "无线耳机", "quantity": 1, "price": 1299.50} ], "created_at": "2025-01-01T10:32:18Z" }接口 2:查询订单汇总统计
GET /api/v1/orders/summary?start_date=2025-01-01&end_date=2025-01-31&granularity=daily返回示例:
{ "period": {"start": "2025-01-01", "end": "2025-01-31"}, "granularity": "daily", "items": [ {"date": "2025-01-01", "order_count": 42, "total_amount": 52300.00} ] }在 Agent 侧,我们把这些接口注册成工具描述,用 Python 代码大致是这样:
tools = [ { "type": "function", "function": { "name": "get_order_detail", "description": "根据订单号查询订单的详细信息,包括订单状态、金额、商品列表、客户ID。当用户询问某个具体订单的详情时使用。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式如 SO20250101001" } }, "required": ["order_id"] } } }, { "type": "function", "function": { "name": "get_order_summary", "description": "按时间段汇总订单数量和销售总金额,可按天或按月统计。当用户询问销量、销售额趋势或汇总数据时使用。", "parameters": { "type": "object", "properties": { "start_date": {"type": "string", "description": "开始日期,YYYY-MM-DD"}, "end_date": {"type": "string", "description": "结束日期,YYYY-MM-DD"}, "granularity": {"type": "string", "enum": ["daily", "monthly"], "description": "统计粒度:按天或按月"} }, "required": ["start_date", "end_date", "granularity"] } } } ]在 API 后端,每个接口内部执行的是预先写好的固定 SQL,比如订单汇总接口:
SELECT DATE_TRUNC(:granularity, created_at) AS period, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM orders WHERE created_at >= :start_date AND created_at < :end_date GROUP BY DATE_TRUNC(:granularity, created_at) ORDER BY period;看到这里你应该能理解,真正访问数据库的 SQL 依然是存在的,但它被锁在 API 内部,Agent 根本接触不到。外部传入的只有业务参数(日期、订单号、粒度),SQL 的结构完全受控。这就是"RESTful API 保护 SQL"的本质——不是不用 SQL,而是不让 SQL 暴露给不可控的调用方。
4.3 工具选型与原生 SQL 的取舍
聊到实现,自然会碰到一个灵魂拷问:用 ORM(对象关系映射)还是原生 SQL?
我在几种方案里都踩过坑,说点真实感受。
如果 API 后端的逻辑比较简单,就是查一张表、返回固定字段,用 ORM 完全没问题,开发效率高。但一旦涉及复杂聚合、报表统计、多表关联,ORM 生成的 SQL 往往不是最优的,而且排查问题难度翻倍——你要先在 ORM 的链路上捋清它最终执行的 SQL 是什么。我更推荐在这类接口中直接用原生 SQL,因为接口是固定的,SQL 也是固定的,你完全可以预先把 SQL 做性能调优、加好索引,并让 DBA 评审。
另外多说一句最近大家关心的话题:AI 生成 SQL 不是不能用,而是要用对位置。现在有很多中间件可以接受自然语言、帮你生成 SQL,然后交给数据库执行。这类工具在"只读副本"或"分析型数据仓库"上体验确实不错,能显著提升临时数据分析的效率。但你要注意,这类工具的本质还是 SQL 直连,只不过替你做了生成环节。在生产业务库上,我仍然持保守态度——除非你能接受上文中提到的那些事故风险,否则不要拿生产库去赌生成式 SQL 的稳定性。
5. 常见问题与排查经验
5.1 接口太宽,Agent 乱调用怎么办
一种典型情况是:你确实提供了 API,但接口的查询参数给得太宽了。比如GET /api/v1/orders?status=paid&customer_id=...&amount_min=...&amount_max=...,参数组合很多,Agent 在规划时可能传一些奇怪的参数组合,导致后端查询条件过于宽泛,拉出海量数据。
我的建议是:接口数量宁可多,单个接口的能力宁可窄。把宽接口拆成多个窄接口,能显著提高 Agent 的工具选择准确率。比如拆成"查单笔订单详情"、"查某个客户的订单列表"、"查销售额汇总",每个接口参数少、语义清晰,Agent 就不容易困惑。
如果实在避免不了参数较多的接口,可以给参数加上严格约束,比如规定枚举值、规定最大值,必要时在后端校验参数,超范围直接返回 400 错误。让 Agent 在报错中学会正确传参,比让它自由发挥要安全得多。
5.2 API 响应慢、信息碎片化怎么优化
很多时候,Agent 为了回答一个问题需要连续调多个接口,比如先查用户信息、再查其订单、再查订单里的商品详情。串行下来可能七八个 HTTP 请求,虽然每个接口只有几十毫秒,累计起来很可能超过用户的响应预期。
解决方法有两个方向:一是为 Agent 场景设计聚合接口。比如新增一个GET /api/v1/customers/{id}/dashboard接口,一次性返回客户的基本信息、最近订单和售后概览。Agent 只需要一次调用就能完成任务规划,耗时和稳定性都会大幅改善。
另一个方向是让接口支持批量查询。例如订单详情接口支持GET /api/v1/orders?ids=SO001,SO002,SO003,这样避免 Agent 对每笔订单各发一次请求。批量接口要注意设置最大数量上限,防止一次拉取太多数据。
5.3 要不要给 Agent 单独建一套专用 Schema
这是一个很值得讨论的问题。如果你的 Agent 只是对接内部系统,且后端 API 结构相对稳定,直接用现有接口即可。但如果你的系统历史包袱重、接口混乱、字段含义不统一,我建议做一个面向 Agent 的专用聚合层,而不是让 Agent 去逐个对接二三十个历史接口。
假设一下:你的系统里查订单状态的接口有两个版本,一个返回status,另一个返回order_state,Agent 每次都要靠提示词去消歧。这种状态下,你应该抽一个新的仅面向 Agent 的接口版本,统一字段命名,统一错误码,只暴露 Agent 需要的那些能力。这也是目前不少团队做 Agent 落地时比较推荐的"Agent 网关"模式。
5.4 避坑清单
最后给一份踩过坑之后的速查清单:
- 拒绝把数据库连接串或 JDBC 配置写进 Agent 的提示词或工具描述里,一旦泄露等同于把生产库交给外部。
- 所有 Agent 调用的 API 都必须做鉴权,至少用 API Key 或服务间认证;认证凭证不要用明文,用环境变量或密钥管理服务。
- 接口日志必须包含请求方会话 ID,方便还原 Agent 的完整思考链路。
- 写操作的接口在网关层做"二次确认"模式:第一次调用返回预执行结果,第二次调用携带确认标记才真正写入。
- 定期给 Agent 的 API 调用做回归测试,因为后端参数变了、接口逻辑变了,Agent 的规划能力会立刻退化。
- 别忽略返回数据量的问题,接口要默认分页,否则 Agent 可能一次拉回几十万行数据,直接把自身上下文窗口撑爆。
我个人在实际操作中最受益的一个习惯是:为每个 Agent 接口都维护一份"人话版使用说明",并且和 Agent 系统提示词里的工具描述保持同步。每次更新接口,都先改使用说明,再改代码,最后验证 Agent 行为。这样折腾几轮以后,Agent 的工具调用准确率肉眼可见地稳定下来。
最后再分享一点个人的体会:很多人觉得 RESTful API 多了一层、慢了半拍,是给 AI Agent 拖后腿。但真正在线上跑过一阵子你就会发现,这层 API 保护的恰恰是你最不能出问题的东西——生产数据和团队的安全底线。它让 Agent 变成一个有边界、可治理、出问题能兜住的员工,而不是一个拿到数据库钥匙就乱跑的小孩。架构无优劣,只有代价和边界,清楚的边界,才是 AI 能力落地的底气。