Your Agent’s Tools Can Make Decisions, Too
TL;DR · AI 摘要
Your Agent’s Tools Can Make Decisions, Too Towards Data Science LLM Applications Your Agent’s Tools Can Make Decisions,...
核心要点
- 主题聚焦:Your Agent’s Tools Can Make Decisions, Too
- 来源:Towards Data Science,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
Your Agent’s Tools Can Make Decisions, Too | Towards Data Science
LLM Applications
Your Agent’s Tools Can Make Decisions, Too
Decision models as tools: a hands-on guide
Shuai Guo
October 30, 2026
10 min read
Prefer us on Google
Share
Decision-model-as-a-tool (Image by author)
We turn an LLM into an agent by giving it tools.
Tools let the agent look things up, call APIs, and take action. You know the drill: the agent reasons; the tools execute.
Usually, we design each tool to handle a small operation.
When facing a larger task, the agent usually needs to work things out in a loop: it first calls one tool, based on the result, the agent decides what to do next, maybe call another tool.
But here is the thing: many of those intermediate decisions aren’t that hard. Oftentimes, the agent is just picking the obvious next step based on what the last tool returned. It still has to make that call itself, though, and each of those round trips is another LLM call, adding time and cost to the task.
So what if we could hand over a whole piece of work through a single tool call?
With a decision model, yes, that's possible! And here is how.
Essentially, we let the decision model drive the flow: inside the tool, it picks an action and looks at what comes back. Based on the results, it determines the next move. This loop goes on until it has enough evidence to hand back to the agent.
Because decision models are trained to make fast decisions cheaply, the routine steps can now be handled with a fraction of the time and cost. The agent really only makes one tool call, and it can focus its reasoning on where it matters.
Here’s the new drill: the agent delegates the work; the tool manages the steps.
Since the decision model is embedded in a tool the agent calls, we call it the decision-model-as-a-tool pattern.
In this post, we'll make this pattern concrete. We’ll consider a case study of building a customer-support agent that investigates a delayed order and finds a way to meet the customer’s delivery deadline. We’ll give the agent an order-specialist tool with a decision model inside.
Is moving those decisions into the tool worth it? Let's find out.
If you want to learn more about patterns for LLM-decision-model collaboration, feel free to check my previous post: Decision Models in Action: Build a Hybrid LLM Workflow
···
1. The Agent We Want to Build
Say a customer sends our agent this request:
My order ORD-1042 still hasn’t shipped, and I need it by Friday, October 23. Could you find out what’s happening and whether there’s a way to get it here in time?
There are two things to figure out: why the order is delayed, and if another fulfillment option could meet the deadline. The agent should investigate and recommend next steps.
1.1 What can the agent look up?
For our case study, we’ll use six Python functions that represent operations the support agent might have access to:
- get_order_details() : It provides order status, item, destination, warehouse, and tracking identifiers;
- get_tracking_events() : contains carrier events and any delivery estimate;
- get_fulfillment_notes() : warehouse notes that may explain the delay;
- get_alternative_inventory() : stock availability for the same item at other warehouses;
- get_shipping_options() : delivery estimates, additional charges, and dispatch cutoffs;
- get_fulfillment_policy() : rules for rerouting an order and waiving shipping surcharges.
These functions only retrieve information from a small, synthetic dataset I curated.
Now, if we were building a conventional agent, what would we do?
We’d expose all six functions as tools, right? That can certainly work.
But does the agent need to manage every step of the investigation?
1.2 What if we gave the agent one tool instead?
Let’s first look at what a typical investigation involves.
For a delayed order, the agent might go like this: check the order → read the warehouse notes → look for stock elsewhere → compare shipping options → check the rerouting policy. Another order could take a different path.
But look closely at what happens between the lookups. Each time, the agent answers the same type of question:
Given the question and the evidence so far, which lookup should come next, or is that enough?
So the investigation is really a series of decisions. Each one picks an option from the available ones based on what has been learned so far.
That’s exactly what a decision model is built for: it takes some context, then picks one of the options.
And if a decision model can make those choices, the agent doesn’t need to make them one by one. The agent passes in the order ID and a question. Given that input, the tool decides which lookups to run.
This way, the agent really only sees one simple interface and does the work that needs an LLM: understanding the request and writing the reply. The decision model handles the steps in between.
1.3 How the pieces fit together
Inside the specialist tool, at each decision point, Python prepares the available functions with their arguments as distinct options, and the evidence collected so far to serve as the context. Then, the model chooses which function to run by returning an action identifier. Python calls the corresponding function and adds its result to the evidence pool. After that, Python requests the next decision.
We also include done and blocked as options, so the model can finish when it has enough evidence or stop when the available operations cannot resolve the question.
This entire loop runs inside an ordinary tool call managed by the OpenAI Agents SDK. We name this tool as order_specialist() , and register it as the agent’s tool.
One thing to keep in mind: the agent doesn't have direct access to those backend operation functions, and it cannot call those functions. It only sees one tool, i.e., order_specialist() , and can call it with an order ID and a question. Once the tool returns, the agent uses the collected evidence to answer the customer.
Here’s what the full interaction looks like:
Figure 1. The decision-model-as-a-tool pattern. (Diagram by author)
2. Build the Specialist Tool
For our current case study, we'll use Decision-1 on Azure as the decision model, and use OpenAI Agents SDK for configuring LLM agent. We'll also briefly show how to call other decision models from OpenRouter.
You can find the complete implementation here .
2.1 Start with the investigation loop
When the agent calls order_specialist() , we run the following loop:
python
while True:
actions = available_actions(order_id, evidence)
choice, _ = await choose_decision(
http, question, evidence, actions
)
if choice in {"done", "blocked"}:
return {
"question": question,
"status": choice,
"evidence": list(evidence),
}
function, arguments = actions[choice]
result = function(**arguments)
evidence.append({
"name": function.__name__,
"arguments": arguments,
"result": result,
})In the code snippet above, we first prepare the available actions, then ask the decision model to select one based on the question and the evidence collected so far. Each action identifier maps to a Python function and its arguments, so actions[choice] gives us everything needed to perform the selected lookup.
After executing it, we add the result to evidence . Then, we repeat. The next decision therefore has access to what we just learned.
When it has enough evidence, the model can also select done . Or, when the available operations cannot resolve the question, the model can select blocked . Either way, the loop ends and returns the collected records to the agent.
So what goes into actions ?
Our available_actions() helper prepares lookups using the information currently available. Initially, we know the order ID, so we can offer order details, warehouse notes, and the policy lookup. Once the order details reveal the item identifier, an inventory lookup becomes available. Shipping lookups follow when we have the warehouse identifiers and destination.
We also remove identical lookups already completed. As the evidence grows, the choices change with it.
2.2 Let the decision model choose
With the prepared possible actions, we can now put them in front of the decision model and ask it to make a selection. Here, we’ll use Decision-1 through Azure.
First, we turn each action into a description:
choices = {
key: (
f"{function.__name__}({json.dumps(arguments)}): "
f"{function.__doc__}"
)
for key, (function, arguments) in actions.items()
}The keys are the action identifiers. The action's description contains the function name, its arguments, and its docstring. For example, a shipping lookup would appear as:
markdown
get_shipping_options({"warehouse_id": "WEST", "destination": "CH-8000"}):
Read shipping estimates, incremental charges, and dispatch cutoff.As mentioned above, we also add the two stopping options:
choices["done"] = (
"Return the observed evidence: "
"it answers the delegated question."
)
choices["blocked"] = (
"Return the observed evidence: "
"available operations cannot answer the question."
)Now we can assemble the request:
body = {
"model": "Decision-1",
"state": {
"question": question,
"as_of": AS_OF,
"evidence": evidence,
},
"questions": {
"action": {
"type": "choice",
"instructions": (
"Choose the next useful operation for this "
"delegated question: " + question +
" Use the observed records and collect missing evidence. "
"Choose done when the evidence answers the question, "
"or blocked if no available operation can obtain "
"a needed fact."
),
"criteria": choices,
}
},
}Here, state holds the context for the decision. We include AS_OF , the current time in our example, because the question involves a delivery deadline.
Under questions , we define one choice question named action . Its criteria contains the options we just prepared. The instruction part explains the model the task objective.
We send this payload to the Azure endpoint:
response = await http.post(
AZURE_ENDPOINT + "/providers/microsoft/v1/systemone",
json=body,
headers={"api-key": os.environ["AZURE_API_KEY"]},
)
response.raise_for_status()
result = response.json()
choice = result["answers"]["action"]["choice"]The response gives us the selected action identifier. Afterward, Python either executes the corresponding function or returns the evidence if the model selected a stopping option.
We now have an intelligent tool that performs an internal decision loop.
2.3 Bonus: Try another decision model
Azure isn’t the only option. We can also access decision models through OpenRouter, including GPT-6 Luna Decisions:
body["model"] = "openai/gpt-6-luna-decisions"
response = await http.post(
"https://openrouter.ai/api/alpha/decisions",
json=body,
headers={
"Authorization": "Bearer " + os.environ["OPENROUTER_API_KEY"],
},
)
response.raise_for_status()
result = response.json()
choice = result["answers"]["action"]["choice"]To try Jev, simply change the model identifier to typesafe/jev-1.14.
2.4 Give the tool to the agent
In the next step, we make the tool available to an LLM agent.
We expose order_specialist() with the OpenAI Agents SDK’s @function_tool decorator. Its interface looks like this:
@function_tool
async def order_specialist(
context: RunContextWrapper[dict],
order_id: str,
question: str,
) -> dict:
"""Gather source records to answer an order-support question.
Args:
order_id: The customer's order identifier.
question: The question to answer from order-support records,
including relevant customer constraints.
"""
evidence = context.context["evidence"]
# HTTP connection and investigation loop from above.The agent supplies order_id and question . The docstring tells the agent what the tool does and what those arguments mean.
So how can we keep the evidence and record the model’s decisions during the investigation? That's the purpose of context .
Unlike the other two arguments, context is supplied by the SDK, rather than the LLM. It carries a Python dictionary where we store those records. RunContextWrapper[dict] is the SDK’s wrapper around that dictionary, which we access through context.context .
For the LLM that powers the agent, we’ll use GPT-6-Luna on Azure with medium reasoning effort:
from openai import AsyncOpenAI
from agents import Agent, ModelSettings, OpenAIResponsesModel
from openai.types.shared import Reasoning
model = OpenAIResponsesModel(
model="gpt-6-luna",
openai_client=AsyncOpenAI(
api_key="AZURE_API_KEY",
base_url="https://YOUR_RESOURCE.openai.azure.com/openai/v1/",
),
)
settings = ModelSettings(
parallel_tool_calls=False,
reasoning=Reasoning(effort="medium"),
)Next, we give the agent the following instructions:
INSTRUCTIONS = (
"You are an order-support assistant. "
"Help the customer using the available tools. "
"Base your answer on returned records. "
"Treat shipping dates as estimates, and "
"describe actions that have not been performed as proposals. "
f"The current fictional time is {AS_OF}."
)We can now configure the agent with our decision-model tool:
hybrid_agent = Agent(
name="Order support with a decision-model tool",
instructions=INSTRUCTIONS,
model=model,
model_settings=settings,
tools=[order_specialist],
)Note that the agent has access to just one tool. The decision model inside the tool manages the investigation.
Now we are ready to run the agent.
3. Run the Agent
To run the agent:
from agents import Runner
hybrid_context = {"evidence": [], "decisions": []}
hybrid_result = await Runner.run(
hybrid_agent,
CUSTOMER_REQUEST,
context=hybrid_context,
)Remember the context parameter in our tool? We initialize its dictionary here, then pass it into the agent run. The tool stores its retrieved records in evidence and its internal selections and measurements in decisions .
In this run, I can see that the agent completed the investigation: it explained why the order was delayed and identified the same potential delivery option.
During this process, Luna called order_specialist once, asking it to investigate the delay and find options for meeting the Friday deadline. Inside the tool, Decision-1 selected five backend operations before choosing done :
get_order_details("ORD-1042")
get_fulfillment_notes("ORD-1042")
get_alternative_inventory("LAMP-01")
get_shipping_options("WEST", "CH-8000")
get_shipping_options("REMOTE", "CH-8000")
doneThe whole run took 8.9 seconds .
Now you might be wondering: how much did we actually gain by moving those decisions into the tool? Let’s find out.
Here, I configured another agent with direct access to our 6 backend functions. Everything else, like the Luna model, instructions, and settings, stay the same:
llm_agent = Agent(
name="Order support with backend tools",
instructions=INSTRUCTIONS,
model=model,
model_settings=settings,
tools=[
function_tool(get_order_details),
function_tool(get_tracking_events),
function_tool(get_fulfillment_notes),
function_tool(get_alternative_inventory),
function_tool(get_shipping_options),
function_tool(get_fulfillment_policy),
],
)Parallel tool calls remain disabled, so this agent selects one backend operation per turn. We then give it the same request:
llm_result = await Runner.run(
llm_agent,
CUSTOMER_REQUEST,
)This agent also completed the investigation, reaching the same core recommendation, but made six Luna calls and took 25.4 seconds. Therefore, our decision-model-as-a-tool setup was 2.85 times faster , with 24.7% lower model cost .
4. Final Thoughts
Most agent tools wrap a single operation, and oftentimes that’s the right call.
But when the agent keeps chaining the same few operations together, it's a good time to ask whether it needs to be in that loop at all.
In our case, moving those routine choices into the tool made the run almost three times faster and about a quarter cheaper, and the agent still reached the same recommendation.
The take-away: your agent doesn’t have to make every decision. Some of them can live in its tools.