XGEN 15일 무료 체험 — 설치 없이 브라우저에서 바로 시작하세요체험 신청
PlateerAI Labs
·Reading Time | 3 min
작성 | 김진수

재시도를 다 썼는데 같은 답을 계속 채점하고 있었습니다(10편)

품질 재시도가 상한을 전부 쓰고 끝났습니다. 기록을 따라가니 모델은 도구를 부르며 진행 중이었고 판정기는 직전 실패 답을 다시 채점하고 있었습니다. 점수와 후보의 수명을 한 회차로 묶은 과정입니다.

재시도를 다 썼는데 같은 답을 계속 채점하고 있었습니다(10편) — 커버 일러스트Series · 하네스 개발기시리즈 10번째 글 · 전체 10편 보기

메모리를 저장하고 품질 재시도를 연결했으니 에이전트가 실패에서 배울 차례였습니다. 실제로 돌려보니 로그에는 '교훈 이월'과 '재시도'가 남는데 결과는 같은 자리를 맴돌았습니다. 교훈에는 실패한 이유 대신 실패한 답이 들어 있었고, 판정기는 이미 반려한 답을 다시 채점하고 있었습니다. 저장이 아니라 상태의 대상과 시점이 문제였습니다.


교훈에 실패한 답을 담고 실패한 이유를 빼먹었습니다

7월 초 판정 점수를 0과 1 사이의 점진적인 값으로 다듬고, 실패 교훈을 다음 실행에서 다시 읽는 경로를 연결했습니다. 같은 기준에서도 충족 정도를 분포로 볼 수 있게 됐고, 한 실행의 실패가 다음 실행의 컨텍스트에 들어오기 시작했습니다.

실제 시나리오를 연속으로 실행하자 교훈이 효과가 없었습니다.

교훈 생성이 사용자 질문과 실패한 답을 요약하고 있었기 때문입니다. 무엇을 시도했는지는 남는데 왜 반려됐는지가 빠집니다.

"최신 뉴스 세 건과 출처가 필요하다"는 기준을 어겼다고 해봅시다. 실패한 답만 요약해 넘기면 다음 실행은 같은 형식의 답을 다시 만듭니다. 교훈이 오답 노트가 아니라 오답 사본이었습니다.

판정기는 이미 기준별 미달 사유를 만들고 있었습니다. 그 값을 쓰지 않고 있었을 뿐입니다.

이전: 사용자 질문 + 실패한 답      → 교훈
변경: 사용자 질문 + 판정 피드백    → 교훈

실패한 답은 피드백이 없을 때의 보조 입력으로만 남겼습니다.

'무엇을 답했는가'는 실행 기록에 이미 있습니다. 다음 실행에 필요한 것은 '어떤 기준을 충족하지 못했는가'였습니다.

회상 순서도 함께 고쳤습니다. 저장소가 돌려주는 순서에 기대면 항목이 많아질수록 최근 교훈이 선택될 확률이 낮아집니다. 생성 시각과 식별자로 최신부터 정렬한 뒤 정한 개수만 컨텍스트에 넣었습니다. 질문 유사도까지 보는 것은 다음 과제이고, 적어도 순서가 정의되지 않은 상태에서 임의의 교훈을 읽지는 않게 했습니다.

로컬 격리 스택에서 연속 실행으로 확인했습니다. 첫 실행에서 미달 사유를 교훈으로 저장하고, 다음 실행에서는 다른 정제 메모리를 제외해 그 교훈의 효과만 남겼습니다. 다음 실행의 첫 답이 같은 기준을 통과했습니다. 저장 성공이 아니라 실제 생성 결과까지 이어졌는지를 본 것입니다.

루프가 멈춘 줄 알았는데 판정기가 과거를 채점하고 있었습니다

교훈 경로를 고친 뒤에도 품질 재시도가 상한을 전부 쓰고 끝나는 경우가 남았습니다.

처음에는 루프가 어딘가에서 멈췄다고 봤습니다. 실행 기록을 따라가 보니 모델은 멈춰 있지 않았습니다. 재시도 후 도구를 호출하며 작업을 진행하고 있었습니다.

문제는 판정기가 종점이 아닌 회차를 종점으로 오인한 데 있었습니다.

