iykyka/docs/tech-design.md

9.7 KiB

기술 설계서 — 분신 v1

PRD.md 범위(자체 앱, L0~L2, 읽씹 종결 + 단톡 따라잡기)를 구현하기 위한 아키텍처 개요. 상세 API 스펙이 아니라 경계와 원칙을 정의하는 문서다 — 실제 구현 시 세부 설계는 별도로 좁혀나간다.

1. 전체 구조

[클라이언트 앱 (Android 우선, iOS 병행 검토)]
   ├─ 메시지 UI / 뱃지 렌더링
   ├─ 온디바이스 말투 모델 (경량, 로컬 추론)
   ├─ 자율성 엔진 (L0~L2 규칙 + 에스컬레이션 판정)
   └─ 로컬 이벤트 로그 (사후 알림 / 되돌리기용)
          │  (암호화된 동기화만, 원문은 최소 전송)
          ▼
[서버]
   ├─ 메시지 릴레이 / 대화방 저장
   ├─ 응답 초안 생성 (온디바이스로 부족한 경우의 폴백 LLM 호출)
   └─ 화이트리스트·설정 동기화 (기기 간)

2. 온디바이스 vs 서버 경계

  • 말투 학습(온보딩, 교정 학습)은 온디바이스 우선. 원문 대화를 서버로 올리지 않고 기기 내에서 스타일 특징만 추출·저장한다. (vision.md의 신뢰 가설과 직결 — 여기서 타협하면 Q4 사칭/프라이버시 우려가 그대로 리스크로 남는다)
  • 응답 초안 생성은 온디바이스 우선 + 서버 폴백. 기기 성능/배터리 제약으로 온디바이스 모델이 처리 못 하는 경우에만 서버 LLM 호출, 이 경우도 필요한 최소 컨텍스트만 전송.
  • 자율성 엔진(L0~L2 판정, 에스컬레이션 규칙)은 클라이언트에 둔다. 금전/약속/민감 감지가 서버 왕복 지연이나 서버 장애에 영향받지 않아야 안전선이 항상 지켜진다.
  • 대화방 저장/릴레이는 서버. 멀티 디바이스 동기화, 상대방에게 메시지를 전달하는 기본 기능.

2-1. 개인화 레이어 — 실제로 어떻게 "그 사람 말투"가 되는가

v1에서는 커스텀 모델을 새로 학습하지 않는다. 대신 검색 기반 few-shot으로 개인화한다 — poc/tone-corpus/generate_draft.py가 이미 구현한 "말투 예시 + 대화 맥락 → LLM 호출" 방식을 그대로 쓰되, 말투 예시를 고정 목록이 아니라 그 순간 대화와 가장 비슷한 과거 발화 5~8개를 그 사람의 메시지 이력에서 검색해 넣는다.

  1. 온디바이스: 사용자의 과거 메시지(온보딩 시 임포트한 50~100개 + 계속 쌓이는 실사용 이력)를 기기 내에서만 저장. 원문은 서버로 안 올라간다 (§ 위 원칙과 동일)
  2. 검색: 지금 답장해야 할 맥락과 유사한 과거 발화를 찾는다 — v1은 가벼운 키워드/최근성 기반 검색으로 시작하고, 임베딩 기반 검색은 필요성이 확인되면 추가한다 (지금부터 임베딩 인프라를 먼저 만들지 않는다). 구현: poc/tone-corpus/retrieve_style.py — 자카드 유사도 + 최근성 가중치. generate_draft.py --history로 바로 연결됨
  3. 생성: 검색된 예시 + 최근 대화 맥락을 generate_draft.py와 같은 프롬프트 계약으로 서버 LLM(Gemini)에 보내 초안 하나를 받는다. 온디바이스로 충분해지면 이 호출을 온디바이스 모델로 교체하되, 프롬프트 계약(예시+맥락 in, 초안 1개 out)은 그대로 둔다

기반 코퍼스(AI-Hub 14.2만 건, poc/tone-corpus/)의 역할은 이 개인화 메커니즘 자체가 아니다. 호스팅된 LLM(Gemini)이 이미 일반적인 한국어 대화 유창성을 갖고 있어서, v1에 별도로 코퍼스를 학습시킬 필요가 없다. 이 코퍼스는 두 가지로만 쓴다:

  • 평가: 검증셋으로 "일반적인 한국어 SNS 대화로서 얼마나 자연스러운가"를 벤치마크 (개인화 여부와 무관한 기초 품질 체크 — poc/tone-corpus/README.md "샘플 검증"이 그 예)
  • v2 이후 온디바이스 증류 대비: 배터리/지연/프라이버시 압박으로 자체 경량 모델이 필요해지면, 이 코퍼스가 그 모델의 기반 학습 데이터가 된다. v1 시점에는 착수하지 않는다

3. 자율성 엔진 (L0~L2)

  1. 수신 메시지 → 에스컬레이션 판정기 먼저 통과 (금전/약속 확정/민감 키워드+의도 분류)
  2. 에스컬레이션 대상이면 무조건 사용자에게 알림만 하고 종료 (자동 처리 안 함) — 이 경로엔 예외 없음
  3. 아니면 자율성 레벨 확인:
    • L0: 초안만 생성해 사용자에게 보여줌, 발송 없음
    • L1: 초안 생성 + 발송 승인 요청 알림
    • L2: 상대·주제가 화이트리스트에 있으면 즉시 발송, 아니면 L1과 동일하게 강등
  4. 발송된 모든 자동 응답은 로컬 이벤트 로그에 기록 (사후 알림 + 되돌리기 버튼 노출)

