Snowflake Cortex AISQLのAI_AGG関数を試してみる

# 1. はじめに

Snowflake Cortex には SQL から LLM の機能を呼び出せる AISQL という関数群があります。以前から LLM functions と呼ばれるものがありましたが、今後は AISQL がメインストリームになっていくのではと思います（2025/06時点では Public Preview です）。

AISQL の中でも AI_AGG 関数というものがあり、テーブルのあるカラムに含まれる複数のテキストデータを集約して LLM に処理させる機能となります。例えば、テキストデータを

- 要約する
- 感情分析する
- 条件に合致する箇所を抽出する

といったことが可能になります。

https://docs.snowflake.com/en/sql-reference/functions/ai_agg

これにより、データを集計した後の分析・解釈を LLM に任せることができるのではという観点で私は AI_AGG 関数に興味を持っています（Cortex Agents と使い分けを考える必要はありますが）。

今回はこの AI_AGG 関数でいろいろ試してみたいと思います。

# 2. 準備

まず AI_AGG に処理させるデータを準備します。今回は書籍のレビューデータを模したものを利用します。

```sql
create or replace table book_reviews (
    review_id integer
        comment 'レビューID',
    book_id integer
        comment '書籍ID',
    review_point integer
        comment '5段階のレビュー点。1=とよて良い/2=良い/3=普通/4=悪い/5=非常に悪い',
    review_comment text
        comment 'レビューコメント'
        
)
comment = 'ECサイトXYZにおける書籍のレビュー情報'
;

insert into book_reviews
values
(101, 201, 1, '非常に実践的な内容で参考になりました。'),
(102, 201, 1, 'この手の情報がまとまっている書籍は他にほぼなく、とても貴重な本です。'),
(103, 201, 2, '少し表現が気になる箇所もありますが、網羅的に説明しており有用。'),
(104, 201, 1, null),
(105, 201, 1, 'この分野の新しい教科書と思ってもよいと思います。'),
(106, 202, 4, 'タイトルに「入門」とありますが、実際にはリファレンス本で初級者が読む本ではない。'),
(107, 202, 5, '１章から説明なく専門用語が使われており、読み進めるのに挫折しました。'),
(108, 202, 5, null),
(109, 203, 1, 'フレームワークXXXのとよてもよい入門書です。'),
(110, 203, 2, 'XXXの初心者でしたが、非常にわかりやすかったです。'),
(111, 203, 4, 'XXXのバージョンが変わって、この本に載っているサンプルコードはほぼ動きません。'),
(112, 203, 5, 'XXXの古いバージョンについての本であり、現在では読む価値はない。');
```

いくつか仕込みを入れています。

- ```review_point``` 列は名前のイメージに反して 1 の方が高評価としています。この旨は列のコメントに記載しています。
    - book_id = 201 は高評価
    - book_id = 202 は低評価
    - book_id = 203 は高評価/低評価が分かれている
- ```review_comment``` 列に null を 2 行含めています。

# 3. AI_AGG を実際に試してみる

## 3-1. レビューコメントの要約（通常の使い方）

```book_id``` ごとに ```review_comment``` を要約してみます。

```sql
select
    book_id,
    ai_agg(review_comment, 'Summarize the book review comments. Reply in Japanese.') output
from
    book_reviews
group by all
order by 1;
```

```ai_agg``` 関数は 2 つの引数を受け取ります。

- 第 1 引数：処理対象のカラム。今回はレビューコメントの要約なので ```review_comment``` を渡します。
- 第 2 引数：処理させたい内容（プロンプト）。マニュアルによると "plain English text" である必要があるようです。ただし、単純に英語で指示すると英語で返ってくるため、```Reply in Japanese``` という追加の指示を入れています。

結果は以下になります。

| BOOK_ID | OUTPUT |
|-:|:-|
| 201 | この本は非常に実践的な内容で、参考になります。他に同様の情報がまとまっている書籍はほぼなく、貴重な本です。表現が気になる箇所もありますが、網羅的に説明しており有用です。内容はこの分野の新しい教科書としても適していると言えるでしょう。|
|202 | この本はタイトルに「入門」とあるものの、実際には初心者向けではなくリファレンス本である。専門用語が最初の章から説明なく使われているため、初級者が読み進めるのは難しい。|
| 203 | この本はフレームワークXXXの入門書としてとてもよいと評価されています。初心者でもわかりやすい内容で書かれています。しかし、XXXのバージョンが変わったため、本に載っているサンプルコードはほとんど動作しないという欠点があります。また、古いバージョンについての本であるため、現在では読む価値はないと考えられます。|

要約というより繋げているだけな気もしますが、まぁ想定通りに動いているようです。

