<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 李丽娜</title>
    <description>The latest articles on DEV Community by 李丽娜 (@easy88ai).</description>
    <link>https://dev.to/easy88ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4105901%2F495ee72e-6111-40bc-8bb5-021577ce76f8.png</url>
      <title>DEV Community: 李丽娜</title>
      <link>https://dev.to/easy88ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/easy88ai"/>
    <language>en</language>
    <item>
      <title>How I Built a Cost-Aware Layer for LLM APIs: Caching, Routing, and Fallback</title>
      <dc:creator>李丽娜</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:48:28 +0000</pubDate>
      <link>https://dev.to/easy88ai/how-i-built-a-cost-aware-layer-for-llm-apis-caching-routing-and-fallback-3cf5</link>
      <guid>https://dev.to/easy88ai/how-i-built-a-cost-aware-layer-for-llm-apis-caching-routing-and-fallback-3cf5</guid>
      <description>&lt;p&gt;Last month my API spend roughly tripled while traffic grew about 40%. Nothing was broken — no runaway loop, no leaked key. The bill was just... honest. I was paying the strongest model to answer "what's the status of order 12345," re-paying for the same prompt hundreds of times a day, and quietly paying again every time a request timed out and got retried.&lt;br&gt;
This post is about the three layers I added to fix that. None of them are exotic, and all of them are things you can drop into an existing codebase this afternoon.&lt;br&gt;
Why the bill grows faster than the traffic&lt;br&gt;
Four things compound, and they're all invisible until you look:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Everything goes to the strongest model. A default model="gpt-4o" set once and never revisited turns your entire product into a premium-tier product.&lt;/li&gt;
&lt;li&gt;Identical prompts get billed repeatedly. Support bots, batch jobs, CI runs — a large share of production traffic is the same request wearing a different timestamp.&lt;/li&gt;
&lt;li&gt;Failures still cost money. A timeout at 30 seconds may have already consumed the tokens. Retry it and you pay twice for one answer.&lt;/li&gt;
&lt;li&gt;Long static prefixes are charged every single call. A 3,000-token system prompt is billed on every request, forever.
The fix isn't "use a cheaper model." That just moves the cost into quality complaints. The fix is three layers, in this order: cache → route → fall back.
Layer 1: Caching
There are two kinds, and most people only know one.
Provider-side caching is automatic on most major APIs now. If the prefix of your request matches a recent one, the cached portion is billed at a steep discount and returns faster. You don't enable it — you qualify for it by structuring your prompts correctly.
And here's the part almost everyone gets wrong: the static part must come first.
# Wrong — the user's question sits in front of the docs,
# so every request has a different prefix and never hits cache
messages = [
{"role": "user", "content": user_question},
{"role": "system", "content": long_policy_docs},
]&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Right — stable prefix first, dynamic input last
&lt;/h1&gt;

&lt;p&gt;messages = [&lt;br&gt;
    {"role": "system", "content": long_policy_docs},&lt;br&gt;
    {"role": "user", "content": user_question},&lt;br&gt;
]&lt;br&gt;
That one reordering is usually worth more than any other single change in this post. Put system prompts, tool definitions, and reference documents at the top. Put anything that changes per request at the bottom.&lt;br&gt;
Your own cache handles the cases provider caching can't — exact repeats of a full request. Here's a small one with no dependencies:&lt;br&gt;
import hashlib&lt;br&gt;
import json&lt;br&gt;
import time&lt;/p&gt;

&lt;p&gt;class ResponseCache:&lt;br&gt;
    def &lt;strong&gt;init&lt;/strong&gt;(self, ttl: int = 3600, max_items: int = 10_000):&lt;br&gt;
        self.ttl = ttl&lt;br&gt;
        self.max_items = max_items&lt;br&gt;
        self._store: dict[str, tuple[float, str]] = {}&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@staticmethod
