byteforce

CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API

도구 사용

세분화된 도구 호출

Fine grained tool calling

도구 호출과 스트리밍을 함께 쓰면, 도구 인자가 만들어지는 동안 큰 지연 뒤에 한꺼번에 텍스트가 쏟아지곤 합니다. 그 이유(API의 JSON 검증 버퍼링)를 알아보고, 그 지연을 없애는 fine-grained tool calling 기능을 켜 봅니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

이번 영상에서는 도구 사용과 스트리밍을 결합합니다. 앞서 보았듯, 스트리밍을 켜고 Claude에 요청하면 처음 응답을 받은 뒤 여러 이벤트가 연달아 옵니다. 각 이벤트는 사용자에게 보여 줄 만한 텍스트 조각을 담고 있죠. 흔히 살펴야 하는 이벤트가 Content Block Delta입니다.

도구를 스트리밍과 함께 쓰려면 처리해야 할 이벤트가 하나 더 생깁니다 — input JSON 이벤트입니다. 이 객체에는 중요한 속성이 둘 있습니다. 첫째 partial_json — Claude가 도구에 보내려는 인자의 일부를 나타내는 JSON 조각. 둘째 snapshot — 지금까지 받은 모든 partial JSON 조각을 누적해 합친 것입니다.

이를 보여 주려고 003_tool_streaming 노트북을 준비했습니다(이 강의에 첨부). 이 노트북에는 수정된 run_conversation이 들어 있고, 새 chat_stream 함수로 Anthropic API에서 응답 스트림을 엽니다. 그 스트림의 각 chunk를 종류에 따라 처리하는데, input_json chunk를 받으면 지금은 그냥 출력만 합니다. 아래쪽에는 새 도구 save_article과 프롬프트를 준비해 두었습니다. 실행하면 응답이 chunk 단위로 빠르게 들어오고, 도구 호출 인자도 함께 보입니다.

사실 도구 스트리밍은 이게 거의 전부입니다 — 추가 이벤트 하나만 처리하면 됩니다. 다만 한 가지, 아주 특수한 기능을 더 소개하고 싶습니다. 노트북을 다시 돌려 보면 텍스트는 chunk 단위로 잘 나타나는데, 도구 호출 인자는 몇 초 기다려야 하고, 그러다 갑자기 거대한 덩어리가 한꺼번에 나타납니다. 이 큰 지연과 한꺼번에 쏟아지는 동작이 거슬릴 수 있는 경우(정밀한 UI 업데이트나 빠른 후처리)를 위해, 그 이유와 해결 기능을 설명합니다.

먼저 도구를 살짝 손봤습니다. 여전히 학술 논문을 생성하지만 입력 구조가 조금 다릅니다. 예시 입력을 보면 abstract(논문 요약 같은 짧은 문장)와 meta 객체가 있고, meta 안에는 word_countreview(긴 리뷰)가 들어갑니다. 여기서 짚을 점 — 전체 큰 객체의 최상위 키(top-level key)는 abstract와 meta 둘뿐입니다. meta는 다시 하위 키-값을 가리키지만, 최상위 키는 abstract와 meta입니다. 이 점을 기억해 두세요.

이제 스트리밍 모드에서 도구 인자를 생성할 때 API 내부에서 무슨 일이 벌어지는지 봅니다. 요청이 API에 도착하면 Claude가 도구 입력이 될 JSON을 생성하기 시작합니다. JSON은 한 번에 만들어지지 않고 chunk 단위로 만들어집니다. 그런데 놀랍게도 API는 이 chunk들을 곧장 우리 서버로 돌려보내지 않고 잠시 붙잡아 둡니다. 이유는 Claude가 가끔 유효하지 않은 JSON을 생성할 수 있기 때문입니다. 그래서 API는 유효한 JSON을 보내려고 검증 단계를 거칩니다.