## 3-2. レビュー点の分析（データの意味が必要な分析）

今度は ```review_point``` を分析してみます。

```sql
select
    book_id,
    ai_agg(review_point, 'Analyze the book review points. Reply in Japanese.') output
from
    book_reviews
group by all
order by 1;
```

```review_point``` 列は integer 型ですが、問題なく渡せるようですね。

結果は以下になります。

| BOOK_ID | OUTPUT |
|-:|:-|
| 201 | このレビューの評価は低いと言える。ほとんどの評価が1で、2が1つあるのみである。全体的な評価は1が多く、平均的な評価は低い。|
| 202 | レビューの評価は、4、5、5と高い評価が与えられています。全体的に高い評価が与えられているため、本書は読者に好意的に受け止められていることがわかります。評価の平均は4.67と高い水準にあり、レビューの内容も大体の評価が高いことが予想されます。|
| 203 | 提供されたレビューは数値のみで構成されており、具体的な内容は不明です。レビューの内容が数値のみである場合、分析することが難しいです。通常、レビューには本の内容、評価、感想などが記載されていますが、この場合は数値のみが記載されているため、分析することができません。もしレビューの内容が提供された場合、具体的な分析を行うことができます。|

book_id = 201/202 のデータに関しては、評価が逆転してますね。想像していましたが、テーブルのコメントは読んでくれないみたいですね（列を渡しているわけではなく値をあたしているだけなので当たり前ですが）。

以下のようにプロンプトに ```review_point``` の定義を付け加えると、期待する動作になります。

```sql
select
    book_id,
    ai_agg(review_point, 'Analyze the book review points. Reply in Japanese. Review point means 1=Very Good, 2=Good, 3=Normal, 4=Bad, 5=Very Bad') output
from
    book_reviews
group by all
order by 1;
```

| BOOK_ID | OUTPUT |
|-:|:-|
| 201 | このレビューの評価は非常に高いです。ほとんどの評価が「１」（非常に良い）で、１つの評価が「２」（良い）であることがわかります。全体的には、レビューの内容は非常に高く評価されており、ほぼ満点の評価と言えるでしょう。|
| 202 | このレビューの評価は、全体的に低いです。3つの評価のうち、2つが「最悪」（5）で、1つが「悪い」（4）です。全体的には、レビューの内容は非常に悪いと言えるでしょう。|
| 203 | このレビューの評価は、1（非常に良い）、2（良い）、4（悪い）、5（非常に悪い）と幅があります。全体的には、評価のバラつきが大きく、賛否両論があるようです。非常に良い評価と悪い評価が混在しているため、レビューの内容は多様で、読者の評価が分かれていると言えるでしょう。|

データの意味などはちゃんとプロンプトに入れましょうということですね。

## 3-3. null のカウント

```book_id``` ごとに null がどれぐらいあるか数えてみます。

```sql
select
    book_id,
    ai_agg(review_comment, 'Tell me how many comments are empty.') output
from
    book_reviews
group by all
order by 1;
```

結果は以下になります。

| BOOK_ID | OUTPUT |
|-:|-:|
| 201 | 0 |
| 202 | 0 |
| 203 | 0 |

想定する結果とは異なります。```ai_agg``` 関数は集約関数なので、null は除外するみたいですね（```count``` や ```sum``` と同様）。

ちなみに、以下のクエリ（null を空文字に置換）では、想定通りの結果になります。

```sql
select
    book_id,
    ai_agg(nvl(review_comment, ''), 'Tell me how many comments are empty.') output
from
    book_reviews
group by all
order by 1;
```

## 3-4. 複数列の分析

ここまで、1 つの列に注目して ```ai_agg``` 関数を使ってきましたが、実際の分析では複数列の情報を考慮した分析もしてほしいところです。ただし、```ai_agg``` 関数は処理対象の式として１つしか受け入れないため、そのままでは複数列を分析できません。

そこで、複数列の値を JSON 文字列に変換して無理やり ```ai_agg``` 関数に渡してみます。

```sql
with book_reviews_json
as (
    select
        object_construct(
            'review_id', review_id,
            'book_id', book_id,
            'review_point', review_point,
            'review_comment', review_comment
        ) review_data
    from
        book_reviews
)
select
    ai_agg(
        to_json(review_data), 
        'Tell me the book ID which has best review points(Review point means 1=Very Good, 2=Good, 3=Normal, 4=Bad, 5=Very Bad). And Summary review comments of this book. Reply in Japanese.'
    ) output
from
    book_reviews_json
;
```

```review_point``` が一番高い ```book_id``` を抽出し、その ```review_comment``` を要約するように指示しています。結果は以下になります。

