장르: 설계 논의 + 실행 경로 분석. 희곡 채점 rubric을 검증하는 내부 도구가 있다. 이걸 작가에게 열어도 되는지 물었다가 약관 원문까지 읽게 됐고, 결론이 두 번 뒤집혔다. 마지막에는 조사와 무관해 보였던 구멍 하나를 코드에서 찾아 메웠다.

희곡 대본을 5축으로 채점하는 rubric을 만들고, 그 rubric이 사람 판단과 얼마나 일치하는지 검증하는 도구를 쓰고 있다. 대본마다 3회씩 독립 채점을 돌려 반복 표준편차와 순위 상관을 본다.

이 도구는 로컬에서만 돈다. 그런데 작가가 대본을 올리고 우리가 내부적으로 채점하는 형태로 열 수 있을지 궁금해졌다. 그래서 먼저 물었다. 어느 파일이 LLM을 부르나?

1. API 호출이 아니라 CLI를 프로세스로 띄우고 있었다

src/services/scoring/session-runner.ts가 실행 지점이었다.

spawn("claude", [
  "-p", "--output-format", "json",
  "--model", model,
  "--setting-sources", "",
  "--strict-mcp-config",
  "--mcp-config", '{"mcpServers":{}}',
  "--disallowedTools", ...DISALLOWED_TOOLS,
], { cwd: tmpCwd, stdio: ["pipe", "pipe", "pipe"] })

SDK 의존성도, ANTHROPIC_API_KEY 참조도, 환경변수 템플릿의 키 항목도 없었다. 확인해 보니 프로젝트 문서가 설계 의도를 그대로 적어 두고 있었다. "전 과정을 대화형 CLI로 수행한다 (API 키·스크립트 불필요, 구독 정액 내 비용 0)."

이 파일의 대부분은 실행이 아니라 오염 차단이다. 채점 세션에 금지된 정보가 새어 들어가면 측정값이 무효가 되기 때문이다.

장치 막는 것
빈 임시 디렉터리를 cwd로 사용 리포 루트에서 띄우면 프로젝트 메모리가 자동 주입된다
--setting-sources "" 사용자·프로젝트 설정과 메모리
--strict-mcp-config + 빈 MCP 설정 MCP 서버
도구 11종 전면 차단 파일 탐색으로 금지 문서가 유입되는 경로
1 프로세스 = 1 회차 앞 대본이 다음 대본의 기준점을 오염시키는 것

응답을 받은 뒤에도 대본 ID, 회차, rubric 버전이 지시한 값과 맞는지 검증한다. 하나라도 어긋나면 예외를 던진다. 엉뚱한 대본을 채점한 결과가 조용히 저장되는 것을 막는 장치다.

2. "OAuth로 연결된 건가"에 답하려다 질문이 틀렸다는 걸 알았다

처음에는 "OAuth가 아니라 CLI를 띄우는 것"이라고 답했다. 그런데 이건 부정확했다.

정확히는 앱에 인증 계층이 없다. 로컬에 설치된 CLI가 이미 로그인해 둔 자격증명을 그대로 상속할 뿐이다. 구독 로그인으로 CLI를 쓰고 있다면 그 자격증명은 십중팔구 OAuth다. 앱이 OAuth를 안 하는 것이지, OAuth가 아닌 게 아니다.

이 구분이 중요한 이유는 서버로 옮길 때 드러난다. 서버에는 브라우저로 로그인할 사람이 없다.

3. 서버에 올릴 수 있나: Workers는 원천 불가, Vercel은 네 군데서 막힌다

Cloudflare Workers는 아예 안 된다. V8 아이솔레이트라 프로세스라는 개념이 없다. nodejs_compat을 켜도 child_process는 제공되지 않고 fs도 없다. 설정으로 우회할 수 있는 종류의 제약이 아니라 런타임 모델과 실행 방식이 서로 배타적인 경우다.

