無駄と文化

実用的ブログ

BigQueryのCASTとSAFE_CASTは名前が逆じゃないか?

ふたつのキャスト

BigQuery には値をキャストする関数として CAST()SAFE_CAST() の二つがある。
このように動作する。

SELECT CAST('0xDEADBEEF' AS INT64) AS number;
/*------------*
 | number     |
 +------------+
 | 3735928559 |
 *------------*/

SELECT SAFE_CAST('0xDEADBEEF' AS INT64) AS number;
/*------------*
 | number     |
 +------------+
 | 3735928559 |
 *------------*/

この時点では挙動が同じなので違いは見えない。キャストに失敗する値を渡してやると違いが見える。

SELECT CAST('0xDEADCHICKEN' AS INT64) AS number;
/* Error: Bad int64 value: 0xDEADCHICKEN */

SELECT SAFE_CAST('0xDEADCHICKEN' AS INT64) AS number;
/*--------*
 | number |
 +--------+
 | NULL   |
 *--------*/

16進数としてパースできない '0xDEADCHICKEN' を渡した場合、CAST はエラーになる。一方で SAFE_CAST はエラーにはならず NULL が返る。

キャスト可能な引数 キャスト不可な引数
CAST CAST('42' AS INT) = 42 CAST('a' AS INT) → エラー
SAFE_CAST SAFE_CAST('42' AS INT) = 42 SAFE_CAST('a' AS INT) IS NULL

 

ドキュメントにも書いてある。

cloud.google.com

If you want to protect your queries from these types of errors, you can use SAFE_CAST.

意訳: 実行時エラーからクエリを守りたいなら SAFE_CAST を使ってくれ

なるほど。SAFE_CAST を使えば安全。

 

逆じゃない?

設計原則に "Let It Crash" というのがある。
私の理解では

  • 正常じゃないのに動き続けている処理はたちが悪い
  • 処理の事前条件が満たされていないならさっさとクラッシュするのが被害最小

というような意味だ。

 

その思想で言えば、キャストが必要な処理にキャスト不可な値が入ってきた場合には即座に処理を中止して全体をロールバックさせるのが安全 (Safe) だ。
キャスト不可な値が NULL に差し替えられて正常終了してしまうほうが危険に思える。

 

というのは言いがかりで

まぁそういうネーミングなので仕方ない。

BigQuery の関数には CAST()SAFE_CAST() のように通常版・安全版がそれぞれ用意された関数がいくつかある。

通常版 安全版
/ (除算) SAFE_DIVIDE(x, y) y=0 のとき NULL を返す
+ (加算) SAFE_ADD(x, y) オーバーフロー時に NULL を返す
PARSE_DATETIME() SAFE.PARSE_DATETIME() パース失敗時に NULL を返す
PARSE_JSON() SAFE.PARSE_JSON() パース失敗時に NULL を返す
arr[0] arr[SAFE_OFFSET(0)] インデックスが存在しない時に NULL を返す

arr[SAFE_OFFSET(0)] めっちゃ癖あるなぁー。

 

まとめ

私は名前が逆だと思いました。みなさんはどう思いますか?

 

 

私からは以上です。

サブエージェント と エージェントツール

最近は Google Agent Developer Kit (通称: ADK) でエージェントを自作して遊んでいます。

サブエージェントにタスクを委譲ってやつをやってみました。そしたら思ってたんと違ったので書き記します。

 

サブエージェントが思ってたんと違った

ADK ではとても簡単にサブエージェントを定義可能です。最小コードはこんな感じ。

from google.adk.agents.llm_agent import Agent

sub_agent = Agent(name="sub", ...)

root_agent = Agent(
    name="root",
    tools=[],
    sub_agents=[sub_agent], # ← サブエージェントとして追加
)

これで親エージェントとの会話中、必要に応じてサブエージェントに処理が委譲されます。が、 サブエージェントから処理が戻ってきません

sequenceDiagram
    participant ユーザー
    participant 親エージェント
    participant サブエージェント
    ユーザー->>親エージェント: このタスク頼む
    activate 親エージェント
    親エージェント->>親エージェント: (これはサブエージェントが得意そうやな...)
    親エージェント->>サブエージェント: このタスク頼む
    deactivate 親エージェント
    activate サブエージェント
    サブエージェント->>サブエージェント: (あれこれ処理する)
    サブエージェント->>ユーザー: タスク完了しました!
    deactivate サブエージェント

サブエージェントに委譲するのはいいとして、コミュニケーション役は親エージェントに一任したい。

私の理想はこうです、

sequenceDiagram
    participant ユーザー
    participant 親エージェント
    participant サブエージェント
    ユーザー->>親エージェント: このタスク頼む
    activate 親エージェント
    親エージェント->>親エージェント: (これはサブエージェントが得意そうやな...)
    親エージェント->>サブエージェント: このタスク頼む
    activate サブエージェント
    サブエージェント->>サブエージェント: (あれこれ処理する)
    サブエージェント->>親エージェント: タスク完了しました!
    deactivate サブエージェント
    親エージェント->>親エージェント: (なるほどなるほど)
    親エージェント->>ユーザー: タスク完了しました!
    deactivate 親エージェント

