C.W.K.
Stream
Lesson 04 of 06 · published

Python의 tool 루프: 한 라운드에서 여러 라운드까지

~18 min · tool-use, tool-loop, function-calling

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

두-라운드 모양

Tool 호출은 최소 두 round-trip — (1) 너가 tools + user 메시지 보냄; 모델이 tool_use 블록과 함께 stop_reason='tool_use' 반환; (2) 너가 도구 로컬 실행하고 tool_result를 대화에 append; 모델이 최종 답 생성. 진짜 루프는 모델이 stop_reason='end_turn' 반환할 때까지 계속.

도구당 핸들러 하나, 이름으로 dispatch

깔끔한 패턴은 {tool_name: callable} 레지스트리. tool_use 블록 보면 callable 찾고 args로 호출, 결과 append. 한 큰 함수 안에 tool dispatch inline 박는 욕망 거슬러 — 도구 쌓이면서 빠르게 커져.

루프 예산

일부 작업은 정당하게 10+ tool 라운드 원해. 일부는 stuck 모델 신호고 잘라야 해. 항상 max-iteration 예산(10으로 시작) 들고, 중단을 user한테 surface해서 왜 에이전트가 멈췄는지 이해하게.

원칙: Tool 루프는 예산과 레지스트리 가진 통제된 while-true. 둘 다 명시적으로 유지.

Code

Tool 레지스트리와 완전한 루프·python
from anthropic import Anthropic
import json

client = Anthropic()

# 1. 모델한테 보내는 도구 정의.
TOOLS = [
    {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "input_schema": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }
]

# 2. 로컬 핸들러 — tool name당 하나.
def get_weather(city: str) -> dict:
    return {"city": city, "temp_c": 22, "condition": "clear"}

HANDLERS = {"get_weather": get_weather}

# 3. 루프.
def run_loop(user_text: str, max_iters: int = 10) -> str:
    messages = [{"role": "user", "content": user_text}]
    for _ in range(max_iters):
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )
        # tool_results 전에 assistant 턴(전체 content 리스트) append.
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason != "tool_use":
            return next(b.text for b in resp.content if b.type == "text")

        # 모든 tool_use 블록 resolve, tool_results 모두를 한 user 턴에 보냄.
        tool_results = []
        for block in resp.content:
            if block.type != "tool_use":
                continue
            handler = HANDLERS[block.name]
            output = handler(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(output),
            })
        messages.append({"role": "user", "content": tool_results})
    raise RuntimeError("tool loop exceeded max_iters")

print(run_loop("What is the weather in Seoul?"))
한 라운드 안에서 병렬 tool 실행·python
import asyncio

async def execute_block(block, async_handlers):
    output = await async_handlers[block.name](**block.input)
    return {
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": json.dumps(output),
    }

# 모델이 한 턴에 도구 여러 개 호출하면 동시 실행.
async def resolve_round(blocks, async_handlers):
    return await asyncio.gather(*(execute_block(b, async_handlers) for b in blocks))

External links

Exercise

도구 두 개(로컬 함수 하나, HTTP fetch 하나)랑 max_iters 예산 가진 tool 루프 만들어. 단순 weather 쿼리에 루프가 5 iteration 넘으면 fail하는 unit test 추가.
Hint
Tool_use 블록을 모델에 다시 append할 때 string 말고 전체 assistant content 리스트 append했는지 확인.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.