用TypeScript构建可核查的数据分析智能体
教程使用AVA 4.0、DuckDB和支持工具调用的模型,构建比较月度收入并按产品归因的命令行智能体。流程校验分项变化与总变化是否一致,并保存SQL、查询结果、报告和交互图表。
AI 深度解读
让 AI 回答「上个月收入涨了,哪些产品贡献最大」,难点不只是算出一个数,还要让人能查清这个数从哪里来。AntV 作者给出的方案是:模型负责选择查询和解释结果,AVA 4.0 负责数据访问、DuckDB 执行和图表生成,同时保存 SQL、查询结果和报告。
这篇教程值得借鉴的是一条可复核的分析链路。不过,示例只用八行模拟数据,验证也没有覆盖真实模型运行。它展示了怎样搭骨架,还不足以证明这套 Agent 在真实业务中可靠。
先把业务口径写清,模型才有计算依据
示例比较 2025 年 1 月和 2 月的产品收入。八行数据以美元计价,结果可以手算:1 月总收入 30,000,2 月 37,500,增加 7,500,增长率为 25%。
作者先明确了四条规则:比较周期由 month 决定;变化量是 2 月减去 1 月;增长率是变化量除以 1 月收入;1 月收入为零时,增长率没有定义,应返回 null。
这些规则看起来普通,却决定了分析是否答对问题。模型即使生成了能执行的 SQL,也可能选错月份、用错分母,或者遗漏只在某个月出现的产品。
程序通过 ava.schema() 获取实际表结构,通过 ava.semantic() 提供业务定义,再把两者放进 Agent 指令。示例加载后的表名是 data。
这里有一个容易误解的地方:业务定义进入提示词,不等于数据库会强制执行这些定义。
作者明确说明,ava.query() 直接执行传入的 SQL,不会替你改写查询来落实业务口径。规则有没有被遵守,仍然要看最终 SQL 和结果。
两个工具,怎样把回答接到真实数据上
运行环境要求 Node.js 22.13 或更高版本、macOS 或 Linux,以及支持工具调用的模型。项目安装命令如下,用来固定教程使用的依赖版本:
mkdir ava-data-agent
cd ava-data-agent
npm init -y
npm install @antv/ava@4.0.0 ai@7.0.112 @ai-sdk/openai@4.0.73 zod@4.6.7
需要留意最后一行的版本号,示例的验证范围对应这组依赖,不能直接推断其他版本具有相同行为。
环境变量包括 OPENAI_API_KEY 和 OPENAI_MODEL;使用其他 OpenAI-compatible 服务时,还可以设置 OPENAI_BASE_URL=https://your-provider.example/v1。代码通过 createOpenAI() 和 provider.chat() 使用 Chat Completions 配置,模型是否支持工具调用取决于服务商。.env 应在提交前加入 .gitignore。
Agent 使用 AI SDK 的 ToolLoopAgent 管理循环:请求模型、执行工具、把结果交回模型,再决定下一步。stopWhen: isStepCount(8) 限制最多八步,parallelToolCalls: false 关闭并行工具调用。
它只暴露两个工具:
query接收 SQL,通过ava.query(sql, { maxRows: 100 })执行,保存 SQL 和完整结果对象,并返回queryId、列信息及实际数据行。如果结果被截断,就抛出错误,要求进一步聚合。plotChanges只接收queryId,从应用内存取出对应结果,检查product是字符串、change是有限数值,且行数在 1 到 50 之间,再交给ava.viz()生成条形图。
第二个工具的设计尤其适合借鉴。模型不用重新抄一遍数字,应用代码直接把查询行映射成图表的 category 和 value,减少了数据在工具之间传递时被改错的机会。
每次运行用 mkdtemp 创建独立输出目录。作者还说明图表写入使用 flag: 'wx',防止重复调用静默覆盖文件。但所提供的原文代码在 join(out 处截断,后半段缺失,不能直接复制成完整程序。
教程给出的启动命令是 node --env-file=.env --experimental-strip-types agent.mts,利用 Node 的类型剥离能力运行 TypeScript。AVA 则通过 createRequire(import.meta.url) 加载 CommonJS 入口:作者本地验证时,默认 ESM 入口遇到了与 mssql 有关的命名导出兼容问题,CommonJS 入口可以工作。
对账能抓住错误,但不能证明分析全对
分析顺序是先查月度总额,再拆产品变化,最后检查各产品变化之和是否等于整体变化。下面的参考 SQL 解决了跨两个月汇总产品、保留单月出现产品的问题:
WITH comparison AS (
SELECT
product,
SUM(
CASE WHEN month = '2025-01' THEN revenue ELSE 0 END
) AS january,
SUM(
CASE WHEN month = '2025-02' THEN revenue ELSE 0 END
) AS february
FROM data
WHERE month IN ('2025-01', '2025-02')
GROUP BY product
)
SELECT
product,
january,
february,
february - january AS change
FROM comparison
ORDER BY change DESC;
重点看 WHERE month IN ('2025-01', '2025-02') 和 GROUP BY product:它在两个周期的产品集合上聚合,不会因为产品只在一个月有记录就将其漏掉。
预期结果中,Analytics 增加 6,000,Automation 增加 2,000,Support 增加 500,Reporting 减少 1,000。对账关系是 6,000 + 2,000 + 500 − 1,000 = 7,500,也等于 37,500 − 30,000 = 7,500。
Analytics 对净增长的贡献占比为 6,000 / 7,500 = 80%。这里的分母是整体净增额,并非 Analytics 的上月收入,因此它不是该产品自身的增长率。
但总额对上,只能说明这条加总关系成立。即使某些收入被分到了错误产品,总额仍可能正确。
落盘文件提供了进一步检查的入口:query-1.sql、query-1.json 等文件记录查询与结果,结果对象含结构、数据和截断信息;revenue-change.html 展示图表,report.md 保存报告。图表 HTML 需要从 CDN 加载 GPT-Vis renderer,因此查看时仍需联网。作者说明,若达到步数上限仍未产出图表或报告,程序会报错,并保留已保存的查询。
数据加载和 DuckDB 执行发生在本地,但表结构、业务定义和查询返回行会进入模型上下文。本地执行不能被理解为数据完全不离开本机。
我的判断
我认同这套方案把计算、解释和图表数据传递分开的做法。相较于直接把整份数据交给模型计算,保存 SQL 和结果更方便定位问题;相较于让模型生成图表数值,使用 queryId 引用已执行结果,也更容易保持一致。
不过,作者没有给出准确率、成本或不同模型的对照数据,不能据此认定两个工具、八步限制就是最佳配置。对于月份固定、口径稳定的报表,参数化 SQL 加固定图表模板同样是合理选择;Agent 的价值需要结合问题是否经常变化来判断。
验证范围也必须看清:示例通过了 TypeScript 检查,以及使用脚本化模型响应的本地工具循环测试。测试实际执行了 AVA/DuckDB 查询,核对总额和对账结果,验证无效 queryId 会被拒绝,并检查生成 HTML 含有查询得到的图表数值。真实模型行为、全新 npm 安装和浏览器渲染均未验证。
最后,收入数据只能支持「Analytics 增加 6,000,贡献净增额的 80%」,不能支持「因为促销所以增长」。数据里没有促销日期、价格、客户数量或对照组。提示词要求模型遵守边界,依然需要人工检查解释。
能拿来做什么
- 做 Node 数据分析原型时,先准备一份能手算的小数据,同时写清周期、差值方向、增长率分母和零基线处理,再检查 Agent 生成的 SQL。
- 给分析工具增加
queryId,让后续图表直接读取应用保存的查询结果,并校验字段、行数和截断状态,减少模型重复传递数字的环节。 - 面对大数据集,先在 DuckDB 中聚合,再把必要结果返回模型;接入多人应用时,为每次分析分别设置数据会话、输出存储和执行边界。
- 按任务选择 AVA 的接口层次。需要检查 SQL 和图表映射时,可以参考这里的
query()、viz();原文还提供了更高层的analyze()、visualize(),但没有展开两种方式的效果比较。