An AI agent without tools is surprisingly limited.
A language model can reason over the information it has been given, but it cannot observe the outside world. It cannot check whether an IP address is malicious, search historical logs, or retrieve the latest threat intelligence on its own.
Tools bridge that gap.
In my SOC Analyst Agent, tools serve as the agent’s interface to external systems. Each tool performs one specialized task and returns structured information that the language model can use during its reasoning process.
Designing Tool Contracts
One lesson became clear almost immediately: the agent should never depend on the raw response of an external API.
Every service exposes its own response format. One threat intelligence provider may return an abuseConfidenceScore, another may use risk_level, while a third may expose completely different field names.
If the language model had to understand each vendor’s response format, replacing one service with another would require changing prompts, reasoning logic, and potentially the agent loop itself.
Instead, every tool in the project returns a standardized contract regardless of how the underlying implementation works.
For example, the threat intelligence tool always returns something conceptually similar to:
{
"ip": "...",
"is_malicious": True,
"risk_score": 87,
"context": "...",
"source": "AbuseIPDB"
}
The language model never sees the raw vendor payload. It only sees a consistent interface.
This separation makes each tool responsible for translating vendor-specific data into an application-specific format. The agent only reasons about the standardized output.
Tool 1: Threat Intelligence (check_ip_reputation)
The first tool enriches an IP address using a threat intelligence provider.
I designed it to behave like a production service rather than a simple API wrapper.
The tool performs four distinct steps:
-
Validate the input.
-
Query the external API.
-
Translate the vendor response into the standardized contract.
-
Return a safe fallback if anything goes wrong.
Input Validation
The tool first validates the IP address before making any network request.
An empty or malformed IP address is rejected immediately, avoiding unnecessary API calls and preventing downstream failures.
Vendor Translation
The external service returns many fields that are useful for humans but unnecessary for the agent.
Rather than exposing the raw payload, the tool extracts only the information the agent actually needs:
-
Is the IP malicious?
-
What is the risk score?
-
What contextual information is available?
-
Which data source produced the result?
This keeps the reasoning process focused while hiding vendor-specific implementation details.
Safe Defaults
Perhaps the most important design decision was defining a fallback contract before making the API request.
External services fail. Networks time out. API keys expire. Rate limits are exceeded.
Instead of propagating exceptions into the agent loop, every failure returns the same contract shape with the reputation marked as unknown.
The agent can continue its investigation instead of crashing because one external dependency became unavailable.
fallback_response = {
"ip": ip_address,
"is_malicious": None,
"risk_score": None,
"context": "Threat intelligence unavailable. Reputation is UNKNOWN.",
"source": "AbuseIPDB (Fallback)"
}
Tool 2: Log Correlation (query_logs)
Threat intelligence tells the agent who an IP address is.
Log correlation tells it what actually happened.
The second tool searches historical authentication and firewall logs within a configurable time window.
Why Simulated Logs?
For this project I used a flat JSON log database instead of integrating with a production SIEM.
This allowed me to focus on the architecture of the agent rather than authentication, infrastructure, or vendor-specific APIs.
The important part was not where the logs came from; it was designing a tool interface that could later be replaced by a real backend.
A future implementation could query Splunk, Elastic, Microsoft Sentinel, or any other log platform without changing the agent’s reasoning logic.
Time Window Filtering
One subtle challenge was preventing the agent from searching an unlimited amount of data.
The tool accepts a time window and filters log entries accordingly before performing the search. It also validates the requested log category and sanitizes parameters to guard against invalid inputs from the language model.
This keeps searches bounded, predictable, and efficient.
current_time = datetime.now(timezone.utc)
cutoff_time = current_time - timedelta(hours=time_window_hours)
log_time = datetime.fromisoformat(log_time_str.replace("Z", "+00:00"))
if log_time < cutoff_time:
continue
Correlating Evidence
One of my test scenarios involved repeated failed VPN login attempts from a suspicious external IP.
The investigation followed a sequence very similar to what a human analyst would perform:
-
Inspect the security alert.
-
Check the IP’s reputation.
-
Search historical authentication logs.
-
Determine whether similar activity occurred previously.
-
Combine both pieces of evidence before making a decision.
In the test, the log query revealed something the alert metadata had not indicated: a successful authentication from the same malicious IP occurring minutes after the failed attempts. The agent correctly escalated from ‘suspicious activity’ to ‘active compromise’ — a distinction that changes the entire response playbook.
The threat intelligence lookup identified a suspicious source, while the log query established whether that source had actually interacted with the environment. Only by combining both could the agent produce a meaningful assessment.
The Lesson: Tools Are Interfaces, Not Implementations
Building these tools changed how I think about AI agents.
The language model doesn’t care whether threat intelligence comes from AbuseIPDB, VirusTotal, or an internal database.
It doesn’t care whether logs come from a JSON file, Splunk, Elastic, or Microsoft Sentinel.
What matters is the interface.
As long as every implementation honors the same input and output contract, I can replace vendors, migrate infrastructure, or completely rewrite a tool without touching the agent loop or modifying the prompts.
The language model continues reasoning exactly as before because the contract never changes.
That separation between what the agent needs and how the information is obtained turned out to be one of the most valuable architectural lessons from the entire project.