ソフトウェア エンジニア
自律型のオンデバイス エージェントを構築して、Android の AI 機能を次のレベルに引き上げましょう。このセッションでは、ML Kit の新しいエージェント API について詳しく説明します。この API は、すでにご存じの直接モデルアクセス API の上に直接構築されています。
オープンソースの Jetpacker アプリを使用して、ローカルデータ全体でマルチステップのアクションをチェーンする方法、分離されたコンテキストでステートフルなマルチターン セッションを管理する方法、Android MCP を接続して、単一のコードベースでアプリ内アシスタントと Android システムレベルのサーフェスの両方を強化する方法を学びます。
AgentRunner は、ツールの実行を調整し、コンテキストを失うことなくマルチターンの会話を行うためのセッション メソッドを提供します。文字起こし
Jolanda Verhoef です。
私は Android のデベロッパー リレーションズ エンジニアです。本日は、AICore エンジニアリング チームの Victoria Lestari とともに、
こんにちは。
本日はご参加いただき、ありがとうございます。
そのため、Google のチームは長年にわたり緊密に連携し、最先端の AI モデルをクラウドから Android デバイスに直接移行して実行できるように取り組んできました。
これらの取り組みは、ML Kit GenAI スイートにまとめられています。
この ML Kit GenAI スイートには、テキスト要約、画像の説明、音声認識などのターンキー ソリューション用の高レベル API があります。
一方、オンデバイス モデルである Gemini Nano への直接アクセスも可能になりました。
そして本日、さらに一歩進んだ取り組みを発表します。
この直接アクセスにより、アプリ内エージェントを構築できるようになります。このエージェントは、複数のターンにわたって推論し、会話のコンテキストを保存し、アプリ内でアクションを実行できます。
ビクトリア、具体的にはどういうことでしょうか?
うん
これまで、オンデバイスのユースケースについて話すときは、詐欺の可能性の検出や通話の要約など、短く、シンプルで、独立したタスクを想定していました。
また、写真カタログについて質問したり、Google マップに質問したりするなど、より複雑なことを構築したい場合は、当然ながら大規模なクラウドモデルを参照します。
つまり、クラウドモデルには膨大な処理能力とリアルタイム データという利点がありますが、オンデバイス AI は低レイテンシ、リアルタイム、ローカル データ処理という新たな可能性を切り開きます。
このように、Gemini Nano はデバイスでできることをすでに拡大しています。
その一例が Pixel スクリーンショットです。このアプリでは、ローカルのスクリーンショットを検索したり、スクリーンショットに関する質問をしたりできます。
そのため、デバイス上でエージェントを実行するには、モデルの機能を大幅に向上させる必要があります。そこで、複数のステップにわたって推論し、実際のアクションを実行できる Gemini Nano 4 を開発しています。
しかし、この新機能は、デベロッパーがターゲットとする実機がある場合にのみ重要になります。
Gemini Nano 4 は、すでに一部のフラッグシップ デバイスで利用可能です。また、AICore デベロッパー プレビューを使用すれば、さらに多くのデバイスでテストできます。
また、来年初頭には 1 億台のデバイスに到達する見込みです。
では、これは何を意味するのでしょうか。
つまり、今日からこれらのエクスペリエンスの構築とテストを開始でき、今日構築しているものが大規模なオーディエンスにスケールされることがわかります。
Gemini Nano 4 と、その潜在的なリーチについて説明しました。次は、エージェント機能が実際に何を意味するのかについて説明します。
つまり、エージェント型 AI は、複雑なタスクを計画し、それらのタスクを複数のステップに分割して、アプリ全体で実行できます。
たとえば、デバイス上の Gemini Nano モデルは、ローカル データベースにクエリを実行し、バックエンドからデータを取得して、複雑な質問を推論して回答し、デバイス上のネイティブ アプリ ワークフローをトリガーできます。
しかし、AI はどのようにして計画を立て、実際の行動に移すのでしょうか。
そして、これが今日のトピックである関数呼び出しです。
これは、エージェント機能の中核となるエンジンです。
自然言語をシステム実行に変換し、複雑なタスクの解決に必要なローカル API を選択して実行することで、モデルを受動的な推論から能動的な実行へと移行させます。
また、Gemini Nano 4 で関数呼び出しがネイティブにサポートされるようになったこともお知らせします。
現在、AICore デベロッパー プレビューで利用可能で、まもなく Nano 4 対応のすべてのデバイスに展開される予定です。
モデルにこのような機能があるのは素晴らしいことですが、モデルの機能は、デベロッパーに API を提供するものではありません。
そこで、エージェントの構築を容易にするために、ML Kit GenAI スイートに適切な API を追加する作業も進めています。
そこで、すでにあった直接アクセス API から始めて、関数タイプと会話履歴タイプを追加しました。
ML Kit の Prompt API に追加されたこれら 2 つの新機能により、この低レベルのエージェント機能をアプリに組み込むことができるようになります。
ただし、その時点で自分でやらなければならないことがたくさんあります。
そのため、関数を手動で実行し、通話履歴を保存し、複数の呼び出しをループ処理する必要があります。
そのため、複数の呼び出しを行う必要がある場合は、自分で何らかのループシステムを構築し、システムに独自のガードレールを構築する必要があります。
これは可能ですが、もちろん、迅速かつ簡単に行いたいものです。
そこで、この Prompt API ライブラリに加えて、新しいエージェント フロー ライブラリも追加しました。
com.mlkit.genai:agent-api で確認できます。現在、アルファ版で利用できます。
この新しい API では、そうした手間のかかる作業の多くが自動化されます。
そのため、多くの手動ステップを実行する必要はなく、わずか数行のコードでエージェントを構築できます。
本日は、まさにその方法をご紹介します。
そこで、これらの API がサンプルアプリ Jetpacker で実際にどのように機能するかを見てみましょう。
Jetpacker は、Google I/O やこのカンファレンスですでに紹介されています。
この QR コードをスキャンすると、コードを確認できます。ご興味があれば、エージェントにコードを指示して、コードを操作し、これらの機能を実際に実装する方法について詳しく学ぶことができます。
Jetpacker は、さまざまなインテリジェント機能を紹介するために構築された最新の旅行アシスタントです。
すでに、あらゆる種類の Prompt API にオンデバイス モデルとクラウドモデルを使用していますが、今回はこれにエージェントを追加します。
このエージェント(Jetpacker)では、先ほど説明した新しい ML Kit GenAI Agent API を使用します。この API は、Android デバイスで実行される基盤となる AI Core システムを使用して Gemini Nano モデルと通信します。
私はいくらでも話せますし、これからたくさん話すつもりですが、もちろん、これをお見せしたいと思っています。
では、スマートフォンに切り替えて、実際にデモとしてお見せしましょう。
ここに Google Pixel 11 Pro があります。
Pro に切り替えると、表示されるはずです。
ありました。
ご覧のとおり、Jetpacker アプリがここで実行されています。
今後の旅行がいくつか表示されています。現在の旅行を見ると、今日は 10 月 29 日なので、オンデバイス AI ワークショップ、デモショーケースがあり、明日はゴールデン ゲート ベイ クルーズ、そして美術館訪問が予定されています。
すべて素晴らしいことです。
次に、この出張の予算を追跡している経費画面に移動します。
右下に、新しい小さなエージェントの輝きが表示されています。これが、私たちが構築したオンデバイス エージェントです。
では、非常に簡単なコマンドから始めましょう。
特定の費用を追加するようリクエストできると思います。
たとえば、「マウンテンビューまでのタクシー代として 45 ドルを追加して」と言ってみましょう。
いいぞ
経費を準備して、追加しました。
いいですね。
デモ 1 はうまくいきました。
スライドに戻って、実際の仕組みを見てみましょう。
まず、お気づきかもしれませんが、私は音声を使用していました。この音声は、Speech Recognition API を使用して Gemini Nano モデルを使用し、私が話した内容をテキストに変換しています。
このテキストは Gemini Nano モデルに送信できます。
しかし、何をするのでしょうか?
たとえば、経費に何かを追加するように指示し、45 ドルについて指示していますが、どのように動作すればよいのかは実際にはわかりません。
この時点では、「何のことかわかりません」と答えるだけです。
そこで、このプロンプトに少し追加します。
プロンプトにはあらゆる種類のツールを追加できます。この例では、旅行予算や旅程などを取得するツールを追加しています。これを API を使用して Nano モデルに渡すと、モデルはテキストを返す代わりに、関数呼び出しを返すことができることを自動的に理解します。
この場合、テキストで応答する代わりに、「マウンテンビューへのタクシー代として 45 ドルの経費を追加する」という処理が行われます。
素晴らしいですね。では、ビクトリアさん、実際にどのように機能するのでしょうか?
これを構築するにはどうすればよいでしょうか。
うん
では、コード内でツールを定義するにはどうすればよいでしょうか。
この点について説明します。
Android MCP では、Jetpack AppFunctions ライブラリを使用してこれらのツールを定義し、公開します。
AppFunctions ライブラリは、アプリのサービス、データ、アクションを Android OS レジストリに公開できるため、まったく同じ関数を再利用して、ツールを宣言して記述し、アプリ全体で使用できます。
また、ユーザーはエージェントやシステムレベルの操作を通じてこのタスクを完了できます。
そこで、コードの具体例をご紹介します。
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}これは AppFunctionDeclaration の例です。
上部の KDoc コメントには、関数の動作とそのパラメータがすべて自然言語で記述されています。
この説明はモデルに送信されるため、できるだけ明確な説明を記述することをおすすめします。
その下に、アノテーション @AppFunctionAccessLevel があります。
これにより、関数の可視性のスコープが設定されます。
この例では、ACCESS_LEVEL_SELF を使用しています。これは、作成する関数がアプリ(アプリのエージェント)にのみ表示されることを意味します。
その後、suspend 関数自体を定義して実装し、できるだけ多くのアプリ関数を記述して、それらをすべて 1 つのサービス クラスにグループ化できます。
違いますね。
ツールがあり、それらのツールを作成できるのは素晴らしいことです。
ただし、エージェントを構築するには、AppFunctions だけでなく、もう少し多くのものが必要です。
エージェントを作成する際に知っておくべき重要な要素が、他にも 3 つあります。
まず、エージェントを定義します。
システム指示、ガードレール、構成、操作方法を定義します。
また、AgentRunner もあります。
AgentRunner は、エージェントを実行するものです。
これは、先ほど説明したループ メカニズムを処理しています。
最後に、AgentEvents があります。AgentEvents は、システム(API)が実際に何を行っているかを知らせるための方法です。
これにより、テキストを返すタイミングやツール呼び出しを行うタイミングなどが通知され、UI に実際に表示できるようになります。
違いますね。
では、実際のコードを見てみましょう。
違いますね。
コードを記述する前に、まずデバイスで関数呼び出しが利用可能かどうかを確認する必要があります。関数呼び出しは Gemini Nano 4 でのみサポートされているためです。
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}まず、生成モデル getAvailableFeatures を呼び出して確認する必要があります。
そして、機能に関数呼び出しがリストされているかどうかを確認するだけです。
サポートされていない場合は、グレースフル フォールバックを実装できます。
違いますね。
それでは、コードを見ていきましょう。
if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}関数呼び出しがデバイスでサポートされていることがわかれば、Agent.builder を使用してエージェントの構築を開始できます。
名前を設定します。
setInstructions を使用して、コンテキストとペルソナを定義できます。
最後に、enableAppFunctions を呼び出します。
この関数は、先ほどリストアップしたすべての AppFunction を自動的に検出し、エージェントを構築します。
次に、AgentRunner を初期化する必要があります。
このエージェント ランナーが実行ループを管理してくれます。
インストラクタ、コンストラクタを呼び出して、エージェントを渡すことで、簡単に呼び出すことができます。
最後に、文字起こしされた音声コマンドをコンテンツ ブロックでラップして、プロンプトを作成します。
そして、ランナーを呼び出します。
プロンプトで runSingleTurnAsync 関数を呼び出すと、エージェントが起動し、推論が実行され、AI 推論全体が実行されます。
また、モデルの最終的なテキスト返信のみを UI にストリーミングする場合は、textTokens を呼び出すだけで、レスポンスが到着すると収集されます。
以上です。
これはほんの数行のコードです。冗談で言ったわけではありません。
わずか数行のコードでエージェントを作成できました。
そのエージェントが、あなたが望むことを実行します。
テキスト レスポンスが返されます。
もちろん、最初のデモでは、非常にシンプルなコマンドのみを実行していました。
1 つのコマンドを実行すると、1 つのアクションが実行されて終了しました。つまり、まだ完全なエージェントではないということですよね?
そこで、少し趣向を変えてみましょう。
次のパートでは、実際にコマンドを与えたいのですが、エージェントにとって少し難しくしたいと考えています。
では、デモデバイスに戻って、もう少し難易度を上げてみましょう。
すでに到着しています。
いいですね。
先ほどお伝えしたとおり、明日は博物館を訪問する予定です。
博物館の訪問を開くと、入場料が 25 ドルであることがわかります。
では、エージェントが実際にそれを自分で判断できるかどうかを見てみましょう。
そこで、経費画面に戻り、「美術館のチケットを経費に追加して」とコマンドを入力します。
いいぞ
ご覧のとおり、旅程が確認され、美術館の訪問が検出されて、経費に追加されています。
このように、実際に目標を達成するために 3 つのステップを踏み、最終的なテキスト レスポンスも返しています。
この処理はすべて自動的に行われます。
デバイスでのそのモデルの実際の速度を確認しました。
では、スライドに戻って、その仕組みを見てみましょう。
ご覧のとおり、最初のデモとは少し異なる点があります。
この場合、ご覧のとおり、私は美術館についても、費用についても伝えていません。そのため、モデルはプロンプトとツールを受け取ったときに、まず何をすべきかを判断しようとしています。
そこで、現在の旅程についてさらに詳しく知る必要があると判断し、getItinerary を呼び出します。
レスポンスを受け取ると、この旅程に美術館訪問が含まれていることがわかり、おそらくそれがユーザーの言っていることだと判断します。
そこで、その特定のイベントの詳細を確認しましょう。
そして、その特定のイベントの他のすべてのメタデータの中で、25 ドルであることがわかります。
エージェントはそれを理解し、戻って、経費の追加を呼び出す必要があることを理解します。
ここでは、複数のステップが次々と実行されています。
ビクトリアさん、この点についてもう少し詳しく教えていただけますか?
うん
Jolanda が説明したのは、先ほど紹介したデモの具体的な例ですが、ここでは、もう少し概念的な抽象化レイヤについて説明します。
これは、マルチステップの関数呼び出しを行うたびに発生します。
まず、これがアプリで、これが Gemini Nano モデルです。
ユーザーは、クエリと利用可能なツールのリストをモデルに送信することから始めます。
モデルはこの情報を使用して、何を返すかを決定します。
そして、最初のケースでは、ツール呼び出し 1 と呼ぶツール呼び出しが返されます。
次に、アプリがこのツール呼び出しを実行し、ツール レスポンスを取得します。
そして、モデルに返されるのは、実際には会話履歴全体です。これには、元のクエリ、ツールのリスト、最初のツール呼び出し、最初のツール レスポンスが含まれます。
そして、モデルは再びこの情報を使用して、次に行う処理を決定します。
そのため、最終的な結果が得られるまで、このやり取りを繰り返すことができます。
たとえば、モデルが n 番目のツール呼び出しを返し、アプリが会話履歴全体を再度返すとします。
モデルが満足すると、つまりクエリが満たされたことがわかると、最終レスポンスをアプリに返します。アプリは UI に完了したことを表示できます。
違いますね。
では、これをコードで実装する方法を説明します。
/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }シングルステップのコードの例を覚えていると思いますが、これはマルチステップのコードです。非常に似ていますが、最も重要なのは、KDoc を非常に明確に記述することです。
この例では、getItinerary 関数はイベントの概要のみを返すことを明示的に示しています。
また、より具体的な旅行の詳細が必要な場合は、eventId を指定して getEventDetails を呼び出す必要があることも示しています。
また、getEventDetails には KDoc コメントもあり、チケット料金やその他のメモなどの詳細を取得できることが示されています。
コメントを使用して、コメントを明確に記述することで、ツールがどのように相互作用するかをモデルに理解させることができます。
違いますね。
このようにして、このループ(エージェント ループ)をすべて実行するエージェントを構築します。
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}これは、1 ステップの例と非常によく似ていますが、2 つの追加があります。
まず、最大ターン数を設定する必要があります。この例では 5 に設定しています。
では、どのような機能があるのでしょうか。
これにより、モデルが無限にループするのを防ぐことができます。
たとえば、曖昧なプロンプトがある場合や、あるツールを呼び出して別のツールを呼び出し、また戻って、というように行ったり来たりを繰り返す場合、これを設定すると、ループが停止します。
その後、同じ関数 runSingleTurnAsync を呼び出して実行します。
シングルターンと呼ばれていますが、実際には 1 ターンは 1 つのユーザークエリを意味します。
つまり、エンドユーザーは、1 つのクエリを送信すると、1 つの回答が返されることを期待しています。内部で何が起こっているか、何回ループしているかなど、詳細にはあまり関心がありません。
そのため、同じ関数を引き続き使用できます。
また、マルチステップのツール呼び出しには少し時間がかかるため、デモでもご覧いただいたように、現在呼び出している関数、現在実行している関数、完了した関数が実際に表示されます。
そのため、UI にライブの進行状況を表示したいと考えています。
そのため、テキスト レスポンスを収集するだけでなく、AgentEvent が FunctionCall または FunctionResponse を出力するたびに、ツール名 event.name を表示できます。完了したら、以前と同様に TextMessage を返すことができます。
お見事です!
この実行を非同期実行シングルターン非同期と呼んでいます。これは、もちろん、別の種類の API もあることを前提としています。
うん
もちろん、1 つのステップを実行するだけでは不十分です。
マルチステップにしたい。
次に、マルチターンのセッションを試してみましょう。デモに戻って、もう少し難易度を上げてみます。
美術館を訪れる予定がある一方で、ゴールデン ゲート ベイ クルーズも予定されているという話をしました。
そのため、実際に予約する前に、予算が足りるかどうかを確認したいと考えています。
では、質問してみましょう。
どうでしょうか
明日、チームをゴールデン ゲート ベイ クルーズに連れて行こうと考えています。180 ドルくらいだったと思います。
その場合、残りの予算は実際にはいくらになるでしょうか?
では、何をしているのでしょうか。
残りの予算がいくらになるかを回答しています。
現在の旅程を把握しています。
その情報があり、それを伝えることができます。
ここでフォローアップの質問をすることもできます。
それはよかったです。
経費に追加してもらえますか?
これで、
通貨が必要ですね。デモ効果です。
ドル単位ですね。
$180。
いいぞ
これは初めての経験です。素晴らしいですね。
では、何をしているのでしょうか。
3 回のターンを要したこと、費用に追加されたことが示されています。また、このセッションを更新することもできます。
上部に更新ボタンのようなものがあり、それをクリックすると、履歴がすべて消去され、最初からやり直すことができます。
では、スライドに戻りましょう。
動作するデモが用意され、すべてが期待どおりに動作しているので、コードに戻りましょう。
うん
これは本当に素晴らしいことです。
Jolanda は、ゴールデン ゲート ベイクルーズのチケットとは言わず、ドル単位であることだけを言っています。
では、モデルはどのようにしてすべてのコンテキストを把握するのでしょうか。
これは、前の 2 つのコード スニペットで示したものとは少し異なります。
// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()そのため、AgentRunner と runSingleTurnAsync を使用する代わりに、セッションを作成する必要があります。
ここでは、tripId を指定して runner.getOrCreateSession を呼び出します。
では、なぜセッションを作成する必要があるのでしょうか?
runSingleTurnAsync はメモリをリセットするため、コンテキストが保持されない。
マルチターンの会話はできません。
マルチターン会話を行う場合は、セッション(エージェント セッション)を使用します。
次に、tripId を渡します。
なぜそれが必要なのでしょうか?
以前に見たことがあると思いますが、Jetpacker アプリは複数の旅行を管理します。
ADS の旅行、パリ旅行、東京旅行などがあります。
そのため、トリップ内でコンテキストを保持する必要があります。
お互いに台無しにしたくないよね。
runSingleTurnAsync を呼び出す代わりに、runAsync を使用します。
これは異なります。
これはセッションで実行され、同じプロンプトが表示されます。
AgentEvent、FunctionCall、FunctionResponse、TextMessage は以前と同じように出力できます。
Jolanda が新しい会話ボタン(更新ボタン)をクリックすると、session.delete が表示されます。
うん
違いますね。
これらはすべて、新しいエージェント API を使用して行いました。これは素晴らしいことであり、多くのユースケースで驚くほど効果的に機能します。
もちろん、私たちは常に、迅速な処理や簡単な処理を可能にしたいと考えていますが、より複雑なケースにも対応できるようにしたいと考えています。
そのため、必要に応じて、直接アクセス API を使用して、これらの機能のより低レベルのバージョンを構築することもできます。
Victoria さん、この直接アクセスレベルの意味と、このレベルに下がるタイミングについて説明していただけますか?
はい、Jolanda さん。
この直接アクセスについては、Prompt API をすでにご存じかもしれません。
そこで、関数呼び出しもサポートしました。この API を、より簡単なエージェント フローの代わりに使用することが望ましいユースケースが 2 つあります。
1 つ目は、非常にシンプルなユースケースです。
エージェント フローでは、会話全体、クエリ、ツール、ツール呼び出し、ツール レスポンスなど、すべてをモデルに返します。
ユースケースによっては、プロンプトを送信するだけで、そのような処理は行わない場合もあります。
たとえば、モデルにプロンプトを送信してツール呼び出しを返してもらい、そのツール呼び出しを返すのではなく、別のプロンプトをモデルに送信したいとします。この場合、履歴を完全に制御できるため、履歴を編集できます。
ここで、より具体的な例を示します。
// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)たとえば、デバイスの背景テーマを変更できるアプリがあるとします。
ユーザーが「背景を夕日に変更して」というクエリを送信したとします。
そして、アプリがこの関数を実行し、背景が夕日に変わりました。
その後、ユーザーが気が変わって、「いや、ビーチのことだった」と言ったとします。
ここで、デバイスがすでに背景を夕日に変更し、今度はビーチに変更したいという完全な状態を送信する必要はありません。
いいえ、その必要はありません。
最初のプロンプトには背景の情報が含まれているため、最初のプロンプトと 2 番目のプロンプトを送信するだけで済みます。
これにより、背景をビーチに変更したいということがわかります。
そこで、Prompt API を使用してこれを行います。
履歴を完全に制御できます。コンテキスト トークンはデバイスに保存します。デバイスのトークン ウィンドウは限られているため、これにより、より多くのトークンを保存できます。
2 つ目のユースケースは、実際には対極にあります。
エージェント API がまだサポートしていない複雑なユースケースがある場合、たとえば、複数のサブエージェントが相互にやり取りして複雑なタスクを解決するマルチエージェント システムや、オンデバイス モデルとクラウド モデルの両方を備えたハイブリッド システム、カスタム メモリとコンテキストの圧縮が必要な場合などです。
これは、先ほどお話しした履歴の完全な管理など、履歴に関連しています。
コンテキスト ウィンドウが制限されているため、すべてをモデルに送信するのではなく、最も重要な部分や最新の部分のみを要約したいとします。この場合、直接アクセス API を使用して、この処理を完全に制御できます。
このユースケースでは、Agent Development Kit(ADK)や Crew などのフレームワークを使用できます。また、独自のカスタム オーケストレーション フレームワークを作成することもできます。
違いますね。
エージェント API については以上です。
しかし、もう一つ触れておきたいことがあります。
これまで、アプリ内エージェントについて説明してきました。つまり、独自のアプリ内で独自のエージェント エクスペリエンスを構築するということです。しかし、AppFunctions の使用方法についても説明しました。独自のアプリ内でのみ行うのであれば、なぜ AppFunctions を使用するのでしょうか。
デベロッパーの皆さんは、同じことを 2 回繰り返すのは好きではないと思います。
コードのコピー&ペーストは好ましくありません。ビジネス ロジックの重複も避けるべきです。
AppFunctions を使用すると、アプリ内エージェントのみがツールを使用できるようにするのではなく、非常に簡単な変更で、システム エージェントもツールを使用できるようにすることができます。
では、コードではどのように記述するのでしょうか。
うん
AppFunction を使用すると、一度作成するだけで、必要に応じてさまざまなレベルのレイヤに公開できます。
以前は ACCESS_LEVEL_SELF を使用していましたが、これは作成した関数をアプリ内エージェントにのみ公開するものでした。このアクセスレベルを ACCESS_LEVEL_ANDROID_TRUSTED に変更することもできます。
これにより、Android OS の信頼できるエージェントに関数が公開されます。
そのため、関数、コマンド、データベース ロジックを書き換える必要はありません。
AppFunctions は、すでにアプリの内部と外部の両方でアプリの音声として機能しています。
そのデモも必要ですよね?
では、スマートフォンに戻りましょう。最後のデモでは、Jetpacker アプリから実際に離れてみます。
Gemini に移動しましょう。
そこで、Gemini で @Jetpacker を呼び出します。
1 つ注意すべき点は、この機能は限定ベータ版としてのみ提供されているため、現時点ではすべてのユーザーが利用できるわけではないということです。ただし、テストして試すことは可能です。
Jetpacker の場合、
何をしましょうか?
今朝コーヒーを飲んだよね?
ADS の出張経費に 7 ドルのコーヒーを追加してもらえますか?
いいぞ
Jetpacker アプリに接続していることがわかります。
すると こうなります
費用が追加されました。
これで、Jetpack のすべての機能を使用する方法を基本的に確認できました。
Jetpacker の優れた点は、機内モードをオンにすることもできることです。
今日はまだお見せしていませんが、機内モードをオンにしても、デモでお見せしたすべての機能が動作します。皆さんにご信頼いただけるよう、Wi-Fi をオフにしてデモをお見せしたいと思います。
追加できる経費のデモ例をお持ちの方はいらっしゃいますか?
どうでしょうか
「これは何?」をやってみよう
レンタカーは便利ですよね。
違いますね。
「レンタカーの費用として 300 ドルを追加して」と入力してみましょう。
これで完了です。
違いますね。
いいですね。
では、スライドに戻りましょう。
AppFunctions の話に戻りましょう。
先ほど、非常に簡単に触れましたが、
詳細については、Dan と Sarah の別の講演をご覧ください。この機能を実際に有効にする方法、テストする方法、利用可能なツールなどについて詳しく説明しています。
本日は、さまざまな内容について説明しました。
シングルステップ、マルチステップ、マルチターン、そして最後にシステム AppFunction の使用方法について説明しました。
最後に、いくつかヒントをご紹介します。
うん
エージェント型エクスペリエンスの構築を始める際は、この 3 つのヒントを念頭に置いてください。
1 つ目は、ツールセットを絞り込むことです。
オンデバイス モデルは規模が小さいため、機能が制限されることに注意してください。
そのため、数十個のツールを登録するのではなく、少数のツールを使用することをおすすめします。
また、前述のとおり、明確な KDoc の説明を記述します。モデルは、この説明に基づいて、何を返すかを決定します。
また、最大ターン数のガードレールも設定する必要があります。
これにより、モデルが混乱した場合に無限ループに陥るのを防ぐこともできます。
これは、ユーザーのバッテリーを消耗させる可能性があることを意味します。
これがあれば、ユーザーのバッテリーを節約できます。
最後に、関数呼び出しがデバイスですでにサポートされているかどうかを確認し、適切なフォールバックを実装する必要があります。
たとえば、サポートされていない場合は、クラウドモデルまたは標準の UI を参照できます。
では、リソースの紹介で締めくくりたいと思います。
もう一度、QR コードを表示します。
ぜひご覧ください。
留意すべき点の一つは、このために Android Skills も用意されているということです。
そのため、すべての詳細を深く掘り下げる必要はありません。
この新しいエージェント API に対応する詳細なドキュメントと Android スキルが用意されています。
ぜひご覧ください。
AICore デベロッパー プレビューをダウンロードして、これらすべてをテストしてみてください。
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)