|OUTPUT|
|:-|
|201 非常に実践的な内容で参考になりました。この手の情報がまとまっている書籍は他にほぼなく、とても貴重な本です。少し表現が気になる箇所もありますが、網羅的に説明しており有用。この分野の新しい教科書と思ってもよいと思います。|

想定通りの回答ですね。

大量のレコードをこの方法でうまく処理できるとは思いませんが（LLM は計算などしているわけではないので）、集計結果などを入力として、そこから何が言えるかを LLM に考えさせるということができる可能性はあるのではないでしょうか。

# 4. 消費トークンの確認

```ai_agg``` 関数に限らず Snowflake Cortex における LLM 消費コストは ```cortex_functions_query_usage_history``` ビューで確認することができます（このビューはクエリ実行から反映まで数時間掛かることがあります）。

https://docs.snowflake.com/en/sql-reference/account-usage/cortex_functions_query_usage_history

```sql
-- 今回実行したクエリの query_id
with query_list
as (
    select '3-1.' section_no, '01bd311c-0205-b76f-0070-e507008f1bb2' query_id
    union all select '3-2.', '01bd3120-0205-b6db-0070-e507008f546e'
    union all select '3-3.', '01bd3128-0205-b6db-0070-e507008f54c2'
    union all select '3-4.', '01bd3130-0205-b6db-0070-e507008f5512'
)
select
    ql.section_no,
    trunc(qh.total_elapsed_time /1000, 1) elapsed_time_sec,
    uh.function_name,
    uh.model_name,
    uh.tokens,
    uh.token_credits,
from
    query_list ql
inner join
    snowflake.account_usage.query_history qh
    on (ql.query_id = qh.query_id)
left outer join
    snowflake.account_usage.cortex_functions_query_usage_history uh
    on (qh.query_id = uh.query_id)
order by 1;
```

|SECTION_NO | ELAPSED_TIME_SEC | FUNCTION_NAME | MODEL_NAME | TOKENS | TOKEN_CREDITS|
|-:|-:|-:|-:|-:|-:|
|3-1. | 7.2 | AI_AGG |  | 1536 | 0.002457600|
|3-2. | 6.8 | AI_AGG |  | 1259 | 0.002014400|
|3-3. | 1.6 | AI_AGG |  | 1145 | 0.001832000|
|3-4. | 4.9 | AI_AGG |  | 1783 | 0.002852800|

次はネタですが、

```sql
with r
as (
    select
        tokens
    from
        table(result_scan(last_query_id()))
)
select ai_agg(tokens, 'Sum all tokens') output from r;
```

```text:出力結果
-1536 + (-1259) = -2795
-2795 + (-1145) = -3940
-3940 + (-1783) = -5723

The sum is -5723.
```

結果はなぜかマイナスになっていますが、5723 というのは合っています。```ai_agg``` 関数は 1.6 クレジット / 1M トークン、今回の Snowflake 環境は $2 / クレジットなので、5723 / 1000000 * 1.6 * 2 = 約 0.2 ドルといったところですね。

# 5. AI_AGG 関数はどの LLM を使っているのか

ドキュメントには ```ai_agg``` 関数がどの LLM を使っているかは明示的に記載されていません。そのため断定はできません。

ちなみに、以下のようなクエリの実行結果を見ると、Llama なのかもしれません。

```sql
select
    ai_agg('What LLM are you?', 'Answer the question.')
;
```

```text:出力結果
I am an instance of a Meta AI model, specifically a variant of the LLaMA (Large Language Model Application) architecture.
```

また、先に記載した通り、1M トークンあたりの消費クレジットは 1.6 になります。普通に Complete API を叩いた時より安くはならないが極端に離れはしないだろうと考えると、候補は以下になるのではと思います。

- deepseek-r1：1.03 クレジット / 1M トークン
- llama3.1-70b：1.21 クレジット / 1M トークン
- openai-gpt-4.1：1.4 クレジット / 1M トークン
- pixtral-large：1.25 クレジット / 1M トークン

プロンプトが英語推奨となるとやっぱり Llama のような気もしますが、結局断定はできないですね。

# 6. まとめ

今回の試行をまとめると、

- プロンプトにはカラムの意味などのメタデータは十分渡すべき
- null は除外されるので注意
- 複数列を処理したい場合は JSON 化して渡すといける可能性がある
- LLM は Llama を使っているような気もするが断定はできない

といったところでしょうか。

あと、少し気になるのはドキュメントには "this function supports datasets larger than the maximum language model context window." と書いてあります。どういう処理をしてモデルコンテキストウィンドウの制約を回避しているのでしょうか。