Agent 查询结果的验证:SQL 返回了空结果,是数据真的没有还是查询写错了

有一次,我在一个数据平台工具里问 Agent:“帮我查一下昨天北京地区下单用户数。” 它迅速生成一条 SQL,执行,返回一个空表格,然后一本正经地说:“昨天北京地区没有下单用户。” 我当场愣了一下——因为这个数据平台我前一天还在用,北京昨天至少有几百个订单。

AI technology illustration

这不是我一个人的经历。凡是让 Agent 写过 SQL 的人,大概率都被那句“没有数据”噎到过。问题在于,当运行结果是一个空集的时候,你根本无法判断是数据库里真的没有这条记录,还是 Agent 生成的查询把数据给过滤掉了。而后者往往比前者更隐蔽,因为它看起来语法完全正常。

这才是最危险的:SQL 返回一个错误数值,你可能因为数字不合理而怀疑它;但返回一个空表,你只会觉得“哦,没有数据”,然后老老实实去干别的。空结果的伪装性,正在于它没有留下任何异常痕迹。

这个问题听着简单,但往深了挖,它牵出一个核心矛盾:Agent 是语言模型,它擅长生成 SQL,却不擅长质疑自己。

别急着接受“无数据”,先怀疑这五种情况

我把这些年见过的“假空”总结了一下,最常见的五种情况是这样的:

原因 典型场景 快速识破的方法
WHERE 条件过严 时间范围用了 < 而不是 <=,边界被划掉了 把时间范围扩大,看有没有数据
字段类型与数据实际不符 日期字段存成 VARCHAR,“2023-10-01” 和 “2023-10-1” 比较时补零不一致 查看该字段的 min/max,看实际格式
NULL 或空字符串 NOT IN 子查询包含 NULL,导致整个条件结果为 NULL,所有行被过滤 换成 NOT EXISTS 或加 IS NOT NULL
JOIN 把主表行数减成零 内连接时右表没有匹配行,主表记录被全部丢弃 改成 LEFT JOIN 重新查
查错了表或分区 表有分区,默认查的是某个空分区 全表或全分区 COUNT(*)

你看,这些原因没有一个是 SQL 语法错误,所以要是人工去读代码,很可能读半天也发现不了问题。但如果你换一种方式,比如先只跑一遍 SELECT COUNT(*) FROM orders WHERE order_date > ...,空结果的真正来源可能瞬间就暴露了。

Agent 没有见过数据,它只会按 schema 猜

为什么会这样?因为 Agent 在生成 SQL 时,能依赖的只有数据库的表结构、字段名、字段类型,以及你问题里的关键词。它看不到表里实际存了什么值,也不知道字段里有没有空格、空字符串、大小写不一、日期格式不统一这类脏数据。

我最早做自然语言转 SQL 的时候,遇到空结果第一反应是去读 Agent 生成的代码。有一次它查昨天某地区的订单,返回空,我看了半天 SQL,逻辑没问题、语法没问题。后来把日期条件去掉,数据出来了;再把日期条件加回去,还是空。最后我单独查了下那个日期字段的取值,才发现所有日期都存成了没有补零的格式,比如 2023-10-1,而 Agent 生成的查询写的是 '2023-10-01'。两个字符串在比较时,’2023-10-01′ 是大于 ‘2023-10-1’ 的——因为多了一个字符,所以整个区间的数据全被过滤了。

这件事让我意识到,空结果问题不是简单的代码 bug,而是 Agent 对数据的无知。它连确认一下字段里有没有数据的能力都没有,就直接下结论,自然会出错。

先摸一下数据:两种探测查询

既然 Agent 看不见数据,那我们就让它先“摸”一把。在正式执行主查询之前,先跑几个低成本的探测查询,把数据分布的大致情况摸清楚。

比如,面对一个复杂的条件查询,Agent 可以先执行这个:

SELECT COUNT(*) AS total_count, MIN(order_date) AS min_date, MAX(order_date) AS max_date, COUNT(DISTINCT city) AS city_count FROM orders WHERE order_date >= '2023-10-01' AND order_date < '2023-11-01';

如果 count 是 0,说明这个时间段内可能真的没有订单;如果 count 不为 0,再查:

SELECT city, COUNT(*) FROM orders WHERE order_date >= '2023-10-01' AND order_date < '2023-11-01' GROUP BY city;

这样就能看到,在所选的时间范围内,有哪些城市的记录。如果列表里根本没有“北京”,那问题可能出在你对“北京”这个值的描述上;如果列表里有“北京市”和“北京”两种写法,那么 Agent 的查询条件可能漏掉了其中一种。

