20 min

Measure the problem

Tool list and tool output are two problems of two different magnitudes.

Milestone A script puts the tokens of a tool list next to those of a single scraped page, and the factor is printed as a number.

The usual way into this subject is a claim: "too many tools blow up the context". This course starts one step earlier, because in that form the claim is false. Tools do not blow up the context. Their outputs do.

That is not hair-splitting, it is the difference between two solutions. Whoever sees the first problem builds a filter. Whoever sees the second builds a boundary. And whoever only builds the filter has solved the cheaper of the two.

So we count first. This chapter ends with an output you will be quoting several times later on.

The goal

Two problems, two magnitudes. A tool definition costs roughly 200 tokens. A tool output costs, depending on the tool, a hundred to a thousand times that. As long as both are called "the context problem", you look for the fix in the wrong place.

The scaffolding

Create your repo. Two levels, because tiny-agents is both the project and the package inside it - the same layout as any Python package:

tiny-agents/                ← your repo
├── tiny/                   ← the package
│   └── __init__.py         (empty)
└── scripts/
    └── 01_token_vergleich.py
mkdir -p tiny-agents/tiny tiny-agents/scripts
cd tiny-agents
touch tiny/__init__.py
python -m venv .venv && source .venv/bin/activate
pip install httpx pyyaml numpy tiktoken

tiktoken is the counter. It is calibrated on OpenAI models and yours is probably a different one - so the numbers are accurate to within a few percent, not exact. For a factor of 100 that is more than enough, and the alternative would be tracking down the right tokenizer for every model. Say so in the output and the number stops lying.

Step 1: what a tool definition really costs

A definition is the JSON schema that ends up in the tools field of the request. Not the call, not the answer - the pure description that rides along in every single turn.

# scripts/01_token_vergleich.py
import json
import tiktoken

ENC = tiktoken.get_encoding("cl100k_base")


def tokens(text: str) -> int:
    return len(ENC.encode(text))


def tool_definition(i: int) -> dict:
    """A realistically verbose definition, the kind a real tool has."""
    return {
        "type": "function",
        "function": {
            "name": f"crm_lookup_contact_{i}",
            "description": (
                "Looks up a contact in the CRM by name, email address or customer "
                "number and returns master data, the associated company and the "
                "most recent activities. Use this tool when a specific person is "
                "being asked about. Do not use it for company searches and not to "
                "create new contacts."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search term: name, email or customer number.",
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum number of hits, default 5.",
                    },
                    "include_activities": {
                        "type": "boolean",
                        "description": "If true, the most recent activities are included.",
                    },
                },
                "required": ["query"],
            },
        },
    }


def cost_of_tool_list(n: int) -> int:
    schema = [tool_definition(i) for i in range(n)]
    return tokens(json.dumps(schema, ensure_ascii=False))

Call that for a single tool and look at the number before you read on. It sits somewhere between 150 and 250. That is the price of the model knowing this tool exists - regardless of whether it is ever used.

Step 2: what a single tool output costs

Now the other side. We fetch a real page and convert its text content into tokens. No library for that, just the standard library plus httpx:

# scripts/01_token_vergleich.py (continued)
import re
import httpx

DROP_TAGS = re.compile(r"<(script|style)[^>]*>.*?</\1>", re.S | re.I)
DROP_MARKUP = re.compile(r"<[^>]+>")
WHITESPACE = re.compile(r"\n{3,}")


def page_as_text(url: str) -> str:
    html = httpx.get(url, timeout=30, follow_redirects=True).text
    without_scripts = DROP_TAGS.sub(" ", html)
    text = DROP_MARKUP.sub(" ", without_scripts)
    return WHITESPACE.sub("\n\n", text).strip()


def cost_of_page(url: str) -> tuple[int, int]:
    raw = httpx.get(url, timeout=30, follow_redirects=True).text
    return tokens(raw), tokens(page_as_text(url))

Two numbers, because both occur in practice: some tools push raw HTML into the context (the bad case), good ones push extracted text (the normal case). The difference between them is the first and cheapest compression there is - and it still is not enough.

Step 3: put them side by side

# scripts/01_token_vergleich.py (end)
URL = "https://en.wikipedia.org/wiki/Tool"

def main() -> None:
    raw, text = cost_of_page(URL)
    print("Counter: tiktoken/cl100k_base (approximation, model-dependent +/- a few percent)\n")
    print(f"{'What':<44}{'Tokens':>10}")
    print("-" * 54)
    for n in (1, 5, 25, 50, 100, 200):
        print(f"{n:>3} tool definitions in the schema{'':<11}{cost_of_tool_list(n):>10}")
    print("-" * 54)
    print(f"{'ONE page, raw HTML':<44}{raw:>10}")
    print(f"{'ONE page, text extracted':<44}{text:>10}")
    print("-" * 54)
    print(f"Factor: one page equals {text / cost_of_tool_list(1):.0f} tool definitions.")


if __name__ == "__main__":
    main()
python scripts/01_token_vergleich.py

What you see

The output looks roughly like this (your numbers will differ, that is fine):

What                                            Tokens
------------------------------------------------------
  1 tool definitions in the schema                 193
  5 tool definitions in the schema                 957
 25 tool definitions in the schema                4772
 50 tool definitions in the schema                9542
100 tool definitions in the schema               19082
200 tool definitions in the schema               38162
------------------------------------------------------
ONE page, raw HTML                              104233
ONE page, text extracted                         21804
------------------------------------------------------
Factor: one page equals 113 tool definitions.

Three things are now established, and they carry the rest of the course.

First: two hundred tools are expensive but not absurd. With modern context windows, 38,000 tokens are annoying, not fatal. Which is exactly why dynamic filtering works as a solution to this problem - it turns 38,000 into about 1,000.

Second: a single page costs as much as a hundred tools. And unlike the tool list it is added per call. After three scrapes a 128k window is full, no matter how many tools were registered.

Third: filtering helps with the first line and changes nothing about the second. The five filtered tools then run in the same context the dialogue lives in, and dump their output right there.

If it looks different for you

If your page comes to only 2,000 tokens, you picked a small page. Try a product page with a price table, a PDF-to-text result, or an API response with a hundred records. The point is not the specific number but that the distribution is skewed: definitions are uniformly expensive, outputs are unpredictably expensive. The outlier is the problem, not the average.

Keep the script. In chapter 06 you use it to measure your digest, and in chapter 08 it is the basis of the whole measurement series.