지난 글에서는 실제 모델을 재기 전에 언제 호출하면 안 되는지부터 코드로 정한 측정 도구를 만들었다. 그 앞의 M4-B1에서는 모델이 경력과 공고를 비교해 설명할 때 근거를 어떤 형식으로 받고 무엇을 검사할지를 정했다. 형식은 있는데 그 형식을 채우는 쪽이 아직 없었다.
이번 M4-B2는 그 자리를 채운다. 기존의 REALISTIC / STRETCH / PASS 판정과 별개로, 이 공고에서 무엇을 조심해야 하는지를 모델에게 묻고, 답을 화면까지 내보낸다.
판정과 별개로 “조심할 점”을 물으면, 모델은 무엇을 근거로 답할까? 근거가 없을 때는 무엇을 답해야 할까?
PR #16은 이 두 질문에 대한 구현이다. 구현은 Claude Code, 리뷰는 Codex가 맡았고, 아래 실행 결과는 모두 합성 데이터와 가짜·모킹한 판정기로 확인했다. 실제 모델은 아직 한 번도 호출하지 않았다.
판정 호출 한 번에 같이 묻는다
새로 묻는 것은 네 가지다. 후보자가 해 온 일의 범위가 공고의 범위와 맞는지(seniorityFit), 경력의 흐름에 이 역할을 위해 설명이 더 필요한 부분이 있는지(careerStoryRisk, 필요하면 확인 질문 하나), 서류 단계에서 걸릴 만한 개별 위험(screeningRisks), 그리고 판단하기에 모자란 사실(unknowns)이다.
이 네 가지를 별도 호출로 만들지 않고 기존 판정 호출의 구조화 출력에 넣었다. 추천 배치는 후보 하나에 추출과 판정, 최대 두 번의 호출을 쓴다. 맥락을 세 번째 호출로 붙이면 지난 글에서 만든 측정 도구의 계획도, 90초 예산에 대한 질문도 처음부터 다시 세워야 한다. 같은 호출에서 출력 항목만 늘리면 호출 수는 그대로다. 프롬프트 버전은 milestone-1-v1에서 milestone-4b2-v1로 올렸다.
판단마다 근거는 B1에서 정한 형식으로 받는다. 어느 쪽 입력인지, 그 안의 어느 위치인지, 그 위치의 문장이 무엇인지다. 예를 들어 후보자의 첫 경력에 적힌 첫 근거 문장은 roles[0].evidence[0], 공고의 첫 필수 요건은 required[0].text처럼 가리킨다.
모델이 지켜야 할 규칙을 프롬프트에 적었다
프롬프트에는 위치 문법을 그대로 적었다. 인덱스는 넘겨준 JSON에서 0부터 세고, 인용문은 그 위치의 문장을 그대로 복사한다.
규칙 몇 가지는 M4 계획을 세울 때부터 정해 둔 것이다. 범위 비교는 후보자의 담당 업무나 근거 문장, 리더십 항목 중 하나와 공고의 요건이나 담당 업무 중 하나를 함께 인용해야 하고, 그럴 수 없으면 uncertain이라고 답한다. 연차나 직함, 한 줄 소개만으로는 “경력이 과하다”거나 “부족하다”고 결론 내리지 않는다. 리더에서 개인 기여자로 옮기는 것도, 직군을 바꾸는 것도 자동으로 위험이 아니다. 의도를 모르면 동기를 지어내는 대신 확인 질문을 하나 묻는다. 나이, 성별, 국적, 공백기, 학교 같은 대리 지표는 근거로 쓰지 않는다. 인용할 수 없는 위험은 적지 않고, 면접 위험 목록과 같은 내용을 반복하지 않는다.
여기까지는 부탁이다. 프롬프트에 규칙을 적었다는 것과 모델이 그 규칙을 지킨다는 것은 다른 문제라서, 받은 뒤에 다시 검사하는 층을 두었다.
받은 뒤에 다시 검사한다
판정 결과는 두 단계를 지나 저장된다. 먼저 M1부터 있던 정책이 적합도 쪽을 처리한다. 인용문이 프로필에 없는 긍정 근거를 지우고, 핵심 요건의 결격을 차단 사유로 올리는 일이다. 그다음 B1의 검증기가 새 맥락의 근거를 위치별로 확인한다. 이때 비교 대상은 저장소에서 다시 읽은 프로필이 아니라 모델에 넘겼던 바로 그 프로필과 공고다.
인용한 위치가 틀렸거나 문장이 다르면 판단을 uncertain으로 낮추고 확신도 낮춘다. 그 판단에 붙어 있던 설명 문장은 중립적인 안내로 바꾼다. 화면에 “모름”이라고 표시하면서 바로 아래에 단정적인 설명을 남겨 두면 사용자는 어느 쪽을 믿어야 할지 알 수 없기 때문이다. 개별 위험은 목록에서 빼고, 왜 뺐는지만 unknowns에 남긴다.
다만 인용한 문장이 입력에 있다는 것과, 그 문장이 해당 판단을 뒷받침한다는 것은 별개다. 후자는 이 검증기만으로 확인하지 못한다.
모델이 계약 밖의 출력을 내놓을 때도 있다. 위험을 아홉 개 적거나, 모르는 것을 서른세 개 적거나, 설명이 상한을 넘는 경우다. 이때는 맥락만 버리고 적합도 판정은 그대로 살린다. 판정 결과에는 “맥락을 버렸다”는 고정 문구 하나만 남는다. 맥락 때문에 판정 자체가 실패하는 일은 없어야 했다.
이 두 단계를 함수 하나로 묶었고, 단일 공고 판정과 추천 배치, 지난 글의 측정 도구가 같은 함수를 쓴다. 측정 도구는 저장된 결과가 “모델 초안에 파이프라인을 적용한 것”과 같은지 다시 계산해 확인하는데, 파이프라인이 하나여야 그 비교가 의미를 가진다.
평가하지 않은 것과 위험이 낮은 것은 다르다
판정 결과 스키마에 선택 필드 하나를 더했다. 맥락이 없으면 평가하지 않은 것이지, 위험이 낮다는 뜻이 아니다. 예전 판정 기록은 그대로 읽히고, 현재 프로필로 맥락을 소급해 만들지도 않는다. 알 수 없는 버전이나 추가 키는 여전히 거부한다.
이 선택 필드 하나 때문에 위젯 주소를 v4에서 v5로 올렸다. 기존 위젯은 받은 결과를 엄격하게 파싱해서, 모르는 필드가 하나만 있어도 결과 전체를 버린다. 같은 주소로 두면 ChatGPT가 이전 위젯을 캐시한 채 새 결과를 받아 아무것도 그리지 못하는 상황이 생긴다.
위젯에는 “Screening context”라는 별도 구역을 두었다. 역할 범위와 경력 서사는 값과 확신을 붙인 표시로 보여주고, uncertain은 다른 색으로 구분한다. 확인 질문이 있으면 그대로 보여준다. 인용한 근거와 모르는 것의 목록은 접힌 영역에 두었다. 구역 끝에는 “이 맥락은 판정이나 순위를 바꾸지 않으며, 모름은 근거가 없거나 확인되지 않았다는 뜻이지 위험이 낮다는 뜻이 아니다”라는 문장을 고정으로 붙였다. 면접 위험은 예전처럼 따로 나열한다.
빌드한 위젯을 로컬 페이지에서 합성 결과로 띄워 봤다. 근거가 모두 확인된 경우, 근거가 없어 uncertain으로 낮아진 경우, 맥락이 없는 경우, 그리고 추천 목록에 두 종류가 섞인 경우 네 가지가 각각 의도대로 그려졌다. 실제 ChatGPT 호스트에서 확인한 것은 아니다.
아직 확인하지 않은 것
테스트는 279개(shared 7 + server 272)이고, 정책 평가 28개와 타입 검사·린트·빌드를 통과했다. 새 테스트는 모킹한 SDK가 돌려준 초안이 같은 호출에서 버전 있는 맥락으로 바뀌는지, 계약을 벗어난 맥락이 판정을 깨뜨리지 않고 버려지는지, 실제 MCP 도구 호출에서 근거 없는 “경력 과다” 주장이 uncertain으로 낮아지고 그 원문이 응답에 남지 않는지를 본다.
이 숫자가 말해 주지 않는 것이 있다. 실제 모델이 위치를 정확히 인용하는지, 근거가 없을 때 정말 판단을 유보하는지, 대리 지표를 피하는지다. 그것을 재는 모델 모드 평가는 평가 실행기에 아직 구현하지 않았고, 실행에는 승인된 호출이 든다. 지난 글에서 만든 측정 도구가 이제 이 파이프라인으로 저장 결과를 다시 계산하니, 그 실행이 오면 판정 무결성까지 같이 확인될 것이다.
이번 작업에서 가장 오래 걸린 부분은 모델에게 더 많이 묻는 쪽이 아니라, 모델이 답하지 못한 것을 답하지 못했다고 남기는 쪽이었다. uncertain이라는 값 하나를 화면까지 정직하게 옮기려면 프롬프트 규칙, 위치 검증, 설명 문장 교체, 스키마의 선택 필드, 위젯의 색과 문구가 전부 같은 방향을 봐야 했다.