에이전트가 비디오 트랜스코딩 엔드포인트를 호출합니다. 엔드포인트는 202 Accepted와 작업 ID를 반환합니다. 202의 의미를 모르는 에이전트는 트랜스코딩이 완료되었다고 보고하고, 아직 존재하지 않는 파일을 읽는 다음 단계로 넘어갑니다.
장기 실행 작업은 에이전트에 특히 취약합니다. 동기 호출은 보내고, 기다리고, 답변을 받는 명확한 계약을 가집니다. 비동기 호출은 시작과 완료를 분리하며, 그 사이에서 에이전트는 성공을 조기에 선언하거나, 수천 번 폴링하거나, 대화 턴을 몇 분 동안 블로킹할 수 있습니다.
이 가이드는 비동기 계약을 에이전트가 정확히 따르도록 설계하고, 폴링·웹훅·핸드오프를 선택하며, 느린 실패 경로까지 테스트하는 방법을 다룹니다. 에이전트 오류 복구는 API 호출의 실패 자체를 다루고, 이 글은 느리게 성공하거나 실패하는 작업을 다룹니다.
Apidog를 사용하면 프로덕션에서 처음 발견하고 싶지 않은 4분짜리 작업 실패를 재현하고 검증할 수 있습니다.
에이전트가 비동기 작업을 잘못 처리하는 이유
대부분의 문제는 세 가지 습관에서 시작됩니다.
모든
2xx를 완료로 해석합니다.
202 Accepted는 요청이 처리를 위해 수락되었음을 의미할 뿐, 처리가 완료되었다는 뜻은 아닙니다. HTTP 의미론 사양도 이를 명시합니다. 하지만 모델은 응답 본문에 별도 지시가 없으면2xx를 성공 완료로 읽기 쉽습니다.추론 루프에서 폴링합니다.
4분짜리 작업을 2초마다 확인하면 120회의 모델 턴이 필요합니다. 토큰·비용·컨텍스트가 빠르게 소진됩니다. 도구 응답을 컨텍스트 창 밖에 두는 방법도 참고하세요.작업 ID를 잊습니다.
긴 대화 중간의 작업 ID는 압축 과정에서 사라질 수 있습니다. 그러면 에이전트는 진행 중인 작업 자체를 잊습니다.
모델이 잘못 읽을 수 없도록 응답을 설계하세요
가장 효과적인 해결책은 복잡한 아키텍처가 아니라 응답 본문의 명시적인 지시입니다. 상태 코드와 별개로 현재 상태와 다음 행동을 분명히 적으세요.
{
"status": "processing",
"job_id": "job_7f21c",
"message": "트랜스코딩이 시작되었으며 완료되지 않았습니다. 성공을 보고하지 마십시오. 최소 30초 후에 getJobStatus(job_id)로 상태를 확인하십시오.",
"poll_after_seconds": 30,
"estimated_duration_seconds": 240,
"status_url": "/v1/jobs/job_7f21c"
}
사람 대상 API에는 강압적으로 보일 수 있지만, 모델은 상태 코드 추론보다 본문의 명시적 지시를 더 안정적으로 따릅니다. 특히 다음 세 가지가 중요합니다.
- “완료되지 않았음”이라는 명시적 표현
- 다음에 호출할 도구 또는 엔드포인트
- 최소 대기 시간
Google의 장기 실행 작업 AIP-151은 done, error, response를 포함한 단일 Operation 객체 형태를 권장합니다. 모든 느린 엔드포인트를 같은 구조로 통일하면, 에이전트는 하나의 폴링 패턴만 학습하면 됩니다.
상태 응답도 명확하게 유지하세요.
{
"job_id": "job_7f21c",
"status": "processing",
"done": false,
"progress_percent": 45,
"elapsed_seconds": 108,
"poll_after_seconds": 45,
"message": "아직 처리 중입니다. 다음 단계로 진행하지 마십시오."
}
완료 시 결과가 작다면 인라인으로 반환해 세 번째 호출을 없애세요.
{
"job_id": "job_7f21c",
"status": "succeeded",
"done": true,
"result": {
"output_url": "https://cdn.example.com/out/7f21c.mp4",
"duration_seconds": 372
}
}
모델 내부가 아닌 외부에서 폴링하세요
가장 중요한 구현 선택은 대기 로직을 에이전트의 추론 루프가 아니라 도구 래퍼에 두는 것입니다.
import time
def start_and_await_transcode(client, source_url, max_wait=600):
job = client.post("/v1/transcode", json={"source_url": source_url}).json()
job_id = job["job_id"]
delay = job.get("poll_after_seconds", 5)
waited = 0
while waited < max_wait:
time.sleep(delay)
waited += delay
status = client.get(f"/v1/jobs/{job_id}").json()
if status.get("done"):
if status["status"] == "succeeded":
return {"status": "succeeded", "result": status["result"]}
return {"status": "failed", "error": status.get("error")}
delay = min(int(delay * 1.5), 60)
return {
"status": "timed_out",
"job_id": job_id,
"message": f"Still running after {max_wait}s. Job {job_id} continues in the background.",
}
모델 입장에서는 시간이 걸린 뒤 최종 결과를 반환하는 단일 도구 호출입니다. 컨텍스트 안의 폴링 루프, 잊힌 작업 ID, 수백 번의 모델 턴이 모두 사라집니다.
구현 규칙은 간단합니다.
- 서버의
poll_after_seconds힌트로 시작합니다. - 약
1.5x지수 백오프를 사용합니다. - 폴링 간격은 약 60초에서 상한을 둡니다.
- 전체 대기 시간에는 엄격한 상한을 둡니다.
- 타임아웃 시 작업 ID를 반드시 반환합니다.
- 결과는
succeeded,failed,timed_out으로 명확히 구분합니다.
지터가 있는 타임아웃, 재시도 및 백오프는 수치를 조정하기 전에 읽어볼 만한 자료입니다.
수 시간 걸리는 작업에는 래퍼 내부 폴링이 적합하지 않습니다. 대신 다음을 사용하세요.
- 작업 시작 도구
- 작업 상태 확인 도구
- 대화 밖의 영구 저장소
job_id, 작업 설명, 시작 시간을 저장하고, 에이전트가 각 실행 시작 시 진행 중인 작업 목록을 읽게 하세요.
웹훅이 더 나은 답변인 경우
폴링은 단순하고 어디서나 동작합니다. 웹훅은 더 효율적이지만 운영 비용이 더 큽니다. 기본 비교는 웹훅 vs 폴링을 참고하세요.
폴링을 사용하세요
다음 조건이라면 폴링이 적합합니다.
- 작업이 몇 초~몇 분 걸림
- 에이전트가 다음 단계 전에 결과를 기다려야 함
- 공개 콜백 엔드포인트를 운영할 수 없음
대부분의 에이전트 워크로드가 여기에 해당합니다.
웹훅을 사용하세요
다음 조건이라면 웹훅이 적합합니다.
- 작업이 수 시간 걸림
- 에이전트가 작업을 시작한 뒤 다른 일을 해야 함
- 동시에 많은 작업이 실행되어 개별 폴링이 비효율적임
웹훅에는 공개 수신기, 서명 검증, 재시도 처리, 콜백 도착 시 에이전트를 깨우는 메커니즘이 필요합니다. 안정적인 웹훅 설계와 웹훅 서명 확인을 함께 적용하세요.
SSE도 고려하세요
서버 전송 이벤트(SSE)는 연결을 유지해 공개 엔드포인트 없이 진행 상황을 푸시할 수 있습니다. 사람이 지켜보는 대화형 에이전트에 적합합니다. 구현은 SSE를 이용한 API 응답 스트리밍를 참고하세요.
어떤 방식을 선택하든 완료 처리는 멱등성(idempotency) 을 가져야 합니다. 웹훅은 재시도되고, 폴링은 경쟁할 수 있으며, 성공 신호를 두 번 받더라도 다운스트림 단계는 한 번만 실행되어야 합니다. AI 에이전트를 위한 멱등성을 적용하세요.
빠른 경로뿐 아니라 느린 경로도 테스트하세요
비동기 버그는 로컬 스텁이 너무 빨라서 숨겨집니다. 프로덕션에서 4분 걸리는 작업이 테스트에서는 200ms 만에 끝나면, 에이전트는 실제 상태를 경험하지 못합니다.
다음 네 가지 시나리오를 저장하고 CI에서 실행하세요.
1. 정말 느린 작업
처음 몇 번은 processing, 이후에는 succeeded를 반환하도록 상태 엔드포인트를 모의합니다. 래퍼가 폴링·백오프·최종 반환을 수행하는지 검증합니다.
Apidog에서는 요청 횟수 또는 제어 매개변수에 따라 응답이 달라지는 모의를 만들 수 있어, 같은 테스트를 반복 가능하게 실행할 수 있습니다.
2. 늦게 실패하는 작업
세 번은 processing, 그다음에는 오류 본문과 함께 failed를 반환하게 하세요. 에이전트는 폴링 완료를 작업 성공으로 오해하지 않고 실패를 보고해야 합니다. 그렇지 않으면 조용한 데이터 손실이 발생할 수 있습니다.
3. 타임아웃
래퍼의 전체 대기 한계를 넘어서도 계속 processing을 반환하게 하세요. 도구가 예외나 가짜 성공이 아니라, 원래 작업 ID를 포함한 timed_out을 반환해야 합니다.
4. 중복 완료
웹훅 재시도 또는 경쟁 폴링으로 성공을 두 번 전달하세요. 다운스트림 단계가 정확히 한 번만 실행되는지 검증하세요.
더 넓은 계약 검증 방법은 API 계약 테스트를 참고하세요.
문제를 드러내는 세 가지 작업
보고서 생성
재무 에이전트가 90초짜리 분기별 내보내기를 요청합니다. 순진한 도구는 작업 ID를 받고 보고서가 준비되었다고 말한 뒤 깨진 다운로드 링크를 제공합니다. 블로킹 래퍼는 실제 URL이 준비된 뒤 반환합니다.
같은 API라도 대기가 발생하는 위치에 따라 결과가 완전히 달라집니다.
대량 가져오기
운영 에이전트가 20,000개 레코드를 업로드합니다. 가져오기는 8분간 실행되고 14,000번째 행에서 부분적으로 실패합니다.
이 경우 done: true만으로 성공을 선언하면 안 됩니다. 작업은 끝났지만 거부된 행이 있을 수 있습니다. 실패 건수와 오류 목록 접근 경로를 명확하게 반환하고, 다음 단계 전에 에이전트가 읽게 하세요.
모델 학습 및 빌드 파이프라인
40분짜리 학습 실행 또는 CI 빌드는 래퍼 안에서 기다리면 안 됩니다. 작업을 시작하고, ID를 영구 저장소에 기록하고, 턴을 종료하세요. 이후 예약 확인 또는 콜백으로 후속 작업을 깨우면 됩니다.
다중 에이전트 핸드오프 및 컨텍스트 전달은 실행 간 상태를 잃지 않고 전달하는 방법을 다룹니다.
부분 결과에 형태를 부여하세요
장기 작업은 성공과 실패의 중간 상태로 끝날 수 있습니다. 이때 두 가지 상태만 사용하면 실제 결과를 왜곡하게 됩니다. completed_with_errors를 명시적으로 모델링하세요.
{
"job_id": "job_a11f",
"status": "completed_with_errors",
"done": true,
"summary": {
"processed": 20000,
"succeeded": 19860,
"failed": 140
},
"errors_url": "/v1/jobs/job_a11f/errors?limit=50",
"message": "가져오기 완료. 140개 행이 실패하여 기록되지 않았습니다. 성공을 보고하기 전에 오류를 검토하십시오."
}
두 가지 원칙이 중요합니다.
- 개수는 인라인으로 제공해 에이전트가 추가 호출 없이 판단하게 합니다.
- 실패 행은 제한된 URL 뒤에 두어 수백 개 오류 객체가 컨텍스트를 점유하지 않게 합니다.
멈춘 작업을 누군가는 확인해야 합니다
타임아웃 시 작업 ID와 “여전히 실행 중”이라는 메시지를 반환하는 것은 올바른 동작입니다. 하지만 담당자가 없다면 “나중에 확인”은 “아무도 확인하지 않음”이 됩니다.
에이전트가 서비스라면 팀이 이미 모니터링하는 대기열로 라우팅하세요. 코딩 런타임이라면 실행 플랫폼의 작업 상태와 결과를 사용하세요. Sharkly에서는 블록된 상태로 끝난 실행이 실행 상태와 결과와 함께 작업에 남고, 받은 편지함은 사람의 답변이나 검토가 필요한 항목을 일반 업데이트와 구분합니다.
중요한 것은 도구 선택이 아니라 “아직 실행 중” 상태에 담당자를 배정하는 것입니다.
간단한 체크리스트
- 모든 느린 엔드포인트는 작업 ID, 상태 URL, 미완료 사실을 담은 명확한 메시지를 반환합니다.
- 상태 응답에는 해석이 필요한 문자열 대신 불리언
done필드를 포함합니다. - 폴링은 지수 백오프와 엄격한 상한선을 가진 도구 래퍼 안에 둡니다.
- 타임아웃은 작업 ID를 반환해 나중에 재개할 수 있게 합니다.
-
succeeded,failed,timed_out은 별개의 반환 값입니다. - 몇 분을 넘는 진행 중 작업은 대화 외부에 기록합니다.
- 폴링과 웹훅 모두 완료 처리를 멱등적으로 만듭니다.
- 느린 작업, 늦은 실패, 타임아웃, 중복 완료 테스트를 CI에 저장합니다.
응답 문구와 도구 래퍼를 올바르게 설계하면 장기 실행 작업은 에이전트에게 특별한 예외가 아닙니다. 에이전트는 도구를 호출하고, 기다리고, 답변을 받습니다. 이것이 에이전트가 가장 안정적으로 처리하는 계약입니다.
느린 작업 모의를 테스트와 함께 구성하려면 Apidog를 다운로드하세요.
자주 묻는 질문
API는 비동기 시작에 대해 202 또는 200을 반환해야 하나요?
202 Accepted가 적절합니다. 표준 클라이언트에 처리가 아직 완료되지 않았음을 전달합니다. 다만 에이전트는 상태 코드보다 본문을 더 신뢰할 수 있으므로, 202에만 의존하지 말고 명시적 상태·다음 행동·done 필드를 함께 제공하세요.
도구 래퍼는 얼마나 오래 기다려야 하나요?
엔드포인트의 현실적인 최악 시간보다 약간 높게 설정하세요. 일반적으로 2~10분이면 충분합니다. 그 이상이면 대화 턴을 너무 오래 점유하므로, 작업 ID를 저장하고 나중에 확인하는 방식이 낫습니다.
어떤 폴링 간격을 사용해야 하나요?
서버의 poll_after_seconds부터 시작해 약 1.5x씩 백오프하고, 약 60초에서 상한을 두세요. 고정 1초 폴링은 요청을 낭비하고 속도 제한에 걸릴 수 있습니다. 자세한 내용은 속도 제한 초과 가이드를 참고하세요.
에이전트가 기다리는 동안 다른 작업을 할 수 있나요?
오케스트레이터가 동시 도구 호출을 지원할 때만 가능합니다. 지원한다면 작업을 시작하고 독립 작업을 수행한 뒤 상태를 확인하세요. 지원하지 않는다면 블로킹 래퍼가 직접 만든 스케줄러보다 단순하고 오류 가능성도 낮습니다.
에이전트가 성공을 조기에 주장하지 못하게 하려면 어떻게 하나요?
응답 본문에 미완료 상태를 명시하고, 불리언 done 필드를 제공하며, 완료 상태 조회 응답에서만 결과를 반환하세요. 시작 응답에 결과가 없다면 모델이 결과를 보고할 근거도 없습니다.
노트북에서 실행되는 에이전트에서도 웹훅이 동작하나요?
공개 엔드포인트가 없으므로 직접 동작하지 않습니다. 개발 중에는 터널을 사용하거나, 에이전트가 주소 지정 가능한 환경에서 실행될 때까지 폴링을 사용하세요. 터널 방식은 웹훅 서비스로 로컬호스트 API 테스트를 참고하세요.


Top comments (0)