블로그 목록
ai

Claude 파인그레인드 툴 스트리밍으로 툴 입력 실시간으로 흘리기 — JSON 완성될 때까지 기다리지 말자

툴을 하나 만들었다. Claude가 주제를 받으면 write_article 툴을 호출하면서 body에 글 본문을 통째로 넣어 넘기는 구조였다. 그런데 스트리밍으로 받는데도 화면이 한참 비어 있었다. 본문이 수천 토큰이다 보니, 그게 다 생성되고 나서야 한꺼번에 툭 떨어졌다.

이유를 찾아보니 기본 동작이 그랬다. Claude는 툴 입력(tool_use의 input)을 완전한 JSON으로 다 만든 뒤 검증하고 나서 내보낸다. 텍스트 응답은 토큰 단위로 흘려주면서, 툴 인자만 버퍼링을 한 번 거친다. 인자가 짧으면 티가 안 나는데, 긴 본문을 인자에 담으면 그 시간이 고스란히 빈 화면으로 남는다.

베타 헤더 하나면 버퍼링이 사라진다

anthropic-beta: fine-grained-tool-streaming-2025-05-14 헤더를 붙이면 Claude가 툴 입력 JSON을 생성되는 대로 그대로 흘려준다. 검증을 건너뛰기 때문에 첫 조각이 바로 날아온다.

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

const stream = await client.messages.create(
  {
    model: 'claude-sonnet-4-5',
    max_tokens: 8192,
    stream: true,
    tools: [
      {
        name: 'write_article',
        description: '주제를 받아 블로그 글 초안을 작성한다',
        input_schema: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            body: { type: 'string' },
          },
          required: ['title', 'body'],
        },
      },
    ],
    tool_choice: { type: 'tool', name: 'write_article' },
    messages: [{ role: 'user', content: 'Next.js App Router 캐싱에 대한 글 써줘' }],
  },
  {
    headers: { 'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14' },
  },
);

let jsonBuffer = '';
for await (const event of stream) {
  if (
    event.type === 'content_block_delta' &&
    event.delta.type === 'input_json_delta'
  ) {
    jsonBuffer += event.delta.partial_json;
    process.stdout.write(event.delta.partial_json); // 오는 대로 바로 화면에
  }
}

이러면 body에 글이 채워지는 과정이 실시간으로 보인다. 긴 결과를 뱉는 툴일수록 체감 차이가 크다.

대신 책임은 내가 진다

편한 만큼 주의할 게 있다. 버퍼링과 검증을 건너뛴다는 건, 중간 조각이 깨진 JSON일 수 있다는 뜻이다. 그래서 오는 족족 JSON.parse를 돌리면 터진다. 조각은 화면에 흘리기만 하고, 파싱은 스트림이 끝난 뒤 한 번만 한다.

const toolInput = JSON.parse(jsonBuffer);

한 가지 더. max_tokens에 걸려 응답이 중간에 잘리면 JSON도 반쪽만 온다. 평소엔 서버가 잡아주던 걸 이제 내가 막아야 하니, 마지막 message_delta의 stop_reason이 max_tokens인지 확인하고, 그러면 파싱을 건너뛰거나 재시도하는 가드를 꼭 넣자.

정리하면, 긴 인자를 담는 툴에서 첫 응답 지연이 거슬린다면 이 베타가 답이다. 다만 "JSON 검증은 서버가 알아서" 해주던 안전장치가 사라지므로, 끝에서 한 번만 파싱 + stop_reason 체크, 이 두 가지를 세트로 챙기면 된다.