모델은 도구를 실행하지 않습니다. 도구 설명서를 읽고 '이 도구를 이 인자로 불러 달라'는 주문서(tool_calls)를 돌려줄 뿐이고, 실행해서 결과를 돌려주는 것은 우리 코드입니다. 질문 하나("박도윤 이메일 알려주고, 부산 날씨도 알려줘.")를 넣었을 때 messages 리스트가 턴마다 어떻게 자라는지 따라갑니다. 모델의 결정과 외부 API 결과는 실행마다 달라서 보라색 자리 표시와 예시로 보여 줍니다.
모델 → 판단 → 실행 → ToolMessage → 다시 모델로 도는 고리가 도구 호출 루프입니다. 모델이 주문서를 내지 않으면 고리를 빠져나와 최종 답변으로 갑니다. 고리는 max_turns 번까지만 돕니다. 3일차에 같은 고리를 ReAct 루프라는 이름으로 그래프에 옮깁니다.
tool_calls 가 있다는 것은 실행됐다는 뜻이 아닙니다. 모델이 '이 도구를 이 인자로 실행해 달라'고 요청했다는 뜻입니다. tool_decision.py 에서는 도구가 한 번도 실행되지 않습니다.call["id"] 로 맞춥니다. 주문서가 여러 장이면 결과마다 tool_call_id 가 있어야 어느 주문의 결과인지 구분됩니다. 결과 내용은 146줄에서 str(result) 로 바꾸므로 multiply 가 돌려준 int 도 글자로 갑니다.get_weather 의 except 는 requests.RequestException 만 잡으므로, 응답에 "current" 키가 없을 때의 KeyError 는 run() 밖으로 올라갑니다. wikipedia_summary 의 r.json()(58줄)은 아예 try 밖에 있습니다.max_turns 는 모델 호출 횟수의 상한입니다. 도구 실행 횟수가 아닙니다. 한 바퀴에 주문서가 여러 장이면 도구는 그보다 많이 실행될 수 있습니다.day1_practice/tool_decision.py도구 두 개를 bind_tools 하고, 질문 세 개에 대해 모델이 내는 주문서(tool_calls)만 출력합니다. 실행은 하지 않습니다.day1_practice/tool_playground.py도구 일곱 개(검색, 위키백과, 날씨, 시각, 환율 포함)를 붙여 결정 → 실행 → 최종 답변까지 루프를 돌리고 토큰을 셉니다.