Skip to Content

Tool-Calling Orchestration (Single-Agent Loops)

When to use

Wire one agent to a library of tools (search, calculator, DB lookup) and let the LLM decide when to use each.

Analogy

A junior analyst with a phone, a calculator, and access to a database. You give them a question; they ask you for facts as they need them.

Data-flow diagram

   user_q -> model -> chooses tool T(args)
                      |
                      v
                  run T -> result R
                      |
                      v
                  model -> final answer or next tool call

Deep explanation

Single-agent tool-calling is simpler than LangGraph/AutoGen/CrewAI when the task is ‘decide which tool then act’. The agent emits a tool call, you run it, you feed the result back, the agent decides next. Use this pattern for moderately complex flows (3-10 tools) where autonomy is bounded but branching is useful.

Examples

Example 1

import openai
tools = [{'type':'function','function':{
    'name':'lookup','description':'Look up an order by id',
    'parameters':{'type':'object','properties':{'order_id':{'type':'string'}},'required':['order_id']}}}]
resp = openai.chat.completions.create(
    model='gpt-4o-mini', tools=tools,
    messages=[{'role':'user','content':'What is the status of order 8821?'}])
if resp.choices[0].message.tool_calls:
    name = resp.choices[0].message.tool_calls[0].function.name
    print('agent chose:', name)

Single agent + tools is enough for moderately complex flows; no need for a graph framework.

Example 2

# the run loop
def run(messages, tools, fn_map, max_iters=5):
    for _ in range(max_iters):
        r = openai.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=tools)
        msg = r.choices[0].message
        messages.append(msg)
        if not msg.tool_calls: return msg.content
        for tc in msg.tool_calls:
            tool_msg = {'role':'tool','name':tc.function.name,
                        'content': fn_map[tc.function.name](**json.loads(tc.function.arguments))}
            messages.append(tool_msg)

The run loop is the canonical pattern: model -> tool -> result -> model -> answer.

Example 3

# validating tool calls (whitelist)
ALLOWED = {'lookup','refund','cancel_order'}
for tc in resp.choices[0].message.tool_calls or []:
    if tc.function.name not in ALLOWED:
        raise ValueError(f'disallowed tool call: {tc.function.name}')

ALLOWED whitelist defends against the model calling a name that isn’t in the schema.

Common mistake

Building a single loop into 50 tools. As soon as tools branch into sub-decisions, reach for LangGraph or LangChain tools.

Key takeaway

Single-agent tool-calling is the right primitive for ‘pick from 3-10 tools, decide on data’. Use max_iters, validate tool names, feed results back into the message list.

Production Failure Playbook

Failure scenario 1: no-iter-cap

Failure scenario 2: no-tool-validation