Files
life-helper/docs/adr/0005-in-app-tool-calling-architecture.md
joungmin eca097aa2c [02-Architect] #260 design spec + ADR-0005
- design/260-gemma-tool-calling/README.md — overall (12 AC + 7 OQ + 모듈 구조)
- fn-tool_dispatcher.md — multi-tool router (validate → confirm gate → handler → envelope)
- fn-add_habit_handler.md — destructive 대표 (R3/R7/R8 enforce)
- fn-confirm_gate.md — 모달 AlertDialog 흐름 (OQ-3 = 모달 확정)
- fn-chat_session_controller.md — multi-turn loop 상태 머신 (MAX_TURNS=4)
- ADR-0005 — in-app tool runtime + R 규칙 = 핸들러 책임 + schema SoT=Dart + 모달

OQ-1/2/4 = README §11 결정. OQ-3 = 모달 (사용자 결정).
신규 OQ-5/6/7 = Developer 가 구현 중 검증.

Refs #260
2026-06-15 10:15:44 +09:00

4.6 KiB

ADR-0005: In-app tool calling architecture (MCP-equivalent)

상태: Accepted 작성: [AI] Architect · 일자: 2026-06-15 추적성 — Redmine #260, 설계서 docs/design/260-gemma-tool-calling/README.md, ADR-0003 (on-device Gemma 채택)

컨텍스트

#218 로 on-device Gemma 4 추론이 동작하고 #226 으로 카탈로그 47 항목이 노출됐다. 사용자가 "이거 내 습관으로 추가해줘" 같은 자연어 요청으로 DB mutation 까지 도달하는 경로가 필요하다.

선택지:

  1. 별도 MCP 서버 띄우기 — 표준 프로토콜, 외부 process. 모바일 환경에서 IPC + 추가 메모리 부담 + 모델 컨텍스트 비용 동일.
  2. In-process Dart 함수 직접 호출 — 같은 프로세스 안에서 tool 정의 + 핸들러. flutter_gemma 0.16.5 의 tools 파라미터 위에 얇은 라우터.
  3. Prompt engineering 으로 mutation 안내 — 모델이 "다음과 같이 하세요" 텍스트로만 응답, 실제 액션은 사용자가 수동 — UX 후퇴, R 규칙 enforce 불가.

추가로 결정해야 할 부수 항목:

  • R1~R10 운영 규칙을 어디서 enforce 할 것인가
  • tool schema 의 source-of-truth (Dart 코드 vs yaml/json 파일)
  • destructive tool 의 사용자 확인 게이트 (모달 vs inline 카드 vs 무게이트)

결정

결정 1: in-process Dart tool runtime

  • 별도 process 띄우지 않는다. lib/ai/tools/ 하위에 ToolDefinition + ToolDispatcher + 핸들러를 Dart 로 작성.
  • flutter_gemma 의 createChat(tools: [...], toolChoice: ToolChoice.auto) 가 모델 ↔ Dart 사이의 protocol layer 역할.
  • FunctionCallResponse 를 받으면 ToolDispatcher.dispatch(name, args, deps) 로 라우팅 → chat.addToolResult(...) 로 회신.

결정 2: R 규칙 enforce 는 tool 핸들러 책임

  • 모델 prompt 에 "R3 quota 는 build ≤ 3" 식 안내를 넣지 않는다 (학습 신뢰성 불충분).
  • 모든 mutation 핸들러는 호출 직전 도메인 함수 (judgeActiveHabitQuota, detectAvoidanceKeywords, assertXorProtocol, validateTrackerValue 등) 를 직접 호출.
  • 위반 시 ToolErr(code: 'r3_quota' | 'r7_avoidance' | ..., reason: 한국어) 반환 → 모델이 사용자에게 안내.

결정 3: tool schema source-of-truth = Dart 코드

  • ToolDefinition.parametersSchema 는 Dart Map<String, dynamic> 리터럴 (draft-07 JSON Schema 형태).
  • yaml/json 별도 파일 두지 않는다.
  • 이유:
    1. 핸들러 시그니처와 schema 가 같은 파일에 있어 drift 방지
    2. yaml 추가 시 codegen + 버전 동기화 부담
    3. IDE 자동완성 / rename / find-usages 활용

결정 4: destructive tool = 모달 Confirm 게이트 의무

  • add_habit, log_tracker_entry(value=done) 등 mutation tool 은 isDestructive=true 플래그.
  • Dispatcher 가 호출 전 ConfirmGate.show(context, tool, args) 로 AlertDialog 표시 → 사용자 OK 시에만 핸들러 실행.
  • inline 카드 (chat 메시지 안에) 대신 모달 채택 — 시각적 안전성과 모달 API 단순성을 위해.
  • 사용자 결정 (2026-06-15).

결과

  • 새 디렉토리 lib/ai/tools/chat_screen.dart.
  • LlmService 인터페이스에 sendChatTurn(...) 추가 — MockLlmService 도 갱신.
  • 별도 server / process 추가 없음. 의존성 증가 없음.
  • 향후 외부 서비스 (예: 클라우드 카탈로그 sync) 도입 시 핸들러 내부에서 fetch 하는 것으로 충분 — MCP 도입 부담 없음.

영향 / 후속

  • (+) tool latency in-process Dart 호출이라 < 100ms. MCP IPC 오버헤드 없음.
  • (+) R 규칙 단일 SoT — Repository 가 검증하므로 UI/CLI/Chat 모두 동일 동작.
  • (-) MCP 표준 호환 X — 외부 tool 가 MCP 서버로 연동하려면 별도 어댑터 필요. Phase 1 범위 아님.
  • (-) Dart schema 가 수십 개 넘으면 가독성 부담 — ≥ 20 tool 시 ADR 후속 (yaml/codegen 도입 재검토).

대안 (기각)

  • A. MCP 서버 별도 띄우기: 모바일에서 native process 띄우려면 platform channel + lifecycle 관리. 메모리 +N MB. 표준 호환 외 이득 없음 — Phase 1 부적합.
  • B. Prompt-only 안내: 모델이 R 규칙을 학습 못 한다는 게 이미 검증됨 (#218 OQ-1 시점). 안전한 mutation 불가.
  • C. inline 확인 카드: chat 메시지 흐름에 자연스럽지만 사용자가 다른 메시지에 묻혀 무심코 진행 위험. 모달이 더 안전.
  • D. yaml schema: codegen 부담. Dart 단일 SoT 가 단순.

참고

  • 설계서: docs/design/260-gemma-tool-calling/README.md
  • 관련 ADR: ADR-0003 (on-device Gemma 채택)
  • 관련 Redmine: #260