Vercel은 spawn 자체는 된다. Node.js 런타임이 전체 Node API를 주고 패키지도 5GB까지 되니 CLI를 번들에 넣는 것까지는 가능하다. 막히는 건 그다음이다.

  1. 실행 시간. 세션 타임아웃이 15 * 60 * 1000(900초)인데 Vercel 최대치는 Hobby 300초, Pro와 Enterprise가 800초다. 회차 한 건도 한도를 넘는다.
  2. 파일시스템. 결과를 data/scores/llm/round-XX/에 쓰는데 함수 파일시스템은 /tmp 말고는 읽기 전용이다. /tmp에 쓴들 인스턴스가 사라지면 같이 사라진다.
  3. 작업 상태. 진행 상태를 globalThis Map에 두고 있다. 단일 장기 프로세스를 전제한 구조라 인스턴스가 갈리면 진행 스트림이 자기 작업을 못 찾는다.
  4. 비공개 코퍼스. gitignore된 대본이 번들에 없어 표본 수가 줄어든다.

일반 서버(VPS, 컨테이너)는 된다. 장기 실행 프로세스와 쓰기 가능한 디스크가 있으면 네 제약이 전부 사라진다. 인증도 경로가 있다. CLI에 setup-token이라는 하위 명령이 있고 설명이 "Set up a long-lived authentication token (requires Claude subscription)"이다. 구독 기반 헤드리스 인증을 하라고 만들어 둔 것이다.

여기까지 오면 "기술적으로 되니까 열자"가 된다. 그래서 약관을 읽었다.

4. 약관 원문이 이 구조를 명시적으로 금지하고 있었다

code.claude.com/docs/en/legal-and-compliance의 Authentication and credential use 절이다.

OAuth authentication is intended exclusively for purchasers of Claude Free, Pro, Max, Team, and Enterprise subscription plans and is designed to support ordinary use of Claude Code and other native Anthropic applications.

Developers building products or services that interact with Claude's capabilities (...) should use API key authentication. Anthropic does not permit third-party developers to offer Claude.ai login into their own applications, or to route requests through Free, Pro, or Max plan credentials on behalf of their users.

마지막 절이 정확히 그 구조다. 열린 도메인에 올려 방문자가 채점을 돌리면 내 구독 자격증명으로 그 사람들의 요청을 대신 태우는 것이다. 명시적 금지 대상이다.

같은 문서의 Acceptable use 절도 짚어 둔다.

Advertised usage limits for Pro and Max plans assume ordinary, individual usage of Claude Code and the Agent SDK.

그리고 소비자 약관 제2조는 계정 로그인 정보나 자격증명을 타인과 공유하거나 계정을 타인에게 제공하는 것을 금지한다. 제3조는 봇이나 스크립트를 통한 자동화 접근을 금지하되 API 키를 통하거나 명시적으로 허용된 경우를 예외로 둔다. CLI는 이 예외에 해당하므로 로컬 자동화 자체는 문제가 없다.

시나리오별로 정리하면 이렇게 갈린다.

상황 판단 근거
로컬에서 혼자 문제 없음 ordinary, individual usage
서버에 올리되 본인만 사용 아마 괜찮음 다른 사용자가 없으니 on behalf of their users에 해당 안 됨
팀원 몇 명이 하나의 구독 공유 금지 route requests through Max plan credentials on behalf of their users
열린 도메인, 아무나 금지 위와 동일, 더 명백
API 키 + 상용 약관 정규 경로 Developers should use API key authentication

조사 전에는 팀 공유를 회색지대로 봤는데 아니었다. 문서가 대안까지 못박아 뒀다.

Each end user must authenticate with their own Anthropic API key, Claude subscription plan credentials, or 3P inference provider credential (...). That usage is billed directly to the end user under their own agreement with Anthropic.

5. "결과를 작가에게 안 보여준다"면 어떤가

여기서 시나리오를 좁혔다. 작가는 대본만 올리고 채점 결과는 보지 않는다. 우리가 내부 판단용으로만 쓴다면?

명시적 금지선은 넘지 않는다. 금지 대상은 "on behalf of their users"인데, 이 경우 Claude 사용의 주체와 수혜자가 모두 운영자다. 작가를 위해 돌리는 게 아니다.