def key(model: str, messages: list, **kwargs) -&amp;gt; str:
    payload = json.dumps(
        {"model": model, "messages": messages, **kwargs},
        sort_keys=True,
        ensure_ascii=False,
        default=str,
    )
    return hashlib.sha256(payload.encode()).hexdigest()

def get(self, key: str) -&amp;gt; str | None:
    hit = self._store.get(key)
    if hit and time.time() - hit[0] &amp;lt; self.ttl:
        return hit[1]
    self._store.pop(key, None)
    return None

def set(self, key: str, value: str) -&amp;gt; None:
    if len(self._store) &amp;gt;= self.max_items:
        self._store.pop(next(iter(self._store)), None)  # drop oldest
    self._store[key] = (time.time(), value)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two rules for using it safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only cache deterministic calls. If temperature is above 0, or the prompt contains a timestamp, a request ID, or anything time-sensitive, don't cache it. You'll serve stale or wrong answers and won't notice for weeks.&lt;/li&gt;
&lt;li&gt;Start with the boring workloads. CI suites, eval runs, and batch jobs are where the hit rate is highest and the risk is lowest. Caching those costs you nothing and can remove a surprising line item.
Layer 2: Routing by complexity
Not every request deserves your most expensive model. The trick is picking a tier without spending a call to decide.
import re&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TIERS = {&lt;br&gt;
    "cheap": "gpt-4o-mini",&lt;br&gt;
    "balanced": "claude-3-5-sonnet",&lt;br&gt;
    "strong": "gpt-4o",&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;_STRONG_SIGNALS = re.compile(&lt;br&gt;
    r"\b(analyze|design|refactor|architecture|trade-?off|prove|debug why|optimize)\b",&lt;br&gt;
    re.I,&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;def route(prompt: str, has_tools: bool = False, turns: int = 1) -&amp;gt; str:&lt;br&gt;
    """Pick a model tier from cheap signals — no extra LLM call required."""&lt;br&gt;
    if len(prompt) &amp;lt; 120 and not _STRONG_SIGNALS.search(prompt) and not has_tools:&lt;br&gt;
        return TIERS["cheap"]&lt;br&gt;
    if _STRONG_SIGNALS.search(prompt) or turns &amp;gt; 6:&lt;br&gt;
        return TIERS["strong"]&lt;br&gt;
    return TIERS["balanced"]&lt;br&gt;
Short, no tools, no reasoning keywords → cheap tier. Explicit reasoning language or a long multi-turn conversation → strong tier. Everything else lands in the middle.&lt;br&gt;
You can use a small model as a classifier instead of regex, and it works better on messy input. But be honest about the math: the classifier call costs money too. It only pays off when the price gap between your tiers is wide and your traffic is high enough to amortize it. For most teams, start with heuristics and only upgrade when you can see the heuristics failing.&lt;br&gt;
One thing worth doing from day one: log which tier handled each request. That distribution is the single most useful artifact you'll have when someone asks "why is the bill going up?"&lt;br&gt;
Layer 3: Fallback&lt;br&gt;
Fallback gets filed under reliability, but it belongs in a cost post too — because the cheap fallback is almost always better than a failed request.&lt;br&gt;
def call_with_fallback(client, messages, chain, **kwargs):&lt;br&gt;
    """Try each model in order. First success wins."""&lt;br&gt;
    last_error = None&lt;br&gt;
    for attempt, model in enumerate(chain, start=1):&lt;br&gt;
        try:&lt;br&gt;
            resp = client.chat.completions.create(&lt;br&gt;
                model=model, messages=messages, **kwargs&lt;br&gt;
            )&lt;br&gt;
            resp._attempt = attempt  # handy for the metrics layer below&lt;br&gt;
            return resp&lt;br&gt;
        except Exception as e:&lt;br&gt;
            last_error = e&lt;br&gt;
            continue&lt;br&gt;
    raise RuntimeError(f"all models failed: {chain}") from last_error&lt;/p&gt;

&lt;p&gt;FALLBACK_CHAIN = ["gpt-4o", "claude-3-5-sonnet", "deepseek-chat"]&lt;br&gt;
Keep retry and fallback as separate concepts, because they solve different problems:&lt;br&gt;
      Retry&lt;br&gt;
      Fallback&lt;br&gt;
      Same model?&lt;br&gt;
      Yes&lt;br&gt;
      No&lt;br&gt;
      Solves&lt;br&gt;
      Transient errors (429, 502, blip)&lt;br&gt;
      Sustained unavailability&lt;br&gt;
      Budget&lt;br&gt;
      2 attempts, with backoff&lt;br&gt;
      Walk the chain once&lt;br&gt;
Retrying the same model three times against a provider that's genuinely down just burns money and latency. Detect the difference by error class: rate limits and 5xx are worth retrying; a persistent 400 or a hard outage is worth falling back on.&lt;br&gt;
Putting it together&lt;br&gt;
Here's the wrapper I actually run. It does all three layers and records enough to answer "where did the money go":&lt;br&gt;
import os&lt;br&gt;
import time&lt;br&gt;
from dataclasses import dataclass, field&lt;br&gt;
from openai import OpenAI&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class CallRecord:&lt;br&gt;
    model: str&lt;br&gt;
    prompt_tokens: int&lt;br&gt;
    completion_tokens: int&lt;br&gt;
    cached: bool&lt;br&gt;
    attempt: int&lt;br&gt;
    latency_ms: int&lt;/p&gt;

&lt;p&gt;class CostAwareClient:&lt;br&gt;
    def &lt;strong&gt;init&lt;/strong&gt;(self, chain: list[str], cache_ttl: int = 3600):&lt;br&gt;
        self.client = OpenAI(&lt;br&gt;
            api_key=os.getenv("LLM_API_KEY"),&lt;br&gt;
            base_url=os.getenv("LLM_BASE_URL", "&lt;a href="https://easy88ai.com/v1%22" rel="noopener noreferrer"&gt;https://easy88ai.com/v1"&lt;/a&gt;),&lt;br&gt;
        )&lt;br&gt;
        self.chain = chain&lt;br&gt;
        self.cache = ResponseCache(ttl=cache_ttl)&lt;br&gt;
        self.records: list[CallRecord] = []&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def complete(self, messages: list, **kwargs):
    model = route(messages[-1].get("content", ""))
    started = time.perf_counter()

    # 1. cache
    ck = self.cache.key(model, messages, **kwargs)
    if (hit := self.cache.get(ck)) is not None:
        self.records.append(CallRecord(model, 0, 0, True, 0, 0))
        return hit

    # 2. route + 3. fall back
    chain = [model] + [m for m in self.chain if m != model]
    try:
        resp = call_with_fallback(self.client, messages, chain, **kwargs)
    except Exception:
        raise

    text = resp.choices[0].message.content
    self.cache.set(ck, text)

    usage = getattr(resp, "usage", None)
    self.records.append(
        CallRecord(
            model=resp.model,
            prompt_tokens=getattr(usage, "prompt_tokens", 0) if usage else 0,
            completion_tokens=getattr(usage, "completion_tokens", 0) if usage else 0,
            cached=False,
            attempt=getattr(resp, "_attempt", 1),
            latency_ms=int((time.perf_counter() - started) * 1000),
        )
    )
    return text

def report(self) -&amp;gt; dict:
    total = len(self.records)
    if not total:
        return {}
    cached = sum(1 for r in self.records if r.cached)
    by_model: dict[str, int] = {}
    for r in self.records:
        by_model[r.model] = by_model.get(r.model, 0) + 1
    return {
        "calls": total,
        "cache_hit_rate": round(cached / total, 3),
        "tier_distribution": by_model,
        "retry_rate": round(
            sum(1 for r in self.records if r.attempt &amp;gt; 1) / total, 3
        ),
        "prompt_tokens": sum(r.prompt_tokens for r in self.records),
        "completion_tokens": sum(r.completion_tokens for r in self.records),
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;How to measure whether it worked&lt;br&gt;
I'm not going to paste a pricing table here — per-token prices change often, vary by tier and region, and any numbers I write will be stale within a few weeks. Pull current rates from your provider's pricing page, or better, read actual charges off your console.&lt;br&gt;
What matters is the metric, not the unit price:&lt;br&gt;
Cost per resolved task = total spend ÷ number of tasks actually completed successfully.&lt;br&gt;
That's the number that moves when you do this right. Everything else is diagnostic:&lt;br&gt;
      Metric&lt;br&gt;
      Healthy&lt;br&gt;
      What a bad value tells you&lt;br&gt;
      Cache hit rate&lt;br&gt;
      20–40% on support/batch workloads&lt;br&gt;
      Your static prefix isn't actually first, or TTL is too short&lt;br&gt;
      Cheap-tier share&lt;br&gt;
      50%+ of calls&lt;br&gt;
      Your router is too conservative&lt;br&gt;
      Retry rate&lt;br&gt;
      &amp;lt; 5%&lt;br&gt;
      Timeouts are set too tight, or you're retrying non-retryable errors&lt;br&gt;
      Fallback rate&lt;br&gt;
      &amp;lt; 2%&lt;br&gt;
      A provider is degrading and you haven't noticed&lt;br&gt;
Track these before you change anything. Without a baseline you can't tell a 40% saving from a slow week.&lt;br&gt;
What I'd skip&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semantic caching, at first. Embedding-similarity caching sounds great and is genuinely powerful, but tuning the similarity threshold takes longer than the money it saves at low volume. Exact-match caching gets you most of the value with none of the false-positive debugging. Add semantic later, once you know your hit rate ceiling.&lt;/li&gt;
&lt;li&gt;An LLM-based router on day one. Start with heuristics. You can always upgrade, and by then you'll have the logs to prove it's worth it.&lt;/li&gt;
&lt;li&gt;Trimming prompts until quality breaks. Cutting a system prompt from 3,000 tokens to 400 saves real money until the model starts ignoring your output format. Measure quality alongside cost, not instead of it.
Three things I'd tell myself at the start&lt;/li&gt;
&lt;li&gt;Prompt order is free money. Moving static content to the front of your messages costs ten minutes and can be the single largest win available to you.&lt;/li&gt;
&lt;li&gt;Route before you optimize. Knowing which tier handled each request turns cost conversations from guesswork into a pie chart.&lt;/li&gt;
&lt;li&gt;Retry and fallback are different tools. Retry absorbs blips; fallback absorbs outages. Conflating them is how you end up paying for three failed attempts.
None of this requires a framework or a vendor. It's a cache, a regex, and a loop — and it's the difference between a bill that scales with your product and one that scales with your inattention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building easy88ai, a unified API gateway that routes GPT, Claude, Gemini and 200+ models through one OpenAI-compatible endpoint — which is what I use as the base_url in the examples above. Happy to swap notes on LLM tooling in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>backend</category>
      <category>llm</category>
    </item>
    <item>
      <title>How I Built a Tool-Calling AI Agent from Scratch (Python) — No Framework Required</title>
      <dc:creator>李丽娜</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:26:27 +0000</pubDate>
      <link>https://dev.to/easy88ai/how-i-built-a-tool-calling-ai-agent-from-scratch-python-no-framework-required-57fa</link>
      <guid>https://dev.to/easy88ai/how-i-built-a-tool-calling-ai-agent-from-scratch-python-no-framework-required-57fa</guid>
      <description>&lt;p&gt;The hottest topic in 2026 might be: "AI can already write code, but how do we make it actually &lt;em&gt;do&lt;/em&gt; things?" For the past two months I've offloaded a few repetitive manual tasks on my team — checking the weather, querying inventory, sending notifications — to an agent. The biggest lesson: &lt;strong&gt;whether an agent can actually work isn't about how smart the model is, it's about how cleanly you hand it the "tools."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post skips the theory and walks through a runnable example of how Function Calling (a.k.a. tool calling) actually works, plus the real pitfalls I hit. The code is copy-paste ready — once you run it, you'll understand the layer underneath every major agent framework (LangChain, AutoGen, OpenAI Agents SDK).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. First, what &lt;em&gt;is&lt;/em&gt; Function Calling?
&lt;/h2&gt;

&lt;p&gt;One sentence: &lt;strong&gt;the model doesn't execute your function — it outputs a structured "call request," and your code does the actual execution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of first-timers assume "the model runs the function for me." It doesn't. The real flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You tell the model "here are the tools you can use" — what each one looks like (name, parameters, description).&lt;/li&gt;
&lt;li&gt;The user asks something. The model decides "this needs a tool" and returns a JSON block: &lt;code&gt;{"name": "get_weather", "arguments": {"city": "Xi'an"}}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your program&lt;/strong&gt; receives that JSON and actually calls &lt;code&gt;get_weather("Xi'an")&lt;/code&gt;, then gets the result.&lt;/li&gt;
&lt;li&gt;You feed the result back into the conversation, and the model composes a natural-language answer based on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model only decides &lt;em&gt;which&lt;/em&gt; tool and &lt;em&gt;what arguments&lt;/em&gt;. Execution always stays in your hands. Once you internalize this, every agent framework is just a wrapper around this loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Environment setup
&lt;/h2&gt;

&lt;p&gt;You only need an OpenAI-compatible Python SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key parameter when initializing the client is &lt;code&gt;base_url&lt;/code&gt;. Any endpoint that follows the OpenAI API spec can plug in here — a local gateway, a cloud inference service, or your team's existing unified access layer. Swap in your address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-your-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://easy88ai.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# replace with your OpenAI-compatible endpoint
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: Hardcoding your key like this is only for demo. In real projects use an env var &lt;code&gt;os.getenv("OPENAI_API_KEY")&lt;/code&gt; and never commit keys to a repo.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Define a tool: give the model a "capability list"
&lt;/h2&gt;

&lt;p&gt;Tools are described with JSON Schema. The model uses &lt;code&gt;description&lt;/code&gt; to decide &lt;em&gt;when&lt;/em&gt; to call a tool, so &lt;strong&gt;writing a clear description matters more than anything.&lt;/strong&gt; Here's a weather-lookup tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query the current weather for a specified city. Use when the user asks about the weather, temperature, or rainfall of a location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City name, e.g.: Xi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;an, Shanghai, Beijing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; says &lt;em&gt;when to use it&lt;/em&gt;, not &lt;em&gt;what the function does&lt;/em&gt;. Those are two different things — the model uses the former to make decisions and the latter to understand boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Full runnable code
&lt;/h2&gt;

&lt;p&gt;This is a minimal closed loop: send a message → the model may return a tool call → you execute it → feed the result back → the model summarizes. Copy and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-your-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://easy88ai.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query the current weather for a specified city. Use when the user asks about the weather, temperature, or rainfall of a location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City name, e.g.: Xi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# A mock weather function (swap in your real API call in production)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fake_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Xi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sunny, 26°C, southeast wind 2级&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shanghai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cloudy, 30°C, high humidity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fake_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: weather data unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather in Xi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;an today? Good for going out?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="c1"&gt;# Round 1: model decides whether to call a tool
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;

&lt;span class="c1"&gt;# If the model wants to call a tool
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Append the model's reply (with tool_calls) back — many people miss this step
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. Feed the tool result back as a tool message
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Round 2: model generates the final answer based on the tool result
&lt;/span&gt;    &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output looks roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xi'an is sunny today, 26°C with a light southeast wind — feels quite pleasant, good for going out.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See? The model never actually "checked" the weather. It just correctly decided to call &lt;code&gt;get_weather('Xi'an')&lt;/code&gt;. The real lookup was done by your &lt;code&gt;get_weather&lt;/code&gt; function.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The pitfalls I actually hit (the valuable part)
&lt;/h2&gt;

&lt;p&gt;Running the example above is easy. Wiring it into real business logic is hard. Here are the ones that bit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 1: Vague tool description → random calls.&lt;/strong&gt; Early on I wrote the &lt;code&gt;description&lt;/code&gt; as "get weather info," and the model tried to call the weather tool even when the user asked "where should I travel tomorrow?" Later I changed it to "Use when the user asks about the weather, temperature, or rainfall of a location," and false calls dropped sharply. The description &lt;em&gt;is&lt;/em&gt; the model's decision boundary — spend 10 minutes polishing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 2: Forgetting to echo back the assistant's &lt;code&gt;tool_calls&lt;/code&gt; message.&lt;/strong&gt; This is the #1 beginner error — &lt;code&gt;tool_call_id&lt;/code&gt; mismatch. The &lt;code&gt;message&lt;/code&gt; returned in round 1 carries &lt;code&gt;tool_calls&lt;/code&gt;; you &lt;strong&gt;must&lt;/strong&gt; append it to &lt;code&gt;messages&lt;/code&gt; verbatim, then append the &lt;code&gt;role: "tool"&lt;/code&gt; result. Skip any step and round 2 throws a 400.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 3: No validation before executing.&lt;/strong&gt; The model returns a JSON string — after parsing, always validate field types and required fields. I had one production incident: the model passed &lt;code&gt;city&lt;/code&gt; as an array &lt;code&gt;["Xi'an", "Shanghai"]&lt;/code&gt;, but my function only accepted a string and crashed. Now every tool entry gets a pydantic / hand-written validation layer first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 4: Tools need timeout and retry.&lt;/strong&gt; Real tools sit behind external APIs — they hang, they're slow. The time I didn't set a timeout, a weather API blocked for 40 seconds and the whole agent froze. Now every tool call is wrapped with &lt;code&gt;timeout&lt;/code&gt; + at most 2 retries; on failure it returns "tool temporarily unavailable" so the model degrades gracefully instead of the whole chain dying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 5: Multi-turn tool calls need a loop, not an &lt;code&gt;if&lt;/code&gt;.&lt;/strong&gt; The example above calls one tool. Real scenarios may chain several (check weather → check transit → check ticket price). The correct pattern wraps "model decides → execute → feed back" in a &lt;code&gt;while&lt;/code&gt; loop that runs until the model stops returning &lt;code&gt;tool_calls&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Advanced: organizing multiple tools
&lt;/h2&gt;

&lt;p&gt;In production you'll have a dozen tools. Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't overload tools.&lt;/strong&gt; Stuffing 20 tools at once drops decision quality. Load relevant tools dynamically per scenario (e.g., the "ordering" scenario only gets menu/payment tools).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a strategy pattern to pick different models for different tasks.&lt;/strong&gt; Cheap model for simple queries, big model for complex reasoning. Same idea as "multi-model routing" — but that's a topic for another post.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Three conclusions from actually building this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Function Calling is the foundation of an agent, not decoration.&lt;/strong&gt; To make AI actually &lt;em&gt;do&lt;/em&gt; work, cleanly describe your tools in JSON Schema first — bigger ROI than buying a more expensive model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution power always stays on your side.&lt;/strong&gt; The model only makes decisions; the real side effects (sending messages, mutating databases, calling APIs) must be guarded by your code with validation and timeouts. That's the production-ready baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-write the minimal loop before adopting a framework.&lt;/strong&gt; LangChain / Agents SDK are just wrappers around that &lt;code&gt;while&lt;/code&gt; loop above. Run it yourself once, then read the framework docs — comprehension speed is completely different.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code in this post is the most bare-bones version. Once you internalize it, you'll read any agent framework's source and think "oh, so that's all it is."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://easy88ai.com" rel="noopener noreferrer"&gt;easy88ai&lt;/a&gt;, a unified API gateway that routes GPT, Claude, Gemini and 200+ models through one OpenAI-compatible endpoint — which is what I use as the &lt;code&gt;base_url&lt;/code&gt; in the examples above. Happy to swap notes on LLM tooling in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
