Shipping AI Agents to Production: Guardrails, Evaluation and Cost Control

Agents fail in new ways: unsafe tool calls, prompt injection and runaway loops. The engineering controls we put around every agent before it meets customers.

DSDilawar Singh 2 min read 1,980 views
Shipping AI Agents to Production: Guardrails, Evaluation and Cost Control

An agent is a loop: the model reads context, chooses a tool, observes the result and decides what to do next. That loop is powerful, and it fails in ways ordinary software does not — it can call the wrong tool with plausible arguments, follow instructions hidden in a document, or spin through dozens of steps on a single request. Production readiness means engineering around those failure modes.

Tools are API contracts

Treat every tool like a public endpoint. Define its arguments with a schema, validate the model's output against it, and reject anything that does not parse.

const tools = {
  lookup_order: {
    schema: z.object({ orderId: z.string().regex(/^ord_[a-z0-9]+$/) }),
    run: (args: { orderId: string }, viewer: Viewer) => orders.getForViewer(viewer, args.orderId),
  },
  request_refund: {
    schema: z.object({ orderId: z.string(), reason: z.string().max(500) }),
    run: (args: { orderId: string; reason: string }, viewer: Viewer) => approvals.queue('refund', viewer, args), // human approves
  },
};

Least privilege, inherited from the user

The agent should act with the permissions of the person it is helping, never with a service account that can see everything. If the user cannot open an order in the UI, the tool must not return it to the model either.

Humans approve irreversible actions

Reading is cheap to get wrong; refunds, emails and deletions are not. Our default is that any action with external side effects becomes a proposal that a human confirms, with the agent's reasoning attached. Teams relax this per action once evaluation data justifies it.

Treat retrieved content as data

Prompt injection arrives through documents, tickets and web pages the agent reads. Wrap untrusted content in clearly delimited data blocks, instruct the model never to follow instructions inside them, and — more importantly — make sure the tools themselves cannot do damage even if the model is tricked.

Bound the loop

const MAX_STEPS = 8;
for (let step = 0; step < MAX_STEPS; step++) {
  const res = await model.generate({ messages, tools: toolSpecs, signal: AbortSignal.timeout(30_000) });
  if (!res.toolCall) return res.text;
  const tool = tools[res.toolCall.name as keyof typeof tools];
  const args = tool?.schema.safeParse(res.toolCall.args);
  if (!tool || !args?.success) { messages.push(toolError('Invalid tool call')); continue; }
  messages.push(toolResult(await tool.run(args.data as never, viewer)));
}
return escalateToHuman(messages); // never loop forever

Evaluate like you test

  • Build a scenario set from real conversations: expected tool calls, forbidden tool calls and acceptable answers.
  • Score every prompt, model or tool change against it in CI before it ships.
  • Trace every production run (inputs, tool calls, tokens, latency) so failures can be replayed.

Control cost per tenant

Log tokens per request and per tenant, set budgets with alerts, cache deterministic sub-steps, and route simple requests to smaller models. Cost surprises are an engineering problem, not a finance one.

Key takeaways

  • Validate every tool call against a schema.
  • Run agents with the user's permissions, not a superuser's.
  • Put a human in front of irreversible actions.
  • Assume injected instructions will arrive; design tools so they cannot cause harm.
  • Bound loops, evaluate continuously, and budget tokens per tenant.
DS

Written by

Dilawar Singh

Chief Technology Officer, DEV3 Technologies

DEV3 Technologies has delivered web platforms, AI systems, SaaS products and mobile apps since 2025. Articles are written and reviewed by the engineers who build and operate these systems.

Keep reading

All articles