그래서 여기서부터는 해석이 된다. 남는 문제가 셋이었다.

  1. 작가의 저작물 이용
  2. 데이터 취급 조건
  3. ordinary, individual usage

6. 동의는 셋 중 하나만 해결한다

"작가 동의를 받으면 되나"가 다음 질문이었다. 답은 부분적이다.

문제 동의로 해결되나
작가의 저작물 이용 완전히 정확히 동의가 하는 일
데이터 취급 조건 조건부 정확히 고지하면 가능하나 고지문이 험해진다
ordinary, individual usage 전혀 작가가 Anthropic을 대신해 동의할 수 없다

셋째가 남는 게 핵심이다. 이건 나와 작가 사이의 문제가 아니라 나와 모델 제공자 사이의 문제라, 작가가 백 명 동의해도 그대로 남는다. 투고 유입이 사용량을 결정하는 구조는 개인 사용에서 멀어진다.

정도 차이는 실재한다. 투고를 모아 뒀다가 내가 원할 때 배치로 돌리면 본인 작업에 가깝고, 업로드가 곧바로 채점을 자동 트리거하면 사실상 서비스다.

7. 구독은 설정이고 API는 계약이다

둘째 문제를 파다가 이 조사에서 가장 쓸모 있는 구분에 도달했다.

상용 약관(2025-06-17 시행) Section B. Customer Content:

"Anthropic may not train models on Customer Content from Services."

끌 수 있는 스위치가 아니라 상대가 못 하게 돼 있는 조항이다. 확인한 범위에서 이 문장에는 별도 예외가 붙어 있지 않았다.

소비자(Free, Pro, Max)는 다르다. 학습 사용이 기본값이 아니라 선택이다.

설정 보관 학습 사용
학습 허용 켬 최대 5년(학습 파이프라인, 비식별화) 사용됨
학습 허용 끔 30일 사용 안 됨

두 가지를 덧붙인다. 공지문에 "coding sessions"가 명시적으로 포함되므로 채팅만이 아니라 CLI 세션도 대상이다. 그리고 설정과 무관한 예외가 하나 있다. 대화가 안전 검토로 플래그되면 설정을 꺼 뒀어도 검토 대상이 될 수 있다.

여기서 처음 세운 가설이 틀렸다는 걸 알았다. "구독은 학습에 쓰이니까 API로 가라"가 아니다. 학습 설정을 꺼 두면 실질 결과는 API와 비슷해 보인다. 둘 다 학습에 안 쓰이고 보관도 30일이다.

진짜 차이는 약속의 근거다.

  • 구독에서 "학습 안 씀"은 내가 언제든 바꿀 수 있는 내 계정 설정이다. 실수로 켤 수도 있고, 계정을 옮기면서 달라질 수도 있고, 작가는 그게 켜졌는지 확인할 방법이 없다.
  • API에서 "학습 안 씀"은 상대가 계약으로 금지당한 상태다. 내가 바꿀 수 있는 게 아니다.

남에게 약속을 하려면 내가 뒤집을 수 없는 근거가 필요하다. 자기 설정을 근거로 남에게 보증하는 것은 성립하지 않는다.

그래서 결론이 바뀌었다. API 전환은 약관 리스크 회피가 아니라 투고를 받을 수 있게 만드는 조건이다. 상용 약관으로 가면 고지문이 "학습에 사용되지 않습니다" 한 줄로 끝난다. 이건 작가가 동의할 만한 문장이다.

8. 그럼 로컬 LLM을 돌려야 하나: 아니다

다음 질문은 "나중에 대본 수가 많아지면 비용 때문에 로컬 LLM을 쓰려 했는데"였다. 비용 걱정은 정당하지만 이 경우엔 로컬이 적극적으로 잘못된 선택이다.

첫째, 이미 닫힌 구멍을 막는 일이다. 문제는 "작가 대본이 학습에 쓰일 수 있다"였고 상용 약관이 그걸 계약으로 금지한다.

