曖昧な判断と厳密な制御を分ける——Agents CLIとADK Graph

Executive Summary

自然言語の問い合わせは曖昧です。一方、返金、出荷、データ更新のような業務フローは、「たぶんこの順序」で動いてはいけません。

この矛盾を解く鍵は、すべてをLLMへ任せることでも、LLMを使わないことでもありません。

LLMには意味の判断を任せ、Schemaで結果を受け、PythonとGraph edgeで許可された経路だけを実行する。

Build With Google Day 1 Lab 2の配送問い合わせAgentは、この境界を小さなコードで示しています。本稿では、Agents CLIによる開発ライフサイクルとADK Graphの構造を、次の順で読み解きます。

  • Scaffold、Lint、Playground、CLIがなぜ一続きなのか
  • Node、Edge、State、Routeとは何か
  • 確率的な分類と決定的な制御をどう分けるか
  • Safety、Liveness、Failure、Safe defaultをどう考えるか

シリーズの現在地

全5回で「AIがものすごく賢くなった。では、本当に仕事を任せられるのか?」を追っています。

  1. Agentic Engineeringとは何か
  2. Antigravityという職場
  3. Agent Skillsという業務マニュアル
  4. Agents CLIとADK Graphによる工程設計(今回)
  5. OutputとTrajectoryによる評価

前回からの接続——マニュアルだけでは工程を制御できない

前回は、Skillを再利用可能な業務マニュアルとして整理しました。

しかし、仕事が複数工程になると、「何をするか」だけでなく「次にどこへ進んでよいか」が必要です。

配送会社の新人へ、次の仕事を任せるとします。

  1. 顧客の問い合わせを読む。
  2. 配送に関する質問か判断する。
  3. 配送ならFAQ担当へ渡す。
  4. 無関係なら、対応範囲外であることを丁寧に伝える。

意味の判断には柔軟さが必要です。一方で、配送と無関係な相談を勝手に別の高権限処理へ送ってはいけません。

つまり、手順の再利用に加えて、状態と遷移を制御するWorkflowが必要になります。

新人の判断と、会社の承認経路を分ける

「荷物はいつ届きますか?」は明らかに配送です。しかし、次の問い合わせはどうでしょうか。

  • 「昨日頼んだもの、週末までに間に合う?」
  • 「住所を間違えた。まだ変えられる?」
  • 「箱が潰れていたので相談したい」

単語一致だけでは分類しにくく、文脈を含む意味判断が要ります。ここはLLMが得意です。

ただし、LLMの回答は確率的です。同じ入力でも表現や中間結果が変わり得ます。そこで、LLMへ「次のPython関数を自由に選んで実行して」と全面委任するのではなく、判断結果を型付きデータへ変換し、コード側で許可経路を選びます。

ADK Graphで意味判断と経路制御を分ける

図の境界は次のようになっています。

担当 性質
LLM classifier 問い合わせの意味を読む 柔軟だが確率的
Typed Schema 判断結果の形を固定する 型付きの受け渡し契約
Python route labelを許可経路へ対応させる 決定的
Graph edge 次へ進めるNodeを列挙する 許可集合
Handler その経路の処理を実行する 責務を局所化

つまり、LLMの不確実性を消すのではなく、不確実性が存在してよい場所を狭くします。

Agents CLIは「生成コマンド」ではない

Lab 2では、最初にagents-cliを準備し、7つの開発用Skillを登録します。

  • google-agents-cli-adk-code
  • google-agents-cli-deploy
  • google-agents-cli-eval
  • google-agents-cli-observability
  • google-agents-cli-publish
  • google-agents-cli-scaffold
  • google-agents-cli-workflow

VM上でも、この7つがSkill directoryに存在することを確認しました。

Agents CLIの7つの開発Skill

この構成から見えるのは、CLIの役割が雛形生成だけではないことです。

要件を言語化する

Scaffoldで構造を作る

Graph codeを読む・直す

Lintで静的な問題を減らす

Playgroundで対話と状態を観察する

CLIで単発実行し、自動化へつなぐ

Eval / Observability / Deploy / Publishへ広げる

Lab 2はこのライフサイクルのうち、Scaffold、Graph、Lint、Playground、単発CLIまでを一周します。

つまり、Agents CLIはAgentを一度生成する道具ではなく、作る・読む・検査する・動かす反復ループの入口です。

