惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cloudbric
Cloudbric
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
A
About on SuperTechFans
宝玉的分享
宝玉的分享
小众软件
小众软件
T
Tor Project blog
The Hacker News
The Hacker News
WordPress大学
WordPress大学
IT之家
IT之家
L
LINUX DO - 热门话题
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Latest news
Latest news
Martin Fowler
Martin Fowler
F
Full Disclosure
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
W
WeLiveSecurity
C
Cisco Blogs
Recorded Future
Recorded Future
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - Franky
美团技术团队
N
Netflix TechBlog - Medium
Know Your Adversary
Know Your Adversary
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Help Net Security
雷峰网
雷峰网
G
Google Developers Blog
人人都是产品经理
人人都是产品经理
Microsoft Azure Blog
Microsoft Azure Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
J
Java Code Geeks
Project Zero
Project Zero
Jina AI
Jina AI
P
Palo Alto Networks Blog
Vercel News
Vercel News
腾讯CDC
N
News | PayPal Newsroom
V
Visual Studio Blog
Cisco Talos Blog
Cisco Talos Blog
V
Vulnerabilities – Threatpost
AWS News Blog
AWS News Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Tool-use API design for LLMs: 5 patterns that prevent agent loops and silent failures
Adamo Softwa · 2026-05-05 · via DEV Community

In July 2025, a developer's Claude Code instance hit a recursion loop and burned through 1.67 billion tokens in 5 hours, generating an estimated $16,000 to $50,000 in API charges before anyone noticed. The agent did not crash. It did not throw an error. It just kept calling tools, getting confused, calling more tools, and silently accumulating cost. Old software crashes. LLM agents spend.

This is the failure mode most teams discover the hard way. You design a clean tool interface, the agent works in your test environment, you ship it to production, and three weeks later something hits an edge case that sends the agent into a loop. The patterns below are what we have used to prevent agent loops and silent failures across production LLM systems handling thousands of tool calls per day. None of them are about better prompts. They are about better tool design.

Why agent loops happen in the first place

Before the patterns, it helps to understand the failure mode precisely.

An LLM agent receives a user request, reasons about which tool to call, calls it, gets a result, reasons about whether the goal is achieved, and either responds or calls another tool. This loop is supposed to terminate when the goal is achieved or the model decides no further action is needed.

It does not terminate when:

  1. The tool result is ambiguous. The model cannot tell whether the call succeeded, so it tries again with slightly different parameters.
  2. The tool fails silently. The model receives a non-error response that does not actually contain the data it needed, so it interprets this as "I should retry."
  3. The tool returns conflicting information. Two consecutive calls return different results, the model loses confidence in either, and tries to "verify" by calling more tools.
  4. The model misreads its own previous output. With long context windows, the model sees a previous tool call's result, forgets it already processed that result, and re-processes it as new information.

Every one of these is preventable with tool design. The model is not the problem. The interface is.

Pattern 1: Make every tool result self-describing

The single most common cause of agent loops is tool results that the model cannot interpret without making assumptions.

A bad tool result:

{
  "results": [
    {"id": "h_1234", "name": "Hotel Granbell", "price": 128},
    {"id": "h_5678", "name": "Shibuya Stream", "price": 142}
  ]
}

Enter fullscreen mode Exit fullscreen mode

The model now has to assume what this means. Are these the only matches? Are there more? Is the search complete? What was searched for? When the model gets confused (and at scale, it always gets confused eventually), it calls the tool again "to verify."

A self-describing tool result:

{
  "status": "success",
  "search_id": "srch_abc123",
  "query_summary": {
    "destination": "Shibuya, Tokyo",
    "check_in": "2026-07-12",
    "check_out": "2026-07-15",
    "guests": 1,
    "max_price": 150
  },
  "results": [
    {"id": "h_1234", "name": "Hotel Granbell", "price": 128, "currency": "USD"},
    {"id": "h_5678", "name": "Shibuya Stream", "price": 142, "currency": "USD"}
  ],
  "total_matches": 2,
  "is_complete": true,
  "next_action_hint": "User has 2 valid options. Present both with prices. Do not search again unless user changes parameters."
}

Enter fullscreen mode Exit fullscreen mode

The is_complete: true and next_action_hint fields are the critical additions. The model can now read this result, understand that the search is finished, and know what to do next without re-querying. The query_summary echo lets the model verify it called the tool with the right parameters.

The next_action_hint is unconventional but extremely effective. It is a short instruction included in the tool response that tells the model what state the conversation is in. Think of it as the tool nudging the model toward correct loop termination.

# Wrapping tools to inject next_action_hint
def with_action_hint(tool_func):
    def wrapper(*args, **kwargs):
        result = tool_func(*args, **kwargs)
        result['next_action_hint'] = derive_hint(result)
        return result
    return wrapper

def derive_hint(result):
    if result['status'] == 'success' and result['total_matches'] == 0:
        return "No matches. Inform user and ask for relaxed criteria. Do not retry."
    if result['status'] == 'success' and result['total_matches'] > 0:
        return f"Found {result['total_matches']} matches. Present to user. Do not search again unless parameters change."
    if result['status'] == 'error':
        return f"Tool failed: {result['error']}. Inform user. Do not retry without user input."
    return "Process result and decide next step."

