Back to resources
// technical · agents

Give an agent tools without it hallucinating

A model alone invents a believable fiction. A tooled model goes and checks. The difference between the two comes down to a few well-described functions — and a few guardrails so it actually uses them.

TechnicalJul 10, 2026~8 min · intermediate
LangGraphLangChainfunction callingZodguardrails
01

Why it hallucinates

An LLM doesn't lie: it completes. When you ask it to summarize the activity of a repo it can't read, it produces the most plausible continuation — a believable but invented roadmap. The problem isn't the model, it's that we cut off its access to reality.

i

This is the tipping point I describe in from a prompt to agentic: giving tools means going from “guessing” to “fetching.”

02

A tool is a described function

A tool is three things: a name, a description (in natural language — that's what the model reads), and an input schema. The model decides when to call it from the description — so it matters as much as the code.

tools.tscopy
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// un outil = une fonction + un schema. Le modele l'appelle quand il en a besoin.
const getGitlabActivity = tool(
async ({ projectId }) => {
const commits = await gitlab.commits(projectId); // donnees REELLES
return JSON.stringify(commits.slice(0, 20));
},
{
name: "get_gitlab_activity",
description: "Lit les commits reels d'un projet GitLab. A utiliser avant de resumer une roadmap.",
schema: z.object({ projectId: z.string() }),
},
);
03

The model decides, the graph executes

You bind the tools to the model. Each turn, it can either answer or request a tool call. The node executes the call, feeds the result back, and hands control back to the model — that’s the agentic loop.

node.tscopy
import { ChatOpenAI } from "@langchain/openai";
// on "donne" les outils au modele — il decide seul quand les appeler
const model = new ChatOpenAI({ model: "gpt-4o" }).bindTools([
getGitlabActivity,
listProjects,
readMemory,
]);
// dans le noeud : si le modele demande un outil, on l'execute et on reboucle
const res = await model.invoke(messages);
if (res.tool_calls?.length) {
// ... execute les outils, ajoute les resultats, rappelle le modele
}
// the graph (langgraph)
gather
context
clarify
ask ?
draft
decide
review
self-review
apply
apply
review ↺ draft — loop if not enoughclarify → human-in-the-loop
Postgres — durable stateLangfuse — traced
04

The guardrails

Giving tools isn't enough: you must frame their use, otherwise the agent loops, over-calls, or ignores the tools and invents anyway.

01
Precise descriptions

Say when to use the tool, not just what it does. “Call before summarizing a roadmap” guides better than “reads GitLab.”

02
Cap the turns

A max-iterations counter prevents an agent from spinning on tool calls without ever concluding.

03
Validate outputs

A Zod output schema: if the model returns a mistyped field, send it back to fix instead of crashing.

04
Trace every call

Without tracing, a tooled agent is undebuggable. You see which tool was called, with what, and what it returned.

!

The most dangerous tool is the one that writes. Read tools are safe; for those that modify (create, delete), add a human confirmation — that’s the whole point of human-in-the-loop, for real.

05

Design the schema as a contract

The input schema isn't paperwork: it's the contract that stops the model from improvising. In strict mode, the provider guarantees the produced arguments match the schema exactly — no invented field, no fuzzy type. On the Anthropic side you add `strict: true` to the definition; on OpenAI, the same flag requires `additionalProperties: false` and every field in `required` (an optional field is modeled as a union with `null`).

With Zod, you write the schema once and LangChain converts it to JSON Schema for the tool call, then validates and parses the response on the fly — no manual parsing on your end (structured output docs). So the same Zod object both describes the input to the model and checks the output before letting it continue.

Name without ambiguity

A `user` parameter invites the model to guess: an id? a name? an email? A `user_id` parameter leaves no choice. Anthropic sums this up as the “intern test”: if a new hire can't use the tool from the description and field names alone, neither can the model (writing tools for agents).

enumAn `enum` (status, category) forbids the model an invalid state — better than a free string you then re-validate.
strictStrict mode turns the schema from a wish into a guarantee: arguments match, or the call doesn't happen.
descriptionThe description says *when* to call, not just what the tool does. That text is what the model reads to decide.
i

Strict mode guarantees the *shape*, not the *meaning*. A JSON Schema can't express which field combinations make sense or which conventions your API expects — that stays on the description and your business-level validation.

06

Catch errors, measure the calls

Even well described, a tool sometimes returns an error, or the model passes an argument your business validation rejects. The right reaction isn't to crash: it's to send the error back *to the model* as a tool result, with an actionable message, so it fixes it on the next turn. Anthropic recommends “specific and actionable” messages over opaque codes (writing tools for agents).

retry.tscopy
const parsed = ToolInput.safeParse(rawArgs);
if (!parsed.success) {
// On ne plante pas : on renvoie l’erreur au modèle comme tool_result
return {
role: "tool",
tool_call_id: call.id,
content: `Validation failed. Fix these fields and retry:\n\${parsed.error.issues
.map((i) => `- \${i.path.join(".")}: \${i.message}`)
.join("\n")}`,
};
}
// parsed.data est typé et validé — on peut exécuter l’outil en confiance
const result = await runTool(parsed.data);
!

A correction loop with no ceiling is an agent that can loop forever (and burn tokens) on an argument it will never produce. Keep the iteration counter from section 04 on the retry loop too.

Limit the tool surface

The more tools you give, the more the model picks the wrong target and the more context fills up. Anthropic advises staying under ~20 active tools and consolidating: one `schedule_event` beats `list_users` + `list_events` + `create_event`. Beyond that, switch to on-demand tool discovery instead of loading everything into context.

Evaluate, don’t just hope

“Works on my example” isn't enough. You evaluate tool use at three levels: the final answer, the isolated step (did it pick the right tool?), and the whole *trajectory* (did it follow the right chain of calls?). The `agentevals` package ships ready-made evaluators: strict, unordered, or subset match on the expected tool calls.

Final answer: does the output actually answer the request, grounded on the fetched data?
Isolated step: on this input, did it call the right tool with the right arguments?
Trajectory: does the sequence of calls match the expected path (ordered or not)?

These evals run on the tracing from section 04: without a trace of which tool was called with what, you can neither debug nor score. Tracing is the foundation, evaluation is what you build on top.

01

Sources & further reading

01Anthropic — Tool use with ClaudeThe reference: defining a tool, `input_schema`, strict mode, and how the model decides to call.02Anthropic — Writing effective tools for agentsThe “intern test,” unambiguous naming, tool consolidation, and actionable error messages.03OpenAI — Function calling guideStrict mode, schema constraints (`additionalProperties: false`), and staying under ~20 functions.04OpenAI — Structured outputsGuaranteeing an output matches a JSON Schema, with the `refusal` field for safety refusals.05LangChain — Structured outputWiring a Zod schema to the model, automatic validation and parsing, tool-calling fallback.06LangChain — agentevalsReady-made evaluators for agent trajectories: strict, unordered, subset match.07LangSmith — Trajectory evaluationsEvaluating an agent's full path — tools chosen, intermediate reasoning — with or without a reference trajectory.

A hallucinating agent is almost always an agent cut off from reality. Well-described tools, a loop that executes, guardrails to frame it: that's what turns a believable fiction into an answer grounded in facts.