Scaffold——自然言語の要件を、検査できる構造へ変える

Lab 2では、配送会社のCustomer Support Agentを自然言語で依頼します。要件の中心は次です。

  • まず問い合わせが配送関連か、無関係かを分類する
  • 配送関連ならShipping FAQ Agentへ送る
  • 無関係なら丁寧に断る
  • Deploymentは行わない

Scaffoldは、このIntentからProjectの雛形を生成します。重要なのは、生成後にコードとして読めることです。

VM上のProjectでは、主に次の構造を確認しました。

customer-support-agent/
├─ app/
│  ├─ agent.py       # Agent、Node、Edge、Workflow
│  └─ schemas.py     # 分類結果の型
├─ tests/
│  ├─ unit/
│  └─ integration/
├─ pyproject.toml    # 依存関係と開発設定
└─ agents-cli-manifest.yaml

自然言語で作ったものが、このようなファイル、型、テストへ変換されると、人間は「Intentをどう解釈したか」を検査できます。

つまり、Scaffoldの価値は速くファイルを増やすことより、曖昧な依頼をレビュー可能な構造へ着地させることです。

ADK Graphの4つの部品

Graphという言葉は難しく見えますが、会社の業務フローに置き換えると単純です。

Node——一つの仕事

分類する、回答する、断る、といった処理単位です。LLMを使うNodeも、通常のPython関数で動くNodeも置けます。

Edge——次へ進んでよい道

Node同士の接続です。GraphにないEdgeは、通常の経路としては選べません。

State / Context——いま分かっていること

入力や中間結果、routeなど、工程間で受け渡す状態です。

Route——条件分岐の行き先

分類結果に応じて、shippingまたはunrelatedのようなlabelを選びます。

LabのProjectを簡略化すると、次の形です。

START

classifier_agent        LLMが配送関連か判断

route_decision          Pythonがroute labelを設定
  ├─ shipping  → shipping_faq_agent
  └─ unrelated → decline_node

ここでGraphは、LLMの思考を可視化する図ではありません。実行可能な処理と許可された遷移を明示するプログラム構造です。

つまり、Nodeは仕事、Edgeは許可された移動、Stateは引き継ぎ情報、Routeは分岐先です。

LLMの出力をSchemaで受ける

自由文のまま制御へ渡すと、表記揺れが問題になります。

「配送です」
「shipping-related」
「これは配達日の質問です」

どれも人間には通じますが、コードの分岐条件としては不安定です。

VM上のschemas.pyでは、分類結果が概ね次の形で定義されていました。

class QueryClassification(BaseModel):
    is_shipping_related: bool
    category: str
    reasoning: str

そしてClassifierのoutput_schemaへこの型を指定します。

classifier_agent = LlmAgent(
    # ...
    output_schema=QueryClassification,
)

SchemaとRouteの契約

Schemaが保証するのは、主に「形」です。is_shipping_relatedがbooleanである、categoryreasoningのfieldがある、といった境界を作ります。

ただし、型が正しくても意味が正しいとは限りません。is_shipping_related=Falseという形式は正しくても、配送質問を誤分類した可能性は残ります。型検査と意味評価は別です。

つまり、SchemaはLLMを正解させる魔法ではなく、確率的な出力を決定的なプログラムへ安全に受け渡す契約です。

Routeを通常のPythonへ戻す

VM上の実装では、分類結果を受けるroute_decisionが通常のPythonで書かれていました。

is_shipping = node_input.get("is_shipping_related", False)
route = "shipping" if is_shipping else "unrelated"
ctx.route = route

LLM分類と決定的Routeの境界

この数行には重要な設計判断があります。

  1. 意味分類はLLM Agent内に閉じる。
  2. 以降の制御はbooleanから二つのlabelへ決定的に写す。
  3. ctx.routeへ入る値をshippingunrelatedに限定する。
  4. 期待するfieldがない場合はFalse側へ落とす。

その後、Graphのedgeがlabelと次のNodeを対応付けます。

edges = [
    ("START", classifier_agent),
    (classifier_agent, route_decision),
    (route_decision, {
        "shipping": shipping_faq_agent,
        "unrelated": decline_node,
    }),
]

Graphに定義された許可遷移