검증은 이렇게 동작합니다. 최상위 키는 abstract와 meta 둘. API는 전체 객체가 끝나길 기다리지 않고, 최상위 키-값 하나가 완성될 때까지 기다립니다. abstract의 경우, 그 값 문자열의 닫는 따옴표가 보일 때까지 기다립니다(예: chunk 4번). 그 닫는 따옴표가 보이면 키-값 하나가 생성된 것이므로, 그 키-값만 따로 떼어 우리가 준 JSON 스키마에 비춰 검증합니다. 유효하면, 원래의 각 chunk들을 그제야 우리 서버로 내려보냅니다. 주의 — 우리는 키-값 하나를 통째로가 아니라 개별 chunk들을 받습니다. 다만 버퍼링됐다가 거의 동시에 도착하는 것입니다.

그다음 meta 최상위 키도 같은 방식 — 전체가 완성될 때까지 기다리고, 검증하고, 개별 chunk들을 내려보냅니다. 그래서 스트리밍을 켜도 chunk들이 API에서 버퍼링되며 큰 멈춤이 생기고, 그러다 한꺼번에 큰 덩어리가 나타나는 것입니다.

이 동작이 괜찮을 수도 있습니다. 하지만 사용자에게 최대한 빨리 업데이트를 보여 주거나, 도구 처리를 최대한 빨리 시작하고 싶다면 이 멈춤이 거슬립니다. 그럴 때 쓰는 기능이 Fine-Grained Tool Calling입니다. 핵심은 단 하나 — JSON 검증 단계를 끕니다. 그러면 API는 chunk 몇 개를 모아 그대로 내려보내, 더 전통적인 스트리밍 출력을 보게 됩니다. 다시 강조하면, fine-grained를 켜면 JSON 검증이 꺼지므로 서버 코드는 유효하지 않은 JSON을 받을 수 있다고 가정하고 적절한 에러 처리를 해야 합니다.

노트북은 이미 fine-grained용으로 준비돼 있습니다. run_conversation 호출에 fine_grained 인자를 더해 켜면, 도구 호출에 이르렀을 때 훨씬 고전적인 스트리밍 — 조금씩 chunk 단위로 — 을 보게 됩니다. 예컨대 word_count 값이 중요하다면, fine-grained 없이는 뒤의 텍스트가 다 생성될 때까지 기다려야 word_count를 얻지만, 켜면 훨씬 빨리 얻습니다.

유효하지 않은 JSON이 나오면 무슨 일이 생길까요? 거의 확실히 invalid JSON을 만들어 내는 프롬프트를 붙이고, force tool call(tool_choice로 save_article을 항상 호출하도록 강제)을 더해 실행합니다. 처음엔 잘 가다가 곧 에러가 납니다. 이 프롬프트는 word_count 값을 undefined로 강제하는데, undefined는 JSON에서 유효한 값이 아닙니다(JSON의 대응값은 null). 그래서 파싱 에러가 나고, undefined 때문이라는 메시지를 보게 됩니다.

fine-grained가 아니었다면? 검증 단계가 동작합니다. 이때 API는 meta 객체를 넣긴 하는데, 객체가 아니라 그 전체를 문자열로 감싸 버립니다. 우리가 준 스키마는 meta가 word_count(number)·review(string)를 가진 객체여야 한다고 했는데, 이제 meta가 객체가 아닌 문자열이 되어 JSON 스펙을 사실상 따르지 않게 됩니다.

정리하면 — 도구 스트리밍 자체는 어렵지 않고, 기존 스트리밍 파이프라인에 쉽게 더할 수 있습니다. 기본 도구 스트리밍에서는 큰 최상위 키-값을 생성할 때 API 검증 단계 때문에 지연이 생깁니다. 그게 문제라면 fine-grained tool streaming을 켜 더 고전적인 스트리밍을 얻되, 검증 단계를 포기하는 것입니다.

이 장에서 배우는 것What you'll learn

약 9분
1

스트리밍 + 도구 → 새 이벤트 input_json(partial_json · snapshot)을 처리

2

chat_stream + chunk 종류별 분기 — input_json이면 일단 출력

3

기본 스트리밍은 API가 JSON을 버퍼링·검증 → 큰 지연 뒤 덩어리로 도착

4

