Agent Mentor Learn
에이전트 도구 호출: 에이전트가 실제로 일하게 만들기 · 2 / 6강

레슨 2: 도구 호출 한 번의 전체 왕복

학습 목표:

  • 도구 호출 한 번의 왕복에서 요청과 응답이 각각 지니는 핵심 필드 짚어 내기
  • tool_use / tool_result 코드가 서로 제대로 짝지어졌는지 판별하기
  • 같은 병렬 배치 안의 호출 사이에 있는 데이터 의존을 알아채고, 언제 두 라운드로 쪼개야 하는지 알기
  • '모델이 도구를 호출한다'는 표현 자체가 왜 부정확한지 설명하기

전제: 레슨 1을 읽었고 에이전트에 도구가 필요한 이유를 안다 | 이전: 레슨 1 << | 다음: 레슨 3 >>

JSON 세 덩어리로 시작한다

여러분이 고객 지원 봇을 만들고 있다고 해 봅시다. 사용자가 "주문 ORD-2026-8842가 어디까지 갔는지 확인해 줄래?"라고 묻습니다. 여러분의 코드는 이 메시지를 도구 정의와 함께 모델에게 보냅니다.

새로 생긴 tools 필드에 주목하세요. 이것은 메시지가 아니라, 모델에게 어떤 도구가 손에 있고 각각이 어떻게 생겼으며 어떤 파라미터가 필요한지 알려 주는 명세서입니다.1 이 명세서는 매 요청마다 함께 보내야 합니다. 모델은 이것을 '기억'하지 않으므로, 여러분의 코드가 매번 넣어 주어야 합니다.

모델은 명세서를 읽고, 주문 상태를 곧바로 답하는 대신 이런 것을 반환합니다.

여기서 새로 등장한 것이 둘 있습니다. stop_reason"tool_use"로 바뀌었고, content 배열에 type: "tool_use"인 블록이 새로 생겼습니다. 모델은 주문 정보를 조회하지 않았습니다. 주문 시스템이 어디에 있는지조차 모릅니다. 그저 "이 파라미터로 get_order_status를 대신 호출하고, 그 결과를 알려 달라"고 말하고 있을 뿐입니다.

여기서부터는 여러분의 코드가 이어받습니다. 실제로 주문 시스템에 질의해 결과를 얻고, 그 결과를 다음 요청에 담아 돌려보냅니다.

무엇이 추가되었는지 보세요. 앞 라운드에서 받은 모델의 응답 전체가 messages에 그대로 다시 들어갔고, 그 뒤에 새 user 메시지가 붙었습니다. 그 메시지에는 사용자가 입력한 텍스트가 아니라 type: "tool_result" 블록이 들어 있고, 그 tool_use_id는 방금 모델이 건네준 id와 정확히 일치합니다.

이 요청을 보고 나서야 모델은 비로소 "주문이 상하이 중간 물류센터를 출발했고, 8월 27일 도착 예정입니다" 같은 말을 합니다. JSON 세 덩어리, 역할 전환 세 번입니다. 모델이 요청하고, 여러분의 코드가 실행하고, 결과가 되먹여집니다. 이것이 도구 호출 한 번의 왕복 전부입니다.

stop_reason은 신호이지, 실행 기록이 아니다

초보자가 가장 자주 틀리는 지점이 여기입니다. stop_reason: "tool_use"를 보고 도구가 이미 호출되었다고 여기는 것입니다. 아닙니다. 이것은 모델이 이 메시지를 끝내면서 왜 멈췄는지를 밝히는 이유일 뿐이며, "end_turn"(할 말을 다 했다)이나 "max_tokens"(자리가 다 찼다)와 똑같은 종류의 필드에 값만 다른 것입니다.2

모델은 스스로 데이터베이스를 건드리거나, HTTP 요청을 쏘거나, 셸 명령을 실행하지 않습니다. 모델이 할 수 있는 것은 구조화된 요청을 내보내는 일뿐이고, 나머지는 여러분의 코드나 Anthropic의 서버 몫입니다.3 도구가 "client tools"(호스트 애플리케이션이 실행)와 "server tools"(Anthropic이 대신 실행)로 나뉘는 것도 그래서입니다. 차이는 이 단계를 누가 실행하느냐일 뿐, 모델이 스스로 실행할 수 있느냐가 아닙니다.2

tool_use 블록의 세 필드, 어느 것도 선택이 아니다