모델 응답에 텍스트 없이 도구 호출만 있으면 이전 구현은 마지막 답 텍스트를 갱신하지 않았습니다. 직전 실패 답이 상태에 그대로 남아 있으니, 판정기는 새 답이 없는 회차에서도 과거 후보를 다시 채점했습니다.

점수도 다음 회차까지 남았습니다. 결정 단계가 새 작업 상태보다 이전 점수를 먼저 읽을 수 있었습니다.

회차 N     답 후보 생성 → 반려 (점수 0.5)
회차 N+1   도구 호출만 있음 → 텍스트 없음
           그런데 상태에는 회차 N의 답이 남아 있다
           → 같은 답을 다시 채점 → 또 0.5 → 재시도 소진

한 점수가 어느 후보의 것인지 정해져 있지 않았습니다. 4편에서 판정 결과를 다음 전이의 입력으로 만들어놓고, 그 입력의 유효 기간을 정하지 않은 것입니다.

두 값의 수명을 명시적으로 바꿨습니다. 모델 호출이 끝날 때 마지막 답 텍스트를 항상 이번 응답 기준으로 갱신합니다. 텍스트가 없으면 빈 값입니다. 그리고 품질 재시도를 소비한 직후 이전 점수를 지웁니다. 새 후보가 만들어지고 판정되기 전에는 유효한 점수가 없습니다.

이 규칙으로 한 점수는 한 후보와 한 회차에만 속하게 됐습니다. 도구를 호출 중인 회차에는 평가할 텍스트가 없으니 결과를 더해 계속하고, 새 답이 만들어졌을 때만 다시 판정합니다.

재시도 상한의 의미도 달라졌습니다. 같은 답을 반복 채점한 횟수가 아니라 실제로 새 후보를 만들 기회를 쓴 횟수입니다.

검증에서는 첫 후보가 미달한 뒤 도구 호출을 거쳐 서로 다른 새 후보가 만들어지는지 봤습니다. 후보가 바뀌면 답 길이와 판정 입력도 달라지고, 마지막 후보가 통과하면 재시도 한 번으로 끝나야 합니다. 최종 성공만 확인하면 중간에 과거 후보를 몇 번 재판정했는지 알 수 없어서, 후보와 회차의 연결을 함께 봤습니다.

상태를 오래 들고 있는 설계는 대체로 편리합니다. 그 편리함의 대가가 이런 형태로 나옵니다. 값은 남아 있고 그 값이 언제 것인지만 사라집니다.

도구 반복을 줄이려던 문장이 교정을 막고 있었습니다

판정 피드백이 "근거를 더 찾아라"라고 말해도 재시도 회차에서 검색이 닫혀 있으면 행동은 달라지지 않습니다.

두 군데가 막고 있었습니다.

첫째, 첫 회차에 도구 검색을 우선하게 하는 설정이 있었는데, 검색 도구가 일정 개수 이상의 카탈로그에서만 등록되는 휴리스틱과 충돌했습니다. 설정을 명시적으로 켜도 작은 워크플로우에서는 검색 도구 자체가 없었습니다. 명시적인 설정이 휴리스틱보다 우선하도록 바꾸고, 제품 노드에서도 켜고 끌 수 있게 연결했습니다. 빈 값이 의도하지 않은 꺼짐으로 해석되지 않게 명시적으로 끈 경우에만 비활성 설정을 전달했습니다.

둘째가 더 뼈아팠습니다. 교정 피드백에 "다시 조사하지 말라"는 일반 문구가 들어 있었습니다.

이 문장은 불필요한 도구 반복을 줄이려고 넣은 것이었습니다. 그런데 근거 부족으로 반려된 답까지 재검색하지 못하게 만들고 있었습니다.

절약하려고 넣은 문장이 이 루프의 목적 자체를 막고 있었습니다. 그 문구를 제거하고, 도구 사용 여부는 현재 판정 피드백과 실행 한도 안에서 다시 결정하게 했습니다.

검색을 강제하는 것이 목표가 아닙니다. 판정기가 근거 부족을 지적했을 때 검색이라는 행동이 가능한 상태로 남아 있어야 한다는 뜻입니다.

