Snowflake交互式数仓上的一体化AI应用与可观测性
DataHot 速览
作者在单个 Snowflake Interactive Warehouse 上同时构建零售运营 AI 应用和其可观测性管线。Cortex Agents 基于 Semantic View 生成 Text-to-SQL,查询约 2500 万行销售数据;AI Observability 自动将 trace、span、工具调用和 token 数写入 SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS,无需 SDK 或 collector。监控看板与 Agent 共用同一仓库,最终发现了一个被 Agent 100% 成功率掩盖的语义视图缺陷。完整 SQL、代码和 Streamlit 应用已发布在 GitHub。
为什么值得关注:对想落地 Data Agent 并解决其可观测性、语义一致性与评估问题的数据团队有直接参考价值;也展示了 Snowflake 交互式数仓、Cortex Agents 与 AI Observability 的一体化实践。
译文
AI 逐段翻译
简介
运行一个 AI 应用及其可观测性管道通常意味着两个独立的系统。我把两者都构建在一个 Snowflake 交互式仓库上。
交互式仓库为你提供亚秒级、高并发的查询执行,具备自动扩缩容能力,并为繁重的异常查询提供回退仓库。Cortex Agents让你能够基于语义视图构建 text-to-SQL 智能体。AI 可观测性会自动将每条 trace、span、工具调用和 token 计数捕获到 SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS 中。无需 SDK,无需采集器。智能体查询 2500 万行数据,可观测性仪表板以同样的速度读取同一个仓库,而监控捕获到了一个语义视图缺陷,而这个缺陷被智能体自身 100% 的成功率所掩盖。
我构建的内容:
- 一个零售运营 AI 智能体,基于 2500 万行销售数据(50 家门店,2 年)
- 一个为 Cortex Analyst 声明数据模型的语义视图
- 一个以并发方式运行智能体所生成 SQL 的交互式仓库
- 一个每小时运行的交互式表,将 AI 可观测性 trace 展平为可查询的列
- 一个用于监控的 Snowflake 内 Streamlit 仪表板,全部运行在同一个仓库上
- 基于监控所揭示的问题对语义视图进行的修复
前提条件: 一个已启用Cortex Agents和交互式分析的 Snowflake 账户(企业版)。

完整代码、SQL 和 Streamlit 应用位于 GitHub。
数据
想象你是一家中型零售连锁店的运营经理。50 家门店,5 个区域,两年的交易历史,数据量略低于 3000 万行。你的团队要处理诸如“本月西部哪些门店未达成目标?”和“第四季度哪些产品类别导致了退货?”之类的问题。这些问题过去意味着要向 BI 团队提交工单。目标是提供一个基于数据的自然语言界面,无需分析师介入。
RETAIL_DEMO 中的八张表:

完整 DDL 位于 GitHub 上的 setup/01_create_raw_tables.sql。
AI 智能体设置
运营经理用自然英语输入一个问题。Cortex Agent 使用语义视图作为数据模型将其转换为 SQL,在交互式仓库上针对零售表运行它,并返回答案。无需 BI 工单,无需 SQL 知识。
有两个对象使这成为可能:一个基于零售表的语义视图,然后在其上叠加一个 Cortex Agent。语义视图声明了存在哪些表、它们如何连接、哪些列是度量值还是属性、某个指标的含义是什么,以及用户可能如何称呼每样东西:
CREATE OR REPLACE SEMANTIC VIEW RETAIL_DEMO.APP.RETAIL_OPS_SV
TABLES (
daily_sales AS RETAIL_DEMO.RAW.DAILY_SALES PRIMARY KEY (sale_id)
WITH SYNONYMS ('sales', 'transactions', 'orders'),
stores AS RETAIL_DEMO.RAW.STORES PRIMARY KEY (store_id),
products AS RETAIL_DEMO.RAW.PRODUCTS PRIMARY KEY (product_id),
inventory AS RETAIL_DEMO.RAW.INVENTORY PRIMARY KEY (snapshot_id),
staff_shifts AS RETAIL_DEMO.RAW.STAFF_SHIFTS PRIMARY KEY (shift_id),
sales_targets AS RETAIL_DEMO.RAW.SALES_TARGETS PRIMARY KEY (store_id, period_month),
returns AS RETAIL_DEMO.RAW.RETURNS PRIMARY KEY (return_id)
)
RELATIONSHIPS (
daily_sales_to_stores AS daily_sales (store_id) REFERENCES stores (store_id),
daily_sales_to_products AS daily_sales (product_id) REFERENCES products (product_id),
inventory_to_stores AS inventory (store_id) REFERENCES stores (store_id),
inventory_to_products AS inventory (product_id) REFERENCES products (product_id),
returns_to_daily_sales AS returns (sale_id) REFERENCES daily_sales (sale_id)
)
FACTS (
daily_sales.revenue_fact AS revenue,
daily_sales.units_sold_fact AS units_sold,
inventory.units_on_hand_fact AS units_on_hand,
sales_targets.target_revenue_fact AS target_revenue
)
DIMENSIONS (
stores.region AS region
WITH SYNONYMS = ('area', 'territory')
SAMPLE_VALUES ('West', 'Northeast', 'Midwest', 'South', 'Southwest') IS_ENUM,
daily_sales.sale_date AS sale_date WITH SYNONYMS = ('transaction date'),
products.category AS category SAMPLE_VALUES ('Apparel', 'Electronics', 'Home', 'Food') IS_ENUM
)
METRICS (
daily_sales.total_revenue AS SUM(revenue_fact),
returns.return_count AS COUNT(*),
return_rate_pct AS returns.return_count / NULLIF(daily_sales.transaction_count, 0) * 100
);完整语义视图包含 7 张表、9 个关系、13 个事实、16 个维度和 17 个指标。完整 DDL 位于 GitHub 上的 setup/08_create_semantic_view_raw.sql。
模型不是在猜测列名。它读取一个声明的模型。同义词和示例值看起来比实际更重要。它们决定了“territory”如何解析为 region,以及模型如何得知 regions 是一组封闭的五个值。
然后智能体:
CREATE OR REPLACE AGENT RETAIL_DEMO.APP.RETAIL_OPS_AGENT
FROM SPECIFICATION $$
models:
orchestration: claude-haiku-4-5
tools:
- tool_spec:
type: "cortex_analyst_text_to_sql"
name: "RetailAnalyst"
tool_resources:
RetailAnalyst:
semantic_view: "RETAIL_DEMO.APP.RETAIL_OPS_SV"
execution_environment:
type: warehouse
warehouse: "RETAIL_AI_WH"
$$;RETAIL_AI_WH 是交互式仓库,本文的两部分都在这里汇合。
交互式仓库设置
零拷贝交互式意味着你完全跳过 CREATE INTERACTIVE TABLE 步骤。没有数据副本,没有 TARGET_LAG 刷新,也没有运行该刷新的额外仓库。将你的基表直接附加到交互式仓库,即可对现有数据获得完整的交互式速度。
大多数人听到“交互式”就以为需要将表转换为一种特殊类型。交互式表是一种选择,但速度来自仓库,而不是表类型。你附加到交互式仓库的任何表都会获得缓存预热和亚秒级执行,包括共享表和诸如 AI_OBSERVABILITY_EVENTS 之类的系统表:
-- Cluster the base tables date-first, matching how queries actually filter
ALTER TABLE RETAIL_DEMO.RAW.DAILY_SALES CLUSTER BY (sale_date, store_id, product_id);
ALTER TABLE RETAIL_DEMO.RAW.INVENTORY CLUSTER BY (snapshot_date, store_id, product_id);
ALTER TABLE RETAIL_DEMO.RAW.RETURNS CLUSTER BY (return_date, store_id);
ALTER TABLE RETAIL_DEMO.RAW.STAFF_SHIFTS CLUSTER BY (shift_date, store_id);
ALTER TABLE RETAIL_DEMO.RAW.SALES_TARGETS CLUSTER BY (period_month, store_id);
-- Search optimization for point lookups on UUID keys
ALTER TABLE RETAIL_DEMO.RAW.DAILY_SALES ADD SEARCH OPTIMIZATION ON EQUALITY(sale_id);
-- The Interactive Warehouse
CREATE OR REPLACE INTERACTIVE WAREHOUSE RETAIL_AI_WH
WAREHOUSE_SIZE = 'MEDIUM'
MIN_CLUSTER_COUNT = 1
MAX_CLUSTER_COUNT = 3;
-- Attach the base tables (triggers cache warming)
ALTER WAREHOUSE RETAIL_AI_WH ADD TABLES (
RETAIL_DEMO.RAW.DAILY_SALES, RETAIL_DEMO.RAW.INVENTORY,
RETAIL_DEMO.RAW.RETURNS, RETAIL_DEMO.RAW.STAFF_SHIFTS,
RETAIL_DEMO.RAW.SALES_TARGETS, RETAIL_DEMO.RAW.STORES,
RETAIL_DEMO.RAW.PRODUCTS
);
-- Fallback for occasional heavy queries
ALTER WAREHOUSE RETAIL_AI_WH SET FALLBACK_WAREHOUSE = RETAIL_FALLBACK_WH;聚类是实现分区裁剪的关键。语义视图必须指向所附加的表。如果它指向其他地方,生成的 SQL 就无法在交互式仓库上执行,整个设置在整体查询运行时间方面也就不会产生任何收益。
完整聚类 DDL 位于 setup/09_cluster_raw_tables.sql。
查询它
运营经理输入:“2025 年第四季度各区域的目标达成率是多少?”智能体读取语义视图,编写一个连接 DAILY_SALES、SALES_TARGETS 和 STORES 的查询,在交互式仓库上运行它,并返回答案。
以下是智能体针对该问题实际生成的 SQL 的末尾部分(完整查询在 GitHub 仓库中):
-- 3 CTEs above: one per table (SALES_TARGETS, STORES, DAILY_SALES),
-- each selecting only the columns needed, then aggregated into
-- q4_targets and q4_sales by region.
SELECT
COALESCE(t.region, a.region) AS region,
t.target_revenue,
a.actual_revenue,
ROUND(a.actual_revenue / NULLIF(t.target_revenue, 0) * 100, 1)
AS revenue_attainment_pct
FROM q4_targets AS t
FULL OUTER JOIN q4_sales AS a ON t.region = a.region
ORDER BY region;那段 SQL 是智能体直接编写的。智能体读取了语义视图中声明的关系、事实和维度,并据此组合了查询。
我测量的内容
接下来两节中的数字来自一个基准测试工具。完整脚本位于 GitHub 上的 setup/agent_latency_test.py。它直接调用智能体的 REST agent:run 端点,解析 SSE 事件流,并记录每轮的挂钟延迟、token 计数、SQL 扇出以及智能体执行的每条语句的查询 ID。这些查询 ID 是关键部分,因为它们让我之后能够将基准测试的各轮次关联回 QUERY_HISTORY 和 AI_OBSERVABILITY_EVENTS。
该基准测试有两层:
- 单用户延迟运行。 一组固定的五个问题——每个表集群一个(收入排名、库存警报、目标达成率、周环比趋势、人员配置与收入)——每个问题问两次。这些运行的中位轮次时间为 18.2 秒。
- 并发运行。 同一工具以 1、5 和 10 个模拟用户同时提交问题进行运行,每次运行 10 到 20 个问题。这产生了 53 个总轮次和 132 条执行的 SQL 语句,每一条都成功了。三个级别的延迟保持平稳(p50:19.1 秒、19.0 秒、23.2 秒),这正是接下来两节要剖析的结果。
在基准测试之前我还做了一个单独的流量模拟:151 轮混合即席问题按顺序运行。这些轮次稍后会在可观测性部分出现。
查询在哪里花费时间
这些是单用户运行的结果:

SQL 执行并不是瓶颈。可观测性 span 将中位数 19.1 秒的一轮 agent 交互拆分为四个部分:
- 规划(约 10,600ms,55%)——agent 读取语义视图、选择工具并编写 SQL。这是模型推理:平均每轮 3.3 个规划步骤,每一步都是一次对 LLM 的往返调用。第一个规划步骤(读取完整的语义视图上下文)大约需要 2.1 秒;后续步骤随着对话的累积而增长。
- 响应生成(约 4,400ms,23%)——模型根据 SQL 结果组织自然语言回答。同样受推理限制。
- SQL 执行(约 1,300ms,7%)——在 Interactive Warehouse 上运行的实际查询。在单条语句层面,p50 为 96ms(p95:438ms),平均扫描 63 MB。每轮总耗时更高,因为一轮平均包含 2.3 条 SQL 语句。
- 其他(约 2,700ms,15%)——路由、SSE 分帧、工具分派以及 span 之间的网络开销。
Interactive Warehouse 只占每轮时间的 7%,并且已经将其完全处理好。其余 93% 是模型推理。
并发
一个自然语言问题平均生成 2.4 条 SQL 语句(尾部可达 13 条)。乘以并发用户数,数字就会迅速增长。一个十人的运维团队同时提问,几秒内就可能有 25 条查询在途。

