143 lines
9.9 KiB
Markdown
143 lines
9.9 KiB
Markdown
# PoC #1 기반 코퍼스 — AI-Hub 한국어 SNS 멀티턴 대화
|
|
|
|
`docs/poc-plan.md`의 PoC #1(온디바이스 말투 학습)에서, 개인화 레이어를 얹기 전 **기반 톤 생성
|
|
모델**을 학습/평가할 코퍼스로 AI-Hub "한국어 SNS 멀티턴 대화" 데이터셋을 사용한다. 이 데이터는
|
|
특정 개인의 말투가 아니라 익명 화자쌍의 일반 대화이므로, PoC #1의 "내 말투 같다" 개인화 검증
|
|
자체를 대체하지는 않는다 — 개인화 검증은 여전히 실제 참가자의 대화 샘플이 필요하다
|
|
(`docs/poc-materials.md` §1 참고).
|
|
|
|
## 사용법
|
|
|
|
```bash
|
|
python3 prepare_dataset.py --input <원본 zip이 있는 디렉토리> --output <출력 디렉토리> [--qa]
|
|
```
|
|
|
|
- `--input`: AI-Hub에서 내려받은 `TS_*.zip`/`TL_*.zip`(학습)·`VS_*.zip`/`VL_*.zip`(검증) 원본 파일이
|
|
있는 디렉토리. 파일명에 `VL_`/`TL_`이 포함된 zip(라벨링 JSON)만 읽는다 — 여기에 발화 텍스트와
|
|
화행(speech_act)·슬롯 라벨이 모두 있어서 `VS_`/`TS_`(원천 CSV)는 출력에 필요 없다.
|
|
- `--output`: `train.jsonl`, `val.jsonl`, `stats.json`을 쓸 디렉토리.
|
|
- `--qa`: 원천 CSV의 대화ID와 라벨 JSON의 대화ID를 교차검증해서 커버리지 차이를 출력.
|
|
|
|
## 실행 결과 (2026-07-29, 이 세션에서 실제로 돌려본 값)
|
|
|
|
- 총 142,575건 (학습 122,951 / 검증 19,624), 평균 발화 16.5턴/대화
|
|
- 주제 9종, 정치(3,242건)·경제및사회(4,257건)가 다른 주제(1.7만~2.2만건)보다 훨씬 적음 — 주제별
|
|
균형이 필요하면 샘플링 시 감안할 것
|
|
- 화자 연령/성별 필드 노이즈 81건은 정제 과정에서 `null`로 치환 (버리지 않고 필드만 비움)
|
|
- **QA 결과**: 원천(CSV) 쪽에는 라벨링된 142,575건 외에 34,030건이 더 있었음 (라벨 없이 텍스트만).
|
|
`build_unlabeled_corpus.py`로 그 34,030건만 뽑아 `unlabeled.jsonl`(63MB)로 정리함 — 화행/슬롯
|
|
없이 화자·발화 텍스트만 있어서 순수 언어모델링(다음 문장 예측 등)용으로만 쓸 것
|
|
|
|
```bash
|
|
python3 build_unlabeled_corpus.py --input <원본 zip 디렉토리> --output <출력 디렉토리>
|
|
```
|
|
|
|
## 응답 초안 생성기 (`generate_draft.py`)
|
|
|
|
PoC #1의 "응답 초안 생성" 단계를 채우는 프로토타입. 말투 예시(그 사람이 쓴 문장 몇 개) +
|
|
최근 대화 맥락을 받아서, `tech-design.md` §2의 서버 LLM 폴백 경로처럼 LLM 호출로 답장 초안
|
|
하나를 만든다. 확정성 있는 내용(금전·약속·감정 이슈)은 `[ESCALATE]`를 출력하도록 시스템
|
|
프롬프트에 못박아뒀다 — `AGENTS.md`의 절대 안전선을 프롬프트 레벨에서도 지키기 위함.
|
|
|
|
```bash
|
|
pip install -r requirements.txt
|
|
export GEMINI_API_KEY=...
|
|
python3 generate_draft.py --style style_examples.txt --context context.txt
|
|
```
|
|
|
|
Gemini(`google-genai`)를 쓴다 — 기본 모델은 `--model`로 바꿀 수 있고 기본값은
|
|
`gemini-2.5-flash`. `GEMINI_API_KEY`가 없으면 실제 전송될 프롬프트만 stdout에 찍고 끝난다.
|
|
|
|
**Cursor 환경에서 `.env`에 실제 키를 넣고 라이브 호출까지 성공 확인됨** (2026-07-29) — 스타일
|
|
예시(반말·ㅋ톤)에 맞는 짧은 캐주얼 답장 초안이 그대로 나왔다. 이 Claude Code 세션에는 키가 없어서,
|
|
동일한 검증은 이 세션에서는 프롬프트 구성 확인 + 사람이 직접 쓴 초안 대조로 대체했다 (아래 참고).
|
|
|
|
### `GEMINI_API_KEY`는 어디에 설정하나
|
|
|
|
- **권장**: 저장소 루트 `.env`에 `GEMINI_API_KEY=...`를 넣는다 (템플릿은 `.env.example`).
|
|
`generate_draft.py`가 실행 시 이 파일을 읽는다. `.env`는 `.gitignore`로 커밋되지 않는다.
|
|
- **대안**: 터미널에서 `export GEMINI_API_KEY=...`(임시) 또는 셸 프로필에 등록. Google AI Studio에서
|
|
발급한 키를 그대로 쓰면 된다.
|
|
- 대화창에 직접 키 값을 붙여넣는 건 대화 기록에 그대로 남으므로 권장하지 않는다 — 위 두 방법 중
|
|
하나로, 사용량 제한을 걸어둔 키를 쓸 것.
|
|
|
|
**샘플 검증** (검증셋 `id=003820`, "고교학점제" 대화, B의 마지막 답장을 가리고 앞 6개 발화만
|
|
스타일 예시로 사용):
|
|
- 실제 답장: `ㅋㅋ 내가 지금 말한 거 고교학점제 홈피에 다 있는 내용이니까 궁금하면 가서 더 찾아봐랑!ㅋ`
|
|
- 생성 초안(수동): `ㅋㅋㅋ 뭘 유식까지야, 그냥 관심있어서 좀 찾아본거임 너도 궁금하면 고교학점제 사이트 가서 찾아봐~ㅋ`
|
|
- 둘 다 "ㅋㅋ로 시작 → 정보 제공자 역할 수용 → '찾아봐'식 권유형 마무리 → ㅋ로 끝" 패턴이 겹침.
|
|
다만 이건 익명 화자의 일반 대화 스타일 재현 여부를 본 것일 뿐, 실제 개인화("이 사람 말투 같다")
|
|
검증은 아니다 — 그건 `poc-materials.md`의 실제 참가자 데이터로만 확인 가능하다.
|
|
|
|
## 에스컬레이션 판정기 (`escalation_filter.py`)
|
|
|
|
`generate_draft.py`가 LLM을 호출하기 **전에** 먼저 통과해야 하는 규칙 기반 하드 게이트.
|
|
금전·약속 확정·감정적으로 무거운 주제면 LLM을 부르지도 않고 즉시 `[ESCALATE:사유]`를 반환한다 —
|
|
`AGENTS.md`의 절대 안전선을 "모델이 알아서 잘 판단하겠지"에 맡기지 않고 코드 레벨에서 강제한다.
|
|
LLM 시스템 프롬프트의 `[ESCALATE]` 지시는 이 규칙이 놓친 케이스를 위한 2차 방어선으로 남겨둔다.
|
|
|
|
```bash
|
|
python3 escalation_filter.py --text "계좌로 3만원만 보내줘"
|
|
python3 escalation_filter.py --selftest # 내장 테스트 케이스 10개, 10/10 통과 확인됨
|
|
```
|
|
|
|
**검증셋(5,000개 대화, 82,305개 발화)에 돌려본 결과**: 767건(0.93%) 트리거 — 금전 523 / 감정 238 /
|
|
약속확정 6. 트리거된 걸 직접 살펴보니 대부분 "월세 85만원", "벌금 500만원" 같은 **일반적인 시사/경제
|
|
얘기에서 나온 액수**였다 — 실제 서비스에서는 1:1 사적 대화라 이런 팩트성 언급보다 진짜 정산·확정
|
|
요청일 가능성이 훨씬 높지만, 이 수치 자체는 오탐률의 상한선 정도로 참고할 것. 규칙은 PoC #1 실사용
|
|
로그로 계속 튜닝한다 (`docs/tech-design.md` §3, `docs/risk-log.md` 참고).
|
|
|
|
## 블라인드 평가 자동화 (`blind_eval.py`)
|
|
|
|
`docs/poc-plan.md`의 블라인드 평가를 검증셋 전체에 대해 자동으로 돌려보는 배치 스크립트. 각
|
|
대화의 마지막 발화를 가리고, 그 화자의 앞선 발화를 말투 예시로 써서 `generate_draft.draft_reply`로
|
|
초안을 생성한 뒤 실제 발화와 나란히 JSONL로 남긴다.
|
|
|
|
```bash
|
|
python3 blind_eval.py --corpus <val.jsonl 경로> --n 30 --output eval_report.jsonl
|
|
```
|
|
|
|
**주의 — 이건 진짜 PoC #1이 아니다.** 익명 화자쌍의 일반 대화라 "내 말투 같다"를 판단할 수 없다.
|
|
자동 계산하는 `length_ratio`(길이 비율), `formal_match`(존댓말/반말 일치 여부)도 사람이 5점 척도로
|
|
평가하는 진짜 지표의 대체재가 아니라, **대량으로 돌려서 파이프라인 자체가 말이 되는 응답을
|
|
내는지**(요청한 형식으로 나오는지, 에스컬레이션이 과하게/적게 걸리는지) 보는 사전 체크용이다.
|
|
진짜 개인화 평가는 `poc-materials.md` §1로 실제 참가자 데이터를 모은 뒤에만 가능하다.
|
|
|
|
`--n 5`로 실제 검증셋에 돌려 확인함 — 샘플링, 컨텍스트/스타일 예시 구성, JSONL 출력 전부 정상
|
|
동작 (API 키 없이 `no_key` 상태로 5/5 처리됨, 리포트 레코드 형식도 의도대로 나옴).
|
|
|
|
## 말투 검색기 (`retrieve_style.py`)
|
|
|
|
`docs/tech-design.md` §2-1에서 설계한 "가벼운 키워드/최근성 기반 검색"의 실제 구현. 그 사람의
|
|
과거 발화 전체(오래된 것부터 한 줄에 하나)와 지금 답장해야 할 메시지를 주면, 키워드 겹침(자카드
|
|
유사도) + 최근성 가중치로 점수를 매겨 가장 비슷한 발화 상위 k개를 돌려준다. 임베딩은 쓰지 않는다 —
|
|
"지금부터 임베딩 인프라를 먼저 만들지 않는다"는 설계 원칙을 그대로 따름.
|
|
|
|
```bash
|
|
python3 retrieve_style.py --history history.txt --query "그 얘기 진짜야?" --k 6
|
|
```
|
|
|
|
`generate_draft.py`에 그대로 연결되어 있다 — `--style` 대신 `--history`를 주면 매번 스타일 예시를
|
|
손으로 고르지 않고 자동으로 검색해서 쓴다:
|
|
|
|
```bash
|
|
python3 generate_draft.py --history history.txt --context context.txt
|
|
```
|
|
|
|
**버그를 하나 잡음**: 처음엔 `overlap + RECENCY_WEIGHT * recency`로 점수를 더했는데, 실제로
|
|
돌려보니 짧은 한국어 문장은 조사가 붙어서("핀란드" vs "핀란드는") 토큰이 잘 안 겹쳐 자카드
|
|
유사도가 거의 항상 최근성 항보다 작았다 — 결과적으로 검색이 사실상 "그냥 최신 메시지 K개"가
|
|
되는 문제였다. `(overlap, recency)` 튜플로 정렬해서 **키워드가 실제로 겹치는 게 항상 먼저 오고,
|
|
동점일 때만 최근성으로 갈리게** 고쳤다. 검증: `--query "핀란드는 교육이 좋대"`로 돌리니 겹치는
|
|
토큰이 있는 과거 발화가 1위로 올라오고, 나머지는 최근 순으로 정렬됨을 확인.
|
|
|
|
## 반드시 지킬 것
|
|
|
|
- **원본 zip과 이 스크립트의 출력(JSONL)을 git에 커밋하지 않는다.** AI-Hub 데이터는 이용약관상
|
|
제3자 재배포가 제한되고, 용량도 수백MB~1GB라 저장소에 맞지 않는다. 저장소 루트의 `.gitignore`에
|
|
`poc/tone-corpus/data/`가 등록되어 있으니 원본·출력은 그 아래에 두고 작업할 것.
|
|
- 이 저장소는 기획 문서 전용이라, 이 스크립트도 "PoC 도구"로만 취급한다 — 여기서 앱 코드/프레임워크로
|
|
확장하지 않는다 (`AGENTS.md` 참고).
|
|
- 실제 모델 학습에 쓰기 전에, 이 데이터가 세션이 아닌 영구 저장소(본인 로컬/스토리지)로 옮겨졌는지
|
|
확인할 것 — 클라우드 세션은 종료되면 임시 파일이 사라진다.
|