161 lines
13 KiB
Markdown
161 lines
13 KiB
Markdown
# 기술 설계서 — 와카뷰 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`](../poc/tone-corpus/generate_draft.py)가 이미 구현한
|
|
"말투 예시 + 대화 맥락 → LLM 호출" 방식을 그대로 쓰되, 말투 예시를 고정 목록이 아니라
|
|
**그 순간 대화와 가장 비슷한 과거 발화 5~8개를 그 사람의 메시지 이력에서 검색해 넣는다.**
|
|
|
|
1. **온디바이스**: 사용자의 과거 메시지(온보딩 시 임포트한 50~100개 + 계속 쌓이는 실사용 이력)를
|
|
기기 내에서만 저장. 원문은 서버로 안 올라간다 (§ 위 원칙과 동일)
|
|
2. **검색**: 지금 답장해야 할 맥락과 유사한 과거 발화를 찾는다 — v1은 가벼운 키워드/최근성
|
|
기반 검색으로 시작하고, 임베딩 기반 검색은 필요성이 확인되면 추가한다 (지금부터 임베딩
|
|
인프라를 먼저 만들지 않는다). 구현: [`poc/tone-corpus/retrieve_style.py`](../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 시점에는 착수하지 않는다
|
|
|
|
## 2-2. 인증 · 세션 (클로즈드 베타)
|
|
|
|
상세 UX: [`account-settings-ia.md`](./account-settings-ia.md) · 운영: [`invite-ops.md`](./invite-ops.md).
|
|
|
|
| 경로 | 역할 |
|
|
|------|------|
|
|
| `POST /auth/signup` | 미사용 초대 코드 + `display_name` → User + Session(Bearer) |
|
|
| `POST /auth/login` | 이미 사용된 초대 코드 → 새 Session (앱 UI 갭) |
|
|
| `GET/DELETE /users/:id/sessions...` | 멀티 디바이스 목록·종료 |
|
|
| 클라이언트 | `shared_preferences`에 토큰 저장; **로그아웃 시 revoke + 로컬 삭제** (갭) |
|
|
|
|
- Phase 1은 **비밀번호·OAuth 없음.** 초대 코드가 비밀에 해당한다.
|
|
- 로그아웃은 서버 세션 무효화만으로는 부족하고, 클라이언트가 토큰을 지워야 입장 화면으로 돌아간다.
|
|
|
|
## 3. 자율성 엔진 (L0~L2)
|
|
|
|
1. 수신 메시지 → 에스컬레이션 판정기 먼저 통과 (금전/약속 확정/민감 키워드+의도 분류)
|
|
2. 에스컬레이션 대상이면 무조건 사용자에게 알림만 하고 종료 (자동 처리 안 함) — **이 경로엔 예외 없음**
|
|
3. 아니면 자율성 레벨 확인:
|
|
- L0: 초안만 생성해 사용자에게 보여줌, 발송 없음
|
|
- L1: 초안 생성 + 발송 승인 요청 알림
|
|
- L2 **(목표)**: 상대·주제가 화이트리스트에 있으면 즉시 발송, 아니면 L1과 동일하게 강등
|
|
4. 발송된 모든 자동 응답은 로컬 이벤트 로그에 기록 (사후 알림 + 되돌리기 버튼 노출)
|
|
|
|
### 3-1. 현재 서버 게이트 vs 목표 오케스트레이션
|
|
|
|
- **현재 (`core-backend` 메시지 POST):** `sender_mode=twin`일 때만 레벨 검사.
|
|
L2 + `whitelistMatches(text)`면 `approved` 없이 통과. 수신 이벤트가 이 경로를
|
|
자동 호출하지는 않는다.
|
|
- **목표 (PRD §2.2 / decision-log Q9):** 상대 메시지 수신 → (게이트 통과 시) 초안 →
|
|
L2면 twin 발송. 구현은 별도 로드맵 항목으로 분리한다.
|
|
|
|
|
|
에스컬레이션 판정기는 v1에서는 규칙 기반(키워드 + 간단한 의도 분류) + 온디바이스 모델의 결합으로
|
|
시작하고, 오탐/누락 사례를 베타 로그로 계속 튜닝한다. 100% 정확도를 목표하지 않는다 — 애매하면
|
|
항상 에스컬레이션 쪽으로 fail-safe.
|
|
|
|
규칙 기반 1차 게이트는 [`poc/tone-corpus/escalation_filter.py`](../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 우선 (v1은 Android만).** 근거 세 가지:
|
|
1. v2 OS 레이어(알림 접근 권한) 확장이 애초에 안드로이드에서만 가능함 — 애플이 다른 앱의
|
|
알림 내용을 읽는 것 자체를 정책적으로 막고 있어, 클라이언트 프레임워크와 무관하게 구조적으로
|
|
안드로이드 전용인 기능임
|
|
2. 개발 환경이 Windows라 iOS 빌드(Xcode/macOS 필요)를 할 수 없음 — v1 시점의 실질적 제약
|
|
3. Flutter라 iOS 코드 재작성 비용 자체는 낮지만, 위 두 이유로 지금은 굳이 열 이유가 없음
|
|
- iOS는 빌드 가능한 환경(Mac)이 갖춰지거나 베타 반응을 보고 병행 착수 여부를 다시 판단한다.
|
|
Flutter를 쓰므로 그때 가서 UI를 다시 만들 필요는 없고, iOS 빌드·서명·APNs 설정 등 플랫폼별
|
|
작업만 추가하면 된다.
|
|
|
|
## 7. v1에서 의도적으로 안 만드는 것
|
|
|
|
- OS 레이어(알림 파싱 기반 타 앱 관통) — v2. 지금 만들면 카카오톡 등 UI 변경에 따라 계속 깨지는
|
|
파싱 로직을 유지보수해야 해서, 아직 검증 안 된 v1 핵심 가설과 리스크가 섞인다.
|
|
- 와카뷰 간 프로토콜(L4) — 네트워크 효과가 필요해 사용자 기반이 있어야 의미 있음.
|
|
- 서버 측 전체 대화 분석/추천 — 온디바이스 우선 원칙과 상충.
|
|
|
|
## 8. 기술 스택 결정 (Phase 1)
|
|
|
|
`roadmap.md` Phase 1 §1의 "착수 전 확정 필요" 항목에 대한 결정. PoC 데이터와 무관하게 지금
|
|
확정할 수 있는 것들이라 여기서 정리한다 — 자율성 기본값 같은 PoC 의존 값은 여전히 미정으로 남는다.
|
|
|
|
백엔드는 단일 서비스가 아니라 **두 개로 나눈다** — 코어(인증·메시지·DB)는 Go, AI 파이프라인은
|
|
Python. 순수 성능/동시성만 보면 이 프로젝트 규모(소규모 지인 네트워크 베타)에서 Python
|
|
비동기(FastAPI/asyncio)로도 충분하다 — 메신저 릴레이는 CPU-bound가 아니라 I/O-bound라 GIL이
|
|
병목이 되지 않고, 실제 지연은 백엔드 언어가 아니라 Gemini API 왕복 시간이 좌우한다. 그럼에도
|
|
Go를 코어에 쓰기로 한 건 향후 스케일 대비 선제적 판단이며, `poc/tone-corpus/`의 이미 검증된
|
|
AI 파이프라인(generate_draft·escalation_filter·retrieve_style)을 다시 짜지 않기 위해 AI 쪽만
|
|
Python으로 남긴다.
|
|
|
|
| 항목 | 결정 | 근거 |
|
|
|---|---|---|
|
|
| 클라이언트 | Flutter (Dart), **v1은 Android 빌드만** | Q7이 안드로이드 우선을 확정했지만 네이티브를 강제하진 않음. 채팅 UI(이미 클릭 프로토타입으로 검증된 디자인)를 핫리로드로 빠르게 만들 수 있어 v1 개발 속도에 유리. v2 OS 레이어의 알림 접근 권한(NotificationListenerService)은 platform channel로 네이티브 Android 모듈을 붙여 해결 — 클라이언트 전체를 네이티브로 갈 필요는 없음. iOS는 개발 환경이 Windows라 지금은 빌드 자체가 안 됨 (§6 참고) — Flutter라 나중에 Mac 환경이 생기면 UI 재작성 없이 iOS 빌드만 추가하면 됨 |
|
|
| 백엔드 — 코어 서비스 | Go (Gin/Echo + `gorilla/websocket`) | 인증, 메시지 릴레이, DB 접근. 동시성·성능 이점, 향후 스케일 대비. 이 프로젝트 규모에선 Python으로도 충분했지만 선제적으로 Go 선택 |
|
|
| 백엔드 — AI 서비스 | Python (FastAPI) | `generate_draft.py`·`escalation_filter.py`·`retrieve_style.py`를 그대로 감싸는 내부 API. 이미 실행 검증까지 끝난 코드를 다시 짜지 않기 위함 |
|
|
| 서비스 간 통신 | Go 코어 → Python AI 서비스, 내부망 HTTP(REST) | 처음부터 gRPC 등으로 과설계하지 않음 — 필요해지면 그때 전환 |
|
|
| 메시지 릴레이 | Go 코어 서비스 내 WebSocket | 자체 서버로 충분한 규모. Kafka·관리형 pub-sub은 지금 시점에 과한 인프라 |
|
|
| 데이터베이스 | PostgreSQL | users/contacts/conversations/messages/escalation_logs/whitelist_rules 관계형 스키마에 적합. Go 쪽 접근은 `pgx`나 GORM |
|
|
| 온디바이스 저장소 | Flutter `drift` + `sqlcipher_flutter_libs` (`PRAGMA key`, 패스프레이즈는 `flutter_secure_storage`) | 말투 이력·설정을 기기 내 암호화 저장한다는 §2/§5 원칙. 구현: `mobile/lib/db/` |
|
|
| Gemini API 키 관리 | Python AI 서비스에서만 보관 (Go 코어는 키를 안 가짐) | `poc/tone-corpus/.env`는 PoC 전용 — 프로덕션 키·쿼터는 AI 서비스 환경에서만 분리 관리 |
|
|
|
|
이 표 밖의 결정(자율성 기본값, 화이트리스트 기본 주제, 신뢰 UX 문구)은 `roadmap.md` Phase 1 §3에
|
|
남아있는 PoC 의존 항목이다 — 여기서 같이 정하지 않는다.
|