Wire one agent to a library of tools (search, calculator, DB lookup) and let the LLM decide when to use each.
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.
user_q -> model -> chooses tool T(args)
|
v
run T -> result R
|
v
model -> final answer or next tool call
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.
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.
# 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.
# 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.
Building a single loop into 50 tools. As soon as tools branch into sub-decisions, reach for LangGraph or LangChain tools.
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.