지난 글에서는 Career Radar에 지원 결과를 기록하면서, 새로운 소식을 추가하는 일과 이전 기록을 정정하는 일을 나눴다. 개인의 맥락을 기억하려면 무엇을 남길지뿐 아니라 언제 바꿔도 되는지도 정해야 했다.
이번에는 그보다 앞선 판단으로 돌아왔다. 이 공고가 경력과 맞는다고 설명할 때, 그 설명은 무엇을 근거로 삼아야 할까?
인용한 문장이 실제로 있다는 것과, 그 문장이 판단을 뒷받침한다는 것은 같은가?
PR #14에 담은 M4-B1 작업은 이 둘을 나누는 데서 시작했다. 화면에 새로운 판단을 보여주기 전에, 근거를 어떤 형식으로 받고 무엇을 검사할지부터 정했다.
이 글은 코드 수정 0f4bf07과 후속 문서 수정 27ab912을 검토하고, PR이 병합된 뒤 남기는 기록이다. 아래 실행 결과는 실제 이력서가 아닌 합성 데이터로 확인했다.
경력 연수만으로는 설명하기 어려운 것
앞으로 붙이려는 설명은 기존의 REALISTIC / STRETCH / PASS 판정과 조금 다르다. 후보자가 해 온 일의 범위와 공고가 요구하는 범위가 맞는지, 경력의 전환 의도에 추가 설명이 필요한지 같은 맥락이다.
오래 일한 사람이 경력 요건이 낮은 공고를 봤다고 무조건 과한 경력인 것은 아니다. 리드 경험이 있는 사람이 개인 기여자 역할에 지원한다고 경력 방향이 이상한 것도 아니다. 실제로 맡았던 일과 지원하려는 역할, 본인이 설명한 의도를 봐야 한다.
그렇다면 “경력이 많으니 이 역할에는 맞지 않는다”는 문장을 생성하는 것보다 먼저 할 일이 있다. 어떤 경력 문장과 어떤 공고 문장을 비교했는지 남기고, 둘 중 하나라도 확인되지 않으면 단정하지 않는 것이다.
이번에는 그 규칙을 코드로 옮겼다. 기존 판정이나 추천 순서를 바꾸지는 않았다.
같은 문장이어도, 어디에 있었는지가 중요하다
근거에는 세 가지를 받도록 했다. 후보자와 공고 중 어느 쪽에서 왔는지, 구조화된 입력의 어느 위치인지, 그곳에 적힌 문장은 무엇인지다.
예를 들면 후보자의 첫 번째 경력에 있는 첫 근거 문장을 가리키는 roles[0].evidence[0]처럼 위치를 지정한다. 공고의 필수 요건은 required[0].text, 우대 요건은 preferred[0].text로 구분한다.
검증은 입력 전체에서 비슷한 문장을 찾는 방식이 아니다. 먼저 허용된 위치를 해석하고, 그곳의 문자열과 인용문을 비교한다. 우대 요건의 문장을 가져와 필수 요건 위치라고 적으면 통과하지 않는다. 다른 경력 항목에 같은 내용이 있더라도 지정한 위치가 틀리면 인정하지 않는다.
이전 판정 정책에서 쓰던 정규화는 재사용했다. 공백이나 대소문자, 감싸는 따옴표와 끝의 구두점 차이는 허용하되, 부정문에서 기술명만 잘라 오거나 원문 뒤에 성과를 덧붙이는 것은 거부한다.
리뷰에서는 공고 전문인 description을 근거 위치에서 뺐다. 전문과 완전히 일치해야 통과하는 규칙이라, 짧은 설명 하나를 뒷받침하려고 공고 전체를 인용하게 될 수 있었기 때문이다. 대신 구조화된 요건이나 담당 업무 항목을 가리키도록 하고, 인용문 상한도 16,000자에서 2,000자로 줄였다.
이렇게 해도 인용 가능한 내용이 항상 생기는 것은 아니다. 구조화된 항목에 필요한 근거가 없다면, 없는 근거를 채우는 대신 판단을 유보해야 한다.
확인에 실패하면, 설명도 함께 낮춘다
경력 범위를 비교하는 판단과 경력 맥락에 대한 판단에는 후보자와 공고 양쪽의 근거를 요구한다. 인용문 중 하나라도 잘못됐거나 필요한 근거가 부족하면 uncertain으로 바꾸고 확신 수준도 낮춘다.
여기서 값만 바꾸면 부족하다. 화면에는 “불확실”이라고 표시하면서 바로 아래에 단정적인 설명이 남아 있으면 사용자는 무엇을 믿어야 할까?
그래서 검증에 실패한 판단의 설명과 질문도 중립적인 안내로 바꾸도록 했다. 개별 위험 항목은 목록에서 제외하되, 왜 제외했는지는 unknowns에 남긴다. 잘못된 설명을 그대로 옮겨 적지 않고 검증 실패 이유만 기록한다.
합성 테스트에서는 일부러 근거 없는 설명과 질문을 넣었다. 검증 후에 불확실한 상태만 남는지뿐 아니라, 넣어 둔 잘못된 문구가 결과에서 사라지는지도 확인했다.
그런데 이 실패 이유를 남기는 기능에서 다른 문제가 나왔다.
검증기가 자기 설명 때문에 실패했다
처음에는 unknowns의 입력과 출력 상한이 모두 32개였다. 검증기는 여기에 잘못된 근거를 발견한 이유를 추가했다.
리뷰에서 합성 입력으로 확인한 상황은 단순했다.
입력에 있던 unknowns 30개
+ 검증에 실패한 위험 항목의 진단 3개
= 출력 33개 → 상한 초과로 실패
입력은 정해진 개수 안에 있었다. 그런데 검증기가 자기 일을 하고 나서 만든 결과를 자기 스키마로 거부했다. 근거가 틀렸을 때 불확실성을 남기려던 기능이, 정작 오류를 여러 개 찾으면 결과를 반환하지 못한 셈이다.
수정 커밋에서는 생성 측이 공급할 수 있는 개수와 검증 후 결과의 한도를 나눴다. 생성 측 입력은 최대 32개로 제한하고, 출력 스키마는 48개까지 허용했다. 검증기가 추가하는 진단은 두 판단과 위험 항목 여덟 개를 합쳐 최대 10개다.
재검토에서는 가장 큰 조합도 별도로 실행했다. 입력 32개에 두 판단과 위험 항목 여덟 개가 모두 검증에 실패하도록 했더니, 진단을 포함한 42개가 정상 반환됐다. 실패 이유를 잘라 버리지 않고도 결과를 돌려줄 수 있었다.
생성 결과를 검증한다는 말에는, 검증기가 추가하는 정보가 어디에 들어갈지도 포함돼 있었다.
지금의 프로필로 과거 판단을 설명하지 않기
근거의 위치를 정하고 나니 시간에 대한 문제도 남았다. 같은 프로필 ID라도 사용자가 내용을 수정할 수 있다. 모델이 판단하는 사이에 프로필이 바뀔 수도 있다.
저장할 때 최신 프로필을 다시 읽어 해시를 만들면, 실제로 판단에 사용한 입력과 다른 내용에 표시를 붙일 수 있다. 그래서 판정에 넘겼던 프로필 객체를 저장 단계까지 전달하고, 그 구조화된 내용과 공고의 해시를 남기도록 했다.
프로필의 저장 ID는 내용 자체가 아니므로 해시에서 제외했다. 같은 원문에서 추출했더라도 구조화된 내용이 달라지면 다른 해시가 나오도록 했다. 반대로 입력 내용이 같고 저장 ID만 다르면 프로필 해시는 같다.
다만 이것은 과거 입력을 복원하는 기능은 아니다. 판정마다 프로필 사본을 추가하지 않았기 때문에, 해시만으로 옛 프로필을 다시 읽을 수는 없다. 모델이 원문을 올바르게 추출했다는 증명도 아니다. 어떤 구조화된 입력을 사용했는지 구분하는 표시에 가깝다.
이 과정에서 해시용 정렬 함수를 평가 도구와 공유하면서 데이터셋 해시가 바뀌는 부수 효과도 있었다. M4-A에서 나눴던 케이스별 계약 해시는 이번 변경의 영향을 받지 않아 28개 사례는 모두 비교 대상으로 남았다. 데이터셋 변경 표시 하나만 보고 회귀 비교까지 끊겼다고 읽으면 안 된다는 점을 문서에 덧붙였다.
아직 판단이 맞다는 것까지 확인한 것은 아니다
0f4bf07의 임시 사본에서 기존 설치 의존성을 재사용해 테스트 191개(shared 7 + server 184), 정책 평가 28개, 타입 검사·린트·빌드 통과를 확인했다. 이후 27ab912에서는 문서 두 개만 바뀐 것을 확인했고, 테스트를 다시 실행한 것으로 기록하지는 않는다.
새 근거 검증 관련 테스트는 28개, 입력 식별자와 저장 관련 테스트는 6개다. 여기에 최대 진단 조건과 이전 정렬 방식의 평가 계약 비교를 별도 실행으로 확인했다. 이 숫자는 추천 정확도나 실제 모델의 판단 성능이 아니다.
이번 작업에서 만든 검증기는 아직 실제 모델 출력이나 MCP 응답, 위젯에 연결하지 않았다. 현재 저장 흐름에 추가한 것은 입력 식별자이고, 새 판단 설명을 화면에 내보내는 일은 다음 단계다. 외부 모델 API와 실제 ChatGPT 호스트 검증도 진행하지 않았다.
이 경계가 이번 작업에서 가장 중요하게 느껴졌다. 문장이 지정한 위치에 있다는 것까지는 코드로 검사할 수 있다. 하지만 그 문장으로부터 “이 역할에는 경력이 과하다”거나 “이직 의도를 더 설명해야 한다”는 결론이 정말 나오는지는 다른 문제다.
올바른 인용문에도 잘못된 해석이 붙을 수 있다. 그 부분은 실제 모델 출력과 사례별 기대 판단을 함께 놓고 검토해야 한다.
개인 비서처럼 맥락을 오래 기억하는 도구를 생각하면, 기억을 많이 쌓는 것만큼 중요한 태도가 있는 것 같다. 무엇을 보고 말했는지 드러내고, 확인하지 못한 부분은 확인하지 못했다고 남기는 일이다.
아직 작은 토이 프로젝트지만, 프롬프트에 “근거를 들어 설명해 줘”라고 적는 것과 그 약속을 앱의 동작으로 만드는 것 사이에는 할 일이 꽤 있었다.