Building Resilient AI Agents: How Function-Calling and Sandboxed Execution Prevent Hallucinations
Published on 2026-10-25 · 6 min read · by Kreggscode
The promise of AI agents—autonomous systems capable of complex reasoning and task execution—is rapidly becoming a reality. However, a persistent nemesis hinders their widespread adoption: hallucinations. Large Language Models (LLMs), the brains of many agents, are prone to generating factually incorrect or nonsensical outputs. This article delves into two foundational techniques for mitigating this problem and building truly resilient AI agents: function-calling and sandboxed execution.
The Hallucination Problem in AI Agents
LLMs are trained on vast datasets, but this training is inherently probabilistic. They learn to predict the next most likely token, not to possess factual knowledge in a verifiable way. When an LLM is asked to perform a task that requires real-world data, precise calculations, or interaction with external systems, it can "hallucinate" information if it lacks direct access or if its internal representation is fuzzy.
Consider an AI agent tasked with booking a flight. If it simply relies on its training data, it might invent flight numbers, prices, or availability. This is unacceptable for production systems.
Function-Calling: Grounding LLMs in Reality
Function-calling is a paradigm where the LLM is empowered to invoke external tools or functions to retrieve accurate information or perform actions. Instead of hallucinating an answer, the LLM recognizes its limitation and signals that a specific function needs to be called with certain arguments.
Architectural Flow:
+-------------------+ +---------------------+ +-----------------+
| User Request | --> | AI Agent Core | --> | LLM (Model) |
+-------------------+ +---------+-----------+ +--------+--------+
| |
| (Tool/Function Call) | (Function Signature)
v v
+-----------------+ +-----------------+
| Function Router | --> | External Tools/ |
| (Argument Parser)| | APIs/Databases |
+-----------------+ +--------+--------+
^ |
| (Function Result) | (Data/Status)
+-------------------------+
How it Works:
1. LLM Invocation: The AI agent's core receives a user request. It passes this request to the LLM.
2. Intent Recognition & Tool Selection: The LLM, trained with function definitions, analyzes the request. If it determines that an external tool is necessary, it doesn't generate an answer directly. Instead, it outputs a structured request to call a specific function with identified arguments. For example, instead of saying "The flight price is $500," it might output:
{
"tool_name": "getFlightPrice",
"arguments": {
"origin": "SFO",
"destination": "LAX",
"date": "2026-11-15"
}
}3. Function Execution: The AI agent's core receives this structured output. A Function Router (or similar middleware) parses the output, identifies the requested tool (
getFlightPrice), and extracts the arguments. It then invokes the actual getFlightPrice function (e.g., a Python script calling a flight API).4. Result Synthesis: The external tool executes and returns a concrete result (e.g.,
{"price": 485.75, "currency": "USD"}). This result is passed back to the LLM.5. Final Response Generation: The LLM uses the factual result from the tool to formulate a coherent and accurate response to the user.
Practical Implementation (Python Example):
import json
from typing import Dict, Any
Assume this is your LLM's output parser
def parse_llm_output(output: str) -> Dict[str, Any]:
try:
return json.loads(output)
except json.JSONDecodeError:
# Handle cases where LLM output is not valid JSON
return {"error": "Invalid LLM output format"}
Example external tool
def get_flight_price(origin: str, destination: str, date: str) -> Dict[str, Any]:
print(f"Calling external API: getFlightPrice(origin={origin}, destination={destination}, date={date})")
# In a real scenario, this would call an actual flight booking API
if origin == "SFO" and destination == "LAX" and date == "2026-11-15":
return {"price": 485.75, "currency": "USD", "status": "success"}
return {"error": "Flight not found", "status": "failed"}
Agent Core Logic
def process_user_request(user_prompt: str, llm_model: callable) -> str:
# In a real agent, llm_model would be an API call to an LLM service
# For demonstration, we simulate LLM output
llm_output_str = llm_model(f"User: {user_prompt}\nAgent:")
parsed_output = parse_llm_output(llm_output_str)
if "tool_name" in parsed_output:
tool_name = parsed_output["tool_name"]
args = parsed_output["arguments"]
if tool_name == "getFlightPrice":
result = get_flight_price(args)
# Feed the result back to the LLM for a final response
final_response_llm_output = llm_model(f"Tool Result: {json.dumps(result)}\nAgent:")
return final_response_llm_output
else:
return f"Error: Unknown tool '{tool_name}' requested."
elif "error" in parsed_output:
return f"LLM Error: {parsed_output['error']}"
else:
# Assume LLM provided a direct answer
return llm_output_str
--- Simulation ---
def mock_llm_flight_request(prompt: str) -> str:
if "book a flight from SFO to LAX on November 15, 2026" in prompt:
return json.dumps({
"tool_name": "getFlightPrice",
"arguments": {
"origin": "SFO",
"destination": "LAX",
"date": "2026-11-15"
}
})
elif "Tool Result: {\"price\": 485.75, \"currency\": \"USD\", \"status\": \"success\"}" in prompt:
return "Okay, I found a flight from SFO to LAX on 2026-11-15 for $485.75 USD."
return "I can help with that. Please specify origin, destination, and date."
user_request = "I need to book a flight from SFO to LAX on November 15, 2026."
response = process_user_request(user_request, mock_llm_flight_request)
print(f"Agent Response: {response}")
Trade-offs:
* Pros: Significantly reduces hallucinations by grounding the LLM with real data. Enables complex task execution. Provides auditability (logs of function calls).
* Cons: Requires defining and maintaining external tools/APIs. Adds complexity to agent architecture. Latency can increase due to external calls. LLM needs to be fine-tuned or prompted effectively for function-calling.
Sandboxed Execution: Containing Unpredictable Behavior
While function-calling ensures factual accuracy, what about the execution environment itself? If an agent needs to run arbitrary code (e.g., data analysis scripts, custom logic), we need to isolate that execution to prevent security breaches or system instability. This is where sandboxed execution comes in.
Architectural Flow:
+-------------------+ +---------------------+ +-----------------+
| User Request | --> | AI Agent Core | --> | LLM (Model) |
+-------------------+ +---------+-----------+ +--------+--------+
|
| (Code Execution Request)
v
+-----------------+
| Sandbox Manager |
| (e.g., Docker, |
| Firecracker) |
+--------+--------+
^|
| (Code Execution)
| (Results/Output)
+--------+--------+
| Isolated Runtime|
| (Container/VM) |
+-----------------+
How it Works:
1. Code Generation/Request: The LLM might be tasked with generating code (e.g., a Python script to analyze a dataset) or instructed to execute a pre-defined script.
2. Sandbox Orchestration: The AI agent's core passes this code or execution request to a Sandbox Manager. This manager is responsible for provisioning and managing isolated execution environments.
3. Isolated Execution: The code runs within a secure sandbox. Technologies like Docker containers, gVisor, or lightweight VMs like Firecracker provide process, network, and filesystem isolation. This prevents the code from accessing sensitive host system resources or interfering with other agent processes.
4. Result Capture & Return: Standard output, errors, and any other defined results from the sandboxed process are captured and returned to the AI agent core.
5. Response Synthesis: The AI agent core uses the results from the sandbox to formulate its final response.
Practical Implementation (Conceptual - using Docker):
Imagine an agent needs to run a Python script to calculate the standard deviation of a list of numbers provided by the user.
import docker
import json
import uuid
def run_in_sandbox(script_code: str, input_data: Any = None) -> Dict[str, Any]:
client = docker.from_env()
container_name = f"agent-sandbox-{uuid.uuid4().hex[:8]}"
image_name = "python:3.9-slim" # Use a minimal Python image
# Prepare the script and input for the container
script_path = "/app/script.py"
input_path = "/app/input.json"
output_path = "/app/output.json"
dockerfile_content = f"""
FROM {image_name}
WORKDIR /app
COPY {script_path} /app/
COPY {input_path} /app/
CMD ["python", "script.py", "--input", "{input_path}", "--output", "{output_path}"]
"""
# Write the script and input to temporary files
with open("Dockerfile", "w") as f:
f.write(dockerfile_content)
with open(script_path, "w") as f:
f.write(script_code)
if input_data is not None:
with open(input_path, "w") as f:
json.dump(input_data, f)
try:
# Build the image
client.images.build(path=".", tag="agent-sandbox-image")
# Run the container with limited resources and network disabled
container = client.containers.run(
"agent-sandbox-image",
detach=True,
name=container_name,
auto_remove=True,
network_disabled=True, # Crucial for security
mem_limit="128m", # Resource limits
cpu_shares=512,
volumes={'.': {'bind': '/app', 'mode': 'rw'}} # Mount current dir to /app
)
# Wait for container to finish and get logs/output
result = container.wait()
logs = container.logs().decode('utf-8')
# Attempt to read output file
try:
with open(output_path, "r") as f:
output_data = json.load(f)
return {"status": "success", "data": output_data, "logs": logs}
except FileNotFoundError:
return {"status": "error", "message": "Output file not found.", "logs": logs, "exit_code": result['StatusCode']}
except json.JSONDecodeError:
return {"status": "error", "message": "Failed to parse output JSON.", "logs": logs, "exit_code": result['StatusCode']}
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
# Clean up temporary files
import os
os.remove("Dockerfile")
os.remove(script_path)
if input_data is not None:
os.remove(input_path)
if os.path.exists(output_path):
os.remove(output_path)
--- Example Usage ---
LLM might generate this script and data
user_script = """
import sys
import json
import statistics
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="Path to input JSON file")
parser.add_argument("--output", required=True, help="Path to output JSON file")
args = parser.parse_args()
with open(args.input, 'r') as f:
data = json.load(f)
if not isinstance(data, list) or not all(isinstance(x, (int, float)) for x in data):
raise ValueError("Input must be a list of numbers")
std_dev = statistics.stdev(data) if len(data) > 1 else 0
with open(args.output, 'w') as f:
json.dump({"standard_deviation": std_dev}, f)
if __name__ == "__main__":
import argparse
main()
"""
user_data = [10, 12, 15, 11, 13, 14]
Agent core would call run_in_sandbox
sandbox_result = run_in_sandbox(user_script, input_data=user_data)
print(json.dumps(sandbox_result, indent=2))
If successful, the agent would then use sandbox_result['data']['standard_deviation']
to formulate a response.
Production Benchmarks & Considerations:
* Technology Choice:
* Docker: Mature, widely adopted. Overhead for small tasks can be significant (pulling images, container startup).
* gVisor: Kernel-level isolation without requiring full OS. Lower overhead than full VMs.
* Firecracker: Lightweight virtual machine monitor (VMM) developed by AWS. Extremely fast startup times (milliseconds), minimal resource footprint. Ideal for serverless and microservices. Often used as the backend for AWS Lambda and Fargate.
* Resource Limits: Crucial to set strict memory, CPU, and network limits to prevent DoS attacks or runaway processes.
* Cold Starts: Container/VM startup time is a critical factor. Firecracker excels here. Caching images or pre-warming environments can mitigate this.
* Security: Carefully manage container image security, host OS hardening, and network policies. Avoid running containers as root.
* Cost: Running many isolated environments can incur significant cloud costs. Optimize resource allocation and container lifecycle management.
Trade-offs:
* Pros: Prevents malicious or buggy code from affecting the host system. Isolates dependencies. Provides a clean, reproducible execution environment. Enhances security and stability.
* Cons: Adds significant architectural complexity and operational overhead. Introduces latency due to environment startup and teardown. Requires careful resource management.
Conclusion: The Pillars of Resilient Agents
Function-calling and sandboxed execution are not mutually exclusive; they are complementary techniques that form the bedrock of resilient AI agents.
* Function-calling grounds LLMs in factual reality by allowing them to interact with external, reliable data sources and services, thereby preventing factual hallucinations.
* Sandboxed execution ensures the integrity and security of the agent's operational environment, preventing code-related issues from causing instability or security breaches.
By strategically implementing these patterns, developers can move beyond experimental AI and build agents that are reliable, trustworthy, and ready for production workloads. The future of AI agents lies in their ability to not only reason but also to act with verifiable accuracy and within secure, controlled boundaries.