Inżynier ds. relacji z deweloperami
Inżynier oprogramowania
Przenieś funkcje AI na Androidzie na wyższy poziom, tworząc autonomiczne agenty na urządzeniu. Podczas tej sesji omówimy nowe interfejsy API ML Kit oparte na agentach, które zostały utworzone bezpośrednio na bazie znanych Ci interfejsów API z bezpośrednim dostępem do modelu.
Korzystając z aplikacji Jetpacker typu open source, dowiesz się, jak łączyć wieloetapowe działania na danych produktów dostępnych lokalnie, zarządzać stanowymi wieloetapowymi sesjami z izolowanymi kontekstami i łączyć protokół MCP na Androidzie, aby obsługiwać asystentów w aplikacji i platformy na poziomie systemu Android za pomocą jednej bazy kodu.
AgentRunner koordynuje wykonywanie narzędzi i udostępnia metody sesji na potrzeby wieloetapowych rozmów bez utraty kontekstu.Transkrypcja
Nazywam się Jolanda Verhoef.
Jestem inżynierem ds. relacji z programistami w zespole Androida, a dzisiaj jest ze mną Victoria Lestari, która należy do zespołu inżynierów AICore.
Dzień dobry,
Dziękuję za udział w dzisiejszym spotkaniu.
Dlatego nasze zespoły od lat ściśle współpracują, aby przenieść najnowocześniejsze modele AI z chmury i uruchamiać je bezpośrednio na urządzeniach z Androidem.
Te działania są zgrupowane w ramach pakietu ML Kit GenAI.
Z jednej strony w tym pakiecie ML Kit GenAI mamy interfejsy API wysokiego poziomu do gotowych rozwiązań, takich jak podsumowywanie tekstu, opisywanie obrazów i rozpoznawanie mowy.
Z drugiej strony udostępniliśmy też bezpośredni dostęp do modelu Gemini Nano działającego na urządzeniu.
Dzisiaj idziemy o krok dalej.
Dzięki temu bezpośredniemu dostępowi umożliwimy Ci tworzenie agentów w aplikacji, którzy mogą rozumować w wielu turach, zapisywać kontekst rozmowy i podejmować działania w aplikacji.
Co to właściwie oznacza, Victoria?
Tak.
Do tej pory, gdy mówiliśmy o przypadkach użycia na urządzeniu, mieliśmy na myśli krótsze, prostsze i odizolowane zadania, takie jak wykrywanie potencjalnego oszustwa czy podsumowywanie rozmowy telefonicznej.
A gdy chcemy stworzyć coś bardziej złożonego, na przykład zadawać pytania dotyczące katalogu zdjęć lub Map Google, naturalnie odwołujemy się do dużych modeli działających w chmurze.
Modele w chmurze oferują więc ogromną moc obliczeniową i dostęp do danych w czasie rzeczywistym, ale AI na urządzeniu otwiera nowe możliwości w zakresie niskiego czasu oczekiwania oraz przetwarzania danych produktów dostępnych lokalnie i w czasie rzeczywistym.
Gemini Nano już teraz rozszerza możliwości urządzeń.
Jednym z przykładów są Zrzuty ekranu na Pixelu, które umożliwiają wyszukiwanie lokalnych zrzutów ekranu i zadawanie pytań na ich temat.
Dlatego gdy uruchamiamy agentów na urządzeniu, potrzebujemy znacznego skoku możliwości modelu. Dlatego tworzymy Gemini Nano 4, który jest w stanie rozumować na różnych etapach i podejmować rzeczywiste działania.
Ta nowa funkcja ma jednak znaczenie tylko wtedy, gdy deweloperzy mają prawdziwe urządzenia, na które mogą kierować reklamy.
Dobra wiadomość jest taka, że Gemini Nano 4 jest już dostępny na wybranych flagowych urządzeniach, a możesz go też przetestować na wielu innych urządzeniach, korzystając z wersji przedpremierowej AICore dla deweloperów.
Na początku przyszłego roku planujemy też osiągnąć liczbę 100 milionów urządzeń.
Co to oznacza?
Oznacza to, że możesz zacząć tworzyć i testować te doświadczenia już dziś, wiedząc, że to, co tworzysz, będzie dostępne dla ogromnej liczby odbiorców.
Omówiliśmy już Gemini Nano 4 i potencjalny zasięg, więc teraz porozmawiajmy o tym, co naprawdę oznaczają funkcje agentowe.
Dzięki temu agentowa AI może planować złożone zadania, dzielić je na kilka etapów, a następnie wykonywać te etapy w aplikacji.
Załóżmy, że nasz model Gemini Nano na urządzeniu może wysyłać zapytania do lokalnej bazy danych, pobierać dane z backendu, rozumować i odpowiadać na złożone pytania, a także uruchamiać przepływ pracy aplikacji natywnej na urządzeniu.
Ale jak AI przechodzi od planowania do rzeczywistych działań?
Dzisiejszy temat to wywoływanie funkcji.
To podstawowy silnik funkcji agentowych.
Tłumaczy język naturalny na wykonywanie systemowe i przekształca model z pasywnego rozumowania na aktywne wykonywanie, wybierając i uruchamiając lokalne interfejsy API potrzebne do rozwiązywania złożonych zadań.
Z przyjemnością informujemy też, że wywoływanie funkcji jest natywnie obsługiwane w Gemini Nano 4.
Obecnie jest dostępny w wersji przedpremierowej AICore dla programistów, a wkrótce zostanie udostępniony na wszystkich urządzeniach obsługujących Nano 4.
To świetnie, że model ma takie możliwości, ale możliwości modelu nie dają jeszcze deweloperom interfejsów API.
Pracujemy też nad dodaniem odpowiednich interfejsów API do pakietu ML Kit GenAI, aby tworzenie agenta było naprawdę proste.
Zaczęliśmy od interfejsów API bezpośredniego dostępu, które już mieliśmy, i dodaliśmy do nich typy funkcji i historii rozmów.
Dzięki tym 2 nowym dodatkom do interfejsu Prompt API w ML Kit możesz teraz tworzyć w aplikacji te niskopoziomowe funkcje agentowe.
Jednak w takim przypadku musisz wykonać sporo czynności samodzielnie.
Musisz więc ręcznie wykonywać funkcje, zapisywać historię połączeń i samodzielnie wykonywać wiele połączeń.
Jeśli więc musisz wykonać kilka wywołań, musisz samodzielnie utworzyć system pętli i zabezpieczenia dla systemu.
Jest to możliwe, ale chcemy, aby było to szybkie i łatwe.
Dlatego oprócz biblioteki interfejsów API promptów dodaliśmy też nową bibliotekę przepływów agentowych.
Możesz ją znaleźć w com.mlkit.genai:agent-api. Obecnie jest dostępna w wersji alfa.
Teraz, dzięki nowemu interfejsowi API, wykonujemy za Ciebie wiele trudnych zadań.
Zamiast wykonywać wiele czynności ręcznie, możesz utworzyć agenta za pomocą kilku wierszy kodu.
I właśnie to dzisiaj zrobimy.
Zobaczmy, jak te interfejsy API działają w naszej przykładowej aplikacji Jetpacker.
Widzieliście już Jetpackera na Google I/O i podczas tej konferencji.
Możesz jednak zeskanować ten kod QR i jeśli chcesz, możesz przyjrzeć się kodowi, wskazać go agentowi, wejść z nim w interakcję i dowiedzieć się więcej o tym, jak wdrożyliśmy wszystkie te funkcje.
Jetpacker to nowoczesny asystent podróży, który stworzyliśmy, aby zaprezentować wiele różnych inteligentnych funkcji.
Korzysta już z modeli na urządzeniu i w chmurze na potrzeby różnych interfejsów API promptów, ale teraz dodamy do niego agenta.
W przypadku tego agenta, Jetpackera, użyjemy nowego interfejsu ML Kit GenAI Agent API, o którym właśnie mówiliśmy. Będzie on używać podstawowego systemu AI Core, który działa na urządzeniach z Androidem, do komunikacji z modelem Gemini Nano.
Mogę mówić, ile chcę, i będziemy dużo rozmawiać, ale oczywiście chcemy to zaprezentować.
Przejdźmy do telefonu i pokażmy, jak to działa.
Mam tutaj Pixela 11 Pro.
Myślę, że możemy przełączyć się na wersję Pro, abyście mogli to zobaczyć.
No i proszę.
Jak widać, mamy tu uruchomioną aplikację Jetpacker.
Znajduje się tam lista naszych nadchodzących podróży. Mogę też sprawdzić bieżącą podróż. W bieżącej podróży widać, że jest 29 października, więc mamy warsztaty dotyczące AI na urządzeniu i prezentację wersji demonstracyjnej. Jutro mam rejs po zatoce Golden Gate i wizytę w muzeum.
To wszystko jest świetne.
Teraz możemy przejść do ekranu wydatków, na którym śledzimy budżet tej podróży.
Jak widać w prawym dolnym rogu, jest tam nowa mała iskierka agentowa, która reprezentuje utworzonego przez nas agenta na urządzeniu.
Zacznijmy od prostego polecenia.
Myślę, że możemy poprosić o dodanie konkretnego wydatku.
Powiedzmy: dodaj wydatek na taksówkę do Mountain View, 45 USD.
I o to chodzi.
Przygotowuje wydatek i dodaje go.
Niesamowite!
OK, pierwsza wersja demonstracyjna zadziałała.
Wróćmy do slajdów i zobaczmy, jak to działało.
Po pierwsze, jak zauważyliście, używałem głosu, który jest również wykorzystywany przez model Gemini Nano za pomocą interfejsu API rozpoznawania mowy do transkrypcji tego, co mówiłem, na tekst.
Ten tekst jest świetny i możemy go wysłać do modelu Gemini Nano.
Ale co by robił?
Na przykład mówię, żeby dodać coś do wydatków, mówię coś o 45 USD, ale model nie wie, jak się zachować, prawda?
W tym momencie powie mi tylko: „Nie wiem, o czym mówisz”.
Chcemy więc nieco rozbudować tego prompta.
Możemy dodać do prompta różne narzędzia. W tym przypadku dodaję narzędzia do uzyskiwania budżetu podróży, planu podróży itp. Jeśli przekażesz to do modelu Nano za pomocą naszych interfejsów API, automatycznie zrozumie on, że zamiast tekstu może teraz zwrócić wywołanie funkcji.
W naszym przypadku zamiast odpowiadać tekstem, doda wydatek w wysokości 45 USD za taksówkę do Mountain View.
To świetnie, ale jak to właściwie działa?
Jak to zrobić?
Tak.
Jak więc definiujemy narzędzia w kodzie?
Wyjaśnię to.
Dlatego w przypadku Android MCP używamy biblioteki Jetpack AppFunctions do definiowania i publikowania tych narzędzi.
Dzięki temu biblioteka AppFunctions może udostępniać usługi, dane i działania aplikacji w rejestrze systemu operacyjnego Android, co pozwala nam ponownie używać tych samych funkcji do deklarowania i opisywania narzędzi oraz używania ich w aplikacji.
Umożliwia to też użytkownikowi wykonanie tego zadania za pomocą agentów i interakcji na poziomie systemu.
Podam konkretny przykład w kodzie.
/**
* 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...
}Jak widać, jest to przykład AppFunctionDeclaration.
Komentarz KDoc u góry opisuje działanie funkcji i jej parametry w języku naturalnym.
Dlatego najlepiej jest podać jak najdokładniejszy opis, ponieważ wyślemy go do modelu.
Poniżej znajduje się adnotacja @AppFunctionAccessLevel.
W ten sposób określamy zakres widoczności funkcji.
W tym przykładzie mamy ACCESS_LEVEL_SELF, co oznacza, że funkcje, które piszemy, są widoczne tylko dla aplikacji i jej agentów.
Następnie możemy zdefiniować i wdrożyć samą funkcję zawieszenia oraz napisać dowolną liczbę funkcji aplikacji i pogrupować je w jedną klasę usługi.
OK.
Mamy więc narzędzia i możemy je pisać, co jest świetne.
Oczywiście do utworzenia agenta potrzebujemy czegoś więcej niż tylko funkcji AppFunctions.
Mamy jeszcze 3 rodzaje elementów, o których warto wiedzieć podczas pisania agenta.
Najpierw definiujemy agenta.
Definiujemy więc instrukcje systemowe, bariery ochronne, konfigurację i sposób pracy z modelem.
Mamy też AgentRunner.
AgentRunner to element, który wykonuje agenta.
W ten sposób zajmujemy się mechanizmem pętli, o którym mówiliśmy.
Na koniec mamy AgentEvents, czyli sposób, w jaki system i interfejs API informują nas o tym, co faktycznie robią.
Dzięki temu będziemy wiedzieć, kiedy zwraca tekst, kiedy wywołuje narzędzie itd., i będziemy mogli wyświetlić te informacje w interfejsie.
OK.
Przyjrzyjmy się temu kodowi.
OK.
Zanim zaczniemy pisać kod, musimy najpierw sprawdzić, czy wywoływanie funkcji jest dostępne na Twoim urządzeniu, ponieważ jest ono obsługiwane tylko na Gemini Nano 4.
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}Najpierw musimy to sprawdzić, wywołując model generatywny getAvailableFeatures.
Możemy po prostu sprawdzić, czy wywoływanie funkcji jest wymienione wśród funkcji.
Jeśli nie, możemy wdrożyć odpowiednią kreację zastępczą.
OK.
Przejdźmy teraz do kodu.
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 }
}Wiemy, że wywoływanie funkcji jest obsługiwane na naszym urządzeniu, więc możemy po prostu zacząć tworzyć agenta za pomocą Agent.builder.
Następnie możemy ustawić jego nazwę.
Możemy setInstructions, aby zdefiniować kontekst i osobę.
Na koniec wywołujemy funkcję enableAppFunctions.
Ta funkcja będzie wywoływać pętlę i automatycznie wykrywać wszystkie funkcje aplikacji, które wymieniliśmy wcześniej, i właśnie utworzyliśmy agenta.
Następnie musimy zainicjować AgentRunner.
Ten moduł uruchamiający agenta będzie zarządzał pętlą wykonywania.
Wywołujemy go, wywołując konstruktor i przekazując mu agenta.
Na koniec możemy umieścić transkrypcję polecenia głosowego w bloku treści, aby utworzyć prompt.
Następnie wywołujemy runnera.
Wywołujemy tę funkcję runSingleTurnAsync w prompcie, co spowoduje uruchomienie agenta, przeprowadzenie wnioskowania i wykonanie całego wnioskowania AI.
Jeśli chcemy tylko przesyłać strumieniowo ostateczną odpowiedź tekstową modelu do interfejsu, wystarczy wywołać textTokens, a odpowiedź będzie zbierana w miarę jej napływania.
To wszystko.
To rzeczywiście tylko kilka linijek kodu, więc nie żartowaliśmy, gdy o tym mówiliśmy.
Wystarczy kilka wierszy kodu, aby utworzyć agenta.
Agent wykona Twoje polecenie.
Otrzymamy odpowiedź tekstową.
Ale oczywiście w tej pierwszej prezentacji, którą pokazaliśmy, wykonywaliśmy tylko bardzo proste polecenie, prawda?
Na przykład wydałem jedno polecenie i wykonał jedno działanie, i to wszystko, więc nie jest to jeszcze w pełni funkcjonalny agent, prawda?
Dodajmy więc trochę pikanterii.
W następnej części chcę wydać polecenie, ale chcę, żeby było ono nieco trudniejsze dla agenta.
Wróćmy do mojego urządzenia demonstracyjnego i zobaczmy, czy możemy nieco utrudnić zadanie.
Już tam jesteśmy.
Niesamowite!
Jak już wspominałem, jutro mamy zaplanowaną wizytę w muzeum.
Jeśli chodzi o wizytę w muzeum, po otwarciu tej pozycji widać, że cena biletu wstępu wynosi 25 USD.
Teraz sprawdźmy, czy nasz agent jest w stanie samodzielnie to ustalić.
Wracam więc do ekranu wydatków i wydaję polecenie: „Dodaj bilet do muzeum do moich wydatków”.
I o to chodzi.
Jak widać, sprawdza mój plan podróży, znajduje wizytę w muzeum i dodaje ją do moich wydatków.
W tym przypadku wykonuje 3 kroki, aby osiągnąć cel, i podaje mi ostateczną odpowiedź tekstową.
Wszystko to działa automatycznie.
Można było zobaczyć rzeczywistą szybkość tego modelu na urządzeniu.
Wróćmy do slajdów i zobaczmy, jak to działa.
Jak widzisz, wprowadziliśmy kilka zmian w porównaniu z pierwszą wersją demonstracyjną.
W tym przypadku, jak widać, nie przekazałem informacji o muzeum ani o wydatkach, więc gdy model otrzymał prompt i narzędzia, próbował zdecydować, co zrobić najpierw.
W tym przypadku model decyduje, że musi dowiedzieć się więcej o moim obecnym planie podróży, więc wywoła funkcję getItinerary.
Otrzymuje odpowiedź i wie, że w planie podróży jest wizyta w muzeum, o którą prawdopodobnie chodziło.
Znajdźmy szczegóły tego konkretnego zdarzenia.
Wśród wszystkich innych metadanych tego konkretnego zdarzenia zobaczy, że jest to 25 USD.
Asystent rozumie, co ma zrobić, i wraca do poprzedniego kroku, wiedząc, że musi wywołać funkcję dodawania wydatku.
Widzimy tu kilka kroków, które następują po sobie.
Victoria, czy możesz powiedzieć o tym coś więcej?
Tak.
To, co Jolanda właśnie opisała, to konkretny przykład z prezentacji, którą właśnie pokazała. Ja jednak przedstawię to w bardziej koncepcyjny, abstrakcyjny sposób.
Będzie się to działo za każdym razem, gdy będziemy wykonywać wieloetapowe wywoływanie funkcji.
Po pierwsze, to jest nasza aplikacja, a to model Gemini Nano.
Użytkownik zacznie od wysłania do modelu zapytania i listy dostępnych narzędzi.
Model użyje tych informacji, aby zdecydować, co odesłać.
Załóżmy, że w pierwszym przypadku wysyła wywołanie narzędzia, które nazwiemy Wywołaniem narzędzia 1.
Następnie aplikacja wykona to wywołanie narzędzia i otrzyma odpowiedź.
Następnie do modelu zostanie wysłana pełna historia rozmowy, która obejmuje pierwotne zapytanie, listę narzędzi, pierwsze wywołanie narzędzia i pierwszą odpowiedź narzędzia.
Następnie model ponownie wykorzystuje te informacje, aby zdecydować, co zrobić dalej.
Może to potrwać, zanim uzyskamy ostateczny wynik.
Załóżmy, że model zwróci n-te wywołanie narzędzia, a następnie aplikacja ponownie zwróci całą historię rozmowy.
Gdy model uzna, że zapytanie zostało zrealizowane, wyśle do aplikacji ostateczną odpowiedź, a aplikacja będzie mogła wyświetlić w interfejsie, że zadanie zostało wykonane.
OK.
Teraz wyjaśnię, jak to zaimplementować w kodzie.
/**
* 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 { ... }Pamiętasz przykład z kodem jednoetapowym. To jest kod wieloetapowy, więc wygląda bardzo podobnie. Najważniejsze jest, aby napisać KDoc bardzo, bardzo wyraźnie.
W tym przykładzie funkcja getItinerary wyraźnie wskazuje, że zwraca tylko ogólne podsumowanie zdarzeń.
Wspomnieliśmy też, że jeśli chcemy uzyskać bardziej szczegółowe informacje o przejeździe, musimy wywołać metodę getEventDetails z parametrem eventId.
getEventDetails zawiera też komentarz KDoc, który informuje, że może pobierać szczegóły, takie jak ceny biletów i inne uwagi.
Dzięki komentarzowi i jego jasnemu opisowi możemy pomóc modelowi zrozumieć, jak narzędzia ze sobą współdziałają.
OK.
W ten sposób tworzymy agenta, który wykonuje całą tę pętlę agentową.
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
}
}Jest bardzo podobny do przykładu z jednym krokiem, ale zawiera 2 dodatkowe elementy.
Najpierw musimy ustawić maksymalną liczbę tur. W tym przykładzie ustawimy 5.
Jak to działa?
Zapobiega to zapętleniu modelu w nieskończoność.
Załóżmy, że prompt jest niejednoznaczny lub model ciągle wywołuje jedno narzędzie po drugim, a potem wraca do poprzedniego. Jeśli ustawimy tę opcję, model się zatrzyma i nie będzie działać w pętli.
Następnie wywołujemy tę samą funkcję, aby ją uruchomić: runSingleTurnAsync.
Chociaż nazywa się to „jedną turą”, w rzeczywistości jedna tura oznacza jedno zapytanie użytkownika.
Użytkownik oczekuje, że wyśle jedno zapytanie, a Ty zwrócisz jedną odpowiedź, bez zagłębiania się w szczegóły, takie jak to, co działo się pod maską, ile razy zapytanie było powtarzane itp.
Możemy więc nadal używać tej samej funkcji.
Ponieważ wieloetapowe wywoływanie narzędzi zajmuje trochę więcej czasu, w prezentacji pokazaliśmy, że narzędzie wyświetla, jaką funkcję aktualnie wywołuje, co jest aktualnie wykonywane i czy zostało już zakończone.
Chcemy więc pokazywać postępy w interfejsie na żywo.
Zamiast więc tylko zbierać odpowiedź tekstową, możemy wyświetlać nazwę narzędzia, event.name, gdy AgentEvent wyemituje FunctionCall lub FunctionResponse, a gdy to nastąpi, możemy po prostu zwrócić TextMessage jak wcześniej.
Super!
So, we're calling this run async run single-turn async, which, of course, kind of assumes that there's also another kind of API, right?
Tak.
Oczywiście nie wystarczy wykonać tylko jednego kroku.
Chcemy, aby było to wieloetapowe.
Przejdźmy teraz do sesji wieloetapowych. Myślę, że możemy wrócić do mojej wersji demonstracyjnej i nieco ją utrudnić.
Rozmawialiśmy o wizycie w muzeum, ale mieliśmy też rejs po zatoce Golden Gate.
W tym przypadku chcę się dowiedzieć, czy mam wystarczający budżet, zanim faktycznie zarezerwuję tę kampanię.
Zadajmy pytanie.
Zobaczmy.
Myślę o tym, żeby jutro zabrać mój zespół na rejs po zatoce Golden Gate. Wydawało mi się, że kosztuje to około 180 USD.
Jeśli to zrobię, jaki będzie mój pozostały budżet?
Co więc robi?
Odpowiada na pytanie, jaki będzie mój pozostały budżet.
Dzięki temu rozumie bieżący plan podróży.
Ma te informacje i może je podać.
Mogę też zadać dodatkowe pytanie.
OK, to dobrze.
Czy możesz dodać to do moich wydatków?
I o to chodzi.
O, potrzebuje waluty, więc fajnie, efekt demonstracyjny.
OK, czyli jest w dolarach.
180 USD.
I o to chodzi.
To pierwszy raz, kiedy coś takiego się zdarzyło. To super.
Co on robi?
Widzę, że zajęło to 3 tury i zwiększyło wydatki. Mogę też odświeżyć tę sesję.
U góry mam przycisk odświeżania, który pozwala mi wyczyścić całą historię i zacząć od nowa.
Myślę, że możemy wrócić do slajdów.
Mamy działające wersje demonstracyjne, które robią to, co chcemy, więc myślę, że możemy wrócić do kodu.
Tak.
To naprawdę świetna funkcja.
Zauważ, że Jolanda nie wspomniała, że chodzi o bilet na rejs po zatoce Golden Gate, tylko że kwota jest podana w dolarach.
Jak model poznaje cały kontekst?
To nieco inne rozwiązanie niż w poprzednich 2 fragmentach kodu.
// 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()Zamiast używać AgentRunner i runSingleTurnAsync, musimy utworzyć sesję.
W tym przypadku wywołujemy runner.getOrCreateSession z tripId.
Dlaczego więc musimy utworzyć sesję?
Ponieważ runSingleTurnAsync zresetuje pamięć, więc nie będzie mieć kontekstu.
Nie będziemy mogli prowadzić rozmowy wieloetapowej.
Jeśli więc chcesz przeprowadzić rozmowę wieloetapową, używamy sesji agenta.
Następnie przekazujemy tripId.
Dlaczego musimy to zrobić?
Aplikacja Jetpacker, którą już znasz, zarządza wieloma podróżami.
Może to być wyjazd do ADS, Paryża czy Tokio.
Dlatego chcemy zachować kontekst przejazdu.
Nie chcemy sobie przeszkadzać.
Zamiast wywoływać runSingleTurnAsync, mamy runAsync.
To jest coś innego.
To polecenie jest uruchamiane w sesji, a potem nadal mamy ten sam prompt.
Możemy nadal emitować AgentEvent, FunctionCall, FunctionResponse i TextMessage tak jak wcześniej.
Gdy Jolanda kliknęła przycisk nowej rozmowy, czyli przycisk odświeżania, możemy zobaczyć session.delete.
Tak.
OK.
Wszystko to zrobiliśmy za pomocą nowych interfejsów API agentów. To niesamowite i będzie świetnie działać w wielu przypadkach użycia.
Oczywiście zawsze chcemy, aby proste rzeczy były możliwe do wykonania, ale chcemy też, aby możliwe były bardziej złożone przypadki.
Chcemy mieć pewność, że w razie potrzeby możesz też używać interfejsów API bezpośredniego dostępu, aby tworzyć bardziej podstawowe wersje tych funkcji.
Victoria, czy możesz wyjaśnić, co to oznacza i kiedy możemy ponownie przejść na ten poziom dostępu bezpośredniego?
Jasne, Jolanda.
Jeśli chodzi o bezpośredni dostęp, być może znasz już interfejs Prompt API.
Dlatego też obsługujemy wywoływanie funkcji i mamy 2 przypadki użycia, w których wolimy używać tego interfejsu API zamiast łatwiejszego przepływu agentowego.
Pierwszy przypadek użycia jest bardzo prosty.
W przepływie agentowym wysyłamy do modelu całą rozmowę, zapytanie, narzędzia, wywołanie narzędzia, odpowiedź narzędzia – wszystko.
Może się zdarzyć, że nie będziemy chcieli tego robić, a jedynie wysłać prompta.
Załóżmy, że chcemy wysłać prompta do modelu, który zwraca wywołanie narzędzia, ale nie chcemy zwracać wywołania narzędzia, tylko wysłać do modelu kolejnego prompta. Możemy edytować historię, ponieważ mamy nad nią pełną kontrolę.
Podam bardziej konkretny przykład.
// 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()
)Załóżmy, że mamy aplikację, która może zmieniać motyw tła na urządzeniu.
Załóżmy, że użytkownik wysyła zapytanie „Zmień tło na zachód słońca”.
Następnie nasza aplikacja wykonała tę funkcję, a tło zmieniło się na zachód słońca.
Następnie użytkownik zmienił zdanie i powiedział: „O, nie, chodziło mi o plażę”.
W tym przypadku nie musimy wysyłać pełnego stanu, że urządzenie zmieniło już tło na zachód słońca, a teraz chce je zmienić na plażę.
Nie, nie musimy tego robić.
Wystarczy, że wyślemy pierwszy i drugi prompt, ponieważ pierwszy zawiera informacje o tle.
Dzięki temu wie, że chcesz zmienić tło na plażę.
Możemy to zrobić za pomocą interfejsu Prompt API.
Mamy pełną kontrolę nad historią, zapisujemy token kontekstowy na urządzeniu, ponieważ na urządzeniu jest bardziej ograniczone okno tokenów, a dzięki temu możemy zapisać więcej tokenów.
Drugi przypadek użycia jest zupełnie inny.
Jeśli mamy złożone przypadki użycia, których interfejs API agenta nie może jeszcze obsłużyć, na przykład systemy wieloagentowe, w których kilka podagentów współdziała ze sobą w celu rozwiązania złożonego zadania, lub system hybrydowy, w którym mamy zarówno modele na urządzeniu, jak i modele w chmurze, albo jeśli chcemy mieć niestandardową pamięć i kompresję kontekstu.
Dotyczy to historii, czyli pełnej kontroli nad historią, o której wspominałem wcześniej.
Załóżmy, że chcemy, aby okno kontekstu było ograniczone, więc nie chcemy wysyłać wszystkiego z powrotem do modelu, tylko podsumować najważniejszą lub najnowszą część. Mamy nad tym pełną kontrolę, więc używamy interfejsu API bezpośredniego dostępu.
W takich przypadkach użycia możemy użyć pakietów takich jak pakiet Agent Development Kit (pakiet ADK) lub Crew, ale możesz też utworzyć własny pakiet do orkiestracji.
OK.
To wszystko, co chcieliśmy powiedzieć o interfejsie API agenta.
Jest jednak jeszcze jedna kwestia, którą chcielibyśmy poruszyć.
Cały czas rozmawiamy o agentach w aplikacji, więc tworzysz własne środowisko agentowe w swojej aplikacji. Mówiliśmy też jednak o tym, jak używamy funkcji aplikacji. Po co ich używać, jeśli wszystko, co robisz, odbywa się w Twojej aplikacji?
Wiemy, że deweloperzy tacy jak Ty nie lubią robić rzeczy dwa razy, prawda?
Nie lubimy kopiować i wklejać kodu ani duplikować logiki biznesowej.
Dzięki AppFunctions, zamiast zezwalać na korzystanie z nich tylko agentowi w aplikacji, możemy za pomocą bardzo prostej zmiany udostępnić nasze narzędzia również agentom systemowym.
Jak więc wygląda kod?
Tak.
W przypadku AppFunctions wystarczy, że napiszemy je raz, a potem możemy je udostępniać na różnych poziomach warstw, zgodnie z naszymi potrzebami.
Pamiętasz, że wcześniej mieliśmy ACCESS_LEVEL_SELF, który udostępniał napisane przez nas funkcje tylko agentowi w aplikacji, ale możemy też zmienić ten poziom dostępu na ACCESS_LEVEL_ANDROID_TRUSTED.
Dzięki temu funkcje będą dostępne dla zaufanych agentów w systemie operacyjnym Android.
Nie trzeba więc przepisywać funkcji, poleceń ani logiki bazy danych.
AppFunctions już teraz działa jako głos aplikacji zarówno wewnętrznie, jak i zewnętrznie.
Chcemy też zobaczyć, jak to działa, prawda?
Wróćmy do telefonu. W ostatniej demonstracji wyjdziemy z aplikacji Jetpacker.
Możemy więc przejść do Gemini.
W Gemini możemy wywołać @Jetpacker.
Pamiętaj, że ta funkcja jest dostępna tylko w ograniczonej wersji beta, więc nie każdy może z niej korzystać, ale można ją przetestować i wypróbować.
Zobaczmy, co się stanie z Jetpackerem.
Co robimy?
O, rano piłam kawę, prawda?
Czy możesz dodać kawę za 7 USD do wydatków na podróż służbową?
I o to chodzi.
Jak widać, łączy się z aplikacją Jetpacker.
I gotowe.
Dodał za mnie wydatek.
Zobaczyliśmy, jak możemy wykorzystać wszystkie funkcje Jetpackera.
W Jetpackerze możemy też włączyć tryb samolotowy.
Nie pokazaliśmy tego dzisiaj, ale możemy po prostu włączyć tryb samolotowy i wszystko, co widzieliście w prezentacjach, będzie działać. Chcę to pokazać, żebyście mi uwierzyli, więc wyłączmy Wi-Fi.
Czy ktoś ma dobry przykład wydatku, który mogę dodać?
Zobaczmy.
Może zrobimy „Co to jest?”.
Wypożyczalnia samochodów byłaby świetna, prawda?
OK.
Powiedzmy: „Dodaj wydatek w wysokości 300 zł za wynajem samochodu”.
I udało się.
OK.
Niesamowite!
OK, wróćmy do slajdów.
Rozmawialiśmy o AppFunctions.
Wiem, że już o tym wspominaliśmy, ale tylko pobieżnie.
Jeśli chcesz dowiedzieć się więcej, obejrzyj też inne wystąpienie Dana i Sarah, w którym szczegółowo omawiają, jak można to włączyć, jak to przetestować, jakie mamy narzędzia itp.
Dzisiaj omówiliśmy wiele różnych kwestii.
Omówiliśmy funkcje jednoetapowe, wieloetapowe i wieloturowe, a na koniec pokazaliśmy, jak można używać funkcji systemowej AppFunction.
Zanim skończymy, mamy jeszcze kilka wskazówek.
Tak.
Pamiętaj o tych 3 wskazówkach, gdy zaczniemy tworzyć te funkcje oparte na agentach.
Pierwsza z nich to skupienie się na zestawach narzędzi.
Pamiętaj, że modele na urządzeniu mają ograniczone możliwości ze względu na mniejszą skalę.
Zamiast rejestrować dziesiątki narzędzi, lepiej wybrać mniejszą ich liczbę.
Jak już wspominałem, pisz jasne opisy KDoc, ponieważ model używa ich do decydowania, co ma zwrócić.
Następnie powinniśmy też ustawić maksymalną liczbę tur.
Zapobiega to też nieskończonemu zapętleniu modelu, gdy się pogubi.
Może to spowodować rozładowanie baterii użytkownika.
Jeśli więc mamy tę funkcję, możemy oszczędzać baterię użytkownika.
Na koniec musimy sprawdzić, czy wywoływanie funkcji jest już obsługiwane na urządzeniu, i wdrożyć odpowiednie rozwiązanie zastępcze.
Jeśli na przykład nie jest obsługiwany, możesz odwołać się do modeli w chmurze lub standardowego interfejsu.
Myślę, że możemy zakończyć ten temat, korzystając z naszych zasobów.
Jeszcze raz pokażę kody QR.
Zapoznaj się z nimi.
Warto pamiętać, że mamy też do tego umiejętności na Androidzie.
Nie musisz więc zagłębiać się w każdy szczegół.
Mamy pełną dokumentację i umiejętność Androida, które można wykorzystać w przypadku tego nowego interfejsu API agenta.
Warto się z nim zapoznać.
Pobierz AICore w wersji przedpremierowej dla programistów, aby przetestować wszystkie te funkcje. Życzymy udanego kodowania.
/**
* 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()
)