EZmap 기술 문서
서버 없이 Android 디바이스 안에서 완전히 동작하는 AI 에이전트 런타임. 사용자가 본인의 API 키를 입력하면 Gemini가 앱 안에서 Function Calling으로 외부 API와 디바이스를 직접 조작합니다.
✓ 모든 수치 소스코드 직접 검증 · 2026-06-22 전수 조사개요 #
EZmap은 '내비/어시스턴트'가 아니라 Personal Agent Runtime — 폰이 모든 앱·기기 위의 사용자-소유 실행 레이어가 되어, 디지털 세계를 조작하고 검증하며 그 폰에 지속 역량을 누적합니다. 어시스턴트는 답하지만, 런타임은 툴을 만들고·검증하고·고칩니다.
아키텍처 #
음성 입력에서 응답까지의 전체 파이프라인. 모든 단계가 단일 패키지 com.example.ez_capstone 안에서 동작합니다.
실행 단계
1. SystemPrompt.build(context) // GPS·시간·프로필·경로 상태 주입 2. GeminiApiClient.generateContent() // Gemini 2.5 Flash 3. if (functionCall) ToolExecutor.execute() → 결과 재주입 4. functionCall 없을 때까지 반복 (MAX_ITERATIONS = 14) 5. 최종 파싱 → AgentResponse { replyText, ttsText, uiAction, uiData }
빌드 & 실행 #
Kotlin 2.0.21 · AGP 8.12.3 · Min SDK 26 · Target/Compile SDK 36.
# 디버그 빌드 / 설치 cd android JAVA_HOME="…/Android Studio/jbr" ./gradlew assembleDebug JAVA_HOME="…/Android Studio/jbr" ./gradlew installDebug # 유닛 테스트 ./gradlew test --tests "com.example.ez_capstone.agent.*"
필수 키 (local.properties, git-ignored)
| 키 | 용도 | 필수 |
|---|---|---|
| GEMINI_API_KEY | 에이전트 엔진 + Trial 모드 | 필수 |
| KAKAO_REST_KEY | 장소 검색 · 경로 탐색 | 필수 |
| KAKAO_NATIVE_APP_KEY | Kakao SDK init | 필수 |
| KMA_API_KEY | 기상청 날씨 | 선택 |
| OPINET_API_KEY | 주유소 가격 | 선택 |
| NAVER_CLIENT_ID / SECRET | Naver Directions (듀얼 경로) | 선택 |
| ODSAY_API_KEY | 대중교통 경로 | 선택 |
| PORCUPINE_ACCESS_KEY | 웨이크워드 "이지야" | 선택 |
Agent Loop #
agent/GeminiAgentEngine.kt · doChatLoop() — Gemini REST API를 직접 호출하는 Function Calling 루프. functionCall이 사라질 때까지 반복하며, 매 iteration마다 응답에 도구 호출이 있으면 ToolExecutor.execute() 결과를 다시 Gemini에 주입합니다.
MAX_ITERATIONS = 14 (GeminiAgentEngine.kt:95). 설계 의도는 "최대 7단계 툴 체이닝 × 2홉 = 14" — open_app → 팝업닫기 → search → screen_read → tap → verify → reply.1 · 동적 도구 라우팅
62개 도구를 매번 전부 보내면 토큰이 폭증하고 Gemini가 혼란스러워집니다. EZmap은 발화를 먼저 IntentDetector로 분류한 뒤, 해당 intent에 매핑된 도구만 추려 전달합니다.
| 집합 | 구성 / 규칙 |
|---|---|
| ALWAYS_INCLUDED | 어떤 intent에도 빠지면 안 되는 전제 도구 — geocode · get_user_profile · save_note · get_notes · search_knowledge · open_app · control_app · media_control 등 |
| INTENT_TOOL_MAP | NAVIGATION · PLACES · SCHEDULE · COMMUNICATION · PROFILE … intent별 관련 도구 집합 (예: NAVIGATION → get_directions·get_transit_route·add_waypoint·get_weather_kma) |
| DRIVING_EXTENDED | 주행 중(navigating)이면 intent와 무관하게 추가되는 안전 도구 — get_traffic_incidents · get_highway_alerts · get_speed_cameras · get_road_risk |
| MCP 동적 도구 | 인앱 MCP 서버에 등록된 도구가 런타임에 합쳐짐 |
2 · 자율 컴파운드 보장
open_app이 검색·작업을 만나면 task_pending을 반환하고, 다음 iteration이 강제 tool 모드(ANY)로 전환됩니다 (GeminiAgentEngine.kt:797). 이로써 에이전트가 도구 대신 텍스트로 "열었어요"라며 멈추는 루프를 근절하고, open_app → control_app(action=search) 체이닝을 결정적으로 완주시킵니다.
도구 실행이 실패하면 다음 iteration에 reflection hint가 주입되어(GeminiAgentEngine.kt:761) Gemini가 오류를 인지하고 자기 교정합니다.
3 · 지연 시간 계측
매 실행은 4개 구간을 측정해 로그로 남깁니다 — STT 완료부터 첫 토큰·도구 완료·TTS 시작까지.
proposeSynthesis()로 스킬 합성을 제안합니다 → 자기확장 참조.Tool Catalog #
Gemini Function Calling에 등록되는 내장 도구 — 정확히 62개 (agent/ToolDeclarations.kt · allTools()). 각 도구는 tool(name, description, params) 헬퍼로 정의되며, 아래 설명은 코드의 실제 description 필드에서 추출했습니다.
INTENT_TOOL_MAP이 발화 intent(NAVIGATION·PLACES·SCHEDULE·COMMUNICATION·PROFILE …)별로 관련 도구만 추려 Gemini에 전달하고, ALWAYS_INCLUDED(geocode·save_note·get_notes 등 전제 도구)는 항상 포함됩니다. 주행 중에는 DRIVING_EXTENDED(돌발·단속·위험)가 intent와 무관하게 추가됩니다 → 토큰 절약 + 안전. 이 중 12개는 인앱 MCP 서버로도 외부 노출됩니다.이동 · 경로 8
| 도구 | 설명 |
|---|---|
| search_places | 카테고리·키워드로 여러 장소 탐색 ('근처 카페', '강남역 맛집', '주유소 찾아줘') |
| geocode | 도로명/장소명 → 좌표 변환 (목적지 좌표 확보의 전제 도구) |
| reverse_geocode | 좌표 → 주소 텍스트 변환 |
| get_directions | 자동차 경로 탐색 (Kakao) · 실시간 교통 반영 · 경유지 최대 5 |
| get_directions_naver | 네이버 길찾기 · 톨비·택시비·연료비 비교 (Kakao와 듀얼 경로) |
| get_transit_route | 대중교통(버스+지하철) 경로 · 환승·소요시간·요금 포함 |
| get_future_eta | 미래 특정 출발 시각의 예상 소요시간 예측 |
| add_waypoint | 주행 중 현재 경로에 경유지 삽입 ('이 스타벅스 들를 수 있어?' 핸즈프리) |
주유 · 충전 · 주차 5
| 도구 | 설명 |
|---|---|
| get_gas_stations | 반경 내 최저가 주유소 · 유종별 가격 비교 (Opinet) |
| get_ev_chargers | 근처 전기차 충전소 · 실시간 충전 가능 여부 포함 |
| get_parking | 근처 주차장 · 요금·운영시간 포함 |
| get_realtime_parking | 서울시 주차장 실시간 잔여석 · 주차 가능 대수·요금 |
| suggest_parking | 목적지 주변 최적 주차장 추천 · 실시간 잔여석·도보 거리 |
교통 · 도로안전 8
| 도구 | 설명 |
|---|---|
| get_traffic_speed | 주변 도로 실시간 교통 속도·정체 상태 (구간별) |
| get_traffic_incidents | 주변 돌발상황(사고·공사·기상이변) · 유형·위치·우회 정보 |
| get_traffic_cctv | 주변 교통 CCTV 위치 + 영상/이미지 URL |
| get_highway_alerts | 고속도로 실시간 교통 정보 (정체·사고·공사 알림) |
| get_road_risk | 주변 도로 사고 위험도 · 위험 구간·최근 사고 건수 |
| get_road_incidents | 경로 상 돌발상황(사고·낙하물·공사) · 한국도로공사 실시간 |
| get_speed_cameras | 경로 주변 구간/고정식 단속 카메라 · 위치·제한속도 |
| get_rest_areas | 고속도로 휴게소 · 편의시설(주유소/충전소/식당)·위치 |
날씨 · 환경 3
| 도구 | 설명 |
|---|---|
| get_weather | 현재 위치 또는 지정 좌표의 날씨 (무키 폴백) |
| get_weather_kma | 기상청 초단기실황 '현재' 날씨 · 기온·강수·풍속·도로상태 |
| get_air_quality | 미세먼지/대기질 · PM10·PM2.5·통합대기질지수(KHAI)·등급 |
생활 · 지식 6
| 도구 | 설명 |
|---|---|
| search_knowledge | 웹/위키백과 검색 ('검색해줘', '○○가 뭐야?') · 무키 가용 |
| fetch_url | 지정 URL 웹페이지 → 본문 텍스트 추출 (뉴스·문서·링크) |
| get_exchange_rate | 실시간 환율 · 기본 KRW 기준, 특정 통화 지정 가능 |
| search_pharmacies | 근처 약국 · 이름·주소·전화번호·거리 |
| search_hospitals | 근처 병원/응급실 · 이름·주소·전화번호·거리 |
| get_driving_retrospective | 저장된 주행 점수 집계 → 주/월 운전 회고 ('내 운전 어때?') |
프로필 · 메모 · 학습 9
| 도구 | 설명 |
|---|---|
| get_user_profile | 집/회사 주소·즐겨찾기·차량 정보 조회 ("집 가자" 해석) |
| update_user_profile | 집/회사 등록·즐겨찾기 추가·차량 정보 변경 |
| get_user_preferences | 개인화 선호도 조회 |
| update_user_preferences | 선호도 업데이트 |
| save_note | 대화에서 발견한 선호·습관·패턴·사실을 메모로 조용히 저장 |
| get_notes | 저장된 에이전트 메모 조회 ('뭘 기억해?') |
| delete_note | 에이전트 메모 삭제 ('잊어줘') |
| analyze_route_patterns | 경로 히스토리 분석 → 출퇴근·주말·야간 반복 패턴 추출 |
| manage_favorites | 즐겨찾기 장소 추가/삭제/조회 |
일정 · 연락 · 커뮤니케이션 11
| 도구 | 설명 |
|---|---|
| get_schedule | 사용자 일정 조회 |
| create_schedule | 새 일정 생성 (제목만 있으면 즉시 생성) |
| update_schedule | 기존 일정 수정 (ID로 제목/시간/장소 변경) |
| delete_schedule | 일정 삭제 (ID로 찾아 삭제) |
| set_alarm | Android 시스템 알람 앱으로 알람 설정 |
| set_timer | 카운트다운 타이머 ('5분 후 알려줘', '30초 타이머') |
| send_message | 연락처에 메시지 전송 · 사용자 확인 후 전송 |
| make_call | 연락처에 전화 걸기 · 사용자 확인 후 실행 |
| manage_contacts | 연락처 추가/수정/삭제/전체 목록 조회 |
| lookup_contact | 특정 사람 존재 여부 조회만 (읽기 전용, 변경 안 함) |
| reply_to_notification | 앱 안 열고 알림에 직접 답장 (카톡/문자 · 주행 핸즈프리) |
미디어 6
| 도구 | 설명 |
|---|---|
| play_music | Spotify 재생 · query에 곡/가수명 주면 검색 후 재생 |
| pause_music | 음악 일시정지 |
| next_track | 다음 곡 |
| previous_track | 이전 곡 |
| current_track | 현재 재생 중인 곡 정보 |
| media_control | 모든 미디어 앱 제어(MediaSession) · 유튜브뮤직·멜론·팟캐스트 등 |
앱 조작 · 기기 · 확장 6
| 도구 | 설명 |
|---|---|
| open_app | 설치된 앱 열기(범용) · '카톡/유튜브/배민/인스타/토스 열어줘' |
| control_app | 외부 앱 화면을 보고 직접 조작(접근성) · 20개 액션 → 상세 |
| set_volume | 폰 볼륨 설정 · stream(미디어/알림/벨) · level 0-100 |
| get_device_status | 배터리 잔량·미디어 볼륨·화면 밝기·Wi-Fi/블루투스 상태 |
| call_desktop | 같은 WiFi PC에서 Claude Code/codex/bash 원격 실행 → AgentLink |
| report_error_log | 오류 로그/Claim을 Desktop Bridge로 PC에 저장 (선택적 서버 전송) |
합계 8+5+8+3+6+9+11+6+6 = 62. call_desktop은 Desktop Bridge 키가 설정된 경우에만 활성화됩니다(ALWAYS_INCLUDED 제외).
컨텍스트 주입 #
agent/SystemPrompt.kt가 매 호출마다 실시간 컨텍스트를 시스템 프롬프트에 합성합니다.
get_directions로 직행합니다 (8 user턴 윈도).① 가르치기 모드 (PbD) #
Programming by Demonstration — 사용자가 직접 시연하면 에이전트가 그 동작을 증류해 재사용 가능한 스킬로 저장합니다.
"이거 가르쳐줄게" 발화 ↓ 접근성 ON 체크 → OFF면 즉시 안내 + abort ↓ DemonstrationRecorder.capture() // 의미 단위 DemoStep[] (좌표 아님) ↓ SkillSynthesizer.distillFromDemo() → Gemini → JS ↓ SkillLearner.promoteFromScript(reason="distill") ↓ LearnedSkill 저장 (confidence = 0.80) ↓ 0.80 > SAFE(0.75) → 즉시 자율 실행 가능
0.80 (SkillLearner.kt:143). 사용자가 검증한 시연이므로 즉시 L3 실행. 좌표가 아닌 의미 단위로 기록해 화면 변동에 강건합니다.② 외부 앱 조작 (control_app) #
접근성(Accessibility) 기반으로 모든 앱을 자율 조작하는 super-tool. 매 액션 후 화면을 다시 read해 Gemini 컨텍스트를 갱신하며 read → tap → type → submit → verify 컴파운드를 결정적으로 수렴시킵니다.
지원 액션 — 20종 (ToolExecutor.executeControlApp() when 분기 검증)
| 그룹 | 액션 | 역할 |
|---|---|---|
| 화면 인지 | read · see | read는 접근성 트리 덤프, see는 트리로 안 읽히는 웹뷰/캔버스/이미지를 스크린샷→Gemini Vision으로 '눈으로' 봄 |
| 포인팅 | tap · tap_text · tap_vision · long_press | 인덱스/텍스트 라벨/비전 좌표로 탭, 길게 누르기 |
| 입력 | type · submit · search · select_all · copy · paste | 텍스트 입력·검색 제출·전체선택·복사·붙여넣기 (search는 입력+제출 결정적 매크로) |
| 스크롤 | scroll · swipe | 제스처 기반 스크롤/스와이프 (노드 ACTION_SCROLL 아님 — 실앱서 동작 보장) |
| 전역 내비 | back · home · notifications · recents | 뒤로·홈·알림 패널·최근 앱 |
| 오버레이 | show_overlay · hide_overlay | 화면 위 안내 오버레이 표시/숨김 |
ControlActionGuard가 송금·이체·출금·계좌이체·transfer·wire·authorize payment를 정규식으로 매칭해 tap·long_press·submit을 자동 차단하고 수동 핸드오프합니다. 음식/쇼핑 '결제하기'는 정책상 제외, submit은 화면 상태인지 게이트가 추가됩니다.error_kind = MISSING_PERMISSION을 반환합니다. 직전 read한 앱과 현재 포그라운드가 다르면 warning을 붙여 "EZmap UI를 대상 앱으로 착각"하는 환각을 차단합니다.핵심 파일: accessibility/EZmapAccessibilityService.kt · agent/ToolExecutor.executeControlApp() · agent/ControlActionGuard.kt
③ AgentLink (Desktop Bridge) #
폰에서 PC의 Claude Code · Codex · bash를 원격 실행합니다. URL과 Token이 설정된 경우에만 call_desktop 도구가 활성화됩니다.
"PC에서 프로젝트 빌드해줘" 발화 ↓ call_desktop(type="claude" | "codex" | "bash") │ bash 위험명령 하드 게이트 (10종 차단) │ rm -rf · mkfs · dd if= · fork bomb · curl|sh · shutdown … ↓ HTTP POST /run → ezmap-desktop-bridge.js (Node.js) │ token 검증 → spawn(claude --print / codex / sh -c) ↓ stdout/stderr → JSON (readTimeout 130s) ↓ output → Gemini 컨텍스트 → 사용자 응답
node ezmap-desktop-bridge.js 실행 → 표시되는 URL(예: http://192.168.x.x:8789)과 Token을 앱 Settings → Desktop Bridge에 입력.④ 자기확장 & 자동 학습 #
능력 갭을 감지하면 JS 스킬을 생성·실행·자기수리하고, 반복 패턴을 자동으로 스킬로 승격합니다. "스킬 추가보다 스킬 복구가 10배 중요"가 설계 철학입니다.
매 대화 후 — skillLearner.observeUtterance() ↓ observeCount++ → 5회마다 GeneralUtteranceLearner │ 동일 패턴 3회+ → LearnedSkill 자동 생성 능력 갭 감지 시: ↓ SkillSynthesizer.synthesizeRunPersist() │ JS 생성 → Rhino 실행 → 실패 시 자기수리 (MAX_REPAIRS = 2) │ 최대 3회 시도 (1 생성 + 2 수리) ↓ promoteFromScript(confidence = 0.60) → 스킬 누적
신뢰도 상태 머신 (검증)
| 전이 | 조건 | 결과 |
|---|---|---|
| 0.60 → 0.80 | 5회 성공 | 안정 |
| 0.80 (PbD) | 시연 직후 | 즉시 자율 실행 |
| 부정 피드백 ≥ 3 | 실패 누적 | DEGRADED |
| 부정 피드백 ≥ 5 | 실패 누적 | AUTO_DISABLED |
| 재합성 차단 | 3회 수리 초과 | 진동 방지 |
Rhino 샌드박스 보안
ScriptSkillEngine.kt — setClassShutter { false }로 Java interop 전면 차단, 모든 툴 호출은 toolBridge를 거쳐 ControlActionGuard·GroundingGuard를 상속받습니다.데이터 계층 #
db/EZMapDatabase.kt — Room @Database version 16 · 15 Entity · SQLCipher AES-256 전체 암호화(di/DatabaseModule.kt SupportFactory).
| Entity | 용도 | Entity | 용도 |
|---|---|---|---|
| ProfileEntity | 집/회사/차량/TTS | DecisionTraceEntity | 에이전트 결정 기록 |
| PreferenceEntity | 경로 선호 통계 | CustomRoutineEntity | 사용자 루틴 |
| RouteHistoryEntity | 경로 히스토리 | DrivingScoreEntity | 주행 점수 |
| ConversationEntity | 대화 세션 | EnergyHistoryEntity | 연료/전기 소비 |
| FrequentPlaceEntity | 자주 가는 곳 | FeedbackEntity | 사용자 피드백 |
| ContactEntity | 연락처 캐시 | AgentNoteEntity | 자기학습 메모 |
| ScheduleEntity | 일정 | LearnedSkillEntity | 학습 스킬 (TOOLCHAIN/SCRIPT) |
| SkillVersionEntity | 스킬 버전 스냅샷 (자가치유 롤백) |
안전 · 거버넌스 #
"안전 정책 > LLM 창의성" — 모든 actuate는 게이트를 통과해야 실행됩니다. 각 게이트는 소스코드로 검증되었습니다.
| 게이트 | 역할 |
|---|---|
| ControlActionGuard | 금융 비가역(송금·이체·출금·transfer·wire) 정규식 차단 + 수동 핸드오프. submit은 화면 상태인지 게이트 추가 |
| GroundingGuard | 쓰기 후 재조회로 거짓주장·silent-fail 동시 차단. 실툴이 실제 한 것만 주장(환각 방지) |
| QuietPolicy · QuietHoursGate | 능동 표출 단일 게이트 — 심야·DND·무음·회의·주행 중엔 침묵 |
| PermissionManager | 런타임 권한 게이팅 · ScriptPermissionMode로 샌드박스 권한 분리 |
| LaneScheduler | 우선순위 레인 LIVE ≻ USER ≻ BACKGROUND ≻ IDLE — LIVE 작업이 백그라운드 선점 |
| Rhino Sandbox | ClassShutter Java interop 전면차단 · FORBIDDEN_GLOBALS · WALL 8s / CALL 15s |
API Key 관리 #
config/ApiKeyProvider.kt — EncryptedSharedPreferences(AES-256-GCM)에 저장. 키는 사용자 디바이스를 벗어나지 않습니다.
| 팩 | 키 |
|---|---|
| 필수 | geminiKey · kakaoRestKey |
| 공공데이터 팩 | dataGoKrKey (날씨·미세먼지·충전소·주차장·약국·병원) · opinetKey (주유소) |
| 프리미엄 팩 | naverClientId/Secret (듀얼 경로) · odsayKey (대중교통) |
시스템 구성 #
단일 패키지 com.example.ez_capstone · 모든 파일 개수는 파일시스템 직접 카운트.
| 디렉토리 | 파일 | 핵심 구성 |
|---|---|---|
| agent/ | 32 | GeminiAgentEngine · ToolExecutor · ToolDeclarations · SystemPrompt · ControlActionGuard · GroundingGuard · SynthesisTrigger |
| api/ + kakaomap/ | 22 | Kakao · Naver · ODsay · 기상청 · AirKorea · Opinet · EvCharger · SpeedCamera · Spotify · WebFetch … |
| db/ | 15 | 15 Entity + 15 DAO · EZMapDatabase v16 · SQLCipher |
| ui/screens/ | 22 | Conversation · Navigation · Home · Onboarding · SkillStore · DeviceMesh · McpServers (23 NavRoute) |
| navi/ | 16 | RestGuide · ShadowReroute · EvRouteOptimizer · DrivingScorer · RoadHazard · SpeedEnforcement |
| voice/ | 10 | VoiceStateCoordinator · NaturalVoiceEngine · Vosk/Porcupine/Google 웨이크 · SttRoutingPolicy |
| skill/ | 13+ | ScriptSkillEngine · SkillSynthesizer · SkillLearner · SkillMatcher · GeneralUtteranceLearner |
| mcp/ | 7 | EZmapMcpServer · McpHttpServer · McpToolRegistry · AgentLinkExporter · CloudflareTunnelManager |
| governance/ | 6 | PermissionManager · QuietPolicy · QuietHoursGate · ScriptPermissionMode |
| context/ | 5 | AmbientContextEngine · GeofenceEngine · DriveDetectionEngine · Activity/Geofence Receiver |
| scheduler/ | 2 | AgentLane · LaneScheduler |
| 진입점 | + | widget/ · auto/(Android Auto) · service/(QuickTile·NotificationListener) · shortcuts.xml ×3 |
MCP 서버 #
인앱 MCP(Model Context Protocol) 서버가 Gemini 에이전트에 도구를 등록하고, Cloudflare Quick Tunnel로 원격 노출합니다.
EZmapMcpHttpServer가 네트워크 너머로 MCP를 서빙하고, AgentLinkExporter가 call_desktop 도구를 노출합니다. SELinux 차단 시 USB fallback을 안내합니다.
음성 파이프라인 #
웨이크워드 "이지야" → STT → 에이전트 → TTS. 온디바이스(Vosk)와 클라우드(Google) STT를 SttRoutingPolicy로 라우팅합니다.
Voice State: IDLE → LISTENING → PROCESSING → SPEAKING → IDLE. NaturalVoiceEngine은 "300m 앞 맥도날드 보이면 우회전" 같은 자연어 안내로 변환합니다.
기술 스택 #
팀 & 연락처 #
지도교수 진동섭 · IT융합학부 · 2026학년도 1학기 캡스톤디자인