둘째, 캘리브레이션이 인계되지 않는다. 이 실험의 산출물은 프로덕션 채점에 그대로 들어갈 rubric이다. 프로젝트 문서는 같은 계열 안에서의 모델 차이조차 확인 라운드를 권하고 있다. 로컬 모델로 눈금을 만들고 프로덕션에서 다른 모델로 재면 다른 자로 잰 눈금을 다른 자에 붙이는 것이다.

셋째, 실험이 자기 질문에 답할 능력을 잃는다. 이게 결정적이다. 종료 기준이 순위 상관 0.7 이상 그리고 반복 표준편차 7점 이하인데, 표준편차가 크면 "rubric 문구가 모호하다"는 신호로 읽는 것이 이 실험의 전제다. 모델을 약한 것으로 바꾸면 표준편차는 rubric과 무관하게 커진다. 그러면 표준편차가 커졌을 때 rubric 탓인지 모델 탓인지 구분할 수 없다.

게다가 지금 라운드는 강한 모델로도 표준편차 판정에 실패한 상태다(최대 7.9). 가장 좋은 도구로도 못 넘은 선인데 여기서 내려가면 실패 원인을 영영 못 가린다.

비용 지렛대는 로컬 전에 세 개 있다.

순서 지렛대 품질 손실
1 프롬프트 캐싱(프로토콜과 rubric 7,325자가 모든 회차에서 동일한 접두부) 없음
2 배치 API(실시간일 이유가 전혀 없다) 없음
3 더 싼 모델 측정 필요
4 로컬 하드웨어, 운영, 그리고 재캘리브레이션

1번과 2번은 공짜다. 이걸 안 쓰고 로컬을 검토하는 건 순서가 뒤집힌 것이다. 물량이 커지면 싼 모델로 전량 1차 선별하고 통과분만 강한 모델로 정밀 채점하는 2단 구조가 정석이다.

로컬이 답이 되는 경우는 하나뿐이다. 작가가 외부 전송 자체를 거부할 때. 그때는 최적화가 아니라 하드 제약이고, 캘리브레이션이 프로덕션으로 인계되지 않는다는 것을 받아들여야 한다.

9. 모델을 바꾸려면 먼저 기록부터 해야 했다

"모델 버전도 같아야 하나"라는 질문에서 코드의 구멍이 드러났다.

실행 시 모델을 고를 수 있다.

export const RUN_MODELS = ["opus", "sonnet", "haiku"] as const;

그런데 저장되는 채점 JSON 스키마에는 model 필드가 없었다. rubricVersionprotocolVersion은 기록하면서 모델만 빠져 있었다. 즉 기존 채점 결과 어디에도 어떤 모델이 냈는지 안 적힌다. 기본값이 있으니 아마 전부 같은 모델이겠지만, "아마"다.

더 나쁜 건 저 값들이 전부 별칭이라는 점이다. 별칭은 시점에 따라 다른 스냅샷으로 해석된다. 설정을 하나도 안 바꿔도 몇 달 뒤 같은 별칭으로 돌린 라운드는 다른 모델의 결과일 수 있다. rubric을 안 고쳤는데 점수가 움직이고 원인을 못 찾는다.

CLI 응답을 실측해 보니 해석 결과가 그대로 들어 있었다.

"modelUsage": {
  "claude-haiku-4-5-20251001": {
    "canonicalModel": "claude-haiku-4-5",
    "provider": "firstParty"
  }
}

날짜까지 박힌 스냅샷 ID다. 그래서 이걸 각인하도록 고쳤다.

  • 세션이 자기 입으로 말한 모델명은 믿지 않고 CLI 응답의 modelUsage 키에서 뽑는다.
  • 스키마의 model은 optional로 둔다. 기존 채점분은 기록 도입 이전이라 값이 없다. 필수로 만들면 기존 파일이 전부 스키마 오류로 뜬다. 값 없음은 "모델이 없음"이 아니라 "기록되지 않음"이다.
  • 라운드 안에서 모델이 섞이면 rubric 버전 혼재와 같은 무게로 경고한다. 표준편차가 rubric 때문인지 모델 때문인지 못 가리기 때문이다.
  • 라운드 목록, 상세, 채점 파일, 리포트 블록에 모두 노출한다. 미기록 라운드는 그 사실을 적는다.

