215 lines
11 KiB
Markdown
215 lines
11 KiB
Markdown
# O2O 표절·AI 의심도·판례 위험도 운영 런북
|
||
|
||
## 안전한 제품 경계
|
||
|
||
세 결과는 서로 다른 증거를 사용하며 합쳐서 하나의 `침해 확정` 값으로 만들지 않는다.
|
||
|
||
1. **유사구간 검색**: 현재 등록 코퍼스 안에서 발견된 후보와 원문 위치
|
||
2. **AI 생성 의심도**: 원문 문체 특징에 대한 검토 우선순위(확정 판정 금지)
|
||
3. **법적 위험도**: 등록 판례와 판단 요소에 대한 검토 보조(법률 자문 아님)
|
||
|
||
후방 호환을 위해 `is_infringement` 필드는 유지하지만 실제 의미는 임계값을 넘은
|
||
`has_similarity_match`와 같다. 신규 연동은 `has_similarity_match`,
|
||
`corpus_scope_note`, `legal_risk`를 사용한다.
|
||
|
||
## 수령 데이터 실사 결과
|
||
|
||
- XLSX 본 데이터 34,105행, 원천 도서 79권
|
||
- 고유 에피소드 31,560개, 같은 책 내부 중복 추가 행 2,545개
|
||
- 고유 본문 약 2,946만 자
|
||
- 페이지/문단 원문 좌표 없음: XLSX 적재 결과는 `coordinate_scope=episode`
|
||
|
||
## King 서버 데이터 적재
|
||
|
||
원고 데이터와 학습 산출물은 Git에 커밋하지 않는다. `/app/data` Docker 볼륨 아래의
|
||
`runtime`, `models`, `input`을 사용한다.
|
||
|
||
```bash
|
||
python -m scripts.ingest_o2o_xlsx \
|
||
data/input/o2o_episodes.xlsx \
|
||
--database data/runtime/corpus.sqlite3
|
||
|
||
python -m scripts.build_persistent_index \
|
||
--database data/runtime/corpus.sqlite3 \
|
||
--index-dir data/runtime/index
|
||
```
|
||
|
||
같은 DB에 신규 세그먼트만 추가한 뒤 `build_persistent_index`를 다시 실행하면 결과의
|
||
`mode`가 `append`이며 기존 행을 다시 벡터화하지 않는다. 삭제 또는 본문 변경이
|
||
감지된 경우에만 `rebuild`한다.
|
||
|
||
`.env`에서 다음을 설정하고 재기동한다.
|
||
|
||
```dotenv
|
||
USE_PERSISTENT_INDEX=true
|
||
CORPUS_DB_PATH=/app/data/runtime/corpus.sqlite3
|
||
PERSISTENT_INDEX_DIR=/app/data/runtime/index
|
||
PERSISTENT_SIMILARITY_THRESHOLD=0.65
|
||
# 바이칼과 키 전달을 합의한 뒤 활성화
|
||
API_KEY=<secret-manager-or-protected-env-value>
|
||
REQUIRE_API_KEY=true
|
||
```
|
||
|
||
`0.65`는 영속 문자 n-gram 후보 검색의 보수적인 시작값일 뿐 운영 확정값이 아니다.
|
||
삽입 복제 테스트와 79권 상호비교 오탐 분포를 측정한 뒤 버전별로 보정한다.
|
||
|
||
## PDF/DOCX 원문 위치 재구축
|
||
|
||
```bash
|
||
python -m scripts.extract_source_documents \
|
||
/mnt/data2/demo/ai_publish/data/combooks \
|
||
--database data/runtime/source_corpus.sqlite3 \
|
||
--chunk-size 1000 --stride 500
|
||
```
|
||
|
||
- PDF: 페이지 번호와 해당 페이지 추출 텍스트의 글자 offset 저장
|
||
- DOCX: 문단 번호와 문단 내부 글자 offset 저장
|
||
- 텍스트가 없는 PDF 페이지는 OCR 필요 경고
|
||
- 구형 `.doc`은 먼저 DOCX 또는 PDF로 변환 필요
|
||
|
||
96개 raw와 79개 처리 도서의 대응표를 검수한 후, 위치 정보가 더 정확한
|
||
`source_corpus.sqlite3`를 운영 코퍼스로 승격한다.
|
||
|
||
## AI 탐지 학습
|
||
|
||
XLSX의 `에피소드` 열은 원문이 사람 작성임이 계약·생성 이력으로 확인된 경우에만
|
||
human 라벨로 사용한다. AI 데이터는 모델·프롬프트·편집 유형별 provenance와
|
||
`source_group`을 가져야 한다. 학습/검증은 행이 아니라 book/source group으로 나눈다.
|
||
|
||
AI 모델 파일이 없거나 단일 클래스 데이터뿐이면 API는 실제 점수를 가장하지 않고
|
||
미학습 상태를 표시해야 한다. 완전 AI, 사람 편집 AI, AI 윤문, 혼합 문서를 각각
|
||
미학습 모델·미학습 도서로 평가한다.
|
||
|
||
## 판례 적재
|
||
|
||
`data/precedents/precedents.jsonl`에는 공식 HTTPS 출처와 사건번호가 확인된 레코드만
|
||
넣는다.
|
||
|
||
```bash
|
||
python -m scripts.validate_precedents data/precedents/precedents.jsonl
|
||
```
|
||
|
||
2026-08-18 기준 공식 상세 출처와 엔진 매칭 라벨이 확인된 국내 사건 545건이
|
||
등록되어 있다. 한국저작권위원회 전체 2,085건을 모두 적재한 것은 아니다. 자동
|
||
선별·라벨 결과이므로 재배포 허용 범위 확인과 저작권 전문가의 다음 항목 검수가 필요하다.
|
||
|
||
- 보호되는 표현 / 아이디어·사실·상투적 표현
|
||
- 의거관계 판단 근거
|
||
- 실질적 유사성 인정·부정 이유
|
||
- 저작물 유형과 결론
|
||
|
||
엔진은 등록된 사건번호만 반환하며 판례를 자유 생성하지 않는다. 적재본 545건 전체를
|
||
검색 후보로 사용하고, 저작물 유형·법적 태그·입력 및 증거 텍스트와 판시 내용의 어휘 관련성으로
|
||
재정렬한다. `precedent_listup.csv`의 A/B/C 등급은 사람 검토 수준을 나타내는 작은 보조
|
||
가중치이며 미등급 판례도 검색에서 제외하지 않는다. 등급을 적재본에 다시 반영할 때는
|
||
다음 명령을 실행한다.
|
||
|
||
```bash
|
||
python scripts/apply_precedent_grades.py --write
|
||
```
|
||
|
||
## 검증 게이트
|
||
|
||
- 데이터: 79권/31,560 고유 XLSX 세그먼트 적재 수 일치
|
||
- 증분성: 신규 문서 추가 후 index sync `mode=append`
|
||
- 검색: 100/200/300/500자 복사·삽입 세트의 Recall@20 기록
|
||
- 근거 위치: 반환한 query/source offset으로 원문 substring이 정확히 복원됨
|
||
- 오탐: 책 단위 분리 및 자서전 공통표현 hard-negative 검수
|
||
- AI: unseen book/model의 AUROC뿐 아니라 FPR, AUPRC, 혼합·편집 유형별 결과 기록
|
||
- 법적 위험도: 미등록 사건번호 0건, 빠진 법적 사실을 항상 명시
|
||
- API: CPU 작업 중 `/v1/health` event loop가 응답 가능
|
||
|
||
## 배포/롤백
|
||
|
||
1. 테스트 통과 및 Git SHA 기록
|
||
2. King에서 `git pull --ff-only`
|
||
3. 데이터 적재/인덱싱/AI 학습은 호스트 또는 일회성 Compose 컨테이너에서 실행
|
||
4. `docker compose up -d --build`
|
||
5. `/v1/health`, `/v1/plagiarism/detect`, 코퍼스 수, 모델 준비 상태 확인
|
||
6. 문제 시 이전 Git SHA의 이미지를 다시 빌드하되 `data/runtime`은 보존
|
||
|
||
Ubuntu 18.04는 지원 종료 상태이므로 OS 업그레이드 전까지 외부 공개 범위를 최소화하고,
|
||
API 인증·방화벽·키 회전을 별도 운영 작업으로 완료해야 한다.
|
||
|
||
## 선택적 KoSimCSE
|
||
|
||
기본 CPU 이미지는 새 영속 문자 인덱스를 사용하며 `torch`와 `sentence-transformers`를
|
||
포함하지 않는다. 일반 PyPI의 최신 torch가 CUDA 런타임 수 GB를 함께 설치할 수 있기
|
||
때문이다. 레거시 KoSimCSE가 반드시 필요한 별도 이미지에서만 해당 Python 버전에 맞는
|
||
공식 CPU 전용 torch wheel을 먼저 설치한 뒤 sentence-transformers를 추가한다.
|
||
|
||
## 인증 fail-closed (#1)
|
||
|
||
| 설정 | 기본 | 의미 |
|
||
|---|---|---|
|
||
| `API_KEY` | 빈 값 | 비어 있으면 **인증이 걸리지 않는다**. 기동 시 critical 로그가 남는다. |
|
||
| `REQUIRE_API_KEY` | `false` | `true` 인데 `API_KEY` 가 비면 **앱이 기동에 실패**한다(`AuthConfigurationError`). |
|
||
| `PUBLIC_HEALTH` | `true` | `/v1/health` 를 무인증 공개할지. 모니터링이 키를 못 넣으면 `true` 유지. |
|
||
| `PUBLIC_DOCS` | `true` | `/docs`, `/openapi.json`, `/redoc` 공개 여부. |
|
||
|
||
- **King 실제 활성화는 바이칼 측 키 전달 후로 보류**한다. 그때까지 `REQUIRE_API_KEY=false`
|
||
로 두되, 서버를 외부에 노출하지 않는다. 키를 받으면 `API_KEY` 설정과 동시에
|
||
`REQUIRE_API_KEY=true` 로 올려 "키를 깜빡한 채 무인증으로 떠 있는" 상태를 원천 차단한다.
|
||
- **운영 키는 반드시 ASCII 로 발급**한다. HTTP 헤더는 비ASCII 를 전송할 수 없어
|
||
한글 키는 인증 자체가 불가능하다(서버는 500 대신 401 을 반환한다).
|
||
- `PUBLIC_DOCS=false` 이면 `/docs`, `/openapi.json`, `/redoc`에도 API 키가
|
||
필요하다. 외부 노출 환경에서는 리버스 프록시 차단도 함께 적용하는 편이 안전하다.
|
||
|
||
## coverage 정의와 CPU 상한 (#3/#5)
|
||
|
||
두 가지 coverage 를 구분한다. 혼동하면 긴 원고에서 위험도가 항상 낮게 나온다.
|
||
|
||
- `matches[].matched_coverage` — **세그먼트 1건**의 일치 구간 / 질의 전체 길이.
|
||
분모가 원고 전체라 30만 자 원고에서는 한 세그먼트가 최대 수천분의 1에 그친다.
|
||
개별 후보의 기여도를 볼 때만 쓴다.
|
||
- `score_semantics.union_coverage` — **정밀 비교한 후보 전체**의 일치 구간을 질의
|
||
좌표에서 **합집합**으로 묶은 비율(중복 구간 1회만 계산). "이 원고의 몇 %가 등록
|
||
코퍼스와 겹치는가"에 답하는 값이며, `PERSISTENT_MIN_COVERAGE` 게이트와 판례
|
||
위험도(`legal_risk`)는 **이 값만** 사용한다.
|
||
|
||
CPU 상한은 `PERSISTENT_RERANK_TOP_K`(기본 20) 하나로 통제한다. 후보 검색은 전량
|
||
행렬곱으로 하되, `SequenceMatcher` 정밀 비교는 상위 N건에만 돌린다. 나머지 후보는
|
||
`reranked=false` 로 반환되며 `evidence`/`coverage`/`longest_span` 이 0 이고 score 만
|
||
의미가 있다. 잘림이 발생하면 `score_semantics.evidence_truncated=true` 로 노출된다.
|
||
이 값을 올리면 요청당 지연이 선형으로 증가한다.
|
||
|
||
참조 lemma/요소는 `scripts/build_persistent_index.py` 가 인덱싱 때 DB 에 사전계산해
|
||
둔다(`--skip-precompute` 로 생략 가능). 캐시가 없으면 질의 시 계산 후 자동 백필된다.
|
||
500 세그먼트×937자 기준 사전계산 시 요청 지연 0.499s → 0.415s (약 17%).
|
||
|
||
## 점수 의미와 잠정 임계값 (#9)
|
||
|
||
`confidence` / `matches[].similarity` 는 hashing 어휘 점수와 lemma 겹침을 설정
|
||
가중치로 섞은 **검색 랭킹 점수**이며 침해 확률이 아니다. 응답의 `score_semantics`
|
||
가 이를 명시한다.
|
||
|
||
- `threshold_source` — `server_default` / `request_override`
|
||
- `threshold_calibrated` — `SIMILARITY_THRESHOLD_CALIBRATED` 설정값. 79권 상호비교
|
||
FP 분포를 측정하기 전까지 `false` 로 두고, `provisional=true` 로 노출된다.
|
||
- `matches[].match_reasons` — 후보가 채택된 이유. `score_threshold`(결합점수 초과),
|
||
`exact_span`(연속 일치 ≥ `PERSISTENT_MIN_EXACT_SPAN`), `coverage`(union coverage
|
||
≥ `PERSISTENT_MIN_COVERAGE`). 이때 후보 채택용 coverage는 해당 출처 문서의
|
||
세그먼트끼리만 합산한다. 서로 다른 출처의 일치를 합쳐 개별 후보를 통과시키지
|
||
않는다. 임계값이 아니라 연속 일치 때문에 올라온 후보도 구분할 수 있다.
|
||
|
||
임계값 수치는 캘리브레이션 전까지 **임의로 바꾸지 않는다.** 기존 값을 유지하고
|
||
provisional 플래그로만 알린다.
|
||
|
||
## 법적 맥락 입력 (#10)
|
||
|
||
`POST /v1/plagiarism/detect` 의 `legal_context` 는 선택 필드다. 엔진은 이 사실들을
|
||
**추론하지 않으며**, 미제공 시 `legal_risk.missing_factors` 에 그대로 남는다.
|
||
|
||
GPT 판례 재판단을 활성화하는 방법과 응답 필드는
|
||
[`LLM_LEGAL_JUDGE.md`](LLM_LEGAL_JUDGE.md)를 따른다. 기본값은 비활성이며,
|
||
원고 증거 구간의 외부 API 전송이 승인된 환경에서만 활성화한다.
|
||
|
||
```json
|
||
{"doc_id":"x","text":"...","legal_context":{
|
||
"work_type":"literary","access_evidence":true,
|
||
"protected_expression_reviewed":true,"rights_verified":true}}
|
||
```
|
||
|
||
판례는 등록된 것만 반환한다(`precedent_ids`). 랭킹은 ① 태그 교집합 수 ②
|
||
`work_type` 일치 ③ 사건번호 순이며, 생성형 인용은 어떤 경로로도 발생하지 않는다.
|