플레이북
verified · type: tutorial · verified: 2026-08 · review: 90d · updated: 2026-08-09 · [api, agentic]

툴 콜링 붙이기 — 처음부터 끝까지

모델에 함수를 쥐여주고 루프를 도는 것까지. 순서대로 따라 하는 레시피다. API 필드 레퍼런스는 anthropic-messages-api 를 보라.

언제 이걸 여나: LLM 이 외부 데이터를 읽거나 뭔가를 실행해야 할 때. 언제 필요 없나: 입력이 이미 프롬프트에 다 있고 출력만 받으면 될 때. 그건 structured-output 으로 충분하다.


1. 툴 정의 — 여기가 성능의 8할이다

Anthropic 문서가 못 박는 지점: "description 이 툴 성능을 좌우하는 가장 큰 요인". 스키마보다 설명이 중요하다.

description 에 네 가지를 넣어라 — 최소 3~4문장 (verified 2026-08):

넣을 것
무엇을 하나 "티커 심볼로 현재 주가를 조회한다"
언제 호출하나 "사용자가 특정 종목의 현재가·최근가를 물을 때 쓴다"
언제 안 쓰나 "그 외 기업 정보는 제공하지 않는다"
파라미터 의미·제약 "ticker: 미국 상장 심볼. 소문자도 허용"

"언제 안 쓰나"를 빼먹으면 모델이 아무 데나 툴을 부른다. 반대로 지시를 과하게 강조하면(CRITICAL: 반드시 이 툴을 써라) 오버트리거가 난다 — anti-patterns 참고.

tool = {
    "name": "get_order",                    # ^[a-zA-Z0-9_-]{1,64}$
    "description": (
        "주문번호로 주문 1건의 상태·품목·배송정보를 조회한다. "
        "사용자가 특정 주문의 현황을 물을 때 사용한다. "
        "주문 목록 검색이나 환불 처리는 하지 않는다 — 그건 search_orders / refund_order 를 써라. "
        "order_id 는 'CAH-' 로 시작하는 문자열이다."
    ),
    "input_schema": {
        "type": "object",
        "properties": {"order_id": {"type": "string", "description": "예: CAH-20260731-0042"}},
        "required": ["order_id"],
        "additionalProperties": False,
    },
    "strict": True,   # 스키마를 정확히 지키게 강제 (top-level 필드다, tool_choice 아님)
}

설계 규칙 3가지

  • 툴을 쪼개지 말고 합쳐라. create_pr/review_pr/merge_pr 대신 action 파라미터 하나. 툴이 적을수록 선택 모호성이 준다.
  • 이름에 네임스페이스를 붙여라. github_list_prs, slack_send_message. 툴이 많아지면 필수.
  • 반환값은 고신호만. uuid·256px_image_url 같은 저수준 식별자 대신 name·image_url 같은 의미 있는 값을 돌려주면 검색 환각이 줄어든다. 페이지네이션·필터·잘라내기 기본값을 넣어 컨텍스트를 아껴라.

2. 루프 — 대부분은 툴러너를 써라

그림 — 툴 콜 한 바퀴
유저 질문모델: tool_use 요청앱이 함수 실행tool_result 회신모델: 최종 답
주의병렬 호출의 결과 여러 개는 한 메시지에 담아 돌려준다 — 따로 보내면 모델이 병렬을 끈다
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=16000,
    output_config={"effort": "high"},
    tools=[get_order],          # @beta_tool 로 데코레이트한 함수
    messages=[{"role": "user", "content": user_input}],
)
for message in runner:
    ...   # 각 턴을 관찰만 하면 된다. 호출·결과 반환·재시도는 SDK 가 처리

수동 루프는 커스텀 배칭·실행 순서·에러 가로채기가 필요할 때만 내려가라. 사람 승인 게이트는 수동 루프의 이유가 못 된다 — 툴 함수 안에서 막으면 된다.

수동으로 짤 때 지켜야 할 뼈대:

while True:
    resp = client.messages.create(model="claude-opus-5", max_tokens=16000,
                                  tools=TOOLS, messages=messages)
    if resp.stop_reason == "end_turn":
        break
    if resp.stop_reason == "pause_turn":       # 서버툴 루프 한도. 그대로 이어붙여 재요청
        messages.append({"role": "assistant", "content": resp.content})
        continue

    messages.append({"role": "assistant", "content": resp.content})
    results = []
    for block in resp.content:                 # tool_use 가 첫 블록이라는 보장 없음 — 순회하라
        if block.type == "tool_use":
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": run(block.name, block.input)})
    messages.append({"role": "user", "content": results})

3. 병렬 호출 — 결과는 반드시 한 메시지에

Claude 4 이상은 기본적으로 독립 작업을 병렬로 부른다. 그런데 결과를 툴마다 따로 user 메시지로 보내면 "병렬 하지 마라"고 모델을 학습시키는 꼴이 된다. 병렬이 안 먹는 원인 1위다.