앞의 tool_use 블록을 다시 봅시다. 필수 필드는 셋뿐입니다.4

  • id: 이번 호출의 고유 식별자로, toolu_01XYZ... 형태입니다. 하는 일은 딱 하나, 나중에 결과를 돌려보낼 때 짝을 맞추는 것입니다.
  • name: 모델이 고른 도구이며, 여러분의 tools 명세서에 있는 도구 중 하나의 name과 정확히 일치해야 합니다.
  • input: 이번 호출의 파라미터를 담은 객체이며, 여러분이 input_schema에 정의한 규칙을 만족하는 형태여야 합니다.

이 세 필드를 합치면 모델이 표현할 수 있는 전부가 됩니다. "이 idname 도구를 호출하고 싶고, input은 이것이다." 모델은 "세 번 재시도하라" 같은 로직을 덧붙이지 않습니다. 그런 것은 여러분이 호스트 코드에 직접 씁니다. 모델이 파라미터를 덜 틀리도록 도구 인터페이스를 설계하는 법은 레슨 3의 영역입니다. 이 레슨은 이 세 필드가 어떻게 담기고 어떻게 읽히는지에만 관심을 둡니다.

tool_result는 tool_use_id로 짝을 맞춘다

모델의 응답 하나에 tool_use 블록이 둘 이상 들어갈 수 있습니다. 사용자가 "주문 ORD-2026-8842가 어디까지 갔는지, 그리고 ORD-2026-9001이 발송됐는지도 확인해 줄래?"라고 묻는 경우를 생각해 봅시다. 모델은 같은 content 배열에 tool_use 블록 두 개를 넣고, stop_reason은 여전히 "tool_use"입니다.

여러분의 코드는 두 주문을 모두 조회한 다음, 같은 user 메시지의 content 배열에 두 결과를 함께 넣어야 하며, 각 tool_result는 자기 tool_use_id로 자기 호출을 붙잡습니다.

지름길을 택해 첫 번째 tool_use_id 하나만 담아 한 라운드를 보내면, 모델은 대화를 이어 가기를 거부합니다. "앞 라운드에 tool_result를 받지 못한 tool_use 블록이 있다"는 이유에서입니다. 두 블록은 다음 user 메시지에서 함께 붙잡혀야 하며, 두 요청으로 쪼개 나눠 보낼 수 없습니다.5 tool_result 블록에는 선택 필드인 is_error도 있습니다. 도구가 실패했을 때 true로 설정하면 모델은 이번 호출이 문제를 만났다는 것을 알게 됩니다.4

같은 배치의 호출은 서로의 결과를 볼 수 없다

한 번에 묶어 돌려보내는 규칙이 정리되었으니, 더 깊은 함정이 하나 있습니다. 같은 배치 안 tool_use 블록 사이의 데이터 의존입니다.

장면을 바꿔 봅시다. 송금 에이전트에 도구 두 개가 붙어 있습니다. read_balance(account_id)는 잔액을 읽고, withdraw(account_id, amount)는 돈을 빼냅니다. 사용자가 말합니다. "잔액이 충분하면 A001에서 $100을 빼 줘." 모델은 응답 하나에 tool_use 블록 두 개를 돌려줍니다. read_balance({"account_id": "A001"})withdraw({"account_id": "A001", "amount": 100})입니다.

withdrawamount를 보세요. 100입니다. 사용자 문장 속 숫자를 그대로 베낀 것이고, 잔액이 충분한지와는 아무 관계가 없습니다. 모델이 게을러서가 아니라 달리 방법이 없어서입니다. 이 응답을 생성하는 시점에 read_balance는 여전히 '하려고 계획한 일'일 뿐이고, 그 반환값은 아직 존재하지도 않으므로 withdraw가 읽을 수 없습니다. 한 배치의 tool_use 블록 안에서는 어떤 호출도 같은 배치의 다른 호출 결과를 볼 수 없습니다. 그 시점에 그 결과는 실행되지도, 돌려보내지지도 않았기 때문입니다.

그래서 스스로 지켜야 할 선이 하나 있습니다. 쓰기 작업의 파라미터가 이론상 같은 배치 안 읽기 작업의 반환값과 같아야 한다면, 그 두 호출은 같은 응답에 나타나서는 안 됩니다. 진짜 안전한 방법은 두 라운드로 쪼개는 것입니다. 먼저 read_balance만 실행하고, 실제 잔액을 tool_result로 돌려보낸 뒤, 모델이 "잔액이 60뿐이다"를 보고 나서 withdraw를 호출할지, 얼마를 뺄지 판단하게 하는 것입니다.

