n4nAI

Debugging tool-calling loops that never terminate

Practical steps to diagnose and fix a LangChain agent that infinitely calls tools, with code to enforce stop conditions and verify termination.

n4n Team3 min read707 words

Audio narration

Coming soon — every post will get a voice note here.

A langchain tool calling loop never ends when the agent repeatedly invokes a tool without ever emitting a final answer. In practice this wastes tokens, blows up latency, and usually masks a schema or control-flow bug rather than a model deficiency. The following steps show how to reproduce, instrument, and harden your agent so it terminates predictably.

Step 1: Reproduce the loop with minimal instrumentation

Start by stripping the agent to its core: one trivial tool, a verbose executor, and a fixed model. You want to see the raw action/observation cycle without noise from retrieval or external APIs.

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def echo(x: str) -> str:
    """Echo the input string back to the caller."""
    return x

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_tool_calling_agent(llm, [echo], prompt="You are a bot. Use tools if needed.")
executor = AgentExecutor(agent=agent, tools=[echo], verbose=True)

executor.invoke({"input": "Call echo with 'hello', then call it again, then again."})

If you watch the terminal spin with repeated Action: echo lines and no Final Answer, you have reproduced the problem. The model keeps calling echo because nothing in the prompt or tool contract signals the task is complete. At this stage, do not try to fix the model—just confirm the loop is real and countable.

Step 2: Audit tool schemas and return strings

A common cause of a langchain tool calling loop never ends scenario is a tool that returns a string the model interprets as a new instruction. Tools should return flat data, not questions or narrative.

Bad pattern:

@tool
def get_status(job_id: str) -> str:
    """Check job status."""
    return f"Job {job_id} is running. Should I check again?"

Good pattern:

@tool
def get_status(job_id: str) -> str:
    """Check job status. Returns JSON with state field."""
    return '{"job_id": "%s", "state": "running"}' % job_id

Also verify the tool name and description are unambiguous. Vague descriptions like "do something" invite recursive calls. If a tool can be called with its own output as input, the model will happily do so. Make the contract strict: typed inputs, no open-ended text returns.

Step 3: Set hard iteration limits

LangChain’s AgentExecutor accepts max_iterations and max_execution_time. Set them aggressively during debugging so a runaway loop fails fast.

executor = AgentExecutor(
    agent=agent,
    tools=[echo],
    verbose=True,
    max_iterations=5,
    max_execution_time=10.0,
    early_stopping_method="force",  # injects a stop message instead of raising
)

With early_stopping_method="force", the executor appends a forced final answer after the cap. This alone breaks most infinite loops, but it is a blunt instrument. You still need to fix the root cause, because a forced stop means the agent did not actually decide to finish.

Step 4: Add a deterministic stop condition

For production, bake termination into the tool layer. A lightweight call counter that raises after N invocations forces the executor to bail with a parsing error you can intercept.

class CallLimit:
    def __init__(self, limit: int):
        self.limit = limit
        self.count = 0

    def check(self):
        self.count += 1
        if self.count > self.limit:
            raise ValueError("TOOL_CALL_LIMIT_EXCEEDED")

limit = CallLimit(3)

@tool
def bounded_echo(x: str) -> str:
    """Echo with a hard call limit."""
    limit.check()
    return x

executor = AgentExecutor(
    agent=agent,
    tools=[bounded_echo],
    handle_parsing_errors="Tool limit hit. Final answer: stopped.",
    verbose=True,
)

When the limit trips, the executor surfaces the handling string as the output. This converts an unbounded langchain tool calling loop never ends into a controlled fallback.

Step 5: Trace with callbacks or LangSmith

Verbose logs are insufficient when the loop spans multiple modules. Use a callback handler to capture each action and decision.

from langchain_core.callbacks import BaseCallbackHandler

class LoopWatcher(BaseCallbackHandler):
    def on_agent_action(self, action, **kwargs):
        print(f"ACTION: {action.tool} -> {action.tool_input}")
    def on_agent_finish(self, finish, **kwargs):
        print(f"FINISH: {finish.return_values}")

watcher = LoopWatcher()
executor.invoke({"input": "Loop me."}, config={"callbacks": [watcher]})

If you run LangSmith, set LANGCHAIN_TRACING_V2=true and inspect the run tree. You will immediately see whether the model emits AgentFinish or just repeated AgentAction. A langchain tool calling loop never ends because AgentFinish is never produced; the trace makes that gap obvious. Add a custom event inside your tool to log return values, so you can confirm the observation text is not tricking the model.

Step 6: Force a termination test in CI

Write a test that asserts the agent stops within a bound. This prevents regressions when you change prompts or add tools.

import pytest

@pytest.fixture
def bounded_executor():
    lim = CallLimit(3)
    @tool
    def bounded_echo(x: str) -> str:
        lim.check()
        return x
    ex = AgentExecutor(
        agent=create_tool_calling_agent(ChatOpenAI(model="gpt-4o-mini"), [bounded_echo], prompt="You are a bot."),
        tools=[bounded_echo],
        handle_parsing_errors="stopped",
        max_iterations=5,
    )
    return ex, lim

def test_agent_terminates(bounded_executor):
    ex, lim = bounded_executor
    result = ex.invoke({"input": "Call echo 100 times"})
    assert "stopped" in result["output"].lower()
    assert lim.count <= 4

Run it:

pytest test_agent.py -q

If the test fails before your fix and passes after, you have verified the loop is bounded. Keep this test in the suite; agents regress silently when prompts are tweaked.

Step 7: Handle provider degradation gracefully

Sometimes the loop is not in your code but in the network. If a provider returns 429s, LangChain’s default retry can look like a stuck tool call. Routing through a gateway that performs automatic fallback when a provider is rate-limited or degraded removes that failure mode. (n4n.ai does this on its OpenAI-compatible endpoint, which also honors client routing directives so you can pin a model per request.) Regardless of gateway, set request_timeout on the LLM client to avoid hanging calls that the executor counts as iterations.

llm = ChatOpenAI(
    model="gpt-4o-mini",
    request_timeout=15,
    max_retries=2,
)

Combine that with the iteration cap from Step 3 and the guard from Step 4, and the loop disappears even under flaky infrastructure.

Verification checklist

  • Agent reproduces the loop with verbose=True and you can count actions
  • Tool returns are non-interactive, structured strings
  • max_iterations set and early_stopping_method="force" engaged
  • Custom guard raises after N calls and is caught by handle_parsing_errors
  • Callback logs show no more than N actions before finish or force-stop
  • CI test fails before fix, passes after

Following these steps turns a mysterious langchain tool calling loop never ends bug into a controlled, observable process. The key is to treat termination as a first-class constraint, not an afterthought the model is expected to infer.

Tagslangchaintool-callingagentsdebugging

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain debugging & observability posts →