블로그 목록
AI

LLM 스트리밍 중간에 멈추기 — useChat의 stop()과 abortSignal로 응답 취소하기

챗봇을 만들다 보면 이런 상황이 온다. 답변이 한참 길게 나오는 중인데 사용자가 "아, 이거 아닌데" 하고 멈추고 싶어 한다. 아니면 답변 받는 도중에 다른 페이지로 넘어가 버린다.

문제는 여기서 스트리밍을 안 끊으면 서버 쪽 LLM 호출이 계속 돌아간다는 거다. 사용자는 화면을 떠났는데 토큰은 계속 소비되고 있다. 개인 프로젝트에서 BYOK로 붙였을 때 이게 그대로 비용으로 돌아온다.

클라이언트: stop() 한 줄

useChat은 stop 함수를 그냥 준다. 스트리밍 중일 때 이걸 호출하면 연결을 끊는다.

'use client';
import { useChat } from '@ai-sdk/react';

export function Chat() {
  const { messages, sendMessage, status, stop } = useChat();

  return (
    <div>
      {status === 'streaming' && (
        <button onClick={stop}>■ 멈추기</button>
      )}
    </div>
  );
}

status가 streaming일 때만 멈춤 버튼을 보여주는 게 자연스럽다. 여기까지만 하면 화면상으로는 답변이 딱 멈춘다.

서버: 여기서 진짜로 끊어야 한다

근데 클라이언트에서 연결을 끊어도, 서버 라우트가 그걸 감지하고 LLM 호출을 취소하지 않으면 반쪽짜리다. 연결이 끊긴 신호(req.signal)를 streamText의 abortSignal로 넘겨줘야 한다.

// app/api/chat/route.ts
import { anthropic } from '@ai-sdk/anthropic';
import { streamText, convertToModelMessages } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: anthropic('claude-sonnet-5'),
    messages: convertToModelMessages(messages),
    abortSignal: req.signal,
  });

  return result.toUIMessageStreamResponse();
}

req.signal은 클라이언트 연결이 끊기면 자동으로 abort 상태가 된다. 이걸 abortSignal에 물려주면 실제 모델 호출까지 취소된다. 이거 안 해두면 화면만 멈추고 뒤에서는 끝까지 다 돌아간다.

중간까지 온 답변은 저장하고 싶다면

멈춰도 그때까지 스트리밍된 텍스트는 남기고 싶을 때가 있다. onAbort 콜백에서 처리하면 된다.

const result = streamText({
  model: anthropic('claude-sonnet-5'),
  messages: convertToModelMessages(messages),
  abortSignal: req.signal,
  onAbort: ({ steps }) => {
    // 중단 시점까지의 steps로 부분 응답 저장
    console.log('중단됨, 지금까지 단계:', steps.length);
  },
});

onFinish는 abort될 때 안 불린다. 그래서 중단 케이스는 onAbort에서 따로 잡아줘야 한다는 걸 알아두면 좋다.


정리하면 stop()으로 화면을 멈추고, abortSignal: req.signal로 서버 호출까지 끊고, 부분 저장이 필요하면 onAbort를 쓴다. 클라이언트만 끊고 끝내면 비용이 새니까, 서버 쪽 abortSignal을 빠뜨리지 않는 게 핵심이다.