軟體工程師
建構自主式裝置端代理,進一步提升 Android AI 功能。在這場講座中,我們將深入探討 ML Kit 的全新代理 API,這些 API 是直接建構在您已熟悉的直接模型存取 API 之上。
您將使用開放原始碼 Jetpacker 應用程式,瞭解如何串連店面資料的多步驟動作、管理具有獨立情境的有狀態多輪工作階段,以及連結 Android MCP,透過單一程式碼集支援應用程式內助理和 Android 系統層級介面。
AgentRunner 會協調工具執行作業,並提供工作階段方法,讓多輪對話不會遺漏相關資訊。轉錄稿
我是 Jolanda Verhoef。
我是 Android 開發人員關係工程師,今天 AICore 工程團隊的 Victoria Lestari 也會加入。
大家好,
感謝各位今天早上撥冗參加。
因此,我們的團隊多年來密切合作,將最先進的 AI 模型從雲端轉移到 Android 裝置上直接執行。
這些成果都收錄在 ML Kit GenAI 套件中。
一方面,ML Kit GenAI 套件提供高階 API,可用於文字摘要、圖片說明和語音辨識等統包解決方案。
另一方面,我們也開放直接存取裝置端模型 Gemini Nano。
今天,我們將更進一步。
因此,透過這項直接存取功能,您將能建構應用程式內代理,這些代理可以跨多輪對話進行推理、儲存對話內容,並在應用程式中採取行動。
Victoria,這句話實際上的意思是什麼?
是啊
到目前為止,我們談到裝置端用途時,都是指較短、較簡單、獨立的工作,例如偵測可能的詐騙行為,或摘要通話內容。
如果想建構更複雜的功能,例如詢問相片目錄相關問題,或向 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 的裝置上推出。
模型具備這類功能固然很好,但開發人員無法直接使用模型功能,
因此,我們也致力於在 ML Kit GenAI 套件中加入合適的 API,確保建構代理變得輕而易舉。
因此,我們一開始先使用現有的直接存取 API,並在其中加入函式類型和對話記錄類型。
現在,您可以使用 ML Kit Prompt API 的這兩項新功能,在應用程式中建構這類低階代理能力。
不過,到時您需要自行處理許多事情。
因此,您需要手動執行函式、儲存通話記錄,以及自行循環執行多個呼叫。
因此,如果需要執行多個呼叫,就必須自行建構某種迴圈系統,並為系統建構自己的護欄。
雖然可以這麼做,但我們當然希望快速又簡單。
因此,除了這個 Prompt API 程式庫外,我們還新增了代理流程程式庫。
您可以使用 com.mlkit.genai:agent-api 找到這項功能,目前為 Alpha 版。
現在,有了這個新 API,我們就能為您處理許多繁重的工作。
因此,您不必執行許多手動步驟,只要幾行程式碼,就能建構代理。
這正是我們今天要做的事。
因此,我們將透過範例應用程式 Jetpacker,瞭解這些 API 的實際運作方式。
您可能已經在 Google I/O 大會和本次會議中看過 Jetpacker。
不過,您可以掃描這個 QR code,查看程式碼、將代理指向程式碼、與程式碼互動,並進一步瞭解我們如何實作所有這些功能。
Jetpacker 是我們打造的現代旅遊助理,主要用於展示許多不同的智慧功能。
因此,它已經使用裝置端模型和雲端模型,來處理各種提示 API,但我們現在要建構的是,在其中加入代理。
對於 Jetpacker 這個代理,我們會使用剛才提到的新 ML Kit GenAI 代理 API,並透過 Android 裝置上執行的基礎 AI Core 系統,與 Gemini Nano 模型通訊。
現在,我可以暢所欲言,我們也會談論很多,但當然,我們想展示這一點。
現在就切換到手機,實際示範這項功能。
我手上有 Pixel 11 Pro。
我想我們可以切換到 Pro,你應該就能看到。
好的,就是這個。
如您所見,Jetpacker 應用程式正在這裡執行。
這裡列出我們即將進行的行程,我可以查看目前的行程,可以看到今天是 10 月 29 日,所以有裝置端 AI 工作坊、展示活動,明天則有金門灣遊船和博物館參觀行程。
這些都很棒。
現在,我們可以前往費用畫面,追蹤這趟行程的預算。
如右下方所示,畫面上出現新的小閃光,代表我們打造的裝置端代理。
首先,我們從相當簡單的指令開始。
我想我們可以要求它新增特定支出。
假設我們要新增一筆費用,內容是搭計程車前往 Mountain View,金額為 $45 美元。
有了
Gemini 正在準備費用,並新增了費用。
有了!
好,示範 1 成功了。
讓我們回到投影片,看看實際運作方式。
首先,如你所見,我剛才使用語音輸入,而這項功能實際上也採用 Gemini Nano 模型,並透過語音辨識 API 將我說的話轉錄成文字。
這段文字很合適,可以傳送給 Gemini Nano 模型。
但這會造成什麼影響?
例如,我要求它將某個項目新增至支出,並提供 $45 美元的資訊,但它並不知道該如何執行,對吧?
此時,代理只會回覆「我不知道你在說什麼」。
因此,我們要在提示中加入一些內容。
我們可以在提示中加入各種工具,例如取得行程預算、行程等。如果透過 API 將這些工具傳遞給 Nano 模型,模型會自動理解,現在可以傳回函式呼叫,而不是文字。
因此,在我們的例子中,它實際上會說:「好吧,我不會用文字回應,而是會新增一筆 $45 美元的計程車費用,前往 Mountain View。」
太棒了,但 Victoria,這實際上是如何運作的呢?
如何建構這個模型?
是啊
那麼,如何在程式碼中定義工具呢?
這就是我要說明的內容。
因此,在 Android MCP 中,我們使用 Jetpack AppFunctions 程式庫來定義及發布這些工具。
因此,AppFunctions 程式庫可將應用程式的服務、資料和動作,公開至 Android 作業系統登錄檔,這樣我們就能重複使用完全相同的函式,宣告及描述工具,並在應用程式中使用。
使用者也能透過代理和系統層級互動完成這項工作。
因此,我會在程式碼中提供具體範例。
/**
* 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,這表示我們編寫的這些函式只能由您的應用程式和應用程式的代理程式看到。
接著,我們就能定義及實作暫停函式本身,並編寫多個應用程式函式,然後將這些函式分組到單一服務類別。
好的。
現在我們有了工具,也能編寫工具,這很棒。
不過,要建構代理,光有這些 AppFunction 還不夠。
因此,在編寫代理時,還有三個重要部分需要瞭解。
首先,我們要定義代理。
因此,我們正在定義系統指令、護欄、設定,以及如何使用。
我們還有 AgentRunner。
AgentRunner 是為我們執行代理的工具。
因此,這會處理我們討論過的迴圈機制。
最後是 AgentEvents,系統和 API 會透過 AgentEvents 告知實際執行的動作。
因此,模型會告訴我們何時傳回文字、何時呼叫工具等,以便我們在 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,系統就會在收到回覆時收集。
就這樣。
這確實只有幾行程式碼,所以我們並非開玩笑。
只要幾行程式碼,就能建立代理。
代理會執行您要求的操作。
就會提供文字回覆。
當然,在第一個示範中,我們只執行了非常簡單的指令,對吧?
就像我下達一個指令,它就執行一個動作,僅此而已,所以還不算是一個完整的代理,對吧?
我們來加點料吧。
接下來,我想下達指令,但要稍微提高難度。
現在回到示範裝置,看看能不能提高難度。
我們已經到了。
有了!
如前所述,我們明天安排了博物館參觀行程。
如果點開博物館行程,可以看到門票價格是 $25 美元。
現在來看看代理是否能自行找出答案。
因此,我回到支出畫面,並下達指令:「將博物館門票新增至支出」。
有了
如你所見,系統正在檢查我的行程、尋找博物館參觀活動,並將其加入我的費用。
因此,這裡需要三個步驟才能實際達到目標,並提供最終文字回覆。
同樣地,這一切都會自動完成。
您已看到該模型在裝置上的實際速度。
現在回到投影片,看看運作方式。
如您所見,我們對第一個示範進行了一些調整。
在這個例子中,如您所見,我並未提供博物館或費用的資訊,因此模型在取得提示和工具後,會先決定要採取什麼行動。
因此,模型會判斷需要進一步瞭解我的目前行程,並呼叫 getItinerary。
收到回覆後,模型就會知道行程中有參觀博物館的活動,這可能就是使用者提到的博物館。
現在來找出該事件的詳細資料。
在該特定事件的所有其他中繼資料中,系統會看到金額為 $25 美元。
它理解了,然後返回,並理解它需要呼叫新增費用。
因此,我們可以看到多個步驟接連發生。
Victoria,可以請你進一步說明嗎?
是啊
Jolanda 剛才描述的是她示範的具體例子,但我會從更抽象的概念層面來探討。
因此,每次執行多步驟呼叫函式時,都會發生這種情況。
首先,這是我們的應用程式,而這是 Gemini Nano 模型。
使用者會先將查詢和可用工具清單傳送給模型。
因此,模型會根據這項資訊決定要傳回的內容。
假設在第一個案例中,模型傳回工具呼叫 1。
接著,應用程式會執行這項工具呼叫,並取得工具回應。
接著,系統會將完整的對話記錄傳回模型,包括原始查詢、工具清單、第一個工具呼叫和第一個工具回覆。
接著,模型會再次使用這項資訊,決定下一步該怎麼做。
因此,這個過程會持續進行,直到得出最終結果。
假設模型傳回第 n 個工具呼叫,應用程式會再次傳回整個對話記錄。
當模型認為查詢已完成,就會將最終回覆傳回應用程式,應用程式則會在使用者介面顯示完成。
好的。
現在,我將說明如何在程式碼中實作這項功能。
/**
* 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
}
}這與單一步驟範例非常相似,只是多了兩個部分。
首先,我們需要設定最大回合數,在本範例中,我們設定為五。
這項功能有什麼作用?
這樣模型就不會無限循環。
假設提示不明確,或是不斷來回呼叫不同工具,設定這個參數就能停止迴圈。
接著,我們仍會呼叫相同的函式來執行,也就是 runSingleTurnAsync。
因此,雖然稱為單輪,但實際上,一輪代表一個使用者查詢。
因此,使用者期望的是,我傳送一個查詢,你回傳一個答案,而不會太在意細節,例如背後發生了什麼事、迴圈了多少次等等。
因此,我們可以繼續使用同一個函式。
此外,由於多步驟工具呼叫需要較多時間,您在示範中也會看到,系統會顯示目前呼叫的函式、正在執行的作業,以及是否已完成。
因此,我們想在 UI 中顯示即時進度。
因此,我們不只收集文字回覆,還能在 AgentEvent 發出 FunctionCall 或 FunctionResponse 時,顯示工具名稱 event.name,並在完成後像之前一樣傳回 TextMessage。
真厲害!
因此,我們將這個執行非同步作業稱為執行單輪非同步作業,這當然假設還有另一種 API,對吧?
是啊
因此,只執行一個步驟當然不夠。
我們想執行多步驟。
接下來,我們來進行多輪對話,我想可以回到我的示範,並在這裡增加一點難度。
我們剛才提到,我們有博物館參觀行程,但也有金門灣遊船行程。
所以我想先確認預算是否足夠,再實際預訂。
現在就來提問吧。
好的,
我打算明天帶團隊去金門灣遊船,我以為費用大概是 $180 美元。
如果我這麼做,實際剩餘預算會是多少?
那麼,這項功能的作用是什麼?
回答剩餘預算。
因此能理解目前的行程。
它有這項資訊,可以告訴你。
不過,我也可以在這裡提出後續問題。
好,這樣很好。
可以幫我新增到支出清單嗎?
這樣就完成了,系統會繼續執行。
噢,需要貨幣,太棒了,示範效果。
好,所以是以美元為單位。
$180。
有了
這是第一次發生這種情況,太酷了。
好,那它在做什麼?
系統會顯示這項作業花了三個回合,並計入費用,同時允許我重新整理這個工作階段。
頂端有一個類似重新整理的按鈕,點選後就能清除完整記錄,重新開始。
接下來,我們回到投影片。
我們有可運作的示範,都能達到我們想要的效果,所以我認為我們可以回到程式碼。
是啊
這真的很棒。
你可能注意到 Jolanda 沒有提到,只說「哦,這是美元」,她沒有說這是金門灣遊輪船票。
那麼,模型如何得知所有背景資訊?
這與前兩個程式碼片段略有不同。
// 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,而非較簡單的代理流程。
第一個是簡單的應用實例,非常簡單。
在代理工作流程中,我們會將完整對話、查詢、工具、工具呼叫、工具回覆等所有內容傳回模型。
在某些情況下,我們可能不想這麼做,只想傳送提示。
假設我們想傳送提示給模型,模型回傳工具呼叫,但我們不想回傳工具呼叫,而是想傳送另一個提示給模型,這時就能編輯記錄,就像完全掌控記錄一樣。
以下提供更具體的例子。
// 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()
)假設我們有一個應用程式,可以變更裝置的背景主題。
假設使用者傳送了「將背景變更為日落」這項查詢。
接著,應用程式執行了這個函式,背景就變成日落。
接著,使用者改變心意,說:「不對,我指的是海灘。」
這樣一來,我們就不必傳送完整狀態,說明裝置已將背景變更為日落,現在要變更為海灘。
不,我們不需要這麼做。
我們只需要傳送第一個和第二個提示詞,因為第一個提示詞包含背景資訊。
因此,它知道這意味著要將背景更改為海灘。
因此,我們可以使用 Prompt API 達成這個目的。
我們能完全掌控記錄,並將脈絡詞元儲存在裝置端,因為裝置端的詞元視窗較有限,這樣可以節省更多詞元。
第二個用途則恰恰相反。
如果我們有複雜的用途,是代理 API 尚不支援的,例如多代理系統,其中有多個子代理互相互動來解決複雜工作,或混合系統,其中既有裝置端模型,也有雲端模型,或我們想要自訂記憶體和內容壓縮。
這與記錄有關,就像我之前提到的,可以完全控制記錄。
假設我們想限制脈絡窗口,不將所有內容傳回模型,只總結最重要的部分,或最新的部分,這樣我們就能完全控制,然後使用直接存取 API。
針對這類用途,您可以使用 Agent Development Kit (ADK) 或 Crew 等框架,也可以自行建立自訂自動化調度管理框架。
好的。
以上就是代理 API 的所有內容。
不過,我們還想談談另一件事。
我們一直在討論應用程式內代理,也就是在自己的應用程式中建構代理體驗,但我們也討論了如何使用 AppFunctions,以及如果只在自己的應用程式中執行操作,為什麼要使用 AppFunctions?
我們知道,像您這樣的開發人員不喜歡重複做同樣的事,對吧?
我們不喜歡複製貼上程式碼,也不喜歡複製任何商業邏輯。
因此,只要透過 AppFunctions 進行簡單的變更,就能讓系統代理使用我們的工具,而不僅限於應用程式內代理。
那麼,程式碼會是什麼樣子?
是啊
因此,我們只需要編寫一次 AppFunctions,就能視需要將其提供給不同層級。
您還記得我們之前有 ACCESS_LEVEL_SELF,它只會將我們編寫的函式公開給應用程式內代理,但我們也可以將該存取層級變更為 ACCESS_LEVEL_ANDROID_TRUSTED。
這樣一來,Android 作業系統中的信任代理程式就能使用這些函式。
因此不必重寫函式、指令或資料庫邏輯。
AppFunctions 已是應用程式內外的語音功能。
我們也想看看這項功能的示範,對吧?
好的,讓我們回到手機,在最後一個示範中,我們實際離開 Jetpacker 應用程式。
所以我們可以退出,然後實際進入 Gemini。
因此,我們可以在 Gemini 中呼叫 @Jetpacker。
請注意,這項功能目前僅開放部分使用者參與 Beta 版測試,因此並非所有人都能立即使用,但可以測試和試用。
我們來看看 Jetpacker。
我們應該怎麼做?
噢,我今天早上喝過咖啡,對吧?
請在 ADS 行程費用中新增 $7 美元的咖啡。
有了
您可以看到它連線到 Jetpacker 應用程式。
大功告成!
它幫我新增了費用。
我們基本上已瞭解如何使用 Jetpacker 中的所有功能。
Jetpacker 的好處是,我們也可以開啟飛航模式。
我們今天還沒展示過這項功能,但只要開啟飛航模式,就能使用剛才示範的所有功能。我想展示這項功能,因為我想確保大家都能信任我,所以我們關閉 Wi-Fi 吧。
有人可以提供適合示範的支出項目嗎?
好的,
或許我們可以做「這是什麼?」
租車應該不錯,對吧?
好的。
我們來試試「新增一筆 $300 美元的租車費用」。
這樣就完成了。
好的。
有了!
好的,我們回到投影片。
我們剛才談到 AppFunctions。
我知道我們剛才只是稍微提到。
如果想瞭解更多資訊,Dan 和 Sarah 還有另一場演講,更詳細地說明如何啟用這項功能、如何測試、我們提供哪些工具等。
今天我們介紹了許多不同的內容。
我們介紹了單一步驟、多步驟和多輪對話,最後也說明瞭如何使用系統 AppFunction。
最後,我們想提供一些建議。
是啊
因此,在開始建構代理體驗時,請記住這三個訣竅。
首先,請專注於工具組。
因此請記住,裝置端模型規模較小,功能有限。
因此,建議您註冊少量工具,而非數十項。
如前所述,請撰寫清楚的 KDoc 說明,因為模型會根據這些說明決定要傳回的內容。
接著,我們也應該設定最大回合數護欄。
這也能避免模型在混淆時陷入無窮迴圈。
這表示我們可能會耗盡使用者的電池電力。
因此,如果我們有這個,就可以節省使用者的電池。
最後,我們需要檢查裝置是否已支援函式呼叫,並實作適當的備用機制。
舉例來說,如果裝置不支援,可以參考雲端模型或標準 UI。
好的,我想我們可以用資源來結束這個話題。
我會再次顯示 QR code。
請參閱。
請注意,我們也提供 Android 技能。
因此您不必深入探究每個細節。
我們提供完整的說明文件和 Android 技能,可搭配這個新的代理式 API 使用。
請務必查看。
請下載 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()
)