지난 글은 합성 프로필과 공고 세 개를 무료 모델에 넣어 본 기록이었다. 끝에 “다음에 손댈 것” 셋을 적었다. 본문에 없는 고용주 이름을 모델이 만들어 낸 것, 0.65라는 점수가 어느 척도인지 알 수 없었던 것, 추론 토큰이 답보다 훨씬 커진 것. 이번 글은 그 셋을 고친 PR #19의 기록이다. 새 기능은 없다.
구현은 Claude Code가, 리뷰는 Codex가 맡았다. 실제 모델은 호출하지 않았고, 아래 검증은 모두 합성 입력이다.
없는 값을 없다고 말할 자리
공고 본문에 고용주가 없었는데 추출 결과에는 실제로 존재하는 다른 기업의 이름이 들어 있었다. 지난 글에서는 모델이 만들었고 스키마가 막지 못했다고만 적었다. 이번에는 고용주를 알 수 없을 때 값을 비워 둘 수 있도록 계약을 바꿨다.
추출 초안의 고용주 항목은 이제 null을 받는다. 프롬프트에는 “본문이 고용주를 밝히지 않으면 null로 두고, 제품이나 도메인이나 비슷한 공고에서 추측하지 말라”는 문장을 넣었다. 앱 안의 공고와 지원 기록에서도 고용주는 있을 수도 없을 수도 있는 값이 됐고, 화면과 도구 응답은 “고용주 미기재”로 표시한다. 저장할 때 빈 자리를 채우지 않는다.
이것으로 모델이 이름을 안 만든다고 말할 수는 없다. 만들지 않아도 되는 자리가 생겼을 뿐이고, 실제로 null을 돌려주는지는 다음 라운드에서 같은 공고로 봐야 한다.
점수의 단위, 그리고 읽기와 만들기의 차이
점수 0.65는 100점 만점의 낮은 점수인지 0에서 1 사이의 비율인지 결과만으로 알 수 없었다. 프롬프트에 “0에서 100 사이의 정수, 100이 가장 강한 근거 기반 적합, 추측 대신 null, 0에서 1 사이 분수 금지”를 적고, 모델에게 보내는 구조화 출력 스키마도 정수로 바꿨다.
여기서 리뷰가 하나를 잡았다. 첫 커밋에서는 앱이 저장된 판정을 읽을 때 쓰는 공유 스키마에도 정수 제약을 넣었다. 그러자 이전 버전이 0.65로 저장해 둔 판정을 새 버전이 읽지 못했다. 그 기록으로 지원 상태를 저장하려던 요청이 검증 오류로 끝났다.
옛 값의 척도는 모른다. 100을 곱하거나 반올림해서 정수로 만드는 것은 모르는 뜻을 아는 척하는 일이다. 그래서 계약을 둘로 나눴다. 저장된 것을 읽는 계약은 예전처럼 0에서 100 사이의 실수를 받아 옛 값을 그대로 둔다. 새로 만드는 계약만 정수를 요구하고, 응답을 앱의 자료형으로 바꾸는 단계에서 전송 방식과 무관하게 다시 검사한다.
읽는 계약과 만드는 계약은 같은 것을 가리키는 것 같지만 시간이 다르다. 하나는 과거를 향하고 하나는 미래를 향한다. 새 규칙을 과거에 소급하면 이전 계약에서는 유효했던 기록을 새 버전이 읽지 못하게 된다.
필수를 선택으로 바꿔도 호환이 아니다
두 번째 지적은 위젯이었다. 고용주 항목을 필수에서 선택으로 바꾸면서 나는 느슨해진 변경이니 기존 위젯의 엄격한 파서와도 호환된다는 취지로 PR에 적었다. 반대였다. 기존 위젯은 받은 결과를 엄격하게 파싱해서, 필수라고 알고 있는 항목이 없으면 결과 전체를 버린다. 고용주가 없는 새 결과를 옛 위젯이 받으면 대기 문구인 “Waiting for Career Radar…”에서 멈춘다. 리뷰어가 옛 파서로 확인한 것이고, 실제 ChatGPT 호스트에서 재현한 것은 아니다.
M4-B2에서 필드를 추가하며 위젯 주소를 v5로 올린 이유와 같다. 그때는 새 필드를 몰라서 깨졌고, 이번에는 있던 필드가 없어서 깨진다. 엄격한 파서 앞에서는 두 방향 모두 호환 변경이 아니다. 위젯 주소를 v6으로 올리고, ChatGPT에서 도구 정보를 새로고침하라는 안내를 고쳤다.
추론 강도는 조절, 상한은 아님
세 번째는 OpenRouter 경로의 추론 토큰이다. 지난 실행에서 한 호출의 추론 토큰이 제공자가 보고한 값으로 9만 개를 넘겼다. OpenRouter에는 추론 강도를 요청에 실어 보내는 항목이 있어, 환경 설정으로 그 값을 켤 수 있게 했다. 기본은 미설정이라 추론 강도 파라미터를 보내지 않고, 요청은 그대로다.
여기도 리뷰가 문구를 바로잡았다. 나는 이것을 “추론 토큰을 제한한다”고 썼는데, 이 항목은 강도를 낮춰 달라는 힌트이지 고정된 상한이 아니다. 실제로 토큰이 줄어드는지는 확인하지 않았다. 문서와 코드 주석은 “강도 조절, 감소 여부 미검증”으로, 설정 예시는 “강도 힌트, 토큰 상한 아님”으로 고쳤다.
확인한 것과 남은 것
타입 검사·린트·빌드와 테스트 318개, 정책 평가 28개가 통과했다. 새 테스트는 초안 스키마에 null 고용주와 정수 점수가 실리는지, 0.65가 새 생성에서는 거부되고 저장된 기록에서는 읽히는지, 고용주 없는 공고의 지원 기록이 이름을 채우지 않고 저장되는지, 추론 강도 값이 설정됐을 때만 요청에 실리는지를 본다.
실제 모델이 고용주를 null로 답하는지, 점수를 정수로 주는지, 추론 강도 힌트가 토큰을 줄이는지는 이 PR이 말하지 않는다. 다음 사용 확인 라운드에서 같은 세 공고로 본다.
오늘은 만든 도구로 합성 프로필과 공개 공고를 실제 모델에 돌려 보고, 거기서 드러난 문제를 고쳤다. 세 수정 자체는 작았다. 더 오래 남을 것은 리뷰가 잡은 두 가지다. 스키마를 조일 때는 읽는 쪽과 만드는 쪽을 따로 생각해야 하고, 엄격한 파서 앞에서는 느슨해지는 변경도 호환이 아니다.