Skip to content
게으른 엔지니어의 기술 블로그
Go back

고치라는 지시부터, 다시 봐야 했다 2편 — 가설은 반은 맞고 반은 틀렸다

고치라는 지시부터, 다시 봐야 했다 · 2/4편

1편에서 경로 충돌 버그를 고치고 릴리스했다. 그 릴리스 직후, 실사용 중이던 사용자에게서 새 보고가 들어왔다 — 원본 노트 50개 넘게 /ingest했는데 결과로 나온 요약 노트 중에 분할된 노트가 다섯 개도 안 된다는 것이었다.

인상이 아니라 사례로

“분할이 거의 안 된다”는 보고만 보고 바로 버그라고 단정하진 않았다. 원본 중 상당수가 애초에 하나의 주제만 다루는 경우도 흔하기 때문이다. 대신 사용자에게 구체적인 반례 — 원본 파일과 그 결과로 나온 요약 노트를 실제로 쌍으로 — 몇 개만 골라달라고 부탁했다.

모은 사례들은 하나의 버그가 아니라 성격이 다른 여러 증상이었다. 한 기사는 도입부에서 실존 인물(국내 1호 기록학자, 정부 자문위원 경력에 베스트셀러 저서까지 있는 교수)을 짧게 소개한 뒤, 본문 전체가 그 사람이 추천한 책 이야기로 넘어가는 구조였다. 결과 노트에는 그 인물이 흔적도 없이 사라져 있었다. 같은 배치 안의 다른 기사(토스 직원의 AI 업무 자동화 이야기)는 반대로 정확한 판단을 보여줬다 — 그녀가 쓴 특정 AI 도구는 본문 밖에서 독립적으로 성립하지 않으니 따로 안 쪼갠 게 맞는 결정이었다. 다만 이 노트에 완전히 무관한 노트로 향하는 링크가 하나 붙어 있었는데, 순전히 같은 배치에서 같이 생성됐다는 이유였다. 세 번째 기사(은퇴 준비 조언)는 인물과 전략 두 노트로 깔끔하게 나뉘어 아무 문제가 없었다.

관찰에서 나온 가설, 그리고 그 검증

세 번째 사례가 잘 된 걸 보고, 사용자가 구체적인 가설을 세웠다 — 한 번의 호출에 원본을 몇 개나 묶어서 보내는지(배치 크기)가 원인일 거라는 것. 실제로 배치가 작았던 사례는 잘 됐고, 컸던 사례는 안 됐던 걸 직접 관찰한 뒤였다.

이 가설을 감으로 받아들이지 않고 직접 측정했다. 프로덕션에 실제로 쓰이는 프롬프트 코드를 그대로 불러와서, 실제 모델을 배치 크기만 바꿔가며 반복 호출하는 방식이었다. 결과는 절반만 가설을 뒷받침했다. 인물이 사라진 사례는 배치 크기를 아무리 줄여도 — 다른 원본과 전혀 경쟁하지 않는 완전한 단독 조건에서도 — 스물네 번 중 스물네 번 전부 실패했다. 배치 희석이 아니라 그 기사 자체의 구조(인물이 잠깐 등장했다가 그가 추천한 것에 완전히 묻히는 서술 방식)에서 오는 문제였다. 반면 또 다른 사례(하나의 원본 안에 서로 다른 개념 네 개와 세 회사 비교까지 들어있던 긴 기술 기사)는 가설 그대로 배치 크기에 뚜렷하게 반응했다 — 혼자 처리될 때는 결과 노트 개수가 확연히 많았고, 다른 원본 하나와 묶이기만 해도 크게 줄었다.

“배치 크기가 원인이냐”는 하나의 질문이 증상에 따라 다른 답을 낸 셈이다. 사용자가 관찰한 것 자체는 틀리지 않았다 — 실제로 작은 배치에서 더 잘 됐으니까. 다만 그 관찰을 만든 메커니즘이 증상마다 달랐고, 그건 직접 측정해보기 전까지는 알 수 없었다.

고치는 지시문 자체가 틀려 있었다

원인이 나뉘어 있으니 고치는 방법도 프롬프트 문구 몇 군데를 보강하는 쪽으로 정했다. 여러 태스크로 나눠 각각 다른 에이전트에게 구현을 맡기는 방식으로 진행했는데, 그중 하나는 이미 다른 곳(비슷한 판단 기준을 다루는 별도 프롬프트)에 있던 예시 문장을 토씨 하나 안 바꾸고 그대로 재사용해야 한다는 제약이 있었다. 예시 문장을 살짝만 바꿔도 LLM에게 주는 의미가 달라질 수 있다는 이유로, 이건 명시적인 필수 조건으로 못박아뒀다.

