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

The Multi-Turn Tool Loop

~24 min · tools, loop, agent

Level 0Downloader
0 XP0/41 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The four-part flow

  1. User turn — the human asks something.
  2. Assistant turn (with tool_calls) — the model says "to answer this, I need to call get_weather(Tokyo)".
  3. Tool turn — your code executes the function and adds {"role": "tool", "content": "..."} to the conversation.
  4. Assistant turn (final) — the model uses the tool result to answer.

It's a loop, not a single round

The model may call tools again after seeing tool results. You loop: send messages → if response has tool_calls, execute them and append → otherwise return the answer. There's no fixed maximum, but in practice 5–10 iterations covers almost everything; cap the loop to prevent runaways.

Append, don't replace

The conversation is the full message history. Each iteration appends: assistant's tool_calls turn, then the tool's result turn. The model needs the whole history to reason correctly on the next turn. Replacing the assistant turn loses the model's plan.

Code

Production-grade tool loop·python
import httpx, json, glob

OLLAMA = "http://localhost:11434/api/chat"

def get_weather(city: str, unit: str = "celsius") -> str:
    # Stub — replace with a real API call
    return json.dumps({"city": city, "temp": 22, "unit": unit, "condition": "sunny"})

def search_files(pattern: str, directory: str = ".") -> str:
    matches = glob.glob(f"{directory}/{pattern}")
    return json.dumps({"files": matches[:10], "count": len(matches)})

REGISTRY = {"get_weather": get_weather, "search_files": search_files}

def chat_with_tools(model: str, user_message: str, tools: list,
                    max_iters: int = 8) -> str:
    messages = [{"role": "user", "content": user_message}]

    for _ in range(max_iters):
        resp = httpx.post(OLLAMA, json={
            "model": model, "messages": messages,
            "tools": tools, "stream": False,
        }, timeout=120.0).json()

        msg = resp["message"]
        messages.append(msg)

        if not msg.get("tool_calls"):
            return msg["content"]

        for call in msg["tool_calls"]:
            name = call["function"]["name"]
            args = call["function"]["arguments"]
            try:
                result = REGISTRY[name](**args) if name in REGISTRY \
                         else json.dumps({"error": f"Unknown tool: {name}"})
            except Exception as e:
                result = json.dumps({"error": str(e)})
            messages.append({"role": "tool", "content": result})

    return f"<<max iterations ({max_iters}) reached>>"

# Usage — needs both tools in sequence
print(chat_with_tools(
    "qwen2.5:7b",
    "Find all .py files under '.' and tell me the weather in Seoul.",
    tools=[
        {"type": "function", "function": {
            "name": "get_weather",
            "description": "Current weather for a city.",
            "parameters": {"type": "object",
                "properties": {"city": {"type": "string"},
                               "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}},
                "required": ["city"]}}},
        {"type": "function", "function": {
            "name": "search_files",
            "description": "Search files by glob pattern under a directory.",
            "parameters": {"type": "object",
                "properties": {"pattern": {"type": "string"},
                               "directory": {"type": "string"}},
                "required": ["pattern"]}}},
    ],
))

External links

Exercise

Implement chat_with_tools(model, user_message, tools, max_iters). Test with three prompts: one needing zero tool calls, one needing one, one needing two in sequence. Log iteration count for each.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.