에이전트 하네스 설계 · 10/10
메모리를 저장하고 품질 재시도를 연결했다고 에이전트가 곧바로 학습하는 것은 아닙니다. 저장된 내용이 다음 실행에서 선택돼야 하고, 그 내용이 실제 실패 원인을 담고 있어야 하며, 재시도 중에는 새 후보와 새 점수만 다음 결정을 움직여야 합니다. 이 중 하나라도 어긋나면 로그에는 ‘교훈 이월’과 ‘재시도’가 남지만 결과는 같은 자리를 맴돕니다.
7월 초에는 판정 점수를 0과 1 사이의 점진적인 값으로 다듬고, 실패 교훈을 다음 실행에서 다시 읽는 경로를 연결했습니다. 같은 기준에서도 답의 충족 정도를 0 또는 1로만 자르지 않고 분포로 볼 수 있게 됐고, 한 실행의 실패가 다음 실행의 컨텍스트에 들어오기 시작했습니다.
실제 시나리오를 연속으로 실행하자 그 다음 문제가 보였습니다. 교훈은 저장됐지만 실패 이유가 아니라 실패한 답 자체를 담고 있었고, 오래 쌓인 항목 중 어느 교훈을 읽을지도 정해지지 않았습니다. 재시도에서는 모델이 도구를 찾는 중인데도 직전 답과 점수가 남아 같은 후보를 다시 판정했습니다. 피드백 루프에 필요한 것은 저장 여부가 아니라 상태의 대상과 시점이 끝까지 맞는가였습니다.
실패한 답보다 판정 사유를 교훈으로 남겼습니다
초기 교훈 생성은 사용자 질문과 실패한 답을 요약했습니다. 무엇을 시도했는지는 남지만 왜 반려됐는지는 빠질 수 있었습니다. 예를 들어 ‘최신 뉴스 세 건과 출처가 필요하다’는 기준을 어겼는데 실패한 답만 요약하면 다음 실행도 같은 형식의 답을 만들 가능성이 큽니다.
판정기는 이미 기준별 미달 사유를 validation_feedback으로 만들고 있었습니다. 재시도 교훈은 이 값을 우선 사용하도록 바꿨습니다. 실패한 답은 피드백이 없을 때의 보조 입력으로만 남겼습니다.
이전: 사용자 질문 + 실패한 답 → 교훈
변경: 사용자 질문 + 판정 피드백 → 교훈
저장할 정보의 양보다 다음 행동을 바꿀 수 있는 교정 신호가 중요했습니다. ‘무엇을 답했는가’는 실행 기록에 이미 있고, 다음 실행에 필요한 것은 ‘어떤 기준을 충족하지 못했는가’였습니다.
회상 순서도 함께 고쳤습니다. 저장소가 돌려주는 순서에 기대면 항목이 많아질수록 최근 교훈을 선택할 확률이 낮아집니다. 생성 시각과 식별자를 기준으로 최신 항목부터 정렬한 뒤, 정한 개수만 컨텍스트에 넣었습니다. 장기적으로는 질문 유사도까지 고려해야 하지만, 적어도 순서가 정의되지 않은 상태에서 임의의 교훈을 읽지는 않게 했습니다.
로컬 격리 스택에서 연속 실행으로 이 경로를 확인했습니다. 첫 실행에서 기준 미달 사유를 교훈으로 저장하고, 다음 실행에서는 다른 정제 메모리를 제외해 해당 교훈의 효과만 남겼습니다. 다음 실행의 첫 답이 같은 판정 기준을 통과하면서, 저장 성공이 아니라 실제 생성 결과까지 이어졌는지를 확인할 수 있었습니다.
답 후보와 판정 점수의 수명을 한 회차로 제한했습니다
교훈 경로를 고친 뒤에도 품질 재시도가 상한을 모두 쓰고 끝나는 경우가 있었습니다. 실행 기록을 따라가 보니 모델은 재시도 후 도구를 호출하며 작업을 진행하고 있었습니다. 문제는 루프가 멈춘 것이 아니라 판정기가 종점이 아닌 회차를 종점으로 오인한 데 있었습니다.
모델 응답에 텍스트 없이 도구 호출만 있으면 이전 구현은 last_assistant_text를 갱신하지 않았습니다. 직전 실패 답이 상태에 남아 있으니 판정기는 새 답이 없는 회차에서도 과거 후보를 다시 채점했습니다. validation_score도 다음 회차까지 남아 결정 단계가 새 작업 상태보다 이전 점수를 먼저 읽을 수 있었습니다.
두 값의 수명을 명시적으로 바꿨습니다.
- 모델 호출이 끝날 때
last_assistant_text를 항상 이번 응답 기준으로 갱신합니다. 텍스트가 없으면 빈 값입니다. - 품질 재시도를 소비한 직후 이전
validation_score를 지웁니다. 새 후보가 만들어지고 판정되기 전에는 유효한 점수가 없습니다.
이 규칙으로 한 점수는 한 후보와 한 회차에만 속하게 됐습니다. 도구 호출 중인 회차에는 평가할 텍스트가 없으므로 도구 결과를 더해 계속하고, 새 답이 만들어졌을 때만 다시 판정합니다. 재시도 상한은 같은 답을 반복 채점한 횟수가 아니라 실제로 새 후보를 만들 기회를 사용한 횟수가 됩니다.
검증에서는 첫 후보가 기준에 미달한 뒤 도구 호출을 거쳐 서로 다른 새 후보가 만들어지는지 봤습니다. 후보가 바뀔 때 답 길이와 판정 입력도 달라지고, 마지막 후보가 통과하면 재시도 한 번으로 종료돼야 했습니다. 단순히 최종 성공만 확인하면 중간에 과거 후보를 몇 번 재판정했는지 알 수 없기 때문에 후보와 회차의 연결을 함께 확인했습니다.
재시도 뒤에도 도구를 다시 찾을 수 있게 했습니다
판정 피드백이 ‘근거를 더 찾아라’라고 말해도 재시도 회차에서 도구 검색이 닫혀 있으면 행동은 달라지지 않습니다. discovery_first는 첫 회차에 도구 검색을 우선하게 하는 설정이었지만, 검색 도구가 일정 개수 이상의 카탈로그에서만 등록되는 휴리스틱과 충돌했습니다. 설정을 명시적으로 켜도 작은 워크플로우에서는 검색 도구가 없을 수 있었습니다.
명시적인 설정을 휴리스틱보다 우선하도록 바꿨습니다. discovery_first가 켜져 있으면 카탈로그 크기와 관계없이 검색 도구를 등록하고, 제품 노드에서도 이 설정을 켜거나 끌 수 있게 연결했습니다. 빈 값이 의도하지 않은 false로 해석되지 않도록 명시적으로 끈 경우에만 비활성 설정을 전달했습니다.
Reflexion 피드백도 ‘다시 조사하지 말라’는 일반 문구를 제거했습니다. 불필요한 도구 반복을 줄이려던 문장이 근거 부족으로 반려된 답까지 재검색하지 못하게 했기 때문입니다. 도구 사용 여부는 현재 판정 피드백과 실행 한도 안에서 다시 결정하게 했습니다.
검색을 강제하는 것이 목표는 아닙니다. 판정기가 근거 부족을 지적했을 때 검색이라는 행동이 가능한 상태로 남아 있어야 한다는 뜻입니다. 도구가 필요 없는 답은 바로 생성할 수 있고, 검색이 필요하면 ToolSearch로 정의를 승격한 뒤 호출합니다. 피드백, 컨텍스트, 행동 경로가 함께 열려 있어야 루프가 실제 교정으로 이어집니다.
구조화된 실행 요약으로 한 경로를 확인했습니다
이전에는 각 기능을 로그에서 따로 확인해야 했습니다. 메모리를 몇 개 읽었는지, 이전 교훈을 상태에 넣었는지, 도구 검색이 일어났는지, 어떤 점수로 판정했고 몇 번 재시도했는지가 긴 문자열 사이에 흩어져 있었습니다. 기능이 각각 동작해도 한 실행 안에서 순서가 맞는지 보기 어려웠습니다.
7월 13일에는 실행 이벤트의 구조화 필드를 스트리밍 경로에서 보존하고, 화면 위에 다음 흐름을 한 줄로 요약하는 UI를 만들었습니다.
Memory → State → ToolSearch → Judge → Loop
새 이벤트가 있으면 구조화 값을 우선 사용하고, 기존 문자열 로그도 읽을 수 있게 호환 경로를 뒀습니다. 로컬 실행에서는 범위 메모리 회상, 이전 교훈 주입, 도구 검색, 품질 통과, 재시도 횟수가 한 실행 요약에서 순서대로 이어지는 것을 확인했습니다. 이 요약은 상태를 보기 좋게 나열하는 데서 끝나지 않습니다. 피드백 경로에서 어느 단계가 끊겼는지 찾기 위한 관찰 화면입니다.
여기에는 반영 범위의 차이가 있습니다. 판정 피드백 우선, 최신 교훈 선택, 후보와 점수 초기화, 검색 설정 보강은 당시 SDK와 기능 브랜치의 엔진·제품 실행 경로에 반영해 테스트했습니다. 반면 구조화 실행 요약 UI는 로컬 스택에서 동작과 화면 검증을 마친 단계이며 아직 제품 배포 범위에는 포함하지 않았습니다.
교훈이 다음 실행에 보인다는 사실만으로 학습이 완성되지는 않습니다. 최신 실패 사유가 선택되고, 새 회차에서는 새 후보와 새 점수만 유효하며, 피드백을 실행할 도구 경로가 열려 있어야 합니다. 마지막에는 이 흐름을 한 실행에서 관찰할 수 있어야 합니다. 하네스의 상태는 많이 저장하는 것보다 누구의 어느 시점 상태인지 끝까지 유지하는 것이 중요했습니다.
이전 편 → 도구 공개와 출력 전달을 실행 컨텍스트로 설계하기(9편) 시리즈 처음 → 검증 루프를 실행 상태로 나눈 이유(1편)