Enter fullscreen mode Exit fullscreen mode

Implementing this across our tool surface reduced retry-driven loops by approximately 60% in production.

Pattern 2: Distinguish between "no results" and "tool failure"

The second most common cause of agent loops: ambiguous failure states.

A search that returns zero matches is a successful tool call. A search that timed out is a failed tool call. To the LLM, both can look identical if the tool just returns an empty results array.

# Bad: indistinguishable from no results
def search_hotels(query):
    try:
        results = supplier_api.search(query)
        return {"results": results}
    except Exception:
        return {"results": []}  # silent failure

# Good: explicit status with retry guidance
def search_hotels(query):
    try:
        results = supplier_api.search(query, timeout=5)
        return {
            "status": "success",
            "results": results,
            "total_matches": len(results),
            "retryable": False,
        }
    except SupplierTimeout:
        return {
            "status": "error",
            "error_type": "timeout",
            "error_message": "Supplier API did not respond within 5 seconds.",
            "retryable": True,
            "retry_after_ms": 2000,
            "max_retries_remaining": get_retry_budget(query),
        }
    except SupplierAuthError:
        return {
            "status": "error",
            "error_type": "auth",
            "error_message": "API authentication failed.",
            "retryable": False,
            "user_facing_message": "We're having trouble accessing hotel data. Please try again later.",
        }
    except RateLimitError as e:
        return {
            "status": "error",
            "error_type": "rate_limit",
            "error_message": f"Rate limit hit. Reset in {e.reset_seconds}s.",
            "retryable": True,
            "retry_after_ms": e.reset_seconds * 1000,
        }

Enter fullscreen mode Exit fullscreen mode

The retryable flag is doing real work. When it is false, the LLM knows there is no point retrying and will inform the user instead. When it is true, the LLM has a structured retry path with explicit limits.

Without this pattern, an authentication failure that looks like an empty result set causes the model to try increasingly creative parameter combinations to "find results," consuming tokens and producing nothing.

Pattern 3: Enforce a hard call budget at the orchestrator level

No matter how well-designed your tools are, the model will occasionally enter a loop. The orchestrator must enforce a hard ceiling.

class AgentOrchestrator:
    def __init__(self, max_tool_calls=15, max_total_cost_usd=0.50):
        self.max_tool_calls = max_tool_calls
        self.max_total_cost_usd = max_total_cost_usd
        self.calls_made = 0
        self.total_cost_usd = 0

    async def run_agent_turn(self, user_message, conversation_history):
        history = conversation_history + [{"role": "user", "content": user_message}]

        while self.calls_made < self.max_tool_calls:
            if self.total_cost_usd >= self.max_total_cost_usd:
                return self._cost_limit_response()

            response = await call_llm(history, tools=self.tools)
            self.total_cost_usd += response.cost_usd

            if not response.tool_calls:
                # Model produced final response, exit loop
                return response.content

            for tool_call in response.tool_calls:
                self.calls_made += 1
                tool_result = await self.execute_tool(tool_call)
                history.append({"role": "tool", "content": tool_result})

        # Hit call limit. Force a final response.
        return await self._force_final_response(history)

    async def _force_final_response(self, history):
        # Add explicit instruction and call LLM with tools=None
        history.append({
            "role": "system",
            "content": "Tool call limit reached. Produce a final response to the user "
                       "based on information already gathered. Do not request more tools."
        })
        response = await call_llm(history, tools=None)
        return response.content

Enter fullscreen mode Exit fullscreen mode

Two safeguards here. First, max_tool_calls prevents infinite loops by capping iterations. Fifteen is our default for booking workflows. Anything more than that is almost always a sign the agent is confused, not productive. Second, max_total_cost_usd is a financial circuit breaker. Even if the agent finds creative ways to make many tool calls, it cannot spend more than the per-conversation budget.

When the limit is hit, the orchestrator does not just return an error. It calls the LLM one more time with tools=None, forcing it to produce a final response from whatever it has gathered. This is much better UX than "Sorry, agent failed."

For high-volume systems, also implement per-tenant rate limiting. The single-developer Claude Code incident burned $16-50K because there was no per-account ceiling. Production systems need both per-conversation and per-tenant limits.

Pattern 4: Detect repeated calls and short-circuit them

Even with call budgets, agents waste budget by repeating the same call with minor variations. The fix is a deduplication layer at the orchestrator.

import hashlib
import json

class ToolCallDeduplicator:
    def __init__(self, window_size=5):
        self.recent_calls = []
        self.window_size = window_size

    def is_duplicate(self, tool_name, arguments):
        signature = self._signature(tool_name, arguments)
        is_dup = any(call == signature for call in self.recent_calls)
        self.recent_calls.append(signature)
        if len(self.recent_calls) > self.window_size:
            self.recent_calls.pop(0)
        return is_dup

    def _signature(self, tool_name, arguments):
        # Normalize arguments for comparison
        normalized = json.dumps(arguments, sort_keys=True, default=str)
        return f"{tool_name}:{hashlib.sha256(normalized.encode()).hexdigest()[:16]}"