기존 데이터로 확인한 결과다.

models   : []
warnings : - 모델 미기록 25건. 모델 기록 도입 이전 채점분이라 회차 간 모델 비교에 쓸 수 없다

리포트에 삽입되는 라운드 블록 헤더에도 rubric 버전, 프로토콜 버전과 나란히 "모델 미기록"이 찍힌다.

작업 중 화면에서 잡은 실수도 하나 있다. 표 헤더에 열을 추가하는 치환이 조용히 실패해서 본문은 6칸인데 헤더가 5칸이었다. 앞선 작업에서 포매터가 들여쓰기를 바꿔 놓은 탓이다. 문자열 치환은 실패해도 예외를 던지지 않는다. 화면을 안 봤으면 그대로 커밋됐을 것이다.

10. 대본 채점 전용 모델은 없다

혹시 이 용도의 전용 모델이 있는지도 찾아봤다. 없다. 있는 것은 범용 모델 위에 올리는 평가 프레임워크들이다.

대상 구조
DramaBench 드라마 대본 이어쓰기 6개 축
CML-Bench 영화 시나리오 생성 3개 차원, 9개 지표

둘 다 지금 만들고 있는 것과 같은 모양이다. 축을 나눈 rubric, 범용 모델, 사람 판단과의 대조. 다운로드해서 쓸 수 있는 "대본 채점 모델" 같은 것은 없고 한국어 희곡은 더더욱 없다. 나쁜 소식이 아니라 접근법이 표준이라는 확인이다.

CHI 2023에 연극과 영화 업계 전문가 15인이 참여하고 생성된 연극 대본을 실제로 무대에 올려 평가한 연구가 있다(DeepMind의 Dramatron). 초록에서 확인되는 한계는 장거리 의미 일관성 부족이고 표절과 편향에 대한 윤리적 논의도 담겨 있다.

여기서 한 번 더 정정할 일이 있었다. 검색 결과 요약을 근거로 "인물 심리 깊이, 주제 일관성, 극적 긴장 구축"을 한계로 인용했는데, 논문 초록에서 확인한 문구가 아니었다. 검색 엔진이 만들어 준 요약과 원문 인용을 같은 무게로 다루면 안 된다. 원문을 열 수 있으면 열어야 한다. 참고로 학회 사이트는 403으로 막혔고 arXiv 판이 열렸다.

11. 남은 결정

조사 결과 방향은 정해졌다.

  1. 프로덕션 모델을 먼저 정한다. 캘리브레이션 모델은 프로덕션 모델과 같아야 하므로 이게 실험을 제약하지, 실험이 이걸 정하는 게 아니다. 비용 상한이 있다면 지금 정해야 한다.
  2. 그 모델로 캘리브레이션한다. 정하기 전이라면 가장 강한 모델로 한다.
  3. 사람 채점을 채운다. 순위 상관 판정이 계속 유보 상태다.
  4. 통과한 뒤에 더 싼 모델을 시험한다. 그때는 "이 모델도 표준편차 기준을 지키는가"라는 깨끗한 질문이 되고, 답이 예면 절감이 그냥 생긴다.

투고를 실제로 받을 거라면 API 전환이 전제다. 실행 파일 하나가 바뀌는 일이고, 오염 차단 장치 대부분은 API에서 불필요해져서 오히려 단순해진다. 대신 인증 계층과 동의 절차가 새로 필요하다.

마지막으로 코드에서 발견한 것 하나를 적어 둔다. 배포 환경에서 쓰기와 실행을 막는 가드가 환경변수 하나만 보고 있었다.

export function isReadOnlyDeployment(): boolean {
  return process.env.VERCEL !== undefined;
}

"공개 배포는 한 곳뿐"이라는 전제가 코드에 박혀 있다는 뜻이다. 다른 곳에 올리는 순간 그 전제가 조용히 깨지면서 가드가 사라진다. 실패했을 때 잠기는 게 아니라 열리는 방향이다. 서버로 옮긴다면 이 함수부터 "명시적 허용이 없으면 잠금"으로 뒤집어야 한다.