Responses API async tool calling 운영법: 긴 에이전트 작업을 끊기지 않게 만드는 방법
요약: OpenAI가 GPT-6 Astra와 함께 Responses API의 async tool calling, mid-turn steering, reasoning effort 변경 기능을 강조했습니다. 긴 에이전트 작업을 제품에 넣으려면 동기 HTTP 요청 하나로 끝내는 구조를 버리고, 작업 상태·도구 실행·사용자 개입을 분리해야 합니다.
문제: 에이전트 작업은 요청 하나 안에 안 끝난다
많은 AI 기능은 아직도 POST /generate 형태로 구현됩니다. 사용자가 요청을 보내면 서버가 모델을 호출하고, 응답을 받아 화면에 보여줍니다. 요약, 번역, 짧은 답변에는 충분합니다. 하지만 에이전트 기능은 다릅니다.
예를 들어 “이 repo의 결제 오류를 찾아서 수정 PR을 만들어줘”라는 작업을 생각해봅시다. 모델은 파일을 검색하고, 로그를 읽고, 테스트를 실행하고, 실패 원인을 좁히고, 코드를 수정하고, 다시 테스트해야 합니다. 외부 도구 하나가 30초 걸릴 수 있고, 전체 작업은 10분 이상 걸릴 수 있습니다. 이걸 동기 HTTP 요청으로 처리하면 타임아웃, 중복 실행, 상태 유실, 사용자 개입 불가 문제가 생깁니다.
async tool calling은 이 문제를 정면으로 다룹니다. 모델이 도구 실행을 요청하면 애플리케이션은 그 도구를 비동기로 실행하고, 결과가 준비되는 대로 이어 붙일 수 있습니다. 핵심은 모델을 오래 붙잡는 것이 아니라 작업 상태를 오래 유지하는 것입니다.
기본 구조: response, job, tool run을 분리한다
실무에서는 세 개의 엔티티를 분리하는 것이 좋습니다.
- Response: 모델과의 논리적 대화 또는 추론 흐름
- Job: 사용자가 요청한 업무 단위
- Tool run: 파일 검색, 테스트 실행, API 호출 같은 개별 작업
이 셋을 하나의 row에 뭉치면 운영이 어려워집니다. response는 모델 공급자 API의 상태를 따라가고, job은 제품 UX의 상태를 따라가며, tool run은 인프라 실행 상태를 따라갑니다. 실패와 재시도 정책도 다릅니다.
예시 상태는 다음처럼 나눌 수 있습니다.
- job: queued, running, waiting_user, waiting_tool, review_required, completed, failed
- tool_run: pending, running, succeeded, failed, timed_out, cancelled
- response: in_progress, requires_tool_result, completed, interrupted
이렇게 저장하면 사용자가 새로고침해도 현재 작업을 복구할 수 있습니다. 워커가 죽어도 어떤 도구 실행이 멈췄는지 알 수 있습니다.
async 도구 실행에서 가장 흔한 장애
첫 번째 장애는 같은 도구를 두 번 실행하는 문제입니다. 네트워크가 끊겨 클라이언트가 재시도하면 서버는 같은 tool call을 다시 받을 수 있습니다. 테스트 실행은 두 번 돌아도 큰 문제가 없을 수 있지만, 결제 취소, 이메일 발송, 배포 같은 도구는 치명적입니다.
해결책은 idempotency key입니다. tool call id와 job id를 묶어 이미 실행한 요청이면 기존 결과를 반환해야 합니다. 외부 API에도 가능하면 idempotency key를 전달합니다.
두 번째 장애는 tool timeout을 모델 실패로 처리하는 것입니다. 도구가 오래 걸린다고 모델을 실패 처리하면 사용자는 “AI가 실패했다”고 봅니다. 실제로는 테스트 워커가 부족하거나 외부 API가 느린 문제일 수 있습니다. tool_run 단위의 timeout과 response 단위의 timeout을 분리해야 합니다.
세 번째 장애는 중간 로그가 없는 것입니다. 긴 작업에서 사용자는 결과보다 진행 상태를 먼저 봅니다. “파일 검색 중”, “테스트 12개 중 9개 통과”, “마이그레이션 파일 수정 중” 같은 이벤트를 저장하고 스트리밍해야 합니다.
mid-turn steering을 UX로 바꾸는 방법
OpenAI는 Responses API에서 WebSocket 기반 mid-turn steering을 언급했습니다. 작업이 진행 중일 때 사용자가 추가 지시를 보낼 수 있다는 뜻입니다. 이 기능은 단순 채팅 입력창으로 붙이면 망가집니다.
사용자가 중간에 보낼 수 있는 지시를 유형화해야 합니다.
- 방향 변경: “테스트 수정 말고 코드만 고쳐”
- 범위 축소: “결제 모듈만 봐”
- 중단: “여기서 멈추고 요약해”
- 승인: “이 diff로 PR 만들어”
- 추가 자료 제공: “이 로그도 참고해”
각 지시가 어떤 상태에서 허용되는지도 정해야 합니다. 이미 배포 도구가 실행 중일 때 방향 변경을 받으면 위험합니다. 반대로 파일 검색 중에는 범위 축소가 유용합니다. steering은 자유 입력이 아니라 작업 제어면입니다.
reasoning effort는 라우팅 정책으로 관리한다
긴 작업에서 reasoning effort를 높이면 품질이 좋아질 수 있지만 비용과 지연 시간이 늘어납니다. 따라서 effort는 사용자가 직접 고르는 토글보다 정책으로 관리하는 편이 안전합니다.
예를 들어 다음 기준을 둘 수 있습니다.
- low: 포맷 변환, 간단 요약, changelog 정리
- medium: 단일 파일 수정, 문서 생성, SQL 쿼리 작성
- high: 다중 파일 리팩터링, 장애 원인 분석, 보안 리뷰
- escalated: 테스트 실패가 반복되거나 사람이 명시적으로 깊은 분석을 요청한 경우
중요한 것은 중간에 effort를 바꿀 수 있어도 무분별하게 올리지 않는 것입니다. 작업 로그에 effort 변경 이유를 남기고, 비용 리포트에서 확인할 수 있어야 합니다.
구현 예시: 큐와 이벤트 스트림
가장 단순한 구현은 서버에 job 테이블, tool_run 테이블, worker queue, 이벤트 스트림을 두는 방식입니다.
- 사용자가 작업을 요청하면 job을 생성합니다.
- 서버가 Responses API response를 시작합니다.
- 모델이 도구 실행을 요청하면 tool_run을 만들고 큐에 넣습니다.
- 워커가 도구를 실행하고 결과를 저장합니다.
- 서버가 결과를 response에 전달합니다.
- 모든 이벤트를 job_events에 저장하고 UI에 스트리밍합니다.
여기서 UI는 모델 토큰 스트림만 보여주면 안 됩니다. tool_run 이벤트, approval 이벤트, safety 이벤트를 같이 보여줘야 합니다. 에이전트 UX에서 “무슨 생각을 했는지”보다 “무슨 작업을 했는지”가 더 중요합니다.
마무리 체크리스트
- 긴 에이전트 작업을 동기 HTTP 요청 하나에 넣지 않습니다.
- response, job, tool_run 상태를 분리합니다.
- 모든 tool call에 idempotency key를 둡니다.
- tool timeout과 모델 실패를 다른 에러로 저장합니다.
- mid-turn steering은 허용 상태와 명령 유형을 정해 노출합니다.
- reasoning effort 변경은 비용 로그와 함께 남깁니다.
- UI에는 모델 출력뿐 아니라 도구 실행 이벤트를 보여줍니다.
async tool calling은 멋진 기능명이 아니라 운영 안정성 기능입니다. 긴 에이전트 일을 제품에 넣고 싶다면 모델보다 먼저 상태 저장, 재시도, 중간 개입, 승인 흐름을 설계해야 합니다.