这些探测查询听起来很简单,但它们就是“空结果归因”的核心。真正的难点不是写这些 SQL,而是让 Agent 在得到一个空结果之后,能主动想到去执行它们。

把验证写进流程,让 Agent 学会自我质疑

理想情况下,Agent 在返回“没有数据”之前,应该强制自己走一套验证流程。我给这个流程起名叫“空结果验证循环”。

  1. 验证表名和字段名:查询 information_schema,确保没有引用不存在的对象。
  2. 验证条件字段的基本分布:对 WHERE 条件涉及的字段执行 SELECT DISTINCT 或 MIN/MAX,确认取值格式。
  3. 验证 JOIN 的影响:把内连接换成 LEFT JOIN 或先只查主表,看行数变化。
  4. 验证是不是全表真的没有数据:去掉所有过滤条件,做一个全表 COUNT。

每一步都可以生成一条独立的诊断 SQL。只有当四步都查完,仍然没有数据,Agent 才有资格说“这数据可能真的不存在”。

这看起来会增加不少查询开销,但其实每次诊断 SQL 都是轻量级聚合,只要数据表不是巨大到变态,成本完全可以接受。更重要的是,它把“空结果”从一个最终结论,变成了一个可以解释的中间状态。

这套流程看起来像是在给 Agent 增加负担,但它其实是在模拟人查数据时最自然的动作:先看全貌,再看细节,最后才下结论。人不会凭空相信一个空结果,除非他已经把各种可能性排除掉。Agent 要想获得同样的信任,就得走同样的路。

为什么 Agent 会一本正经地说谎?

你可能会问,Agent 看到空结果,为什么不说“我可能写错了”呢?这和语言模型的本性有关。模型生成回答时,只是根据概率选择最高分的符号序列。训练数据里,很多“查询结果为空”的场景,标准回应恰恰就是“没有数据”。于是它学会了模仿这个回应,而不是理解“空”背后的语义。

换句话说,它不是在撒谎,它是在背台词。但对我们用户来说,结果是一样的——被误导。所以,如果你想用 Agent 做查询,不能只给它生成 SQL 的能力,还要给它一套怀疑和验证的机制。人之所以能在空结果面前保持清醒,不是因为人更聪明,而是因为人知道数据世界里充满了脏数据、类型陷阱和命名差异。Agent 如果连这种常识都没有,空结果就注定是它最大的盲区。

如果说 Agent 生成 SQL 的能力已经接近初级数据分析师,它在空结果处理上的表现却像一个刚入行的实习生——既不会检查数据范围,也不会怀疑 WHERE 条件写错,只会照着模板回答“查无数据”。这也是为什么我坚持认为,Agent 的可靠性不在于它多会写 SQL,而在于它能不能在结果异常时进行自我审查。

Q1:为什么不能只看 Agent 生成的 SQL 代码?

因为问题往往不在 SQL 的语法,而在数据本身。代码看起来完全正常,但字段取值和你预期的不一样。比如你写 WHERE city = '北京市',实际数据是 北京,语法没错,结果却空。眼睛看代码解决不了这个问题。

Q2:如果表里真的没有数据,Agent 如何知道?

需要先做全表 COUNT 或分区大小检查。如果全表都没数据,才算真正的缺失;如果全表有数据,但加上过滤条件后为空,那就是查询条件问题。这个检查必须在最终回答之前完成。

Q3:对于空结果,用户希望 Agent 给出什么反馈?

希望它给出三件事:使用了什么条件、每组条件的返回行数、以及它认为最可能的原因。比如“按时间范围过滤后有 120 条记录,再加上北京地区后变为 0,说明该地区在所选时间段内无订单,或城市名称存在其他写法。”

坦率地说,现在大多数 Agent 工具在空结果处理上还远不够及格。它们在“有数据”的情况下能表现出色,但一遇到空集就暴露出缺乏怀疑机制的问题。

要做成一个可信的数据助手,就必须在架构里加入一层“空结果校验器”,这层不依赖大模型的推理,而是依赖元数据、信息模式视图和结构化的诊断查询。我甚至会想,未来的 Agent 会把空结果视作一次内部审查的触发器,而不是最终输出。

到那时候,你再问它“北京昨天有没有订单”,它会先跑一堆诊断查询,然后告诉你:北京昨天订单数为 0,但前天有 124 条,城市名称统一为“北京”,目标条件没有问题——这份底气,才是我们真正需要的东西。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/732.html

(0)
上一篇 58分钟前
下一篇 51分钟前

相关推荐