피드백 루프를 만들 때 신호가 전달되는지만 확인하기 쉽습니다. 신호를 받고 취할 행동이 그 시점에 가능한지는 따로 봐야 합니다. 피드백, 컨텍스트, 행동 경로가 함께 열려 있어야 루프가 실제 교정으로 이어집니다.

기능이 각각 동작하는데 한 실행에서 순서가 맞는지 볼 수가 없었습니다

여기까지 고치는 동안 저희를 가장 오래 붙잡은 것은 각각의 버그가 아니라 그것들을 찾는 방식이었습니다.

메모리를 몇 개 읽었는지, 이전 교훈을 상태에 넣었는지, 도구 검색이 일어났는지, 어떤 점수로 판정했고 몇 번 재시도했는지가 긴 문자열 로그 사이에 흩어져 있었습니다.

기능이 각각 동작해도 한 실행 안에서 순서가 맞는지는 보이지 않았습니다. 앞의 두 문제가 오래 발견되지 않은 이유도 그것입니다. 저장은 성공했고, 재시도는 일어났고, 로그는 정상이었습니다.

7월 중순 실행 이벤트의 구조화 필드를 스트리밍 경로에서 보존하고, 화면 위에 흐름을 한 줄로 요약하는 UI를 만들었습니다.

Memory → State → ToolSearch → Judge → Loop

새 이벤트가 있으면 구조화 값을 우선 쓰고, 기존 문자열 로그도 읽을 수 있게 호환 경로를 뒀습니다. 로컬 실행에서 범위 메모리 회상, 이전 교훈 주입, 도구 검색, 품질 통과, 재시도 횟수가 한 요약에서 순서대로 이어지는 것을 확인했습니다.

이 요약은 상태를 보기 좋게 나열하는 화면이 아닙니다. 피드백 경로에서 어느 단계가 끊겼는지 찾기 위한 관찰 화면입니다.

반영 범위에는 차이가 있습니다. 판정 피드백 우선, 최신 교훈 선택, 후보와 점수 초기화, 검색 설정 보강은 당시 SDK와 기능 브랜치의 엔진·제품 실행 경로에 반영해 테스트했습니다. 구조화 실행 요약 UI는 로컬 스택에서 동작과 화면 검증을 마친 단계이며 제품 배포 범위에는 아직 포함하지 않았습니다.

열 편 내내 같은 문제를 다른 층에서 만났습니다

시리즈를 마치며 돌아보니 열 편이 각각 다른 주제 같지만 같은 자리로 계속 돌아왔습니다.

1편   '재시도 3회'가 서로 다른 세 실행을 같은 이름으로 불렀다
2편   저장소를 나눈 것을 의존성을 나눈 것으로 불렀다
3편   설치된 것을 같은 실행이라고 불렀다
5편   대화 이력을 현재 상태라고 읽었다
6편   회귀 방어선을 held-out 검증이라고 불렀다
7편   모델과 설정을 합친 실행을 모델 비교라고 불렀다
9편   목록에 보이는 것을 부를 수 있는 것으로 다뤘다
10편  이전 회차의 점수를 이번 후보의 점수로 읽었다

매번 저희는 무언가를 실제보다 강한 이름으로 부르고 있었습니다. 그 이름 때문에 검증할 것을 검증하지 않았고, 문제는 이름이 틀렸다는 것이 드러날 때까지 조용히 남아 있었습니다.

에이전트 실행 환경을 만드는 일에서 반복해서 필요했던 능력은 새 기능을 넣는 것이 아니라, 같은 이름으로 부르고 있던 것들을 갈라내는 것이었습니다. 갈라내면 그다음 결정은 대체로 따라왔습니다. 무엇을 넘길지, 누가 소유할지, 무엇을 기록할지, 무엇을 테스트할지가 차례로 정해졌습니다.

하네스의 상태도 마찬가지였습니다. 많이 저장하는 것보다 누구의 어느 시점 상태인지 끝까지 유지하는 것이 어려웠고, 실제로 값어치가 있었습니다.


이전 편 → 도구가 목록에는 보이는데 부를 수는 없었습니다 (9편)

시리즈 처음 → 재시도 횟수로는 실행을 설명할 수 없었습니다 (1편)

#하네스#상태 최신성#피드백 루프
블로그 목록으로