지난 글까지는 만들고, 써 보고, 고친 기록이었다. 이번 PR #20은 코드가 아니라 계획 문서다. M4를 시작할 때도 한 번 그랬다. 그때는 어떤 앱을 만들지 방향을 다시 묻는 글이었고, 이번에는 방향은 그대로 두고 지금까지 만든 것을 세어 본 뒤 다음에 만들 것을 줄이는 글이다.
초안은 내가 썼고, 저장소와 대조해 다시 짜는 일은 Claude Code가, 리뷰는 Codex가 맡았다. 이 PR에는 런타임 변경이 없다. 계획 문서 한 장과 결정 기록 한 건, README 포인터가 전부다. 아래에 적은 M5 항목은 모두 아직 만들지 않은 것이다.
지금까지 만든 것
Career Radar는 이력서와 채용 공고를 비교해 REALISTIC, STRETCH, PASS 가운데 하나로 판정하는 ChatGPT MCP 앱이다. 마일스톤 넷을 지나며 붙은 것을 한 표에 놓으면 이렇다.
| 마일스톤 | 만든 것 | 병합할 때 확인한 방식 |
|---|---|---|
| M1 | 요건과 근거로 판정하는 결정적 정책, PR 리뷰로 검증 | 합성 입력 |
| M2 | 로컬 저장, 삭제 경로, 루프백 접근 검사 | 합성 입력 |
| M3 | 공고 검색과 추천, 호출 상한 | 합성 입력 |
| M4-A | 평가 보고서의 버전과 해시, 기준선 비교 | 합성 입력 |
| M4-B1 | 인용은 구조화된 프로필과 공고의 지정 위치에 있는 문장만, 없으면 uncertain으로 강등 |
합성 입력 |
| M4-C | 지원 결과를 단계별로 기록, 판정의 정답으로는 쓰지 않음 | 합성 입력 |
| M4-B2 | 서류 검토 맥락, 근거 없으면 모름으로 남김 | 합성 입력 |
| 측정·제공자 | 승인 없이는 호출하지 않는 라이브 측정 하네스, OpenRouter 제공자 어댑터 | 합성 입력 |
| 사용 확인 | 프로필 하나와 공고 세 개를 실제 도구 경로로 통과시키는 실행기, 1차 실행 | 무료 모델로 실제 호출, 판정을 돌려준 모델은 하나 |
| 후속 수정 | 고용주 없음 허용, 점수 정수 척도, 추론 강도 설정 | 합성 입력 |
실제 모델을 부른 것은 사용 확인 1차 한 번이고, 그것도 OpenRouter의 무료 모델뿐이었다. 넷을 같은 입력으로 시도해 판정을 돌려준 것은 하나였다. 그 한 번이 프로필 저장, 공고 추출, 판정까지 실제 도구 경로를 지나갔으니, 표의 판정 정책과 인용 검증과 검토 맥락도 그때 한 모델의 출력을 받아 보기는 했다. 다만 공고 세 개 중 둘이 판정된 한 번의 실행이라, 표에는 병합할 때의 확인 방식만 적었다. OpenAI 경로는 M1 때 크레딧 부족으로 멈춘 뒤 아직 판정을 받아 본 적이 없고, ChatGPT 호스트에서의 확인도 남아 있다. “합성 입력”은 가짜 분석기와 합성 이력서로만 확인했다는 뜻이다. 이 구분은 M4부터 이어 온 규칙이고, M5 계획에서도 synthetic-verified, live-verified, not verified 세 표시 말고는 쓰지 않기로 했다.
표의 첫 줄부터 마지막 줄까지 달력으로는 나흘이다. 수요일 오후에 저장소를 만들었고, 토요일 오후에 후속 수정을 병합했다. 그 사이 병합된 PR 열두 개 가운데 여덟 개가 금요일과 토요일에 몰렸다. 구현과 리뷰는 에이전트 둘이 나눠 맡았고 나는 방향과 리뷰 의견과 병합을 맡았으니, 나흘 내내 혼자 직접 구현하고 검증한 작업량을 뜻하지는 않는다. 그리고 확인 방식이 대부분 합성 입력이었으니, 빨랐다는 말이 검증됐다는 말은 아니다. 남은 것은 속도가 아니라 실제 호출과 호스트에서의 확인이다.
초안을 저장소와 대조했더니
M5의 목표는 이 저장소가 “근거 기반 GenAI 앱을 설계하고 평가하고 운영할 수 있다”는 증거가 되는 것이다. 모델 훈련이나 대규모 프로덕션 RAG는 아니다. 초안에는 여섯 항목이 있었다. 버전 기록, 검색, 인용, 도구 사용, 모델 평가, 관측.
계획을 다시 짜며 처음 한 일은 초안의 각 항목을 저장소 안의 파일과 나란히 놓는 것이었다. 그랬더니 셋은 이미 상당 부분 있었다.
- 버전 기록은 M4-A에 있다. 코드 SHA와 정책·스키마·데이터셋 해시가 평가 보고서에 실리고, 기준선과 비교하는 옵션도 있다. 프롬프트 버전은 코드에 상수로 있다. 없는 것은 커밋된 기준선 보고서와, 기능마다 확인 상태를 한 자리에 적은 페이지다.
- 인용 계약은 M4-B1에 있다. 근거 문장이 구조화된 프로필과 공고의 어디에 있는지 위치까지 검증하고, 없으면 강등한다. 없는 것은 검색으로 가져온 조각을 가리키는 문법과, 인용이 얼마나 유효한지 재는 지표다.
- 관측은 두 제공자 모두에 호출별 텔레메트리가 있다. 지연, 종료 상태, 토큰, 상위 제공자. 없는 것은 추출부터 검증까지 한 실행을 묶는 실행 ID와 단계별 시간이다.
세 항목을 처음부터 다시 만들었다면, OpenRouter를 붙이던 글에서 걱정했던 대로 앱 대신 검증 도구만 계속 만드는 일을 되풀이했을 것이다. 계획서에는 항목마다 “이미 있는 것”과 “M5가 채울 빈틈”을 표로 적었다.
두 항목은 모양을 바꿨다. 초안은 지원 결과 조회를 모델이 부를 수 있는 도구로 두었다. 그런데 같은 초안에 지원 결과가 자동으로 판정의 근거가 되어서는 안 된다고 적혀 있다. 그렇다면 판정 중에 모델이 그것을 읽을 수 있어서는 안 된다. 도구를 열어 두고 쓰지 말라고 부탁하는 것은 가장 약한 방어다. 지원 결과는 M4-C 그대로 별도의 결정적 화면으로 남긴다. 임베딩 검색은 별도의 모델과 제공자 선택, 코퍼스의 추가 전송, 예산, 색인 저장이 따라온다. 검토본의 첫 버전에는 임베딩에 OpenRouter가 제공하지 않는 제공자가 필요하다고 적었는데, 리뷰에서 임베딩 API가 있다는 정정을 받아 근거를 고쳤다. 없어서 미루는 것이 아니라 비용과 전송이 따라오기 때문이다. 먼저 어휘 기반 검색만 만들고, 임베딩 어댑터는 다른 모델 호출과 같은 승인 게이트 뒤에 둔다. 넣을지는 Recall@K 수치를 본 뒤 정한다.
그래서 순서는 이렇게
슬라이스는 PR 하나나 둘 크기로 잘랐고, 순서는 0, A, B, D, E, C, F다. 근거의 품질을 잴 수 있게 된 뒤에 모델에게 자율성을 준다는 순서다.
- M5-0 기준선 동결. 지금 SHA에서 정책 평가 보고서를 커밋하고, 기능마다 확인 상태를 적은 페이지를 만든다. 런타임 변경은 없고, 평가 실행기에서 손보는 것은 비교기 하나다. 지금 비교기는 스키마 해시가 다르면 비교 불가로 끝내는데, 그러면 M5-A가 공유 패키지에 새 스키마를 추가하는 순간 이 기준선을 쓸 수 없게 된다.
- M5-A 근거 코퍼스와 어휘 검색. 프로필과 프로젝트와 글을 조각으로 나눈다. 조각 ID는 내용 해시로 만들고, 인용할 때는 이번 실행에서 검색된 조각인지 별도로 확인한다. BM25를 프로세스 안에서 돌리고, 새 서비스는 붙이지 않는다. 검색 실패는 빈 목록이지 지어낸 조각이 아니다. 완료 기준은 합성 코퍼스에 대한 Recall@K다.
- M5-B 검색 조각 인용. 검색이 판정 경로에 처음 들어가는 슬라이스다. 모델을 부르기 전에 요건 문장으로 검색하고, 가져온 조각을 입력에 붙인다. 인용은 결과 전체가 아니라 주장 하나하나에 붙고, 이번 실행의 검색 흔적 안에 있는 조각만 가리킬 수 있다. 위젯 주소는 v7로 오른다.
- M5-D 모델 모드 평가. 지금 평가는 정책만 돌린다. 미리 써 둔 판정 초안을 주입해 정책 뒤의 결과를 비교하는 방식이라, 실제 모델 경로는 한 번도 평가 대상이 아니었다. 실제 제공자로 추출과 판정을 돌리고, 스키마 실패, 거부, 잘림, 시간 초과를 실행 결과로 기록한다. PASS로 세지 않는다. 승인 게이트와 호출 상한은 하네스의 것을 그대로 쓴다.
- M5-E 실행 ID. 도구 호출마다 실행 ID를 만들어 추출, 검색, 모델, 검증 단계를 잇는다. 저장하는 흔적에는 이력서도 공고도 프롬프트도 검색 질의 원문도 넣지 않는다. 질의는 모델이 쓰는 문장이라 입력의 경력 문장이나 고용주 이름을 그대로 옮겨 올 수 있다. 해시와 길이만 남긴다.
- M5-C 제한된 도구 사용. 마지막이고, OpenAI 경로부터다. 함수 도구는 하나, 근거 검색뿐이다. 도구 호출은 셋까지다. 도구가 실패하거나 도구 호출 예산을 다 쓰면 남은 요청·시간 예산 안에서 지금의 도구 없는 경로로 돌아가고, 남은 예산이 없으면 추가 호출 없이 실패로 끝낸다. 기본은 꺼 둔다. 켤지는 승인된 실행에서 잰 지연 비용이 정한다.
- M5-F 글. 코드가 없는 슬라이스다. 이 시리즈가 이어지는 것이다.
M4의 규칙 둘은 그대로 게이트로 남는다. 판정 결과가 바뀌는 슬라이스인 B와 C는 같은 공고 세 개로 사용 확인을 다시 돌려, 세 질문(근거를 따라갈 수 있는가, 몰랐던 차이나 질문이 생겼는가, 일반 채팅보다 편한가)의 답이 나빠지면 다음 판정 경로 슬라이스를 병합하기 전에 고친다. 그동안 A, D, E는 각자의 평가로 진행한다. 다시 돌리는 데는 승인된 호출이 필요하므로, 그 전에는 synthetic-verified로 병합할 수 있되 기준선 페이지의 사용 확인 행은 승인된 실행이 기록될 때까지 not verified로 남는다.
초안에 없던 다섯 가지 제약
초안은 무엇을 만들지 적었고, 무엇이 그것을 막는지는 적지 않았다. 지난 마일스톤에서 겪은 것을 다섯 줄로 옮겼다.
- 지연 예산. 검색과 모델의 도구 사용은 MCP 도구 호출 하나 안에서 돈다. 배치 제한은 90초, MCP 클라이언트 기본값은 60초, ChatGPT 호스트의 한계는 모른다. 무료 엔드포인트에서는 판정 호출 하나가 이미 1분을 넘겼다. 도구 호출 안에 일을 더하는 슬라이스는 늘어난 지연을 사용 확인에서 보고해야 한다.
- 계약이 바뀌면 위젯 주소도 바뀐다. 지난 글의 v5에서 v6 교훈이다. 엄격한 파서 앞에서는 느슨해지는 변경도 호환이 아니다.
- 읽는 계약과 만드는 계약. 제약 강화는 생성 계약인 초안 스키마와 프롬프트에 적용하고, 공유 읽기 계약은 넓히더라도 기존 저장 기록은 계속 읽을 수 있게 한다.
- 코퍼스 공개 범위. 저장소에 커밋하는 코퍼스는 합성만이 기본이다. 내 공개 글과 README는 공개 자료지만 저장소에 데이터로 넣지 않고, 로컬에서만 색인한다.
- 제공자 편차. OpenRouter의 도구 호출은 라우팅된 엔드포인트에 달려 있다. 도구 사용은 OpenAI부터 붙이고, OpenRouter 경로는 단일 호출을 유지한 채
not verified로 둔다.
리뷰가 잡은 수식 하나와 분모 하나
계획 문서에도 리뷰가 붙었다. 인라인 다섯 건에 재리뷰 세 건. 그중 둘은 문서에 적은 수식과 지표가 그대로 구현되면 틀리는 것이었다.
첫째는 인용을 주장에 묶는 ID다. 1차 수정에서 나는 주장 ID를 종류, 요건 ID(없으면 정규화한 요건 문장), 근거 문장을 이어 붙여 해시한다고 적었다. 리뷰어는 이 수식을 합성 값으로 직접 실행했다. JavaScript에서는 +가 ??보다 먼저 계산되므로, 같은 요건에 다른 근거를 넣어도 ID가 모두 같아지고, 요건 ID가 없는 서로 다른 요건도 모두 같은 ID가 됐다. 서로 다른 근거가 같은 ID를 가지면 제거된 주장의 인용이 살아남은 주장에 붙는다. 이어 붙이지 말고, 근거가 붙는 주장은 필드를 배열로 직렬화한 뒤 해시하고, 요건만 있는 주장은 요건 ID나 정규화한 요건 문장에 접두어를 붙여 그대로 쓰는 것으로 고쳤다. 같은 ID가 나오면 안 되는 두 경우와 하나로 합쳐져야 하는 한 경우를 테스트 계획에 대조군으로 적었다.
둘째는 결격 사유 재현율의 분모다. 모델 모드 평가는 공고를 새로 추출하므로, 정답과 추출 결과의 요건을 문장으로 맞춰야 한다. 나는 맞춰진 요건에 대해서만 재현율을 계산하겠다고 적었다. 리뷰어의 예는 이렇다. 정답 결격 사유가 둘인데 하나를 추출에서 놓치고 남은 하나만 찾으면, 맞춰진 요건 기준 재현율은 전부 찾은 것으로 나온다. 추출에서 놓친 항목이 분모에서 빠져 수치가 좋아 보일 수 있다. 두 수를 같이 보고하는 것으로 고쳤다. 맞춰진 요건 기준의 조건부 재현율과, 놓친 것을 실패로 세는 전체 정답 기준의 재현율. 맞춰진 요건이 없으면 만점이 아니라 해당 없음이다. 맞추지 못한 요건은 표현 차이인지 의미가 다른 것인지 실행기가 판정하지 않고, 사람이 검토하는 자리에 남긴다.
셋째는 작은 것이었다. 없는 명령 옵션을 참조하고 있었다. 계획서에 적힌 것은 코드가 아니라서 타입 검사가 잡아 주지 않는다. 문서 안의 수식과 옵션 이름도 실행해 봐야 한다는 것을 계획 단계에서 배웠다.
과정에서 하나 더. 1차 리뷰를 반영한 수정을 로컬에 둔 채 2차 리뷰를 받았다. 리뷰어는 로컬 파일을 읽고 “푸시 전 로컬 수정본의 보완 사항”이라고 제목부터 못 박았다. 한 라운드가 끝나면 바로 푸시해야 리뷰가 원격을 보고 이뤄진다. 1차 수정은 2차 리뷰가 끝난 뒤에야 2차 수정과 잇달아 원격에 올라갔다.
아직 정하지 않은 것, 아직 만들지 않은 것
계획서에는 M5-A 전에 내가 정해야 할 것 셋이 남아 있다. 임베딩을 M5에서 아예 빼 둘지 게이트 뒤의 선택지로 남길지, 호스트와 지연이 미지수인 채로 도구 사용을 범위에 둘지 E에서 끝낼지, 코퍼스는 합성만 커밋한다는 기본을 그대로 확정할지. 셋 다 지금 답을 정하지 않았고, 정할 차례가 왔을 때 이유와 함께 적으려 한다.
M5에서 더할 근거 검색과 인용 확장, 모델 모드 평가는 아직 계획이고, 결정 기록의 상태도 “제안”이다. 다음 글은 M5-0, 기준선을 동결하는 PR이다. 만드는 것이 가장 적은 슬라이스를 먼저 하는 이유는, 그 뒤의 모든 비교가 거기서 시작하기 때문이다.
계획 문서 전문은 저장소의 MILESTONE_5.md에 있다.