에스컬레이션 판정기는 v1에서는 규칙 기반(키워드 + 간단한 의도 분류) + 온디바이스 모델의 결합으로 시작하고, 오탐/누락 사례를 베타 로그로 계속 튜닝한다. 100% 정확도를 목표하지 않는다 — 애매하면 항상 에스컬레이션 쪽으로 fail-safe.

규칙 기반 1차 게이트는 poc/tone-corpus/escalation_filter.py로 구현해뒀다 — generate_draft.py가 LLM을 부르기 전에 먼저 통과해야 하며, 걸리면 LLM 호출 자체를 건너뛴다. 검증셋 82,305개 발화로 측정한 트리거율은 0.93% (poc/tone-corpus/README.md 참고) — 실제 1:1 사적 대화가 아닌 일반 SNS 코퍼스 기준이라 상한선 참고용이다.

4. 투명성 구현

  • 분신 뱃지: 메시지 객체에 sender_mode: human | twin 필드, 클라이언트가 이를 렌더링에만 사용 (서버가 신뢰의 원천 — 클라이언트 임의 조작 방지를 위해 서명 포함)
  • 실시간 본인 확인: "본인/분신" 질문은 별도 API 없이 자율성 엔진이 인식하는 고정 인텐트로 처리, 분신은 항상 정직하게 "저는 분신입니다"로 응답 (프롬프트 레벨에서 이 사실을 숨기지 않도록 고정)
  • 거부권: 대화방 단위 플래그(twin_disabled_by_peer) — 상대방의 거부 요청이 감지되면 즉시 해당 대화방에서 L1/L2 자동 동작을 끄고 L0으로 강등

5. 데이터/프라이버시 원칙

  • 원문 대화 내용은 기본적으로 기기 내 저장, 서버는 릴레이에 필요한 최소 기간만 보관
  • 말투 특징 벡터 등 학습 산출물은 암호화 저장, 사용자가 언제든 초기화 가능
  • 어떤 데이터가 어디로 가는지 설정 화면에서 실시간으로 확인 가능한 대시보드 제공 (베타 초기엔 최소한 "이 대화는 서버로 갔음/기기에만 있음" 표시 수준으로 시작)

6. 플랫폼 전략

  • Android 우선. 알림 접근 권한 기반 확장(v2 OS 레이어)을 염두에 둔 선택이기도 하고, v1 자체 앱 단계에서도 베타 규모라면 단일 플랫폼으로 좁혀 개발 속도를 확보하는 게 유리하다.
  • iOS는 베타 반응 확인 후 병행 착수. (근거: decision-log.md Q7, 원본 회의자료 §oslayer 리스크 "iOS는 안드로이드보다 훨씬 제한적")

7. v1에서 의도적으로 안 만드는 것

  • OS 레이어(알림 파싱 기반 타 앱 관통) — v2. 지금 만들면 카카오톡 등 UI 변경에 따라 계속 깨지는 파싱 로직을 유지보수해야 해서, 아직 검증 안 된 v1 핵심 가설과 리스크가 섞인다.
  • 분신 간 프로토콜(L4) — 네트워크 효과가 필요해 사용자 기반이 있어야 의미 있음.
  • 서버 측 전체 대화 분석/추천 — 온디바이스 우선 원칙과 상충.

8. 기술 스택 결정 (Phase 1)

roadmap.md Phase 1 §1의 "착수 전 확정 필요" 항목에 대한 결정. PoC 데이터와 무관하게 지금 확정할 수 있는 것들이라 여기서 정리한다 — 자율성 기본값 같은 PoC 의존 값은 여전히 미정으로 남는다.

항목 결정 근거
클라이언트 Flutter (Dart) Q7이 안드로이드 우선을 확정했지만 네이티브를 강제하진 않음. 채팅 UI(이미 클릭 프로토타입으로 검증된 디자인)를 핫리로드로 빠르게 만들 수 있어 v1 개발 속도에 유리. v2 OS 레이어의 알림 접근 권한(NotificationListenerService)은 platform channel로 네이티브 Android 모듈을 붙여 해결 — 클라이언트 전체를 네이티브로 갈 필요는 없음
백엔드 Python (FastAPI) poc/tone-corpus/의 AI 파이프라인(generate_draft·escalation_filter·retrieve_style)이 이미 Python — 언어를 바꾸면 그대로 재사용 못 하고 다시 짜야 함. 클로즈드 베타 규모에서 성능은 병목이 아님
메시지 릴레이 FastAPI WebSocket 자체 서버로 충분한 규모(소규모 지인 네트워크 베타). Kafka·관리형 pub-sub 같은 건 지금 시점에 과한 인프라
데이터베이스 PostgreSQL users/contacts/conversations/messages/escalation_logs/whitelist_rules 관계형 스키마에 적합, 운영 경험 풍부
온디바이스 저장소 Flutter drift(SQLite) + sqlcipher_flutter_libs 암호화 말투 이력·설정을 기기 내 암호화 저장한다는 §2/§5 원칙을 그대로 구현
Gemini API 키 관리 프로덕션 키는 서버 환경변수/시크릿 매니저로, PoC 키와 분리 poc/tone-corpus/.env는 PoC 전용 — 프로덕션 트래픽과 쿼터를 섞지 않음

이 표 밖의 결정(자율성 기본값, 화이트리스트 기본 주제, 신뢰 UX 문구)은 roadmap.md Phase 1 §3에 남아있는 PoC 의존 항목이다 — 여기서 같이 정하지 않는다.