LLMが配送質問だと判断しても、Graphに返金実行NodeへのEdgeがなければ、その分岐から通常経路として返金処理へ進むことはできません。

もちろん、Graphだけでシステム全体の安全性が自動的に成立するわけではありません。各Handlerが危険なToolを持つなら、Permissionsや入力検査も必要です。それでも、制御経路を列挙できる価値は大きいのです。

つまり、LLMが「どの種類の相談か」を判断し、コードが「その種類からどこへ進めるか」を決めます。

Lint、Unit test、Playground、CLIは何を別々に確かめるのか

一つの実行が成功しても、すべてが正しいとは限りません。Lab 2が複数の検査方法を使うのは、それぞれ見つける問題が違うからです。

Lint

構文、format、型、importなど、実行前に機械的に見つけやすい問題を減らします。業務上の回答が正しいことまでは保証しません。

Unit test

小さな関数の契約を切り出します。VM上では、is_shipping_related=Trueならctx.route == "shipping"Falseなら"unrelated"になることを確認するtestがあり、通過しました。

RouteのUnit test

ここではLLMの精度ではなく、分類結果を受けた後の決定的な写像を検査しています。

Playground

会話しながら、入力、中間状態、分岐、回答を観察します。Labガイドはコード変更を即時反映するhot reloadも扱います。今回はその機能を改めて検証していないため、ここではガイド記載の開発体験としてのみ扱います。

Single-turn CLI

一つの入力を非対話で実行します。人が画面で試すだけでなく、scriptやCIへ組み込みやすくなります。

VM上では、配送に関する入力と無関係な入力の二つを実行し、それぞれshipping側とunrelated側へ分かれることを確認しました。

二つの入力に対するCLI経路

これは二つの代表例が通った証拠であり、あらゆる表現を正しく分類する統計的な精度証明ではありません。

つまり、Lintは形、Unit testは局所契約、Playgroundは対話的な挙動、CLIは反復可能な実行を見ます。

もう一段深く:GraphをSafetyとLivenessで読む

Workflowを高度に検討するときは、単に「正しい答えが出たか」ではなく、二種類の性質を考えます。

Safety property——悪いことが起きない

  • 無関係な問い合わせが配送専用処理へ入らない
  • 承認前に変更Toolを呼ばない
  • 未知のroute labelが高権限Nodeへ流れない
  • 失敗した中間結果を成功として次へ渡さない

Graph edge、Schema、Permissions、禁止Tool検査は、この性質を支えます。

Liveness property——正しい仕事が前へ進む

  • 正当な配送質問が無限loopせず回答へ到達する
  • 一時的な失敗後に、定めた再試行または人間確認へ進む
  • どのNodeにも到達できず停止するdead endがない

安全に止めるだけでは、すべてを拒否するAgentになってしまいます。安全性と、仕事を完了できる性質の両方が必要です。

Failure boundary——どこで失敗を処理するか

失敗 検出場所 安全な扱いの例
LLM呼び出し失敗 Classifier 有限回retry後に人間へ
Schema不適合 型境界 Routeへ渡さず停止
未知label Router General、人間確認、拒否
Handler失敗 各Node 変更の有無を確認して再試行
Tool権限不足 Tool境界 権限を勝手に広げず報告

Safe defaultとは「とりあえず一般回答する」ことではありません。失敗時に、取り返しのつかない操作へ進まない既定値を選ぶことです。業務によってはGeneral route、Human review、明示的な拒否のどれが安全かが変わります。

今回の持ち帰り

  • 柔軟さと厳密さは分担できる。 LLMが意味を判断し、Schemaとコードが許可経路へ変換する。
  • Graphは思考図ではなく実行契約。 Node、Edge、State、Routeで、可能な遷移を明示する。
  • Agents CLIは開発ループを支える。 Scaffold、Lint、Playground、単発CLIは、それぞれ異なる失敗を見つける。

次回へ

配送と無関係の二つの入力が、期待した経路へ進みました。では、これでAgentを信頼してよいでしょうか。

最終回答が正しくても、途中で不要なToolを呼んだかもしれません。正しい経路でも、回答内容が間違っているかもしれません。一度通っただけでは、表現の違う入力や失敗時の挙動は分かりません。

次回は、OutputとTrajectoryを分け、Agentの「できた」を検査可能な証拠へ変えます

参考資料