문제는 구현을 맡기면서 넘긴 지시문 자체였다. “토씨 하나 안 바꾸고 재사용하라”는 요구는 정확히 적었는데, 정작 그 지시문 안에 예시로 붙여넣은 문장이 결론부만 살짝 다르게 바뀐 채였다. 구현한 쪽은 지시문을 충실히 그대로 따랐을 뿐이고, 그 결과물이 틀린 것도 지시문이 틀렸기 때문이었다.

이걸 잡아낸 건 태스크 리뷰였다. 지시문에 적힌 예시를 그대로 믿지 않고, 원래 문장이 있는 파일을 직접 다시 읽어서 대조했다. 그리고 이 결함을 “구현한 쪽의 실수”가 아니라 “지시문 자체의 결함”으로 정확히 짚었다. 고치는 방법도 원래 문장은 정말 한 글자도 안 건드리고, 그 뒤에 새로운 지시 문장 하나만 덧붙이는 쪽을 택했다 — verbatim 요구는 지키면서 하고 싶은 말은 그대로 전달하는 방식이었다.

검증 스크립트도 다시 검증받았다 — 제외 목록 카운터 버그

프롬프트를 고친 뒤에는 같은 원본들로 다시 실측해서 개선됐는지 확인했다. 보고된 수치는 좋아 보였다. 하지만 태스크 리뷰는 그 결과 수치를 그대로 믿지 않고, 실행 중에 남은 원본 로그를 직접 읽어서 손으로 다시 세봤다.

거기서 진짜 버그 두 개가 나왔다 — 둘 다 프로덕션 코드가 아니라 검증 스크립트 쪽이었다. 하나는 특정 주제에 관련된 노트만 골라 세는 카운터였는데, “이 단어들이 포함되면 세지 마라”는 제외 목록 방식으로 짜여 있었다. 그런데 모델이 그 회차에서 우연히 쓴 새로운 표현 하나가 제외 목록에 없어서, 원래 다른 주제에 속하는 노트가 잘못 포함돼 평균값이 실제보다 살짝 부풀려져 있었다. 다른 하나는 무관한 노트 사이에 엉뚱한 링크가 생기는지 확인하는 스크립트였는데, 문제를 발견했을 때만 로그를 남기게 짜여 있었다 — 즉 깨끗한 회차는 아무 흔적도 안 남는다는 뜻이었다. “문제 없음”이라는 결론을 나중에 로그만 보고 검증할 방법 자체가 없었던 것이다.

둘 다 최종 결론(개선됐다는 것) 자체를 뒤집진 않았지만, 둘 다 고쳤다. 제외 목록 대신 “이 단어들이 포함돼야만 세라”는 포함 목록 방식으로 바꾸고, 링크 검사 스크립트는 회차마다 판정한 내용을 문제가 있든 없든 전부 로그로 남기게 바꿨다. “결과가 좋게 나왔다”와 “그 결과를 내는 방법이 맞다”는 서로 다른 질문이고, 그건 검증에 쓰는 코드에도 똑같이 적용되는 얘기였다.

남는 생각

이번 편의 세 장면 — 사용자의 가설, 구현자에게 넘긴 지시문, 결과를 확인하는 검증 스크립트 — 은 전부 “확인 없이 그대로 믿었으면 넘어갔을 자리”였다. 가설은 관찰 자체는 맞았지만 메커니즘까지는 맞지 않았고, 지시문은 요구 사항은 정확했지만 그 요구 사항이 담긴 예시 자체가 틀려 있었고, 검증 스크립트는 결론은 맞았지만 그 결론을 내는 방법에 구멍이 있었다. 셋 다 겉보기엔 문제없어 보였다는 공통점이 있다.

4편에서 리뷰어가 커밋 메시지에 적힌 기술적 주장을 실제 코드와 대조해서 팩트체크한 적이 있었다. 이번엔 그 대상이 커밋 메시지가 아니라 사람의 가설, 내가 쓴 지시문, 그리고 결과를 재는 도구 자체로 넓어진 셈이다. 검증이 필요한 대상은 결국 “새로 짠 코드”만이 아니라, 그 코드를 만들어낸 판단과 그 판단이 맞았는지 확인하는 방법까지 전부 포함된다는 걸 이번에 다시 확인했다.


Share this post:

고치라는 지시부터, 다시 봐야 했다

  1. 1. 고치라는 지시부터, 다시 봐야 했다 1편 — 죽은 주석을 고치다가, 또 틀렸다
  2. 2. 고치라는 지시부터, 다시 봐야 했다 2편 — 가설은 반은 맞고 반은 틀렸다
  3. 3. 고치라는 지시부터, 다시 봐야 했다 3편 — 고치기 전에 한 번 더 물었다
  4. 4. 고치라는 지시부터, 다시 봐야 했다 4편 — 지우라는 지시와, 어떻게 지울지는 다른 질문이었다