别让 Data Agent 只会“给答案”:从 !assert 到 save,把分析变成可验证的生产数据资产
大多数 Data Agent 的演示,都停在同一个漂亮瞬间:
用户问了一个问题,Agent 连上数据库,写出 SQL,返回一张表和几句结论。
这当然有价值。但如果结果只存在于聊天记录里,它还不是生产数据资产。下一次 BI 报表要用,调度任务要用,另一个 Agent 要接着分析,团队往往还得把聊天里的逻辑重新翻译成 ETL、表结构和验证脚本。
真正困难的,从来不是“让模型给出一个答案”,而是让这个答案满足四个条件:
- 可验证:脏数据或错误口径不能悄悄越过边界;
- 可落地:结果能写入明确的数据目标,而不是只显示在对话框里;
- 可回读:写入后的真实状态能被再次读取和核对;
- 可复用:BI、调度任务和后续 Agent 能把它当成普通数据资产继续消费。
过去几天,我们在 Infinity SQL 测试环境里分别验证了两个关键环节:
!assert:确定性质量门会在发现脏数据时抛错,并停止后续语句;save:分析结果可以写入 MySQL 生产表形态,再通过load回读验证。
把它们放进同一套生产思路,Data Agent 才从“回答问题的聊天工具”迈向“交付可验证数据资产的执行系统”。
一条可信分析链路,至少要闭环四次
可以把理想的执行链路压缩成下面六个动作:
load → select → !assert → save → reload → verify 读取 计算 放行 写入 回读 核验其中有两条边界尤其重要:
!assert决定结果能不能影响外部世界;reload + verify决定外部世界是否真的变成了预期状态。
模型负责提出计算步骤,运行时负责执行;确定性规则负责放行,目标系统的回读结果负责验收。概率能力和确定性约束各司其职,才是可审计的 Agent 工作流。
第一关:不要让概率系统用概率审计概率
假设 Agent 收到四行订单事件,其中一行user_id是空值:
setevents=''' {"id":1,"user_id":"u1","amount":120} {"id":2,"user_id":"u2","amount":80} {"id":3,"user_id":null,"amount":56} {"id":4,"user_id":"u4","amount":230} ''';loadjsonStr.`events`asevents_table;selectcount(*)asnull_usersfromevents_tablewhereuser_idisnullasnull_check;!assert null_check''' :null_users == 0 '''"user_id 存在空值,阻断下游写入";select"quality gate passed"asmessageasoutput;在测试环境中,引擎返回:
java.lang.RuntimeException: user_id 存在空值,阻断下游写入更重要的是,后面的quality gate passed没有执行。
这和让模型“再检查一次结果是否合理”有本质区别。后者仍然是一次概率判断;!assert则把业务规则变成了运行时必须满足的确定条件。条件不成立,管道就停止。
!assert的设计还有一个很实用的特点:它断言的是一张表。
想验证什么,先用select把证据算成一张小表,再对表中字段做判断。Agent 不需要学习另一套测试语言,它已经会的查询能力可以直接变成质量门:
- 行数必须大于零;
- 主键不能为空;
- 唯一键不能重复;
- 新旧口径必须对账;
- 金额波动不能超过业务阈值;
- 写回前的目标分区必须符合预期。
因此,质量验证不再是分析完成后的人工附注,而是管道本身的一部分。
第二关:把质量门放在不可逆动作之前
质量门最有价值的位置,不是在所有探索之前,而是在每个不可逆动作之前:
自由探索区 外部影响区 load → 清洗 → 聚合 → 口径对账 → !assert → save / 发报告 / 触发下游在自由探索区里,Agent 可以尝试不同查询、修正语法、比较多种解释。错误的成本很低。
一旦要写库、发报告或触发下游任务,风险就完全不同。此时应该由确定性规则决定能否跨过边界,而不是由模型用一句“看起来没问题”自我批准。
这和软件工程中的 CI 很像:
- 开发者可以自由修改代码;
- 测试不通过,代码不能合并;
- Data Agent 可以自由探索;
- 质量门不通过,结果不能写入生产目标。
关键不在于让 Agent 永远不犯错,而在于让错误无法悄悄扩大影响范围。
第三关:save让探索结果直接成为表
质量门通过之后,分析结果还需要一个明确的生产出口。
我们在另一个独立测试中,让同一条 InfiniSQL 管道读取 MySQL 订单、计算区域报表、写回region_report_daily,再回读验证:
loadjdbc.`biz_mysql.orders`asorders;selectregion,count(*)asorder_cnt,sum(amount)asgmvfromorderswherestatus='paid'groupbyregionasregion_report;saveoverwrite region_reportasjdbc.`biz_mysql.region_report_daily`;loadjdbc.`biz_mysql.region_report_daily`asverify_report;select*fromverify_reportorderbygmvdescasoutput;测试环境的真实回读结果是:
| 区域 | 已支付订单数 | GMV |
|---|---|---|
| 华东 | 8 | 274,600 |
| 华北 | 2 | 14,000 |
| 华南 | 1 | 12,000 |
| 西南 | 2 | 7,200 |
这里最值得关注的,不是少写了一段 ETL,而是分析与交付没有发生语言切换:
- 读取数据用
load; - 计算口径用
select ... as; - 写入目标用
save; - 验收结果仍然用
load和select。
探索阶段确认过的逻辑,不需要再被另一套框架、另一种语言或另一个团队手工翻译。少一次翻译,就少一类口径漂移。
第四关:为什么写完还必须回读
很多自动化流程把“写入命令没有报错”当成成功,但对生产系统来说,这个证据还不够。
写入动作返回成功,只能说明请求完成;回读才能回答更关键的问题:
- 目标表是否真的存在?
- 字段和值是否符合预期?
- 行数是否正确?
- 覆盖写是否只影响了预定目标?
- 下一个消费者能否用标准读取方式获得结果?
因此,reload不是多余的重复,而是一次状态确认。
一个更稳妥的生产模板应该是:
计算候选结果 ↓ 写前断言:数据质量、口径、目标范围 ↓ save:执行明确写入语义 ↓ load:从目标系统重新读取 ↓ 写后断言:行数、关键指标、幂等键、分区范围 ↓ 交付给 BI、调度任务或后续 Agent写前断言约束“准备写什么”,写后断言验证“实际写成了什么”。两者解决的是不同问题。
save的四种语义,其实是四种风险选择
save支持四种明确模式:
| 模式 | 语义 | 适合场景 | 主要风险 |
|---|---|---|---|
overwrite | 覆盖目标 | 全量刷新报表 | 错误目标会造成覆盖 |
append | 追加数据 | 增量流水、事件日志 | 重跑可能重复 |
ignore | 目标存在时跳过 | 幂等初始化 | 可能保留旧状态 |
errorIfExists | 目标存在时报错 | 防止误覆盖 | 需要显式处理冲突 |
这张表说明,save不是一个模糊的“帮我存一下”。每次写入都必须声明语义,也就是主动选择风险模型。
对 Agent 来说,默认策略应该更保守:
- 未确认目标时,优先
errorIfExists; - 使用
overwrite前,先断言目标名称、分区和关键指标; - 使用
append时,必须设计业务唯一键或幂等规则; - 写入完成后,必须回读并核验;
- 高风险目标还需要权限、审计和人工审批。
语言可以提供确定性原语,但不能替团队替代所有生产治理。
从一次回答,到一个可持续消费的资产
当链路闭合之后,同一份结果会获得完全不同的生命周期。
如果它只在聊天记录里:
- 用户离开页面后很难复用;
- 口径与来源不容易追踪;
- 下一个 Agent 需要重新理解上下文;
- BI 和调度系统无法直接消费;
- 失败后很难判断执行到了哪一步。
如果它通过质量门写入表,并完成回读验证:
- 表名成为稳定引用;
- 口径可以和管道一起审计;
- BI 可以直接读取;
- 调度任务可以按周期刷新;
- 后续 Agent 可以从这张表继续分析;
- 失败可以定位在读取、计算、放行、写入或回读中的具体阶段。
这才是“分析结果成为资产”的准确含义:不是把答案存成一段文本,而是把它变成一个有名字、有结构、有验证证据、能被其他系统继续消费的数据对象。
InfiniSynapse 真正需要提供的,不只是聊天体验
用户仍然可以用自然语言提问。自然语言入口没有问题,问题在于底层执行不能只依赖自然语言。
一个可信的产品层应该把用户意图逐步落到可检查的执行对象:
用户目标 → Agent 生成 InfiniSQL 管道 → 运行时执行并保留中间结果 → 确定性质量门决定是否放行 → 明确写入语义落到目标系统 → 回读与断言生成验收证据 → 结果进入 BI、调度或下一轮 Agent在这个结构里,大模型负责理解模糊目标、生成候选步骤和解释结果;InfiniSQL 负责把数据操作、状态、验证与交付收束到同一套执行语言中。
这比单纯追求“Agent 一次回答得更聪明”更接近企业真实需求。企业需要的不是一句自信的回答,而是一条能够追溯、复跑、阻断和验收的链路。
必须讲清楚的事实边界
这篇文章组合了两个分别完成的测试,不应把它们夸大成一次端到端生产验收:
!assert测试验证的是:带空值的数据会触发运行时错误,后续语句停止;该脚本没有连接真实目标库执行写入。save测试验证的是:区域报表写入 MySQLregion_report_daily后,可以被load回读,并返回上述四个区域的结果。- 当前证据没有证明生产 SLA、并发吞吐、事务回滚、权限模型或故障恢复时间。
- 在真实生产环境中,还需要补充访问控制、凭据隔离、审计日志、目标白名单、幂等策略、事务边界、监控告警和人工审批。
诚实地保留这些边界,不会削弱方案,反而说明这是一套可以继续工程化验证的架构,而不是一段营销魔法。
结语:不要只问 Agent“答案是什么”
更好的问题是:
- 这个答案基于哪份数据?
- 哪些规则决定它可以被交付?
- 它写到了哪里?
- 写入后是否被重新读取和核验?
- 下一个系统或 Agent 如何继续消费?
- 如果失败,我们能否知道停在了哪一步?
当load → select → !assert → save → reload → verify成为一条完整链路,Data Agent 才不只是会给答案。
它开始具备交付数据资产的能力。
本文中的!assert拦截与save写回/回读数据均来自 Infinity SQL 测试环境的真实执行结果。InfiniSQL 是 InfiniSynapse 数据分析 Agent 的底座引擎。