에러 처리와 재시도
에러 처리와 재시도
LLM 호출은 네트워크 문제나 프로바이더 오류, 도구 실행 오류 등 여러 지점에서 실패할 수 있어요. AI SDK Core는 일반(비스트리밍) 호출과 스트리밍 호출에서 에러를 다루는 방식을 다르게 잡고 있어요. 이 글에서는 try/catch로 잡는 일반 에러, 스트림 안의 에러 파트, 그리고 스트리밍 시작 후에도 재시도하는 streamRetries까지 정리해 볼게요.
출처: 공식문서
본문
일반 에러 처리
일반 호출은 에러를 throw 하므로 try/catch로 잡으면 돼요.
import { generateText } from 'ai';
try {
const { text } = await generateText({
model: "xai/grok-4.6",
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
} catch (error) {
// handle error
}
어떤 종류의 에러가 던져질 수 있는지는 AI SDK의 에러 타입 목록을 참고하면 돼요.
스트리밍 에러 처리
에러 청크를 지원하지 않는 단순 스트림은 스트림 중 에러가 나면 일반 에러처럼 throw 돼요. try/catch로 감싸서 처리하면 되죠. 반면 stream 결과처럼 에러 파트를 지원하는 스트림은 다른 파트와 똑같이 error 파트를 순회하며 처리할 수 있어요. 스트리밍 밖에서 일어난 에러까지 잡기 위해 바깥에 try/catch를 두는 것도 권장해요.
import { StreamProviderError, streamText } from 'ai';
try {
const { stream } = streamText({
model: "xai/grok-4.6",
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
for await (const part of stream) {
switch (part.type) {
case 'error': {
const error = part.error;
if (StreamProviderError.isInstance(error)) {
console.error(error.message, {
type: error.type,
code: error.code,
statusCode: error.statusCode,
isRetryable: error.isRetryable,
});
}
break;
}
case 'abort': {
// handle stream abort
break;
}
case 'tool-error': {
const error = part.error;
// handle error
break;
}
}
}
} catch (error) {
// handle error
}
스트리밍 시작 후 프로바이더 에러 재시도
maxRetries는 모델 호출을 시작하는 동안 발생한 실패만 재시도해요. 스트리밍이 시작된 뒤에 잘 정형화된 프로바이더 에러 이벤트를 받았을 때 재시도하고 싶다면 streamRetries를 설정하면 돼요.
import { streamText } from 'ai';
const { textStream } = streamText({
model: "xai/grok-4.6",
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
streamRetries: 2,
});
스트림 재시도는 실패한 모델 스텝만 같은 대화·생성 컨텍스트로 다시 실행해요. 이미 완료된 앞선 스텝(그 도구 호출·결과 포함)은 다시 재생되지 않아요. 실패한 시도의 도구 입력·호출·승인 요청·클라이언트 도구 실행은 폐기되고, 성공한 모델 호출 완료 후에만 노출되거나 실행돼요.
잘 정형화된 프로바이더 에러 이벤트는 StreamProviderError 인스턴스로 정규화돼요. 같은 인스턴스가 onError에 전달되고, 복구가 요청되지 않거나 재시도가 소진되면 전체 스트림의 error 파트로도 전달돼요. 복구 여부를 결정할 때 isRetryable 메타데이터를 쓰면 돼요.
재시도는 여전히 같은 논리 스텝의 일부예요. onStepStart는 그 스텝에 한 번만, onLanguageModelCallStart는 프로바이더 호출 시도마다, onLanguageModelCallEnd는 모델 호출 완료에 도달한 시도에서만 실행돼요.
onError에서 동적으로도 결정할 수 있어요. streamRetries: 0으로 두면 자동 재시도는 끄고 콜백 지향 복구만 쓰는 셈이에요.
const result = streamText({
model: "xai/grok-4.6",
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
streamRetries: 0,
onError: ({ error }) => {
if (isTransientProviderError(error)) {
return { retry: true };
}
},
});
콜백 지향 복구는 논리 스텝당 한 번으로 제한돼요. 자동 재시도가 설정된 경우 onError가 자동 재시도 예산이 소진된 뒤 한 번 더 재시도를 요청할 수 있어서, 한 스텝의 총 복구 호출 수는 streamRetries + 1로 묶여요.
주의할 점이 몇 가지 있어요. 프로바이더 에러 전에 이미 내보낸 비-도구 출력은 되돌릴 수 없어서, 재시도된 스텝이 중복되거나 다른 부분 텍스트를 스트림에 추가할 수 있어요. 또 실패한 시도의 출력은 복구된 스텝 결과·구조화 출력 파싱·응답 메시지·이후 모델 스텝에서 모두 제외되고, 최종 요청·응답 메타데이터는 복구된 시도의 것을 써요. 재시도는 지연을 더하고 추가 프로바이더 사용량·비용이 들 수 있어요.
실패 시도 격리는 AI SDK 클라이언트 도구의 실행을 막지만, 프로바이더가 실행한 도구가 이미 한 일은 되돌리지 못해요. 프로바이더 측 부수 효과를 다시 실행해 비용이 중복될 수 있으니, 멱등성 제어를 써야 해요.
스트림 중단 처리
채팅의 정지 버튼처럼 AbortSignal로 스트림이 중단되면 onAbort 콜백이 호출돼요. 이때 onEnd는 호출되지 않으므로, 저장된 메시지를 갱신하는 등의 정리 작업을 onAbort에서 하면 돼요.
const { textStream } = streamText({
model: "xai/grok-4.6",
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
onAbort: ({ steps }) => {
// Update stored messages or perform cleanup
console.log('Stream aborted after', steps.length, 'steps');
},
onEnd: ({ steps, totalUsage }) => {
// This is called on normal completion
console.log('Stream completed normally');
},
});
onAbort는 중단 전까지 완료된 모든 스텝을 담은 steps 배열을 받아요. 스트림을 직접 순회하며 abort 타입 파트로 중단을 감지할 수도 있어요.
더 알아보기
- 설정(Settings) —
maxRetries,timeout,abortSignal등 전송·재시도 옵션 - 테스팅 — 에러 케이스를 포함한 호출 검증
- 라이프사이클 콜백 —
onError외의 관찰 지점