실제로 통하는 세 가지 방법이 있습니다.

  1. 전제 조건을 도구 description에 적는다. withdrawdescription에 한 줄을 넣습니다. "read_balance가 반환한 최신 잔액을 확인한 뒤에만 호출할 것." 도구 description 자체가 모델이 읽을 수 있는 프롬프트의 일부이고, 모델이 의존 관계를 스스로 알아차리기를 바라는 것보다 훨씬 믿을 만합니다.6
  2. disable_parallel_tool_use로 병렬 실행을 끈다. 요청의 tool_choice{"type": "auto", "disable_parallel_tool_use": true}를 설정하면 모델은 응답 하나당 도구를 최대 하나만 호출합니다.5 먼저 한 번에 하나씩으로 동작을 조인 다음, 단계 사이의 의존 관계를 머릿속에서 분명히 하고, 그다음에야 느슨하게 풀 것을 고려하세요.
  3. 실행 계층에서 한 번 더 막는다. withdraw를 실행하는 코드가 스스로 최신 잔액을 다시 확인하게 하고, 조건이 맞지 않으면 실행을 거부하고, 그 이유를 성공한 척하는 대신 tool_result 에러 정보에 적어 모델이 보게 하세요. 이번에도 모델이 두 호출을 묶어 보내더라도 이 점검이 위험을 잡아냅니다.

그림으로 그려 본다

위의 왕복을 그림으로 그리면 이렇게 됩니다.

mermaid
sequenceDiagram    participant U as 사용자    participant H as 호스트 앱 (여러분의 코드)    participant M as 모델
    U->>H: "내 주문이 어디까지 갔는지 확인해 줘"    H->>M: 요청 (tools 명세서 + messages)    M-->>H: 응답 (stop_reason: tool_use, tool_use 블록)    Note over H: name / input을 읽고 실제 조회를 실행    H->>M: 새 요청 (assistant의 tool_use + user의 tool_result를 messages에 추가)    M-->>H: 응답 (stop_reason: end_turn, 최종 텍스트 답변)    H->>U: "주문이 상하이 중간 물류센터를 출발했습니다..."

이 그림에서 가장 자주 틀리는 단계는 '추가' 화살표입니다. tool_result만 달랑 보내고, 그 라운드의 모델 응답인 tool_use 전체를 messages에 다시 넣는 것을 잊는 것입니다. 그러면 모델은 난데없이 나타난 도구 결과를 받게 되고, 자기가 한 요청의 기록이 컨텍스트에 없으니, 앞뒤가 맞지 않는 말이나 명백한 에러가 나오기 쉽습니다. 올바른 방법은 매 라운드의 응답을 그대로 이력에 저장하는 것입니다. messages는 길어지기만 할 뿐 잘려 나가지 않습니다.3

한 작업에 왕복이 여러 번 필요할 수 있다

위의 예시는 도구 호출 한 번으로 끝났습니다. 실제 상황에서는 모델이 여러 번 오가야 마무리되는 경우가 많습니다. 배포 봇을 떠올려 보세요. 사용자가 말합니다. "서비스를 재시작하고, 로그에 에러가 있으면 알려 줘."

  1. 첫 라운드에 모델이 tool_use를 반환하며 restart_service를 호출하고, 여러분은 실행해 결과를 돌려보냅니다
  2. 둘째 라운드에 모델이 다시 tool_use를 반환하며 에러를 확인하려고 read_logs를 호출하고, 여러분은 실행해 로그를 돌려보냅니다
  3. 셋째 라운드에 모델이 비로소 stop_reason: "end_turn"을 반환하며 텍스트로 요약합니다

호스트 쪽 코드 로직은 본질적으로 루프입니다. stop_reason이 여전히 "tool_use"인 동안에는 도구를 실행하고 결과를 담아 한 라운드를 더 보내고, "end_turn"으로 바뀌면 최종 텍스트를 사용자에게 건넵니다.3

이 루프에는 반복 횟수의 고정된 상한이 없습니다. 사용자 요청 하나에 대해 모델은 도구를 딱 한 번 호출할 수도 있고, 충분히 모을 때까지 대여섯 번 호출할 수도 있습니다. 도구 인터페이스 설계로 왕복 횟수를 줄이는 방법은 레슨 3에서 다룹니다. 이 레슨에서는 이것만 기억하세요. 여러 번의 왕복은 예외가 아니라 일상입니다.

