o2o-plagiarism-ai/docs/IMPLEMENTATION_RUNBOOK.md

11 KiB
Raw Blame History

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을 사용한다.

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에서 다음을 설정하고 재기동한다.

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 원문 위치 재구축

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 출처와 사건번호가 확인된 레코드만 넣는다.

python -m scripts.validate_precedents data/precedents/precedents.jsonl

2026-08-18 기준 공식 상세 출처와 엔진 매칭 라벨이 확인된 국내 사건 545건이 등록되어 있다. 한국저작권위원회 전체 2,085건을 모두 적재한 것은 아니다. 자동 선별·라벨 결과이므로 재배포 허용 범위 확인과 저작권 전문가의 다음 항목 검수가 필요하다.

  • 보호되는 표현 / 아이디어·사실·상투적 표현
  • 의거관계 판단 근거
  • 실질적 유사성 인정·부정 이유
  • 저작물 유형과 결론

엔진은 등록된 사건번호만 반환하며 판례를 자유 생성하지 않는다.

검증 게이트

  • 데이터: 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를 따른다. 기본값은 비활성이며, 원고 증거 구간의 외부 API 전송이 승인된 환경에서만 활성화한다.

{"doc_id":"x","text":"...","legal_context":{
  "work_type":"literary","access_evidence":true,
  "protected_expression_reviewed":true,"rights_verified":true}}

판례는 등록된 것만 반환한다(precedent_ids). 랭킹은 ① 태그 교집합 수 ② work_type 일치 ③ 사건번호 순이며, 생성형 인용은 어떤 경로로도 발생하지 않는다.