我在并发度为 1、5 和 10 的情况下运行了基准测试,每次运行包含 10 到 20 个问题。p50 每轮延迟保持平稳:19.1 秒、19.0 秒和 23.2 秒。所有运行中的全部 53 轮均成功,共生成 132 条 SQL 语句。更重要的是,看看 Interactive Warehouse 在底层做了什么:在三个并发级别下,SQL p50 都保持在 80–95ms,即使在 10 个并发用户下,SQL p95 也保持在 410ms 以下。随着并发度提高,仓库从 1 个集群自动扩展到 3 个集群,吸收了查询突发,而 SQL 执行时间没有任何下降。Interactive Warehouse 正是为此而构建的:高并发、短查询的大量负载,同时保持一致的响应时间。
这就是执行侧的正常工作。一位运维经理用通俗英语提问,agent 跨三张表编写 SQL,Interactive Warehouse 在 100ms 以内运行完成,十个人可以同时这样做而互不干扰。下一个问题是,agent 是否在做正确的事。
AI 可观测性
这个应用 agent 运行得非常好!现在我需要看看 AI 究竟做了什么才得出这些答案。
每次 Cortex Agent 轮次都会自动写入 SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS。trace、span、工具调用、延迟、状态、token 计数,以及生成的 SQL 本身。无需 SDK,无需 collector,由已有的 RBAC 进行治理。数据以原始 VARIANT 形式落地:
RECORD_ATTRIBUTES:"snow.ai.observability.agent.tool.sql_execution.status"::STRING
RECORD_ATTRIBUTES:"snow.ai.observability.agent.planning.token_count.total"::STRING
DATEDIFF('millisecond', START_TIMESTAMP, TIMESTAMP) -- no native DURATION_MS columnSpan 属性嵌套在长长的点分键下。Duration 每次都必须派生计算。每次仪表板刷新都要对每一行重新进行 JSON 提取。与那些已经位于可查询列中的零售表不同,这些数据需要一个物化步骤,将 VARIANT 一次性展平为带类型的列,而不是在每次读取时展平。
可观测性 Interactive Table
一条 DDL 语句即可将原始 VARIANT 展平为带类型的列,为仪表板查询进行聚簇,并每小时刷新:
CREATE OR REPLACE INTERACTIVE TABLE RETAIL_DEMO.APP.AGENT_TRACES_IT
CLUSTER BY (event_date, span_name, trace_id)
TARGET_LAG = '1 hour'
WAREHOUSE = RETAIL_REFRESH_WH
AS SELECT
TRACE:trace_id::STRING AS trace_id,
RECORD:name::STRING AS span_name,
DATE(TIMESTAMP) AS event_date,
DATEDIFF('millisecond', START_TIMESTAMP, TIMESTAMP) AS duration_ms,
RECORD_ATTRIBUTES:"snow.ai.observability.agent.thread_id"::STRING AS thread_id,
RECORD_ATTRIBUTES:"snow.ai.observability.agent.status"::STRING AS status,
RECORD_ATTRIBUTES:"snow.ai.observability.agent.tool.sql_execution.status"::STRING AS sql_status,
RECORD_ATTRIBUTES:"snow.ai.observability.agent.tool.sql_execution.final_sql"::STRING AS final_sql,
TRY_CAST(RECORD_ATTRIBUTES:"snow.ai.observability.agent.planning.token_count.total"::STRING AS NUMBER) AS tokens_total
-- 26 columns total, full DDL in setup/07
FROM SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS
WHERE RECORD_ATTRIBUTES:"snow.ai.observability.object.name" = 'RETAIL_OPS_AGENT';
-- Attach to the SAME Interactive Warehouse
ALTER WAREHOUSE RETAIL_AI_WH ADD TABLES (RETAIL_DEMO.APP.AGENT_TRACES_IT);Interactive Table 通过 TARGET_LAG 每小时自行刷新。无需定时任务,无需 INSERT OVERWRITE。而且它挂载在 RETAIL_AI_WH 上,也就是运行 agent 生成 SQL 的同一个 Interactive Warehouse。可观测性仪表板和 AI 应用共用一个仓库。
Streamlit 监控器
我在 Snowflake 中部署了一个 Streamlit 应用,通过同一个 Interactive Warehouse 读取 AGENT_TRACES_IT。四个标签页:
- 突发与延迟——一个问题触发多少条 SQL 查询?时间花在哪里?并发:每秒启动的 SQL 数。
- 可靠性——轮次级成功与 SQL 级失败。agent 凭空捏造的列。错误分类与自我修正模式。
- 成本与 Token——输入与输出 token 的拆解。最昂贵的轮次。缓存命中率。
- Trace 浏览器——选取任意单个轮次,查看完整的 span 瀑布图,阅读 agent 实际编写的 SQL。