검증 단위는 최상위 키-값(abstract·meta) 하나씩 — 닫는 따옴표를 기다린다

5

fine-grained tool calling: 검증을 끄고 고전적 chunk 스트리밍을 얻음

6

대가 — 유효하지 않은 JSON이 올 수 있으니 에러 처리 필수

먼저 짚고 갈 용어
input_json (이벤트)
스트리밍 + 도구일 때 오는 이벤트. 도구 인자 JSON 조각을 나른다.
partial_json
Claude가 보내려는 인자의 일부 JSON 조각.
snapshot
지금까지 받은 partial_json을 누적해 합친 현재까지의 전체.
top-level key (최상위 키)
전체 입력 객체의 가장 바깥 키. 예시에선 abstract·meta. API 검증의 단위.
fine-grained tool calling
베타 헤더로 켜는 기능. API의 JSON 검증을 꺼 chunk를 즉시 흘려보낸다.

스트리밍 + 도구 = 새 이벤트 하나

The input_json event

스트리밍을 켜고 도구를 쓰면 처리할 이벤트가 하나 늘어납니다 — input_json. 여기엔 partial_json(인자 JSON 조각)과 snapshot(지금까지의 누적)이 담깁니다. chat_stream으로 스트림을 열고, chunk 종류에 따라 분기만 더하면 됩니다.

003_tool_streaming.ipynb · chat_stream
def chat_stream(
    messages, system=None, temperature=1.0,
    stop_sequences=[], tools=None, tool_choice=None,
    betas=[],
):
    params = {
        "model": model,
        "max_tokens": 1000,
        "messages": messages,
        "temperature": temperature,
        "stop_sequences": stop_sequences,
    }
    if tool_choice: params["tool_choice"] = tool_choice
    if tools: params["tools"] = tools
    if system: params["system"] = system
    if betas: params["betas"] = betas

    # 스트리밍 응답을 연다 (beta 네임스페이스)
    return client.beta.messages.stream(**params)

스트림의 각 chunk를 종류별로 처리합니다. input_json chunk를 받으면 지금은 그냥 출력만 합니다.

run_conversation · chunk 분기 (+ fine_grained 인자)
def run_conversation(messages, tools=[], tool_choice=None, fine_grained=False):
    while True:
        with chat_stream(
            messages,
            tools=tools,
            betas=["fine-grained-tool-streaming-2025-05-14"] if fine_grained else [],
            tool_choice=tool_choice,
        ) as stream:
            for chunk in stream:
                if chunk.type == "text":
                    print(chunk.text, end="")

                if chunk.type == "content_block_start":
                    if chunk.content_block.type == "tool_use":
                        print(f'\n>>> Tool Call: "{chunk.content_block.name}"')

                # -- 새 이벤트: 도구 인자 JSON 조각 --
                if chunk.type == "input_json" and chunk.partial_json:
                    print(chunk.partial_json, end="")

                if chunk.type == "content_block_stop":
                    print("\n")

            response = stream.get_final_message()

        add_assistant_message(messages, response)
        if response.stop_reason != "tool_use": break

        tool_results = run_tools(response)
        add_user_message(messages, tool_results)
        if tool_choice: break
    return messages

왜 큰 지연 뒤 덩어리로 오나

Buffering & validation

텍스트는 chunk로 잘 흐르는데, 도구 인자는 몇 초 멈췄다가 한꺼번에 쏟아집니다. 이유는 API가 chunk를 곧장 보내지 않고 붙잡아 검증하기 때문입니다 — Claude가 가끔 유효하지 않은 JSON을 만들 수 있으니까요.

save_article 스키마 · 최상위 키는 abstract·meta
from anthropic.types import ToolParam

save_article_schema = ToolParam({
    "name": "save_article",
    "description": "Saves a scholarly journal article",
    "input_schema": {
        "type": "object",
        "properties": {
            "abstract": {"type": "string", "description": "..."},
            "meta": {                       # 최상위 키 (객체)
                "type": "object",
                "properties": {
                    "word_count": {"type": "integer"},
                    "review": {"type": "string"},
                },
                "required": ["word_count", "review"],
            },
        },
        "required": ["abstract", "meta"],   # 최상위 키: abstract, meta
    },
})
기본 스트리밍 · API가 최상위 키-값을 검증하는 단위
abstract
닫는 따옴표까지 대기 → 검증 → chunk 방출
meta
}
객체 전체 완성까지 대기 → 검증 → chunk 방출

