소프트웨어 엔지니어
자율 온디바이스 에이전트를 빌드하여 Android AI 기능을 한 단계 업그레이드하세요. 이 세션에서는 이미 알고 계신 직접 모델 액세스 API를 기반으로 빌드된 ML Kit의 새로운 에이전트 API를 자세히 살펴봅니다.
오픈소스 Jetpacker 앱을 사용하여 로컬 데이터에서 여러 단계의 작업을 연결하고, 격리된 컨텍스트로 스테이트풀(Stateful) 멀티턴 세션을 관리하고, Android MCP를 연결하여 단일 코드베이스로 인앱 어시스턴트와 Android 시스템 수준 표시 경로를 모두 지원하는 방법을 알아봅니다.
AgentRunner는 도구 실행을 조정하고 컨텍스트를 잃지 않고 멀티턴 대화를 위한 세션 메서드를 제공합니다.스크립트
제 이름은 욜란다 페르후프입니다.
저는 Android의 개발자 관계 엔지니어입니다. 오늘은 AICore 엔지니어링팀의 빅토리아 레스타리와 함께합니다.
안녕하세요.
오늘 아침 함께해 주셔서 감사합니다.
따라서 Google팀은 현존 최고 수준의 AI 모델을 클라우드에서 가져와 Android 기기에서 직접 실행하기 위해 수년간 긴밀히 협력해 왔습니다.
이러한 노력은 ML Kit 생성형 AI 제품군으로 그룹화됩니다.
이 ML Kit GenAI 제품군에는 텍스트 요약, 이미지 설명, 음성 인식과 같은 턴키 솔루션을 위한 고수준 API가 있습니다.
한편, 기기 내 모델인 Gemini Nano에 직접 액세스할 수 있는 기능도 제공하고 있습니다.
오늘은 한 단계 더 나아가 보겠습니다.
이러한 직접 액세스를 통해 인앱 에이전트, 즉 여러 차례에 걸쳐 추론하고, 대화 컨텍스트를 저장하고, 앱에서 작업을 수행할 수 있는 에이전트를 빌드할 수 있습니다.
빅토리아, 이 말은 실제로 어떤 의미인가요?
네.
지금까지 온디바이스 사용 사례에 대해 이야기할 때는 사기 가능성을 감지하거나 전화 통화를 요약하는 등 더 짧고 간단하며 격리된 작업을 생각했습니다.
사진 카탈로그에 관해 질문하거나 Google 지도에 질문하는 등 더 복잡한 작업을 수행하려면 자연스럽게 대규모 클라우드 모델을 참조합니다.
따라서 클라우드 모델은 엄청난 처리 능력과 실시간 데이터라는 이점을 제공하지만, 온디바이스 AI는 짧은 지연 시간과 실시간 및 로컬 데이터 처리라는 새로운 가능성을 열어줍니다.
이처럼 Gemini Nano는 이미 기기에서 할 수 있는 작업을 확장했습니다.
예를 들어 Pixel 스크린샷을 사용하면 로컬 스크린샷을 검색하고 이에 관해 질문할 수 있습니다.
따라서 기기에서 에이전트를 실행하려면 모델 기능이 크게 향상되어야 합니다. 이것이 바로 Google이 여러 단계에 걸쳐 추론하고 실제 작업을 수행할 수 있는 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에 추가된 이 두 가지 새로운 기능을 통해 이제 앱에 이러한 하위 수준의 에이전트 기능을 빌드할 수 있습니다.
하지만 이 시점에서는 직접 해야 할 일이 많습니다.
따라서 함수를 수동으로 실행하고, 호출 기록을 저장하고, 여러 호출을 직접 반복해야 합니다.
따라서 여러 번 호출해야 하는 경우 일종의 루프 시스템을 직접 빌드해야 하고 시스템에 대한 자체 가드레일을 빌드해야 합니다.
가능한 방법이지만, 물론 이 작업을 빠르고 쉽게 수행하고 싶습니다.
따라서 이 Prompt API 라이브러리 외에도 새로운 에이전트형 흐름 라이브러리를 추가했습니다.
com.mlkit.genai:agent-api에서 찾을 수 있으며 현재 알파 버전으로 제공됩니다.
이제 이 새로운 API를 사용하면 Google에서 많은 번거로운 작업을 대신 처리해 드립니다.
따라서 많은 수동 단계를 거치지 않고도 몇 줄의 코드만으로 에이전트를 빌드할 수 있습니다.
오늘 바로 이 작업을 진행할 예정입니다.
이제 샘플 앱 Jetpacker를 통해 이러한 API가 실제로 어떻게 작동하는지 살펴보겠습니다.
Google I/O와 이번 컨퍼런스에서 Jetpacker를 이미 보셨을 것입니다.
하지만 이 QR 코드를 스캔하면 코드를 살펴보고, 에이전트가 코드를 가리키도록 하고, 코드와 상호작용하고, 이러한 모든 기능을 실제로 구현한 방법에 대해 자세히 알아볼 수 있습니다.
Jetpacker는 다양한 지능형 기능을 보여주기 위해 빌드된 최신 여행 어시스턴트입니다.
따라서 이미 모든 종류의 Prompt API에 기기 내 모델과 클라우드 모델을 사용하고 있지만, 이제 여기에 에이전트를 추가할 예정입니다.
이 에이전트(Jetpacker)의 경우 방금 설명한 새로운 ML Kit GenAI Agent API를 사용하고, 이 API는 Android 기기에서 실행되는 기본 AI Core 시스템을 사용하여 Gemini Nano 모델과 통신합니다.
제가 원하는 만큼 이야기할 수 있고, 앞으로도 많은 이야기를 나누겠지만, 물론 이를 보여드리고 싶습니다.
이제 휴대전화로 전환하여 데모를 보여드리겠습니다.
여기 Pixel 11 Pro가 있습니다.
Pro로 전환하면 확인할 수 있습니다.
자, 여기 있습니다.
보시다시피 Jetpacker 앱이 실행되고 있습니다.
예정된 여행이 여러 개 있는데, 현재 여행을 살펴보면 현재 날짜가 10월 29일이므로 온디바이스 AI 워크숍, 데모 쇼케이스가 있고, 내일은 골든 게이트 베이 크루즈와 박물관 방문이 예정되어 있습니다.
모두 좋은 방법입니다.
이제 이 여행의 예산을 추적하는 비용 화면으로 이동할 수 있습니다.
오른쪽 하단에 새로운 작은 에이전트 반짝임이 표시되며, 이는 Google에서 빌드한 온디바이스 에이전트입니다.
간단한 명령어부터 시작해 보겠습니다.
특정 지출을 추가해 달라고 요청할 수 있을 것 같습니다.
예를 들어 '마운틴 뷰까지 택시 비용 45달러 추가해 줘'라고 말하면 됩니다.
다음은요
비용을 준비하고 비용을 추가했습니다.
이제
좋습니다. 데모 1이 작동했습니다.
슬라이드로 돌아가서 실제로 어떻게 작동했는지 살펴보겠습니다.
먼저, 눈치채셨겠지만 저는 음성을 사용하고 있었고, 이 음성은 실제로 음성 인식 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가 있습니다. 즉, 작성 중인 이러한 함수는 앱, 앱의 에이전트에만 표시됩니다.
그런 다음 정지 함수 자체를 정의하고 구현할 수 있으며, 가능한 한 많은 앱 함수를 작성하고 이를 모두 단일 서비스 클래스로 그룹화할 수 있습니다.
좋습니다.
도구를 사용하고 도구를 작성할 수 있다는 것은 좋은 일입니다.
하지만 에이전트를 빌드하려면 AppFunctions 외에도 몇 가지가 더 필요합니다.
에이전트를 작성할 때 알아야 할 중요한 부분이 세 가지 더 있습니다.
우선 에이전트를 정의합니다.
시스템 안내, 가드레일, 구성, 작업 방식을 정의합니다.
그리고 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를 호출하면 됩니다. 그러면 대답이 도착하는 대로 수집됩니다.
이상입니다.
코드 몇 줄만 있으면 된다는 말이 농담이 아니었음을 알 수 있습니다.
이 몇 줄의 코드만으로 에이전트가 생성됩니다.
에이전트가 원하는 작업을 수행합니다.
텍스트 응답을 제공합니다.
하지만 첫 번째 데모에서는 매우 간단한 명령어만 사용했습니다.
제가 명령을 하나 내리면 하나의 작업을 수행하고 끝났으니 아직 완전한 에이전트는 아니죠?
그럼 조금 더 재미있게 만들어 보겠습니다.
다음 단계에서는 실제로 명령어를 제공하되 에이전트가 조금 더 어렵게 느끼도록 만들고 싶습니다.
데모 기기로 돌아가서 조금 더 어렵게 만들어 보겠습니다.
이미 도착했습니다.
이제
말씀드린 대로 내일 박물관 방문이 예정되어 있습니다.
박물관 방문의 경우, 열어보면 입장료가 25달러인 것을 확인할 수 있습니다.
이제 에이전트가 실제로 이를 스스로 파악할 수 있는지 살펴보겠습니다.
지출 화면으로 돌아가서 명령어를 입력하겠습니다. "지출에 박물관 티켓을 추가해 줘"라고 말하겠습니다.
다음은요
이제 보시다시피 제 일정을 확인하고 박물관 방문을 찾아 비용에 추가하고 있습니다.
여기서는 실제로 목표를 달성하기 위해 세 단계를 거치고 있으며 최종 텍스트 응답도 제공했습니다.
이 모든 작업은 자동으로 이루어집니다.
기기에서 해당 모델의 실제 속도를 확인했습니다.
이제 슬라이드로 돌아가서 어떻게 작동하는지 살펴보겠습니다.
보시다시피 첫 번째 데모와 비교하여 약간의 변경사항이 있습니다.
이 경우 보시다시피 박물관에 대해 말하지 않았고 비용에 대해서도 말하지 않았으므로 모델이 프롬프트와 도구를 받았을 때 무엇을 먼저 해야 할지 결정하려고 합니다.
여기서는 현재 운항 일정에 대해 더 자세히 알아야 한다고 판단하여 getItinerary를 호출합니다.
응답을 받고 나면 이 일정에 박물관 방문이 있다는 것을 알게 되고, 이것이 아마도 사용자가 말한 일정일 것이라고 판단합니다.
그럼 해당 이벤트의 세부정보를 찾아보겠습니다.
그리고 해당 이벤트의 다른 모든 메타데이터 중에서 25달러라는 것을 확인합니다.
이해하고 돌아가서 비용 추가를 호출해야 한다는 것을 이해합니다.
여기서는 여러 단계가 차례로 진행되는 것을 볼 수 있습니다.
빅토리아, 이 부분에 대해 조금 더 자세히 설명해 주시겠어요?
네.
욜란다가 방금 설명한 것은 방금 보여준 데모의 구체적인 예이지만, 여기서는 좀 더 개념적이고 추상적인 레이어로 가져가 보겠습니다.
따라서 다단계 함수 호출을 할 때마다 이 작업이 수행됩니다.
먼저, 이 앱은 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
}
}단일 단계 예시와 매우 유사하지만 두 가지가 추가되었습니다.
첫 번째는 최대 차례를 설정해야 하며 이 예시에서는 5로 설정합니다.
그렇다면 이 기능은 어떤 역할을 할까요?
이렇게 하면 모델이 끝없이 반복되는 것을 방지할 수 있습니다.
예를 들어 모호한 프롬프트가 있거나 한 도구를 호출했다가 다른 도구를 호출하는 작업을 계속 반복하는 경우 이 값을 설정하면 루프가 계속되지 않고 중지됩니다.
그런 다음 동일한 함수를 호출하여 실행합니다(runSingleTurnAsync).
따라서 단일 차례라고 하지만 실제로는 한 차례가 한 번의 사용자 쿼리를 의미합니다.
따라서 최종 사용자는 쿼리를 하나 보내면 답변을 하나 받기를 기대하며, 내부적으로 어떤 일이 일어나는지, 몇 번이나 루프가 실행되는지 등 세부적인 사항에는 크게 신경 쓰지 않습니다.
따라서 동일한 함수를 계속 사용할 수 있습니다.
또한 멀티스텝 도구 호출은 시간이 조금 더 걸리기 때문에 데모에서 현재 호출 중인 함수, 현재 실행 중인 함수, 완료 여부를 실제로 보여주는 것을 확인할 수 있었습니다.
따라서 UI에 실시간 진행 상황을 표시하려고 합니다.
따라서 텍스트 응답을 수집하는 대신 AgentEvent가 FunctionCall 또는 FunctionResponse를 내보낼 때마다 도구 이름인 event.name를 표시하고, 완료되면 이전과 같이 TextMessage를 반환할 수 있습니다.
대단해요!
이 실행을 비동기 실행 단일 차례 비동기라고 부르는데, 이는 다른 종류의 API도 있다는 것을 의미합니다.
네.
물론 한 단계만으로는 충분하지 않습니다.
여러 단계로 구성된 작업을 수행하려고 합니다.
다음으로 멀티턴 세션을 진행해 보겠습니다. 제 데모로 돌아가서 조금 더 어렵게 만들어 보겠습니다.
박물관 방문에 관해 이야기하고 있었는데, 골든 게이트 베이 크루즈도 진행 중이었습니다.
그래서 실제로 예약하기 전에 예산이 충분한지 확인하고 싶습니다.
질문을 해 보겠습니다.
살펴봅시다.
내일 팀원들과 골든 게이트 베이 크루즈를 타러 갈까 하는데 180달러 정도라고 생각했거든요.
그렇게 하면 실제 잔여 예산은 얼마가 될까요?
그렇다면 이 기능은 어떤 역할을 할까요?
남은 예산이 얼마인지 알려줍니다.
현재 운항 일정을 이해하고 있습니다.
이 정보는 이미 가지고 있으므로 알려줄 수 있습니다.
여기에서 후속 질문을 할 수도 있습니다.
좋습니다.
내 지출에 추가해 줄래?
이제 진행됩니다.
통화가 필요하네요. 데모 효과입니다.
달러로 표시되어 있습니다.
$180.
다음은요
이런 일은 처음이네요. 멋집니다.
그렇다면 이 도구는 어떤 역할을 할까요?
세 번의 대화가 진행되었고 비용이 추가되었다고 알려줍니다. 또한 이 세션을 새로고침할 수도 있습니다.
상단에 새로고침 버튼이 있는데, 이 버튼을 누르면 전체 기록이 지워지고 다시 시작할 수 있습니다.
이제 슬라이드로 돌아가겠습니다.
작동하는 데모가 있고 모두 원하는 대로 작동하므로 코드로 돌아가면 될 것 같습니다.
네.
정말 멋진 기능입니다.
욜란다는 골든 게이트 베이 크루즈 티켓이라고 말하지 않고, 그냥 달러라고만 말합니다.
그렇다면 모델은 어떻게 모든 컨텍스트를 알 수 있을까요?
이전 두 코드 스니펫에서 보여드린 것과는 약간 다릅니다.
// 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를 통해 수행했습니다. 정말 놀라운 기능이며, 여러분의 다양한 사용 사례에서도 놀라운 효과를 발휘할 것입니다.
물론 Google은 항상 빠른 작업이나 쉬운 작업을 가능하게 만들고자 하지만, 더 복잡한 사례도 가능하게 만들고자 합니다.
따라서 필요한 경우 직접 액세스 API를 사용하여 이러한 기능의 하위 수준 버전을 빌드할 수도 있습니다.
빅토리아, 이 직접 액세스 수준이 무엇을 의미하는지, 그리고 언제 이 수준으로 내려가는지 설명해 주시겠어요?
네, 물론입니다, 욜란다님.
이러한 직접 액세스는 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를 사용하면 이를 완전히 제어할 수 있습니다.
이러한 사용 사례에는 에이전트 개발 키트(ADK) 또는 Crew와 같은 프레임워크를 사용하거나 자체 커스텀 조정 프레임워크를 만들 수 있습니다.
좋습니다.
지금까지 에이전트 API에 대해 알아봤습니다.
하지만 한 가지 더 언급하고 싶은 것이 있습니다.
지금까지 인앱 에이전트에 대해 계속 이야기했습니다. 자체 앱 내에서 자체 에이전트 환경을 빌드하는 것이죠. 하지만 AppFunctions를 사용하는 방법에 대해서도 이야기했습니다. 자체 앱 내에서만 작업하는 경우 AppFunctions를 사용하는 이유는 무엇인가요?
여러분과 같은 개발자는 같은 작업을 두 번 하는 것을 좋아하지 않습니다.
코드를 복사하여 붙여넣는 것을 좋아하지 않으며 비즈니스 로직을 중복하는 것도 좋아하지 않습니다.
따라서 AppFunctions를 사용하면 인앱 에이전트만 이를 사용하도록 허용하는 대신 매우 간단한 변경을 통해 시스템 에이전트도 도구를 사용할 수 있도록 할 수 있습니다.
그렇다면 코드는 어떻게 보일까요?
네.
AppFunction을 사용하면 한 번만 작성하면 원하는 대로 다양한 수준의 레이어에 노출할 수 있습니다.
이전에는 ACCESS_LEVEL_SELF가 있었는데, 이는 작성한 함수를 인앱 에이전트에만 노출하지만 액세스 수준을 ACCESS_LEVEL_ANDROID_TRUSTED로 변경할 수도 있습니다.
이렇게 하면 Android OS의 신뢰할 수 있는 에이전트에 함수가 노출됩니다.
따라서 함수, 명령어, 데이터베이스 로직을 다시 작성할 필요가 없습니다.
AppFunctions는 이미 내부 및 외부에서 앱의 음성 역할을 합니다.
이 기능도 데모를 통해 확인해 보겠습니다.
이제 휴대전화로 돌아가서 마지막 데모를 위해 Jetpacker 앱을 종료해 보겠습니다.
이제 Gemini로 이동해 보겠습니다.
Gemini에서 @Jetpacker를 호출할 수 있습니다.
한 가지 유의할 점은 이 기능은 현재 제한된 베타 버전으로만 제공되므로 지금 당장 모든 사용자가 사용할 수 있는 것은 아니지만 테스트해 보고 사용해 볼 수 있다는 것입니다.
Jetpacker를 사용하면 어떻게 되는지 살펴보겠습니다.
어떻게 하면 좋을까요?
아, 오늘 아침에 커피를 마셨지?
ADS 출장 경비에 7달러짜리 커피를 추가해 줄 수 있어?
다음은요
Jetpacker 앱에 연결되는 것을 확인할 수 있습니다.
이제 됐습니다.
지출이 추가되었습니다.
Jetpack Compose에서 모든 것을 사용하는 방법을 기본적으로 살펴보았습니다.
Jetpacker의 장점은 비행기 모드를 사용 설정할 수도 있다는 것입니다.
오늘 보여드리지는 않았지만, 비행기 모드를 켜도 데모에서 보여드린 모든 기능이 작동합니다. 여러분이 저를 믿으실 수 있도록 보여드리고 싶네요. Wi-Fi를 꺼보겠습니다.
추가할 수 있는 좋은 데모 예시가 있는 분이 있나요?
살펴봅시다.
'What is it?'을 해볼까요?
렌터카가 좋겠네요. 그렇죠?
좋습니다.
'렌터카 비용 300달러 추가해 줘'라고 해보겠습니다.
자, 이제 됐습니다.
좋습니다.
이제
좋습니다. 슬라이드로 돌아가겠습니다.
AppFunctions에 대해 이야기하고 있었죠.
아주 가볍게 다루었던 것 같은데요.
자세히 알고 싶다면 댄과 사라의 다른 강연도 참고하세요. 이 기능을 실제로 사용 설정하는 방법, 테스트하는 방법, 제공되는 도구 등에 관해 훨씬 더 자세히 설명합니다.
오늘 우리는 다양한 내용을 살펴봤습니다.
단일 단계, 다단계, 멀티턴을 살펴봤고 마지막으로 시스템 AppFunction을 사용하는 방법도 살펴봤습니다.
이제 마무리하기 전에 몇 가지 팁을 알려드리겠습니다.
네.
에이전트 환경을 구축할 때는 이 세 가지 도움말을 기억하세요.
첫 번째는 도구 세트에 집중하는 것입니다.
따라서 온디바이스 모델은 규모가 작기 때문에 기능이 제한적이라는 점을 기억하세요.
따라서 수십 개의 도구를 등록하는 대신 더 적은 수의 도구를 사용하는 것이 좋습니다.
그리고 앞서 말씀드린 것처럼 명확한 KDoc 설명을 작성하세요. 모델이 이를 사용하여 반환할 내용을 결정하기 때문입니다.
또한 최대 대화 차례 가드레일도 설정해야 합니다.
이렇게 하면 모델이 혼동될 경우 무한 루프에 빠지는 것을 방지할 수 있습니다.
이는 사용자의 배터리를 소모할 수 있음을 의미합니다.
이 기능이 있으면 사용자의 배터리를 절약할 수 있습니다.
마지막으로 기기에서 함수 호출이 이미 지원되는지 확인하고 적절한 대체 기능을 구현해야 합니다.
예를 들어 지원되지 않는 경우 클라우드 모델이나 표준 UI를 참조할 수 있습니다.
좋습니다. 이 문제는 Google 리소스를 통해 해결할 수 있을 것 같습니다.
다시 한번 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()
)