この挙動の差は「サブエージェント」と「エージェントツール」の違いを認識することで理解できるようになります。

 

「サブエージェント」と「エージェントツール」という別概念

親エージェントから別のエージェントを利用する方法には「サブエージェント」と「エージェントツール」の二つあり、これらは別の概念です。

まずはコードで見比べてみましょう。

# サブエージェント
from google.adk.agents.llm_agent import Agent

sub_agent = Agent(name="sub", ...)

root_agent = Agent(
    name="root",
    tools=[],
    sub_agents=[sub_agent], # ← サブエージェントとして追加
)
# エージェントツール
from google.adk.agents.llm_agent import Agent
from google.adk.tools.agent_tool import AgentTool

sub_agent = Agent(name="sub", ...)

root_agent = Agent(
    name="root",
    tools=[AgentTool(agent=sub_agent)], # ← エージェントツールとして追加
    sub_agents=[],
)

親エージェントに sub_agents=[sub_agent] で渡すか tools=[AgentTool(agent=sub_agent)] で渡すかの違いです。

 

そもそも、なぜサブエージェントが必要か

エージェントを賢くするには様々なツールを追加することになります。が、実はツール説明もコンテキストを圧迫します。
そのためエージェント強化のためにツールを追加しまくるとコンテキストが埋まってしまい、ユーザーの指示をうまく理解できなくなるジレンマが起こります。

そこでサブエージェントです。専門性の高いエージェントを定義し、専門に合ったツールだけ渡すことでコンテキストを節約します。そして専門知識が必要なときだけサブエージェントを呼んでもらうわけです。

指示 (Instruction) についても親エージェントのものとは別に専用のものを渡せるので、挙動を細かくカスタマイズする余地が生まれていいですね。

 

サブエージェントの特徴

sub_agents=[sub_agent] でサブエージェントを渡した場合、コミュニケーション役が親エージェントからサブエージェントに完全に切り替わる挙動になります。

実生活で例えるなら、窓口での会話中に「詳しい者に担当者代わります」と別の担当者が出てくる感じです。

 

さらに親エージェントとサブエージェントはコンテキストを共有しています。

  • メリット: サブエージェントから元の会話履歴が見えるのでコミュニケーションがスムーズ
  • デメリット: サブエージェントに切り替え後もメインのコンテキストを圧迫し続ける
    • コンテキストをサブエージェントに閉じて親コンテキストを節約する効果は期待できない

 

このような挙動は汎用性の高い (やれることの多い) エージェントにおいて、ユーザーの要望に応じて順次エージェントを切り替えて処理するのに使えそうです。

例えば「旅行計画エージェント」を作っているとして。最初は親エージェントが要望を聞き、内容に応じて

  • 目的地調査エージェント
  • 交通手段調査エージェント
  • 予約エージェント

などに切り替えながら処理をするのに使えそうです。

 

エージェントツールの特徴

対して、エージェントツールは AgentTool(agent=sub_agent) を作成し、tools= 引数に渡すことで実装します。

エージェントツールにおいては、一貫して親エージェントがオーケストレーションとコミュニケーション役を担います。ユーザーがエージェントツールと会話することはありません。
実生活で例えると、窓口担当者は一人で、裏側の処理中に専門家が監修・分業しているイメージです。

さらに親エージェントとエージェントツール (サブエージェント) でコンテキストが切り離されます。

  • メリット: サブエージェントのコンテキストが親コンテキストを圧迫しない
    • 親コンテキストの圧縮・忘却・ハルシネーションを抑止できる
  • デメリット: 親エージェント・サブエージェント間で指示とレスポンスの受け渡しが発生する分、処理時間が増える
    • サブエージェントに委譲する前の親エージェントの思考時間
    • サブエージェントから結果を受けた後の親エージェントの思考時間

 

親エージェントのコンテキスト圧迫を防ぎつつ出来ることを増やすにはエージェントツールが適しています。

 

まとめ

サブエージェントとエージェントツールを表で比較すると。

サブエージェント エージェントツール
会話主体 切り替わる 親のまま
コンテキスト 共有 分離
役割 担当交代 内部処理
向いている用途 対話の文脈ごと切り替え 処理の委譲

「親エージェントから別のエージェントを呼ぶ」は同じでもユーザー体験は大きく違います。特徴を理解して使い分けることが大切です。

 

 

私からは以上です。

今週うまくいったプレゼンテーション

「今週」と言いつつ実際には先々週の話です。
3月24日25日と二日連続でイベントに参加して LT をしました。そのスライド作りがめちゃくちゃ上手くいったのでノウハウを書き記します。

ちなみに LT 資料がこのへんにあります。

blog.mudatobunka.org

 

上手くいったスライド作りワークフロー

  1. スライドの下書きを Markdown で作る
  2. LLM に投げてごっそり削る&推敲
  3. k1LoW/deck で Google スライドに流し込む

各手順を見ていきましょう。

 

1. スライドの下書きを Markdown で作る

いきなり Google スライドを開くのではなく、テキストエディターを使って下書きを Markdown で書きました。

