If you ship an LLM agent that calls APIs, you must test AI agent tool use like any other integration surface. Guessing from a handful of manual chats misses the edge cases that break production: malformed arguments, silent retries, and wrong tool selection. This guide walks through a concrete pipeline to validate tool calls with deterministic harnesses and model-in-the-loop tests before you deploy.
Step 1: Pin the tool schema and side-effect contract
Start by writing the tool definition as strict JSON Schema. The agent’s model will receive this, and your tests will assert against it. Treat the schema as an API contract, not a suggestion. If the model can emit a field your backend rejects, that is a test failure waiting to happen.
{
"name": "create_refund",
"description": "Issue a refund for an order. Only use after confirming fraud or duplicate charge.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "pattern": "^o_[0-9a-z]+$"},
"amount_cents": {"type": "integer", "minimum": 1, "maximum": 1000000},
"reason": {"type": "string", "enum": ["duplicate", "fraud", "customer_request"]}
},
"required": ["order_id", "amount_cents", "reason"]
}
}
Document the side effects next to the schema. A refund tool hits a payments API and sends a customer email. Your test plan must state whether calling it twice with the same order_id is idempotent. If it is not, your agent needs a dedupe layer, and your tests need to prove that layer works. Use a typed validator (Pydantic or similar) in the agent code so the schema is loaded once and shared between runtime and tests.
Step 2: Stand up a mock tool endpoint
You cannot let the agent call production during tests. Build a local mock that records invocations and returns canned responses. Below is a minimal FastAPI app that logs the call and echoes a success. Run it in a fixture so every test starts clean.
from fastapi import FastAPI, Request
import json
app = FastAPI()
calls = []
@app.post("/tools/create_refund")
async def create_refund(req: Request):
body = await req.json()
calls.append(body)
return {"status": "ok", "refund_id": "ref_123"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, port=8080)
Wrap this in a pytest fixture that resets calls and starts the server on a free port:
import pytest, threading, time
from mock_tools import app, calls
from uvicorn import Server, Config
@pytest.fixture
def tool_server():
config = Config(app=app, port=8081, log_level="warning")
server = Server(config)
t = threading.Thread(target=server.run, daemon=True)
t.start()
time.sleep(0.5)
calls.clear()
yield "http://localhost:8081/tools"
server.should_exit = True
Your agent config points its tool base URL to that yield. This gives you a deterministic surface to test AI agent tool use without risking real money or sending email.
Step 3: Capture and replay agent transcripts for unit tests
Model outputs are non-deterministic, but the agent’s post-processing logic should not be. Record a real or synthetic model response that includes a tool call, then feed it to your agent’s parsing layer in a unit test. Store these as “golden” fixtures.
def test_agent_parses_refund_call():
llm_response = {
"tool_calls": [{
"function": {
"name": "create_refund",
"arguments": '{"order_id":"o_1","amount_cents":500,"reason":"fraud"}'
}
}]
}
parsed = agent.extract_tool_call(llm_response)
assert parsed.name == "create_refund"
assert parsed.args.amount_cents == 500
assert parsed.args.reason == "fraud"
Add a test that feeds a malformed argument string and confirms your code raises a validation error rather than crashing. When you test AI agent tool use at this layer, you catch schema drift and JSON serialization bugs before they reach the model loop.
Step 4: Run model-in-the-loop tests against multiple providers
Unit tests are not enough. You need to verify the model actually selects the right tool given a prompt. Use a client that talks to several models behind one interface. Point your test suite at an OpenAI-compatible gateway such as n4n.ai, which exposes one endpoint for 240+ models, honors client routing directives, forwards provider cache-control hints, and handles automatic fallback when a provider is degraded. That lets you test AI agent tool use across model families without rewriting client code.
from openai import OpenAI
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="test-key")
def run_agent_turn(user_msg: str, model: str = "auto"):
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": user_msg}],
tools=[REFUND_TOOL_SCHEMA]
)
return resp.choices[0].message
def test_model_picks_refund_tool():
msg = run_agent_turn("Refund order o_1 for $5 due to fraud")
assert msg.tool_calls[0].function.name == "create_refund"
Run this for a matrix of models by iterating over a list: "gpt-4o-mini", "claude-3-haiku", "mistral-small". The gateway’s fallback means a rate-limited provider won’t fail your suite silently; it reroutes. You now have evidence that your tool descriptions are not over-fit to one vendor.
Step 5: Assert on tool call structure, not natural language
Never assert that the model said “I issued a refund”. Assert the tool was called with correct arguments. Extend the test to check the mock’s recorded calls after a full agent loop.
def test_full_agent_loop_calls_mock(tool_server):
agent.configure(tool_base_url=tool_server)
agent.run("Refund order o_2 for 1000 cents, duplicate charge")
assert len(calls) == 1
assert calls[0]["order_id"] == "o_2"
assert calls[0]["amount_cents"] == 1000
assert calls[0]["reason"] == "duplicate"
Add negative tests: prompts that should NOT trigger a tool. If the agent calls create_refund on “What is a refund policy?”, your guardrails fail. When you test AI agent tool use at the integration level, include at least five negative examples per tool. Use a Pydantic model to validate the recorded call so type mismatches surface as test errors, not as 500s in production.
Step 6: Shadow test with recorded traffic
Before flipping the switch, run the agent against historical support tickets in shadow mode. Log every tool call attempt and compare with human annotations. Use per-token metering if your gateway provides it to track cost of the test runs. This step surfaces mismatch between training data and real queries.
Collect precision and recall on tool selection. If the agent misses 5% of refunds on shadow data, tighten the schema description or add a pre-classifier. Run the shadow suite for a few thousand examples to get a stable read. Pay attention to partial argument fills: a call with reason missing is a silent failure if your backend defaults to something unsafe.
Verify success
You have a solid test AI agent tool use pipeline when:
pytestpasses on schema parsing and mock invocation tests in under a second, with zero network calls to production.- Model-in-the-loop tests pass on at least three different model families using the same agent code and same tool schema.
- Shadow run shows tool selection precision > 95% on a labeled sample of 200 real prompts, and no false positives on negative set.
- No production-side effects occur during any test stage, confirmed by mock assertions and network isolation.
Ship only when those hold. The cost of a bad tool call in production is always higher than the effort to test it here.