두 번째 시도는 첫 시도가 남긴 것부터 센다
실패한 도구 호출을 다른 형식으로 다시 부르다 부작용을 두 번 낸 결함과, 폴백 모델에 원래 요청의 설정을 그대로 넘기다 폴백까지 실패한 결함을 따라 재시도의 설계를 다룹니다.
초록 에이전트 프레임워크에서 재시도는 대개 안전한 기본값처럼 다뤄집니다. 실패했으니 한 번 더 불러 보면 된다는 생각입니다. 지난 두 달 사이 두 오픈소스 프레임워크가 이 생각의 빈틈을 각각 고쳤습니다. 하나는 실패한 도구 호출을 다른 인자 형식으로 다시 부르다가, 이미 바깥에 부작용을 남긴 도구를 두 번 실행할 수 있던 결함입니다. 다른 하나는 다른 모델로 넘어가는 폴백에 원래 요청의 설정을 그대로 실어 보내다가, 그 설정을 받지 못하는 모델에서 폴백까지 실패하던 결함입니다. 이 칼럼은 두 수정이 공통으로 그은 선을 읽고, 그 선을 법률 AI 의 발행과 검증 경로에 옮겨 봅니다.
실패에는 두 종류가 있습니다. 아무 일도 일어나지 않은 채 끝난 실패와, 무언가가 이미 일어난 뒤에 끝난 실패입니다. 앞의 것은 다시 불러도 손해가 없습니다. 뒤의 것은 다시 부르는 순간 같은 일이 한 번 더 일어납니다. 메일이 두 번 나가고, 결제가 두 번 잡히고, 게시물이 두 번 올라갑니다. 문제는 호출하는 쪽이 받는 것이 대개 예외 하나뿐이라는 데 있습니다. 예외는 실패했다고 말할 뿐, 실패하기 전에 무엇을 했는지는 말해 주지 않습니다. 그래서 재시도의 설계는 예외를 받은 뒤가 아니라 호출하기 전에 시작돼야 합니다. 무엇을 다시 불러도 되는지, 다시 부를 때 무엇을 바꾸고 무엇을 그대로 둘지를 미리 정해 두는 일입니다. 오늘 읽는 두 수정은 이 일을 프레임워크 차원에서 한 사례입니다.
핵심 기술 개념
부작용 뒤의 예외(Exception after Side Effect)
호출이 되돌릴 수 없는 일을 이미 바깥에 남긴 다음에 오류를 던지는 경우입니다. 호출하는 쪽에서는 아무 일도 없었던 실패와 똑같은 예외로 보이므로, 예외만 보고 다시 부르면 그 일이 한 번 더 일어납니다.
호출 규약의 사전 결정
도구를 어떤 인자 형식으로 부를지를 실패를 겪어 본 뒤에 바꾸지 않고, 감싼 함수의 시그니처를 보고 호출하기 전에 정하는 방식입니다. 시도를 바꿔 가며 맞는 형식을 찾는 탐색이 사라지므로, 실패한 호출이 다른 형식으로 다시 실행될 길도 함께 사라집니다.
폴백 요청 정리(Fallback Request Sanitization)
다른 모델로 넘어갈 때 원래 요청에 실려 있던 설정 가운데 그 모델이 받지 못하는 것만 걷어 내는 일입니다. 받을 수 있는 설정은 남기고, 원래 요청 자체는 고치지 않은 채 폴백 시도마다 사본을 만들어 정리합니다.
기술 심층 분석
예외 하나가 두 가지 뜻을 지닐 때
llama_index 의 수정(#22841, 「fix(core): avoid retrying failed function tools」)은 도구 호출 함수가 예외를 읽는 방식을 고쳤습니다. 고치기 전에는 단일 인자를 위치 인자로 넘겨 부른 호출에서 어떤 예외가 나든, 그것을 같은 도구를 키워드 인자로 다시 불러 보라는 신호로 해석했습니다. 인자 형식이 맞지 않아 난 예외라면 이 해석이 맞습니다. 그러나 PR 은 다른 경우를 짚습니다. 도구가 되돌릴 수 없는 부작용을 일으킨 다음 업무 예외나 전송 예외를 던질 수 있고, 그러면 이 동작이 도구를 두 번 실행한다는 것입니다. 같은 예외 타입이 '아무 일도 안 일어났다'와 '이미 일어났는데 그 뒤에 실패했다'라는 정반대의 두 상황을 함께 덮고 있었던 셈입니다. 호출하는 쪽은 예외만으로는 둘을 가를 수 없습니다.
실패를 겪기 전에 형식을 정한다
그래서 수정은 예외를 더 정교하게 해석하는 쪽으로 가지 않았습니다. 대신 해석할 필요 자체를 없앴습니다. FunctionTool 에 대해서는 감싼 함수의 시그니처를 보고 지원하는 호출 규약을 호출하기 전에 정합니다. 키워드 전용 도구는 처음부터 키워드 인자를 받고, 위치 전용 도구는 처음부터 단일 위치 값을 받습니다. 그 밖의 도구 구현에 대한 기존 폴백 동작은 그대로 두었습니다. 이 설계의 요점은 시행착오로 맞는 형식을 찾는 탐색을 호출 전의 판정으로 바꾼 데 있습니다. 탐색은 실패를 재료로 삼기 때문에, 실패가 부작용을 동반하는 순간 탐색 자체가 위험해집니다. 판정은 아무것도 실행하지 않고 끝나므로 부작용이 끼어들 틈이 없습니다. PR 은 이 결함을 직접 재현하는 회귀 테스트를 함께 넣었다고 적었습니다.
폴백은 같은 요청을 다른 곳에 보내는 일이 아니다
langchain 의 수정(#40886, 「fix(langchain): sanitize cache settings for fallback models」)은 다른 종류의 두 번째 시도를 다룹니다. 에이전트가 한 제공자에서 실패해 다른 제공자의 모델로 넘어갈 때, 요청의 모델 설정에 명시적으로 실려 있던 캐시 설정이 그것을 받지 않는 모델에 닿아 폴백 자체를 실패시킬 수 있었습니다. 첫 시도가 실패한 이유와는 아무 상관 없는 이유로 두 번째 시도가 죽는 것입니다. 수정된 폴백 미들웨어는 특정 제공자에서만 쓰는 세션 고정 헤더와 프롬프트 캐시 키를, 그것을 지원하지 않는 제공자로 넘어갈 때 걷어 냅니다. PR 은 프롬프트 캐시 키가 여러 제공자가 함께 쓰는 설정이므로 한 제공자 전용으로 다뤄서는 안 된다는 점을 검토 초점으로 적었습니다. 무엇을 지울지는 넘어갈 모델이 무엇을 받는지로 정한다는 뜻입니다.
원래 요청은 그대로 남긴다
같은 PR 에서 덜 눈에 띄지만 중요한 문장은 원래 요청을 바꾸지 않는다는 대목입니다. 폴백 시도마다 정리된 사본이 나가고, 처음 요청은 손대지 않은 채 남습니다. 이 선택은 두 가지를 지킵니다. 하나는 폴백이 실패해도 다음 후보나 호출자가 받는 요청이 앞선 시도의 정리 결과로 오염되지 않는다는 것입니다. 다른 하나는 사후에 무엇이 요청됐는지를 물었을 때 정본이 하나로 남는다는 것입니다. 시도마다 요청을 제자리에서 고쳐 쓰면, 마지막에 남은 요청은 처음 사용자가 보낸 것이 아니라 여러 번의 정리를 거친 무엇이 됩니다. 두 수정을 겹쳐 읽으면 한 줄로 줄어듭니다. 두 번째 시도는 형식과 설정을 바꿀 수 있지만, 원래 요청과 이미 일어난 행동은 바꾸지 않습니다. 앞의 것은 사본에서, 뒤의 것은 호출 전의 판정에서 지켜집니다.
기술적 트레이드오프
긴장 관계 재시도를 넉넉하게 허용하면 일시적인 장애는 사람 손 없이 지나가지만, 부작용을 이미 낸 호출까지 다시 불러 같은 일을 두 번 일으킬 수 있다. 재시도를 막으면 중복은 사라지지만, 아무 일도 일어나지 않은 단순한 실패까지 사람이 다시 손대야 하므로 처리량이 떨어지고 대기열이 길어진다.
실무적 해소 재시도를 허용할지는 실패 뒤의 예외가 아니라 호출 전의 성질로 정합니다. 바깥에 아무것도 남기지 않는 호출, 예컨대 조회와 대조는 실패하면 그대로 다시 불러도 됩니다. 바깥에 무언가를 남기는 호출, 예컨대 게시와 전송과 기록은 실패하면 다시 부르기 전에 그 일이 이미 일어났는지를 따로 확인하고, 확인되지 않으면 사람에게 넘깁니다. 그리고 어느 쪽이든 다시 부를 때 바꾸는 것은 형식과 설정의 사본뿐이고, 원래 요청과 이미 남은 기록은 고쳐 쓰지 않습니다.
이 주장이 틀리는 조건
반증 조건 호출 전 성질로 재시도를 가르자는 이 판단은 운영에서 깨질 수 있습니다. 조회로 분류해 자유롭게 다시 부르던 호출이 실제로는 상대 서버에 요청 기록이나 사용량을 남기고, 그 흔적이 쌓여 차단이나 과금 같은 뚜렷한 피해로 이어지는 일이 반복된다면 바깥에 아무것도 남기지 않는 호출이라는 분류 자체가 틀린 것입니다. 거꾸로 게시처럼 확인 뒤 사람에게 넘기는 호출에서 확인 단계가 매번 '아직 일어나지 않음'으로 끝나 사람의 손만 늘린다면, 그 확인은 비용만 남기는 의식이 되고 이 설계는 다시 짜야 합니다.
법마디 OS에 적용한다면
법마디 OS 의 운영 경로에도 바깥에 무언가를 남기는 호출이 있습니다. 데일리 카드뉴스를 소셜 채널에 게시하는 호출이 그 예입니다. 이 경로는 게시 요청이 시간 초과로 끝나도 바로 다시 부르지 않습니다. 실제로 응답은 시간 초과였는데 게시물은 이미 만들어져 있던 날이 있었고, 그 채널은 게시물을 API 로 지울 수 없습니다. 그래서 다시 부르기 전에 계정의 최근 게시물을 한 번 조회해 이미 올라갔는지를 먼저 확인합니다. 법률상식 결번을 사람이 메우는 수기 발행 스크립트도 같은 교훈을 거쳤습니다. 스크립트가 실행될 때마다 담당 리더를 다시 뽑던 시절에는, 문장 하나를 고쳐 다시 돌리면 앞선 실행이 이월 순서를 이미 소모한 탓에 다른 리더가 나왔습니다. 지금은 자동 실행이 판정한 리더를 상수로 박고, 다시 돌려도 같은 리더가 나오는지를 실행 시작에서 확인합니다. 답변 생성 경로에서는 주 모델 호출이 실패해도 다른 모델로 조용히 넘어가지 않습니다. 넘어간 사실이 드러나지 않은 채 품질이 바뀌는 것을 막기 위한 선택입니다. 반대로 조문과 판례를 국가법령정보센터 원문과 대조하는 조회는 바깥에 아무것도 남기지 않으므로, 서버에 닿지 못한 회차는 실패로 숨기지 않고 보류로 남긴 뒤 다시 확인합니다. 남은 과제는 이 분류를 호출마다 코드에 적어 두는 일입니다. 지금은 경로마다 따로 지켜지고 있어, 새 경로가 생기면 같은 판단을 다시 해야 합니다.
기술적 함의
- 예외는 실패했다는 사실만 전한다. 실패하기 전에 바깥에 무엇이 남았는지는 예외가 아니라 호출의 성질과 별도의 확인으로 알아야 한다.
- 맞는 형식을 시행착오로 찾지 말고 호출 전에 정한다. 실패를 재료로 삼는 탐색은 실패가 부작용을 동반하는 순간 위험해진다.
- 폴백과 재시도는 사본을 고친다. 원래 요청과 이미 일어난 행동을 제자리에서 고쳐 쓰면 사후에 무엇이 일어났는지의 정본이 사라진다.
"다시 부르는 일은 처음 부르는 일보다 어렵습니다. 처음에는 세상이 깨끗하지만, 두 번째에는 첫 시도가 남긴 것이 있을 수 있기 때문입니다. 좋은 재시도는 더 끈질긴 재시도가 아니라, 무엇이 이미 일어났는지를 먼저 세고 원래 요청은 그대로 둔 채 사본만 고치는 재시도입니다. 법률 AI 에서 두 번 나간 게시물과 바뀐 저자는 사용자가 되돌려야 하는 비용이 됩니다."