호스트를 바꾸면 필드 이름은 달라지지만 구조는 그대로다

OpenAI 호환 API를 쓰고 있다면 같은 메커니즘이 다른 포장으로 옵니다. 호출 요청은 choices[0].message.tool_calls 배열에 나타나고, 종료 신호는 stop_reason이 아니라 finish_reason이라 불리며, 그 값은 "tool_use"가 아니라 "tool_calls"입니다.7 OpenAI 공식 문서는 이 과정을 "a multi-step conversation between your application and a model via the OpenAI API. When the model calls a function, you must execute it and return the result"(OpenAI API를 통한 애플리케이션과 모델 사이의 여러 단계에 걸친 대화다. 모델이 함수를 호출하면, 여러분이 그것을 실행하고 결과를 반환해야 한다)라고 설명합니다. 모델이 호출 요청을 내고, 애플리케이션이 실행해 결과를 돌려보낸다는 점에서 Claude와 완전히 같습니다.8

필드 이름은 API마다 달라지지만, 뼈대는 보편적입니다. 모델은 요청만 보내고, 실행은 호스트가 맡고, 결과는 식별자를 달고 돌아오며, 여러 라운드를 돌 수 있다는 것 말입니다.

정리

  • 모델은 무엇도 직접 실행하지 않는다. stop_reason: "tool_use"와 하나 이상의 tool_use 블록을 내보낼 뿐이고, 실행은 호스트 애플리케이션에 남는다
  • tool_use 블록의 필수 필드는 셋뿐이다. id(짝 맞추기용), name(고른 도구), input(파라미터)이다
  • 결과는 tool_result 블록으로 돌아가고, tool_use_id는 대응하는 tool_use 블록의 id와 정확히 일치해야 한다
  • 응답 하나에 tool_use 블록이 여럿일 수 있고, 짝이 되는 tool_result 블록들은 여러 요청으로 쪼개지 말고 같은 user 메시지에 담아야 한다
  • 같은 배치의 tool_use 블록은 서로의 실행 결과를 볼 수 없다. 쓰기 작업의 파라미터가 같은 배치 읽기 작업의 반환값에 의존한다면 두 라운드로 쪼개거나, disable_parallel_tool_use로 한 번에 하나씩으로 강제한다
  • 한 작업에 왕복이 여러 번 필요할 수 있다. 호스트 쪽 구현은 본질적으로 루프이며, stop_reason이 여전히 "tool_use"인 동안 실행과 되돌려보내기를 반복하고, "end_turn"으로 바뀌어야 끝난다

>> 레슨 3: 흔한 도구 다섯 유형: 읽기, 쓰기, 실행, 검색, 호출

Footnotes

  1. Define tools — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools

  2. Tool use with Claude — Overview — https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview 2

  3. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2 3

  4. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  5. Parallel tool use — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use 2

  6. Writing effective tools for AI agents—using AI agents | Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents

  7. Tool Use — LM Studio Docs (OpenAI-compatible API) — https://lmstudio.ai/docs/developer/openai-compat/tools

  8. Function calling — OpenAI API Guides — https://developers.openai.com/api/docs/guides/function-calling

연습

01

모델이 이런 응답을 반환했습니다(stop_reason은 "tool_use"입니다).

레벨 1: tool_result 요청 직접 작성하기

여러분은 직접 만든 날씨 조회 함수를 호출해 결과를 얻었습니다. 맑음, 섭씨 26도입니다.

에디터에서 다음 요청에 보낼 완전한 messages 배열(원래의 user 메시지 + 이번 라운드의 assistant 응답 + 여러분이 구성한 tool_result 메시지)을 작성하되, 다음 조건을 지키세요.

  1. tool_use_id가 위 응답의 id와 정확히 일치할 것
  2. tool_resultcontent가 프로그램이 파싱할 수 있는 텍스트일 것(예: JSON 문자열)
  3. 배열의 세 메시지가 user, assistant, user 순서의 role 값을 가질 것
완료 기준 · 로컬에서 확인
02

아래 코드는 '도구를 호출하고 결과를 돌려보내는' 로직을 구현하려 하지만, 세 가지가 모델이 올바른 결과를 얻지 못하게 하거나 대화를 에러로 만듭니다. 문제를 찾아 고친 버전을 작성해 보세요.(모델의 응답에 tool_use 블록이 하나뿐이라고 가정합니다.)

레벨 2: 왕복 코드에서 버그 세 개 찾기
완료 기준 · 로컬에서 확인