형태
❌ 틀림 user(결과1) → user(결과2) → user(결과3)
✅ 맞음 user([결과1, 결과2, 결과3]) — 한 메시지에 전부

포맷 규칙도 엄격하다 (어기면 400): - tool_result 는 대응하는 tool_use 메시지 바로 다음에. 사이에 다른 메시지 금지. - user 메시지 안에서 tool_result 블록이 먼저, 텍스트는 모든 tool_result 뒤에. - tool_use 하나당 tool_result 하나. tool_use_id 로 매칭.

병렬을 더 밀고 싶으면 시스템 프롬프트에: "독립적인 작업이 여러 개면 순차가 아니라 한꺼번에 호출하라." 반대로 의존 호출이 뭉치면: "서로 독립인 것만 묶어서 호출하라." 끄고 싶으면 tool_choice 객체 안에 disable_parallel_tool_use: true (최상위 파라미터가 아니다).

측정: 메시지당 평균 툴 수가 1.0 을 넘으면 병렬이 돌고 있는 것이다.


4. 에러 — 예외로 던지지 말고 결과로 돌려줘라

툴이 실패해도 tool_result 를 반환한다. is_error: true 를 붙이고 다음에 뭘 해야 할지 적어라.

{"type": "tool_result", "tool_use_id": "toolu_01",
 "content": "Rate limit exceeded. Retry after 60 seconds.", "is_error": True}

"failed" 같은 무의미한 메시지는 금물이다. 파라미터가 틀려서 실패한 경우 이렇게 돌려주면 모델이 스스로 고쳐 2~3회 재시도한다. 그게 반복되면 근본 원인은 description 이거나 strict: true 누락이다.

순차 실행 중 앞 호출이 실패해서 아예 실행하지 않은 호출도 결과를 빼먹지 마라:

{"type": "tool_result", "tool_use_id": "toolu_02", "is_error": True,
 "content": "Not executed: the preceding write_file call failed."}

서버툴(web search 등)의 에러는 모델이 알아서 처리하므로 직접 다룰 필요 없다.


5. 승인 게이트와 신뢰 경계

⚠️ 툴 결과는 신뢰할 수 없는 데이터다. 웹페이지·수신 메일·업로드 파일·서드파티 API 응답에는 간접 프롬프트 인젝션이 섞일 수 있다. 그래서 신뢰 불가 콘텐츠는 system 프롬프트나 일반 user 텍스트가 아니라 tool_result 블록 안에 가둬라. MCP 서버를 쓸 때도 같은 원칙이다 — mcp 보안 섹션.

읽기와 쓰기를 나눠 게이트를 걸어라:

등급 처리
읽기 조회·검색·상태확인 자동 실행
쓰기·비가역 레코드 생성, 메일 발송, 환불, 삭제 실행 전 사람 확인

툴러너를 쓰면서도 가능하다 — 툴 함수 안에서 확인을 받고, 거절되면 "사용자가 거부함"을 결과로 돌려주면 된다. 굳이 수동 루프로 내려갈 필요 없다.


6. 관측성

실패했을 때 어느 단계가 깨졌는지 드릴다운할 수 있어야 한다: 어떤 툴이 왜 선택됐나 → 입력이 뭐였나 → 결과가 뭐였나 → 다음 턴 프롬프트가 어떻게 조립됐나. LangSmith·Langfuse·Phoenix 같은 트레이싱 도구가 이 용도다 (verified 2026-08).

최소한 툴 이름·입력·소요시간·에러 여부는 로그로 남겨라. 툴 description 개선은 감이 아니라 평가 기반 반복으로 해야 효과가 난다.


체크리스트

  • [ ] 모든 툴 description 이 3~4문장이고 "언제 안 쓰나"가 들어 있다
  • [ ] 툴이 잘게 쪼개져 있지 않다 (관련 동작은 action 파라미터로 통합)
  • [ ] 반환값이 고신호다 (저수준 ID 대신 의미 있는 값, 기본 잘라내기 있음)
  • [ ] 병렬 결과를 한 user 메시지에 몰아서 보낸다
  • [ ] tool_result 가 content 배열 맨 앞, 텍스트는 뒤
  • [ ] 에러를 예외로 던지지 않고 is_error + 조치 안내로 돌려준다
  • [ ] 실행 안 한 호출도 결과를 반환한다
  • [ ] pause_turn 을 처리한다
  • [ ] 비가역 동작에 사람 확인이 걸려 있다
  • [ ] 툴 호출 트레이스가 남는다

참고

출처: Anthropic 공식 문서(platform.claude.com, anthropic.com/engineering) [HIGH]. 승인 게이트 2단계 구분은 서드파티 프레이밍 [MED]. 원본 → 2026-08-01-playbooks-tooluse-rag