我查看了可靠性标签页,发现了一些有趣的东西……

监控标签页
我构建的第一个指标是轮次级状态:151 轮中 151 次成功。这看起来不错,直到我检查了 span 级数据:

agent 在大约三分之一的 SQL 语句上进行了静默重试和自我修正。每一轮最终仍然得到了成功的回答,因此轮次级指标从未标记出任何问题。
这些失败遵循一种模式。116 次中有 115 次是错误 000904,无效标识符,集中在一小组模型凭空捏造的列名上。最常见的捏造列是 ST.TOTAL_TARGET_REVENUE,出现了 28 次:

我追溯到了语义视图中。SALES_TARGETS 是唯一没有注册 fact 的度量表。它的指标聚合了一个裸物理列,而其他六张表都投影了一个 _FACT 别名。模型在其他所有地方都学会了 _FACT 模式,在这张表上却找不到落点,于是回退到了指标名称。这一处不一致导致了所有标识符错误的 39%。修复只需两行:
FACTS (
...
sales_targets.target_revenue_fact AS target_revenue,
sales_targets.target_units_fact AS target_units
)该视图自身的 SQL 生成指令已经警告过这种确切情况。它仍然失败了。文字说明无法弥补结构上不一致的 schema。这就是可观测性的论据。失败只是一个列的注册问题,而它在 span 中清晰可见。
结论
AI 应用只有快到人们真正愿意使用,并且可靠到足以托付真实决策,才算有用。一位运维经理问“这个月哪些门店没有完成目标?”需要在几秒内得到答案,而不是几分钟。而当答案错误时,你需要在用户发现之前知道原因。
我已经向你展示了 Interactive Warehouses 如何解决速度问题,让 Cortex Agent 能在超过 2500 万行数据上以亚秒级执行 SQL。以及 AI Observability 如何解决信任问题,自动捕获每一条追踪、每一条失败的 SQL 语句,以及 agent 做出的每一次自我修正。监控发现了一个 semantic view bug,它在 40% 的轮次中静默消耗 token 并增加延迟。
以下是你如何在自己的账户中进行设置:克隆 repo 并运行 setup/01 到 setup/09。下面的 Resources 部分链接了相关文档。
资源
- GitHub:https://github.com/sfc-gh-wreerink/interactive-ai-blog-public
- Snowflake Interactive Analytics
- AI Observability 文档
- Semantic Views
- Cortex Agents
感谢你阅读这篇博客,请告诉我你计划如何在你的组织中使用 Interactive Warehouses 和 AI Observability。谢谢!
免责声明:本文中表达的观点仅代表我个人,不一定代表我的雇主(Snowflake)的观点。
在一个 Snowflake Interactive Warehouse 上运行 AI App 及其 Observability最初发表于 Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science 在 Medium 上,人们在那里通过高亮和回应这个故事来继续对话。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