검증은 전체 객체가 아니라 최상위 키-값 하나씩 일어납니다. 그래서 큰 키-값일수록 멈춤이 길고, 통과하면 그 키-값의 chunk들이 거의 동시에 쏟아집니다.

검증 단위 = 최상위 키-값

API는 전체 객체를 기다리지 않고 최상위 키-값 하나(예: abstract)가 끝날 때까지 — 그 값 문자열의 닫는 따옴표가 보일 때까지 — 기다린 뒤 그 조각만 스키마로 검증합니다. 통과하면 개별 chunk들을 그제야 내려보냅니다(통째로가 아니라 거의 동시에 도착).

fine-grained로 검증 끄기

Disable the validation step

최대한 빨리 UI를 갱신하거나 도구 인자를 일찍 받아 처리하고 싶다면, fine-grained tool calling을 켭니다. 핵심은 단 하나 — API의 JSON 검증을 끕니다. 베타 헤더 fine-grained-tool-streaming-2025-05-14betas로 넘기면 됩니다.

일반 vs fine-grained 비교 · 버튼을 눌러 같은 도구 호출을 스트리밍
대기 중 — 스트리밍 실행을 누르세요

기본 모드는 abstract·meta가 통째로 멈췄다가 한꺼번에 도착합니다. fine-grained로 바꿔 다시 실행하면 word_count를 훨씬 일찍 볼 수 있습니다.

시연용 모형입니다. 핵심 차이 — 기본은 최상위 키-값 단위로 버퍼링(큰 지연 뒤 덩어리), fine-grained는 검증을 꺼 chunk가 도착하는 즉시 흘러나옵니다.

아래는 유효하지 않은 JSON이 나오는 경우입니다. word_count를 undefined로 강제하는 프롬프트에 tool_choice로 호출을 강제하고 fine-grained를 켜면, 검증이 꺼져 있어 invalid JSON이 그대로 흘러와 파싱 에러가 납니다.

fine-grained ON + 도구 호출 강제
# fine-grained 켜기 + (시연용) 도구 호출 강제
run_conversation(
    messages,
    tools=[save_article_schema],
    fine_grained=True,
    tool_choice={"type": "tool", "name": "save_article"},
)
출력 · invalid JSON 에러
>>> Tool Call: "save_article"
{"abstract": "...", "meta": {"word_count": undefined ...

# fine-grained ON: 검증이 꺼져 invalid JSON이 그대로 흘러옴
# undefined 는 JSON에서 유효하지 않음 (대응값은 null)
Error: ... invalid JSON ... "word_count": undefined
대가

fine-grained를 켜면 검증이 꺼지므로, 서버 코드는 유효하지 않은 JSON을 받을 수 있다고 가정하고 적절한 에러 처리를 해야 합니다. (검증이 살아 있으면 API는 invalid한 meta를 객체 대신 문자열로 감싸 스펙을 어기기도 합니다.)

정리 & 점검

Recap & check
핵심 정리
  • 스트리밍 + 도구 → 새 이벤트 input_json(partial_json·snapshot) 처리만 추가.
  • 기본 스트리밍은 API가 최상위 키-값 단위로 JSON을 버퍼링·검증 → 큰 지연 뒤 덩어리.
  • fine-grained-tool-streaming-2025-05-14 베타로 켜면 검증을 고전적 chunk 스트리밍.
  • 대가 — 유효하지 않은 JSON이 올 수 있으니 에러 처리 필수.

Q1스트리밍 + 도구일 때 추가로 처리할 이벤트는?

Q2기본 스트리밍에서 도구 인자가 큰 지연 뒤 덩어리로 오는 이유는?

Q3fine-grained tool calling이 하는 일은?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.