# In the orchestrator
async def execute_tool(self, tool_call):
    if self.deduplicator.is_duplicate(tool_call.name, tool_call.arguments):
        return {
            "status": "duplicate_call_blocked",
            "message": (
                f"This exact {tool_call.name} call was made earlier in this conversation "
                f"with the same arguments. The previous result is already in your context. "
                f"Use it instead of calling again."
            ),
            "retryable": False,
        }

    return await self._actually_execute(tool_call)

Enter fullscreen mode Exit fullscreen mode

When the model calls the same tool with the same arguments twice in a 5-call window, the orchestrator returns a structured "this is a duplicate" message instead of executing again. The model sees this and almost always recovers, often by referring back to the earlier result.

This pattern caught about 8% of calls in our production systems. Eight percent of total tool calls were unnecessary repeats. Blocking them saved both cost and latency.

A subtle detail: the deduplication signature should be lossy enough to catch near-duplicates. We use exact argument matching, but for some tools (search queries that differ only in word order), a normalization step before hashing would catch more.

Pattern 5: Parameter validation at the boundary, not inside the LLM

The slowest path to detecting a bad tool call is letting the LLM make it, the tool execute it, and the failure propagate back. The fastest path is validating parameters before the tool runs.

from pydantic import BaseModel, Field, validator
from datetime import date, timedelta


class SearchHotelsArgs(BaseModel):
    destination: str = Field(min_length=2, max_length=100)
    check_in: date
    check_out: date
    guests: int = Field(ge=1, le=20)
    max_price: float = Field(gt=0, le=10000)

    @validator('check_in')
    def check_in_not_in_past(cls, v):
        if v < date.today():
            raise ValueError(f"check_in date {v} is in the past")
        return v

    @validator('check_out')
    def check_out_after_check_in(cls, v, values):
        if 'check_in' in values and v <= values['check_in']:
            raise ValueError("check_out must be after check_in")
        if 'check_in' in values and (v - values['check_in']) > timedelta(days=90):
            raise ValueError("Stay length cannot exceed 90 days")
        return v


async def execute_tool(self, tool_call):
    if tool_call.name == "search_hotels":
        try:
            args = SearchHotelsArgs(**tool_call.arguments)
        except ValidationError as e:
            return {
                "status": "validation_error",
                "errors": e.errors(),
                "user_facing_hint": (
                    "Some search parameters were invalid. Confirm with the user before retrying."
                ),
                "retryable_after_correction": True,
            }
        return await self._search_hotels(args)

Enter fullscreen mode Exit fullscreen mode

This catches three classes of bad calls:

  1. Type errors: the LLM passes a string where the tool expects an integer.
  2. Range errors: the LLM tries to search for 50 guests in one room.
  3. Logical errors: check-out before check-in, dates in the past.

By rejecting these at the boundary with structured error responses, we prevent the deeper failure mode where the supplier API gets called with bad data, returns a cryptic error, the LLM cannot interpret the error, and a loop starts.

The Pydantic-based approach also gives you JSON schema generation for free, which feeds directly into the tool definitions you send to the LLM. Schema-aligned validation across both ends.

What I would not recommend

A few approaches we tried and abandoned:

  • Asking the LLM "are you done?" prompts mid-loop. Slows everything down and only works inconsistently. The orchestrator-level call budget is more reliable.
  • Letting the LLM see the full call history in every iteration. Increases context cost dramatically and provides little benefit. Pattern 4 (deduplication with structured feedback) is more efficient.
  • Streaming tool execution with partial results. Looks attractive but creates new failure modes where the LLM acts on incomplete data. Stick with atomic tool calls that either complete or fail cleanly.
  • Auto-generating tool definitions from API specs. Tempting because it sounds DRY, but auto-generated descriptions are usually not what the LLM needs. Hand-written tool descriptions, with explicit guidance about when to use the tool and when not to, work better.

Production results

After implementing these five patterns across our LLM-powered booking systems:

  • Agent loop incidents: dropped from 3 to 5 per week to under 1 per month.
  • Average tool calls per conversation: dropped 22%, mostly by eliminating duplicates and unnecessary retries.
  • Time-to-final-response: improved 18%, primarily from earlier short-circuiting of bad parameter calls.
  • Cost per conversation: dropped 31%, combination of fewer tool calls and tighter budget enforcement.

The patterns are not glamorous. They are mostly defensive engineering. But the alternative is the Claude Code incident: a $16,000 to $50,000 loss from an agent that "did not crash" but kept spending. In production LLM systems, the difference between cost-stable and not is exactly this kind of unsexy infrastructure.

If you are designing tools for an LLM agent, treat the tool interface as a contract that must hold even when the model is confused. The model will be confused. The contract will be tested. Patterns 1 through 5 are how it survives.


These patterns were developed across production builds at Adamo Software, including AI travel assistant deployments and agentic AI systems where tool-use reliability is non-negotiable.