BigQuery调用Gemini成本揭秘: 优化省钱指南
2025-05-02 17:14:53
BigQuery 调用 Gemini 大模型:成本账怎么算?
最近在捣鼓 BigQuery 的生成式 AI 函数,特别是 ml.generate_text
,想用它(比如搭配 Gemini 1.5 Flash)来给数据做点“智能”标注,丰富一下信息维度。直接在 BigQuery 里就能调用大模型,整合进现有的 ETL 流程里看着确实方便。比如,想区分一大堆客户邮箱域名,哪些是公共邮箱(像 gmail.com, yahoo.com 这类),哪些是公司或私有域名。
但有个头疼的问题:这玩意儿的成本到底怎么算?要是真像示例里那样,对着十几万甚至更多的独立域名,每一行都跑一次 ml.generate_text
,这靠谱吗?
-- 假设有个包含不同域名的临时表 dist_domain
-- email_domain 列包含像 "gmail.com", "mycompany.com" 这样的值
SELECT
prompt,
email_domain,
ml_generate_text_llm_result
FROM
ML.GENERATE_TEXT(
MODEL `your-project.your-dataset.your_bqml_llm_model`, -- 指定你创建的指向 Gemini 的模型
(
SELECT
CONCAT(
'判断以下邮箱域名: (', email_domain, ') 是否为常见的公共邮箱域名。',
'常见的公共邮箱提供商包括 Gmail, Yahoo, Hotmail, Outlook 等。',
'结果请以 JSON 格式返回,结构如下: ',
CHR(10), -- 换行符
'{ "is_common": true/false }',
CHR(10),
'无需外部查找,基于你的知识库判断即可。不要解释,只要格式化的 JSON 结果。'
) AS prompt,
email_domain -- 将 email_domain 传递出去,方便结果关联
FROM
dist_domain -- 从你的域名来源表查询
-- LIMIT 10 -- 测试阶段可以加上限制,降低成本和时间
),
STRUCT(
TRUE AS flatten_json_output, -- 直接将 JSON 内容打平输出成列
0.2 AS temperature -- 控制创造性,对于分类任务可以低一些
-- 其他参数按需添加,比如 top_k, top_p 等
)
);
像上面这样,给每一行数据(email_domain
)都生成一个独立的 prompt,然后调用 ml.generate_text
。如果数据量是 10 万行,感觉这查询会跑很久,而且是不是意味着要发 10 万次请求给 Gemini?费用是按所有 prompt 返回的 token 总量算,还是每次调用都要单独收费?另外,BigQuery 本身的计算资源(槽位 slot)因为等待 AI 模型返回结果而长时间运行,这部分的费用会跟着涨吗?
这些问题不搞清楚,贸然在生产环境用心里实在没底。咱们就来拆解一下 BigQuery 调用生成式 AI 的成本构成,以及怎么尽可能地省点钱。
一、成本构成:钱花在哪儿了?
理解成本的第一步,是弄清楚计费项。在 BigQuery 里用 ml.generate_text
调用 Gemini 这类模型,成本主要来自两个方面:
-
BigQuery ML 外部模型调用费 (Vertex AI 定价):
- 这是大头。
ml.generate_text
本质上是通过 BigQuery 这个入口,调用了 Google Cloud 上的 Vertex AI 平台托管的生成式 AI 模型(比如 Gemini 系列)。 - 收费方其实是 Vertex AI,而不是 BigQuery 计算本身。费用的依据是模型的输入 token 数量 和输出 token 数量 。
- 重点来了: 针对一个 SQL 查询里的
ml.generate_text
调用,即使你处理了 10 万行,产生了 10 万个内部 prompt(根据你的 SQL 逻辑),Vertex AI 会将这次查询涉及的所有行的输入 token 和输出 token 加起来,进行一次总的计费 。不是按内部调用的次数(行数)来独立计费的。简单来说,是“批量处理,统一结算”。 - 不同模型(比如 Gemini 1.5 Pro vs Gemini 1.5 Flash)的 token 单价是不一样的,Flash 通常便宜很多。计费单位通常是每千个 token (input/output 分别计价)。具体价格要去查阅 Google Cloud Vertex AI 的最新定价页面。
- 这是大头。
-
BigQuery 标准计算费 (Analysis Pricing):
- 这个是你跑任何 BigQuery SQL 查询都会涉及的成本。它可以是按需付费(按处理的字节数),或者是按容量付费(预留或 Flex Slot)。
- 当你的 SQL 查询包含
ml.generate_text
时,BigQuery 的计算引擎需要等待 Vertex AI 模型处理完所有行的请求并返回结果。 - 所以,是的,如果 AI 模型处理慢,导致你的整个 SQL 查询运行时间变长,那么 BigQuery 的计算资源(slots)占用时间也会相应增加,这部分的费用自然会上升 。特别是如果你使用的是按需付费或者 Flex Slot,运行时间越长,消耗的 slot-milliseconds 就越多,费用越高。
总结一下:你需要支付 Vertex AI 的模型调用费(按 token 总量) 加上 BigQuery 的计算资源费(按查询运行消耗) 。
二、如何优化成本?省钱妙招来了!
知道了钱是怎么花的,接下来就好办了。下面是一些实用的省钱策略和最佳实践:
1. 批量处理输入 (Batching Prompts)
既然是按 token 总量计费,而且单次查询内的多次调用会聚合计算,那么是不是可以把多个数据项打包到一个 prompt 里,减少调用次数(虽然内部 BigQuery 可能会优化,但显式打包有时更优),同时也可能减少总的 token 消耗(特别是 prompt 的固定说明部分)。
-
原理: 将多个 email domain 合并到一个 prompt 里,让模型一次性判断多个。这样,任务的指令文本(比如 "请判断以下域名是否为公共邮箱...")只需要发送一次,而不是每行重复发送。
-
代码示例 (使用
STRING_AGG
):WITH BatchedDomains AS ( SELECT -- 将多个域名用特定分隔符(比如换行符)拼接起来 STRING_AGG(email_domain, '\n') AS domain_batch, -- 可以加个批次 ID 或者记录包含的域名列表,方便后续拆分结果 ARRAY_AGG(email_domain) AS original_domains FROM dist_domain -- 这里需要根据 prompt 长度限制和模型处理能力来决定批次大小 -- 下面是一个按行号分组的简单示例,实际可能需要更复杂的逻辑 GROUP BY CAST(ROW_NUMBER() OVER () / 100 AS INT64) -- 每 100 个域名一个批次 ) SELECT -- 这里需要写一个能够解析批量结果的逻辑 -- 可能需要模型返回一个 JSON 数组,或者特定格式的文本 ml_generate_text_llm_result, original_domains FROM ML.GENERATE_TEXT( MODEL `your-project.your-dataset.your_bqml_llm_model`, ( SELECT CONCAT( '判断以下每个邮箱域名是否为常见的公共邮箱域名。', '常见的公共邮箱提供商包括 Gmail, Yahoo, Hotmail, Outlook 等。', '请为列表中的每个域名返回结果。结果请以 JSON 数组格式返回,每个对象包含 "domain" 和 "is_common" 字段。', '例如: [{"domain": "gmail.com", "is_common": true}, {"domain": "mycompany.com", "is_common": false}]。', CHR(10), -- 换行符 '域名列表如下:', CHR(10), domain_batch -- 传入拼接好的域名字符串 ) AS prompt, original_domains -- 把原始域名数组传出去,方便对齐结果 FROM BatchedDomains ), STRUCT( TRUE AS flatten_json_output -- 这个可能需要关掉,或者后续自行解析 JSON 字符串 -- ... 其他参数 ) ); -- 注意:后续你需要用 JSON 函数(如 JSON_EXTRACT_ARRAY, UNNEST)来解析 ml_generate_text_llm_result 列里的 JSON 数组, -- 并将其与 original_domains 数组关联起来,得到每个域名的判断结果。
-
额外提醒:
- 注意模型的输入 token 限制 。一个 prompt 不能无限长。Gemini 模型有较大的上下文窗口,但也有限制(比如 Gemini 1.5 Flash 可能是 1M token,但具体查阅文档)。你需要估算拼接后的
domain_batch
加上指令文本的总 token 数。 - 结果解析变复杂: 模型返回的结果需要你额外处理,比如用 JSON 函数解析返回的数组,然后
UNNEST
,再与原始数据关联。 - 错误处理: 如果一个批次中的某个域名导致模型出错,可能会影响整个批次的结果。
- 注意模型的输入 token 限制 。一个 prompt 不能无限长。Gemini 模型有较大的上下文窗口,但也有限制(比如 Gemini 1.5 Flash 可能是 1M token,但具体查阅文档)。你需要估算拼接后的
2. 减少不必要的调用:缓存结果
对于已经处理过的域名,没必要反复让模型再判断一次。
-
原理: 把已经获取到的结果存起来(比如存到另一张 BigQuery 表里),每次处理新数据前,先查一下缓存表。
-
步骤:
- 创建一张缓存表
domain_classification_cache
,包含email_domain
和is_common
(或其他结果) 字段,并将email_domain
设置为主键或唯一索引。 - 在你的主 ETL 流程或查询中,使用
LEFT JOIN
连接缓存表。 - 只对缓存表中没有记录(
cache.email_domain IS NULL
)的域名调用ml.generate_text
。 - 将新获取的结果
INSERT
或MERGE
到缓存表中。
- 创建一张缓存表
-
代码片段 (示意):
-- 假设已有缓存表: domain_classification_cache (email_domain STRING, is_common BOOL) -- 1. 找出需要调用 LLM 的新域名 WITH NewDomains AS ( SELECT DISTINCT T.email_domain FROM dist_domain AS T LEFT JOIN domain_classification_cache AS C ON T.email_domain = C.email_domain WHERE C.email_domain IS NULL ), -- 2. 调用 LLM 处理新域名 LlmResults AS ( SELECT prompt, email_domain, -- 假设模型返回 { "is_common": true/false } 格式的 JSON JSON_EXTRACT_SCALAR(ml_generate_text_llm_result, '$.is_common') = 'true' AS is_common_result FROM ML.GENERATE_TEXT( MODEL `your-project.your-dataset.your_bqml_llm_model`, ( SELECT CONCAT( -- ... 你的 prompt ... ) AS prompt, email_domain FROM NewDomains ), STRUCT(TRUE AS flatten_json_output) ) ) -- 3. (可选,但推荐) 将新结果写入缓存表 -- 使用 MERGE INTO 更健壮 -- MERGE domain_classification_cache C -- USING LlmResults R ON C.email_domain = R.email_domain -- WHEN NOT MATCHED THEN -- INSERT (email_domain, is_common) VALUES (R.email_domain, R.is_common_result); -- 4. 合并缓存结果和新结果 SELECT T.email_domain, COALESCE(C.is_common, R.is_common_result) AS is_common FROM dist_domain AS T LEFT JOIN domain_classification_cache AS C ON T.email_domain = C.email_domain LEFT JOIN LlmResults AS R ON T.email_domain = R.email_domain;
-
进阶技巧:
- 考虑缓存的时效性 。如果判断标准可能变化(虽然邮箱域名是否公共变化不大),需要考虑缓存刷新策略。
- 对于超大规模缓存,确保查询缓存表的性能。
3. 精简 Prompt (Prompt Engineering)
Prompt 的内容直接影响输入 token 数量,模型的回应方式也影响输出 token 数量。
- 原理: 写出简洁、明确的 prompt。去掉不必要的寒暄、背景介绍和过于冗长的指令。精确指定输出格式,避免模型自由发挥产生冗余信息。
- 示例:
- 原始:
Using the following email domain: (gmail.com) provide results in JSON with the following structure: \n- is_common: BOOLEAN result if the domain is widely used by the public, comes from a common email provider, or comes from an internet service provider. Domains like gmail/yahoo/hotmail/etc are considered public domains. \nDo not perform any external lookups or searches. Base your decision on your existing knowledge. An explanation is not required. Just the formatted results.
- 精简后:
Is 'gmail.com' a common public email domain (like gmail, yahoo, hotmail)? Answer in JSON: {"is_common": boolean}
- 对比一下,精简版的 token 数会少很多。同时,明确要求 JSON 输出
{"is_common": boolean}
能有效控制输出的格式和长度。
- 原始:
- 额外提醒:
- 测试! 不同模型的理解能力不同,过于精简可能导致模型误解。需要反复测试找到效果和成本的最佳平衡点。
- 使用
flatten_json_output = TRUE
参数可以省去自己解析 JSON 的麻烦,但也可能在某些复杂场景下不够灵活。
4. 选择合适的模型
不是所有任务都需要最强、最贵的模型。
-
原理: Vertex AI 提供了不同能力和价位的模型。比如 Gemini 1.5 Flash 通常比 Gemini 1.5 Pro 便宜,处理速度可能也更快。对于相对简单的任务(如判断邮箱域名是否公共),Flash 可能就足够了。
-
操作: 在创建 BigQuery ML 模型时,或者在
ML.GENERATE_TEXT
的MODEL
参数里,指定你想要使用的模型端点(endpoint)。-- 创建模型时指定 endpoint CREATE OR REPLACE MODEL `your-project.your-dataset.gemini_15_flash_model` REMOTE WITH CONNECTION `your-project.your-region.your-vertex-connection` OPTIONS (endpoint = 'gemini-1.5-flash-001'); -- 在查询中直接指定不同模型(如果模型已创建) SELECT ... FROM ML.GENERATE_TEXT(MODEL `your_gemini_flash_model`, ...); -- 或者 SELECT ... FROM ML.GENERATE_TEXT(MODEL `your_gemini_pro_model`, ...);
-
额外提醒:
- 先用小数据集测试不同模型的效果(准确率)和成本 ,再做决定。
- Google Cloud 的模型列表和定价会更新,保持关注。
5. 规则预处理 + AI 兜底 (Hybrid Approach)
很多常见的公共域名是固定的,根本不需要 AI 来判断。
-
原理: 先用 SQL 的规则(比如
LIKE
,IN
, 正则表达式)处理掉那些确定是公共或私有的域名。只有规则无法覆盖的“疑难杂症”才交给 AI 处理。 -
代码示例:
WITH PreFilteredDomains AS ( SELECT email_domain, CASE WHEN email_domain IN ('gmail.com', 'googlemail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'live.com', 'aol.com', 'msn.com' /* 添加更多已知公共域名 */) THEN TRUE WHEN email_domain LIKE '%.gov' OR email_domain LIKE '%.edu' THEN FALSE -- 假设 gov/edu 不是公共邮箱定义 -- 添加更多明确的规则 ELSE NULL -- 规则无法判断,标记为 NULL,交给 LLM END AS preliminary_is_common FROM dist_domain ), DomainsForLlm AS ( SELECT email_domain FROM PreFilteredDomains WHERE preliminary_is_common IS NULL ), LlmResults AS ( -- 只对 DomainsForLlm 调用 ML.GENERATE_TEXT SELECT email_domain, JSON_EXTRACT_SCALAR(ml_generate_text_llm_result, '$.is_common') = 'true' AS llm_is_common FROM ML.GENERATE_TEXT( MODEL `your-project.your-dataset.your_bqml_llm_model`, ( SELECT CONCAT( -- 你的 prompt ) AS prompt, email_domain FROM DomainsForLlm ), STRUCT(TRUE AS flatten_json_output) ) ) -- 合并结果 SELECT P.email_domain, COALESCE(P.preliminary_is_common, L.llm_is_common) AS final_is_common FROM PreFilteredDomains AS P LEFT JOIN LlmResults AS L ON P.email_domain = L.email_domain;
-
额外提醒:
- 这种方法可以极大减少 对 LLM 的调用次数和 token 消耗,特别是当数据中包含大量常见域名时。
- 维护规则列表需要一些成本,但对于稳定性要求高的场景是值得的。
6. 控制并发和设置预算
- 原理: BigQuery 对
ml.generate_text
调用 Vertex AI 端点有并发数限制。虽然不直接省钱,但可以避免瞬间产生过高费用和请求被限制。同时,可以在 Google Cloud 上设置预算提醒。 - 操作:
- 了解并遵守 BigQuery ML 对外部模型的并发查询限制。如果需要处理海量数据,可能需要分批执行查询。
- 在 Google Cloud Billing 中为你的项目或特定服务(如 Vertex AI)设置预算和告警,当费用接近或超过阈值时收到通知。
- 额外提醒: 这更多是风险控制,而非直接优化单次查询成本,但对于成本管理非常重要。
回到最初的问题:对 10 万行数据逐行调用 ml.generate_text
是否常见?可能在简单测试或小规模数据上会这样做,但对于大规模数据,直接这么干通常不是 最佳实践,因为成本和性能都可能成为问题。更明智的做法是结合使用上述提到的缓存、规则预处理、批量处理和选择合适模型等策略。
BigQuery 集成生成式 AI 确实为数据处理打开了新的想象空间,但“智能”是有代价的。理解成本结构,并且主动采取优化措施,才能让这项技术用得既有效,又经济。