たとえば「とにかくエージェントにSQLを書かせたい」の原稿はこんな感じ。

---
presentationID: 1i2zdjLzTs4EImUt0L_gVXbURbevqtylpOnt4JNAJ_BQ
title: とにかくエージェントにSQLを書かせたい
author: 今日の三井君
theme: default
---

# とにかくエージェントにSQLを書かせたい

## 今日の三井君

---

<!-- {"layout": "自己紹介"} -->

## 自己紹介

- 名前: 三井翔吾
  - みついしょうご
- 所属: 株式会社はてな
- 職業: Webアプリケーションエンジニア
- マンガメディアを作りつつ、データ分析基盤を整備しています
- X: [@todays_mitsui](https://x.com/todays_mitsui), GitHub: [todays-mitsui](https://github.com/todays-mitsui)

- - -

![It's me!](img/itsme.jpg)

---

## とにかくエージェントにSQLを書かせたい

- なぜですか?
  - SQLを書くのは大変
  - ボイラープレートが多い
  - 私よりエージェントの方がSQLが上手い
  - ノンエンジニアにも気軽に分析してもらいたい

---

<!-- 中略... -->

---

## まとめ

- エージェントはSQLを書くときの頼もしい味方
- ただしSQLを書くことがゴールじゃない
- 分析エージェントを使いこなせば試行錯誤のスピードが上がる

---

## おまけ: 分析エージェントを作ってみる

- 構成
  - エージェント: Google Agent Developer Kit
  - モデル: gemini-2.5-pro
  - データストア: BigQuery
  - 実装: Python

短い文章を箇条書きにするのが私のスタイルなので。とりあえず思いつくままにどんどん書き出していきます。

ポイントは、

  • ページ数は気にしない
  • 文章量も気にしない
  • 前後のつながりも気にしない
  • 喋りたいことを最大限書き出しておく

こんな感じです。後工程で整える前提です。

削ったり整えたりの引き算は LLM が上手くやってくれます。語りたいことを情熱を持って書き出すのは、LLM に頼らず自分でやるのをおすすめします。

 

2. LLM に投げてごっそり削る&推敲

そのようにして Markdown 形式で書いた下書きを LLM に投げます。

10分のLTで下記の資料を作ってるんだけど。どうなんですかね?
とりあえず10分では厳しいボリュームな気がする。

```markdown
(下書きをコピペ)
```

こんな問いかけ方で OK。

そうすると、

文章量の多さを冷静に嗜めてくれたり

頑張って書いた箇所を「興味ない」とバッサリ切り捨ててくれたり

欲張りすぎやでと止めてくれたり

冷静かつ聴講者目線でダメ出ししてくれます。

プレゼンって、「あれもこれも語りたいぜ!」という情熱が必要な一方で、重要度の低い話題は勇気を持って切り捨てるのも大切です。
LLM をブレーキ役に使うことで聴講者の価値にならない話題で時間を食ってしまう事故が避けられます。

 

とはいえ全部が全部 LLM の言うことをきく必要もなく。「いや、この話題こそ価値があると思う」などとちょい反論もしつつ、何往復かさせて推敲を進めていきます。
読みづらい箇所のリライト、表記揺れや誤字のチェックなどもしてくれるので最大限活用しましょう。

 

k1LoW/deck で Google スライドに流し込む

完成した Markdown 原稿をスライド資料に仕上げるには k1LoW さんが作っている deck を使います。

github.com

以前から気になっていたので、2つのスライドを deck を使って作ってみました。

 

使ってみてわかったのが、

  • スライド作りが劇的に早くなるツール というわけではない
    • 画像貼るのは deck に頼らずに手でやる方が早いかも
    • とはいえ deck のために自分用のテンプレートを作り込んでおく価値はある
  • 原稿&推敲が先、資料作りは後、という切り分けが強制されるのが心地よい
    • これこそ聴講者にとってのクオリティを担保するために重要

このようなことです。

最初は k1LoW/deck っていい感じのスライドを素早く生成してくれるツールなのかな?と思っていましたが違いました。 むしろ初回はセットアップや調整の手間で資料作りは遅くなると思います。

 

deck がやってくれるのはスライドのテンプレートにテキストや画像を流し込むだけなので、見た目をいい感じにする恩恵はほぼありません。 それでも先に Markdown で原稿を作ってからスライドに流し込むワークフローが資料のクオリティを押し上げてくれます。

  • テキストベースの Markdown で内容を推敲するので LLM の支援を受けやすい
  • Markdown を前にして内容を考えることで「見た目にこだわる」誘惑から逃れられる

この辺りが重要だと思います。

 

ちなみに、 k1LoW/deck の具体的な機能や使い方を知りたい人は下記の資料を見てみてください。

 

まとめ

k1LoW/deck は素晴らしいツールです。

一方で、Markdown で原稿を FIX してからスライドに流し込むワークフローこそが資料のクオリティを押し上げてくれる、という点を強調しておきます。
LLM によるツッコミ・推敲・チェックの支援を大いに享受しましょう。

 

 

私からは以上です。