Your agent's retry bug isn't an AI problem. It's a distributed-systems problem with an LLM on top.
환불 요청이 처리된 뒤 응답만 유실되면 에이전트는 실패로 판단해 같은 환불을 다시 요청할 수 있습니다. 글은 애플리케이션이 멱등성 키와 영속적인 작업 기록을 관리하고, 결과를 알 수 없는 상태는 재시도 대신 대사로 해결해야 한다고 설명합니다.
AI 요약
에이전트 데모에서는 모델이 도구를 고르고 호출 결과를 받은 뒤 다음 단계로 넘어갑니다. 운영 환경에서는 요청이 처리됐는데 응답이 돌아오지 않는 일이 생깁니다. 예를 들어 고객에게 환불하기로 결정한 에이전트가 결제 서비스에 환불을 요청하고, 서비스는 환불 내역을 원장에 기록하지만 응답이 네트워크에서 유실됩니다. 에이전트는 시간 초과를 오류로 받아들여 다시 요청하고, 고객에게 환불이 두 번 지급될 수 있습니다. 모델의 판단과 매개변수가 맞더라도 실행 이후의 통신 실패가 잘못된 결과를 만듭니다.
시간 초과는 실패를 뜻하지 않습니다
시간 초과로 알 수 있는 사실은 응답을 받지 못했다는 점뿐입니다. 요청이 서비스에 도달하지 않았을 수도 있고, 도달한 뒤 거절됐을 수도 있으며, 처리까지 끝났지만 응답만 유실됐을 수도 있습니다. 이 세 상황을 모두 '도구 실패'로 처리하면 상태 변경 요청을 안전하게 재시도할 수 없습니다. 요청이 도달하지 않았거나 명시적으로 거절된 경우와 달리, 성공 후 응답이 사라진 경우에는 재시도가 중복 실행을 일으킵니다.
이를 막으려면 애플리케이션이 첫 시도 전에 멱등성 키(idempotency key)를 만들고 작업 의도(intent)와 함께 저장해야 합니다. 같은 의도에 대한 재시도에는 같은 키를 써야 하며, 결제 서비스가 키를 지원하면 두 번째 환불 요청은 기존 결과를 돌려주는 무효 작업이 됩니다. 모델이 키를 만들거나 매 시도마다 새 키를 생성하면 중복을 막지 못합니다. 대화 기록에 '이미 환불을 시도했다'는 사실만 남겨도 작업자 재시작, 문맥 잘림, 장애 뒤 복구 과정에서 기록이 사라질 수 있습니다. 실행 전에 영속 저장소에 의도를 기록해야 다른 프로세스도 상태를 이어받습니다.
unknown 상태와 대사
성공과 실패만으로는 실행 결과를 표현하기 어렵습니다. 글은 submitted, confirmed, failed, unknown 상태를 제안합니다. 요청을 보냈지만 답을 받지 못한 경우는 failed가 아니라 unknown입니다. unknown 상태에서는 자동 재시도를 막고, 멱등성 키나 주문 ID로 결제 서비스에 조회하거나 원장을 확인해 실제 결과를 대사해야 합니다. 이를 백그라운드 작업이나 예약 검사로 처리할 수 있고, 금액이 큰 작업은 사람에게 넘길 수 있습니다. 에이전트가 '아마 성공했을 것'이라고 추측해 다음 단계로 가도록 맡기면 안 됩니다.
모델과 애플리케이션의 역할
도구 호출은 LLM이 어떤 절차를 호출할지 고르는 원격 프로시저 호출(RPC)입니다. 불안정한 네트워크에서 RPC를 다룰 때 필요한 멱등성, 영속적인 의도 기록, 불확실성의 명시, 대사는 에이전트에도 그대로 적용됩니다. 모델은 행동을 제안하고 애플리케이션은 키, 작업 기록, 상태, 재시도 정책을 관리합니다. 읽기 요청은 재시도가 기본값이어도 괜찮지만 상태를 바꾸는 작업은 첫 요청이 도달하지 않았음을 확인했거나, 하위 시스템이 중복 적용을 막는 경우에만 안전하게 재시도할 수 있습니다. 이미 결제나 주문 흐름에서 멱등성 키와 아웃박스(outbox), 원장 패턴을 쓴다면 에이전트의 쓰기 요청도 그 경로로 보내야 합니다.
dev.to 반응
- @abhinavs1920 — 에이전트 프레임워크가 자주 잘못 설계하는 부분은 실제로는 세 가지인 상태를 코드에서 '도구 실패' 하나로 합치는 점입니다. 다만 멱등성 해법은 서비스가 키를 받아줘야 작동하고, 많은 서드파티 API는 지원하지 않습니다. 상대 시스템을 믿을 수 없을 때 에이전트가 재시도 전에 상태를 읽게 하나요, 아니면 위험을 감수하나요?
- @cyclopt_dimitrisk — API가 멱등성 키를 지원하지 않으면 재시도 전에 하위 시스템 상태를 확인하겠습니다. 트랜잭션 ID처럼 결과를 신뢰성 있게 확인할 방법이 있으면 좋습니다. 다만 읽은 값이 오래됐거나 모호할 수 있어 위험을 완전히 없애지는 못합니다. 영향이 큰 작업은 에이전트가 추측해 중복 실행하기보다 unknown 상태에 두고 사람에게 넘기겠습니다.
- @jessie_ilands — 반대 방향의 문제도 있습니다. 도구가 200을 반환해 프레임워크가 단계를 완료 처리했지만 실제로는 아무 일도 일어나지 않는 조용한 무동작입니다. 배송 경로가 메시지를 받아들이고 조용히 버리는 경우를 겪었습니다. 호출이 반환됐다는 이유만으로 완료 처리하면 오류나 시간 초과도 없어 이를 잡을 분기가 없습니다. 쓰기 뒤 영속 상태를 다시 읽고 확인된 경우에만 완료로 처리했습니다. 반환값은 요청이 접수됐다는 뜻이고, 다시 읽은 결과가 실제 실행 여부를 보여줍니다. 확인되지 않으면 성공이 아니라 unknown으로 두고 같은 키로 재시도합니다. 멱등성 키는 중복을 막고 읽기 검증은 조용한 실패를 잡으므로 둘 다 필요합니다.
- @cyclopt_dimitrisk — 동의합니다. 시간 초과는 눈에 띄지만 성공 응답이 실제 결과로 이어지지 않는 경우는 더 잡기 어려울 수 있습니다. 요청 접수와 작업 확인을 구분해야 합니다. 단, 다시 읽는 쪽도 충분히 권위 있는 데이터여야 합니다. 최종 일관성 때문에 성공한 작업이 무동작처럼 보일 수 있습니다.
- @jessie_ilands — 읽기는 실제 상태를 소유한 시스템에서 해야 합니다. 방금 쓴 값을 제공하는 캐시에 다시 묻는다면 캐시의 의견만 듣는 셈입니다. 제가 정착한 방식은 방금 쓴 대상을 확인하는 대신 실제로 원하는 결과를 확인하는 것입니다. 행이 저장됐는지, 파일이 존재하는지, 메시지가 수신자의 편지함에 도달했는지 봅니다. 그 외에는 모두 unknown으로 둡니다. unknown은 늦게 성공한 상태가 아니라 실제 상태입니다.
- @kevinhall — 조회 API를 운영하는데 구매자 에이전트가 한 키에 약 1,000행씩 103회 병렬 실행한 일이 있었습니다. 요청의 31%가 429를 받았습니다. 클라이언트는 거절을 실패한 답으로 처리해 고정된 2초 간격으로 여섯 번 재시도한 뒤 행을 오류로 내보냈습니다. 그렇게 빠진 행은 약 2%였고 구매자 데이터에서는 조회 결과가 없는 경우와 구별되지 않았습니다. 재시도마다 대기 시간을 늘리고 Retry-After를 따르되 실행 시간 한도 안에서 처리해 제한을 데이터 손실이 아니라 지연으로 만들었습니다. 행마다 키를 부여해 응답이 유실된 뒤 재시도해도 첫 응답을 돌려받고 중복 과금도 막았습니다.
- @sinarezaei — 동시성 문제도 따로 봐야 합니다. 멱등성 키는 같은 논리 작업의 중복 실행을 막지만 두 작업자가 같은 에이전트 실행을 동시에 처리하는 문제까지 막지는 않습니다. 배포 상태를 기다리던 작업자가 임대를 잃고 큐가 작업을 다시 전달하면 두 번째 작업자가 저장된 체크포인트부터 이어갈 수 있습니다. 두 작업자가 오래된 상태를 보고 각자 결정을 내릴 수 있습니다. 실행 소유권과 부수 효과 보호는 별개로 다뤄야 합니다. 영속 워크플로 상태와 임대 기반 소유권, 지원되는 경우 펜싱 토큰, 업무 작업별 멱등성 키가 필요합니다.
- @cyclopt_dimitrisk — 맞습니다. 멱등성과 실행 소유권은 서로 다른 문제를 해결합니다. 같은 작업이 두 번 실행되지 않게 해도 두 작업자가 오래된 상태를 바탕으로 충돌하는 결정을 내릴 수 있습니다. 임대가 만료돼도 이전 작업자가 깨어나 다시 쓸 수 있으므로 펜싱 토큰이 중요합니다.
- @sinarezaei — 펜싱도 쓰기를 받는 시스템이 토큰을 실제로 확인해야만 작동합니다. 오래된 작업자가 새 작업자의 롤백 결정을 덮어쓰지 못하게 하려면 실행 세대 번호와 예상 상태 버전을 함께 확인해야 합니다. Kubernetes 리소스의 resourceVersion은 오래된 갱신을 거부하는 데 도움이 되지만, 외부 배포 API나 클라우드 제어면까지 보호하지는 않습니다. 그런 시스템에는 자체 동시성 보장이나 실제 상태와 원하는 상태를 비교하는 대사 루프가 필요합니다.
- @glenallen — 여러 단계로 이뤄진 워크플로에서는 부분 완료도 문제입니다. 환불 단계의 중복을 멱등성으로 막아도 환불은 성공하고 주문 상태 갱신은 실패할 수 있습니다. 각 단계를 명시적인 상태 전이로 모델링하고 확인된 단계를 저장하며, 중간 상태마다 복구나 보상 경로를 정해야 합니다. 그래야 재개한 에이전트가 대화 기록에서 무슨 일이 있었는지 추측하거나 전체 작업을 무작정 다시 시작하지 않습니다.
- @alexcodebytes — unknown 상태가 자동 재시도를 막고 대사를 시작하게 해야 한다는 점이 에이전트 프레임워크에서 자주 빠집니다. 도구 호출을 반환값이나 예외만 있는 함수 호출처럼 취급하기 때문입니다. 멱등성 키는 어디서 만들었는지만큼 어디에 저장했는지도 중요합니다. 키를 문맥이나 메모리에만 두면 시도 기록을 거기에만 저장하는 것과 같은 문제가 생깁니다. 주문 ID, 의도 유형, 금액처럼 안정적인 값의 해시로 키를 만들면 의도 기록이 사라져도 같은 논리 작업에 같은 키를 쓸 수 있습니다. 매개변수가 정당하게 바뀔 수 있는 작업에서는 어떻게 처리하나요?
- @vishisht_choudhary_a12d08 — 멱등성 키를 일정 시간 동안만 인정하는 API도 있습니다. Stripe는 약 24시간이 지나면 키를 버릴 수 있으므로 장기 장애 뒤 대사 작업에서 같은 키를 다시 보내도 안전하다고 단정할 수 없습니다. 저는 자동 재시도를 막고 키로 대사하되 조회 결과가 모호할 때만 사람에게 넘깁니다.
- @build996 — 원래 환불이 unknown인 동안 금액을 바꾸는 것은 특히 위험해 보입니다. 변경을 막거나 기존 작업이 해결된 뒤 진행하는 별도 의도로 기록해야 합니다. 그렇지 않으면 서로 다른 키를 가진 환불 두 건이 주문에 각각 적용될 수 있습니다. 대사 기록에는 키뿐 아니라 원래 승인된 내용도 변경 불가능하게 저장하나요?
- @qisuancloud — LLM에는 RPC 재시도와 다른 점이 하나 있습니다. POST /refunds를 다시 보내는 것은 같은 요청이지만 모델 생성을 다시 실행하면 샘플링에 따라 완전히 다른 도구 경로가 나올 수 있습니다. 같은 의도를 다시 시도한 것과 새로운 의도가 나온 것을 구분하려면 의도 기록에 모델 버전, 시드, 정확한 도구 호출 계획도 고정해야 합니다.
- @ascendra_ventures_92c21f2 — 의도 기록에 호출 전에 기대하는 최종 상태도 저장하면 좋겠습니다. 이 주문에 이 금액을 환불하고 원장 항목 하나를 남긴 뒤 상태를 confirmed로 만드는 식입니다. 그러면 대사 기준이 분명해지고 unknown을 판단에 맡기지 않아도 됩니다. 의도가 제공자의 멱등성 키 보관 기간보다 오래 unknown으로 남으면 어떻게 처리하나요?
원문: dev.to / 번역·요약: Trawling