Building Production AI Agents: Tool Calling, Memory, and Orchestration That Actually Work

Building Production AI Agents: Tool Calling, Memory, and Orchestration That Actually Work
12 views

Building an AI agent that works in a demo is easy. Building one that works reliably in production is a completely different engineering problem. Tool calling breaks silently. Memory grows unbounded. Reasoning loops get stuck. The patterns here separate hobbyist agents from systems running at scale. This guide focuses on production AI agents, with the production trade-offs senior engineers need to make sound decisions.

⚡ TL;DR: Production agents need structured tool schemas, bounded memory with summarization, deterministic orchestration graphs (not recursive self-calls), and cost+latency budgets per task. Use LangGraph or raw API tool_use for control.

Tool calling done right — schema design matters — production AI agents

// Raw Anthropic API tool calling
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();

const tools = [{
  name: 'search_codebase',
  description: 'Search codebase for files matching a pattern. Returns file paths and snippets.',
  input_schema: {
    type: 'object',
    properties: {
      query: { type: 'string', description: 'Search query — filename, function name, or code snippet' },
      file_type: { type: 'string', enum: ['ts', 'js', 'py', 'go', 'all'] },
      max_results: { type: 'number', description: 'Max results. Default: 10. Max: 50.' }
    },
    required: ['query', 'file_type']
  }
},
{
  name: 'run_tests',
  description: 'Run test suite for a file and return results. Only call after code changes.',
  input_schema: {
    type: 'object',
    properties: {
      test_path: { type: 'string', description: 'Path to test file or directory' },
      timeout_seconds: { type: 'number', description: 'Max wait time. Default: 30.' }
    },
    required: ['test_path']
  }
}];

// Agent loop with proper tool execution
async function runAgent(task, maxSteps = 10) {
  const messages = [{ role: 'user', content: task }];
  let steps = 0;

  while (steps < maxSteps) {
    const response = await client.messages.create({
      model: 'claude-opus-4-5',
      max_tokens: 4096,
      tools,
      messages,
    });

    messages.push({ role: 'assistant', content: response.content });
    if (response.stop_reason === 'end_turn') break;

    if (response.stop_reason === 'tool_use') {
      const results = [];
      for (const block of response.content) {
        if (block.type === 'tool_use') {
          const result = await executeTool(block.name, block.input);
          results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) });
        }
      }
      messages.push({ role: 'user', content: results });
    }
    steps++;
  }

  if (steps >= maxSteps) console.warn('Agent reached max steps — possible loop');
  return messages;
}

Memory architecture — four types every production agent needs

// 1. Working memory — current conversation (messages array)
// 2. Episodic memory — summaries of past interactions
// 3. Semantic memory — facts in vector store (pgvector, Pinecone)
// 4. Procedural memory — tool usage examples in system prompt

class EpisodicMemory {
  summaries = [];

  async store(messages, outcome) {
    // Use cheap model for summarization
    const summary = await client.messages.create({
      model: 'claude-haiku-4-5-20251001',
      max_tokens: 200,
      messages: [{ role: 'user', content:
        'Summarize in 2 sentences: what task, what worked, what failed. Outcome: ' + outcome
      }]
    });
    this.summaries.push({ ts: Date.now(), text: summary.content[0].text });
  }

  getRecent(n = 3) {
    // Production: use vector similarity (pgvector, Pinecone)
    return this.summaries.slice(-n).map(s => s.text);
  }
}

// Inject into system prompt:
const systemPrompt = 'You are a code assistant agent.'
  + (memory.length ? '\n\nRELEVANT PAST EXPERIENCE:\n' + memory.join('\n') : '');

LangGraph orchestration — deterministic state machines

// LangGraph: agent as state machine, not recursive loop
// Gives deterministic flow, breakpoints, and resumability
import { StateGraph, END } from '@langchain/langgraph';

const workflow = new StateGraph({
  channels: {
    messages: { reducer: (a, b) => [...a, ...b] },
    plan: { reducer: (_, b) => b },
    step: { reducer: (_, b) => b },
    results: { reducer: (a, b) => ({...a, ...b}) },
  }
});

// Each node: state -> state (pure functions)
workflow.addNode('plan', async (state) => {
  const plan = await generatePlan(state.messages[0]);
  return { plan, step: 0 };
});

workflow.addNode('execute', async (state) => {
  const result = await executeStep(state.plan[state.step]);
  return { results: { [state.plan[state.step]]: result }, step: state.step + 1 };
});

// Conditional routing — no recursive self-calls
workflow.addConditionalEdges('execute', (state) => {
  if (state.step >= state.plan.length) return END;
  if (state.error) return 'handle_error';
  return 'execute';
});

workflow.setEntryPoint('plan');
const app = workflow.compile();

Cost and latency budgets — mandatory for production

class AgentBudget {
  tokensUsed = 0;
  startTime = Date.now();

  constructor(
    maxTokens = 100000,   // ~$0.30 with Claude claude-sonnet-4-6
    maxLatencyMs = 30000  // 30 second hard timeout
  ) {
    this.maxTokens = maxTokens;
    this.maxLatencyMs = maxLatencyMs;
  }

  check(newTokens) {
    this.tokensUsed += newTokens;
    if (this.tokensUsed > this.maxTokens)
      throw new Error('Token budget exceeded: ' + this.tokensUsed);
    if (Date.now() - this.startTime > this.maxLatencyMs)
      throw new Error('Timeout exceeded');
  }

  get estimatedCost() {
    return (this.tokensUsed / 1000000) * 9; // ~$9/M average
  }
}

Production agent checklist

  • ✅ Structured JSON tool schemas with explicit types and required fields
  • ✅ Bounded context window — summarize old messages, never grow unboundedly
  • ✅ Explicit max_steps and token budget on every agent run
  • ✅ LangGraph state machine for complex multi-step agents
  • ✅ Structured logging of every tool call and result
  • ✅ Graceful degradation — return partial results when budget exceeded
  • ❌ Never use recursive self-calls — loops are impossible to detect
  • ❌ Never run agents without explicit timeouts and step limits

Production agents pair naturally with Lambda Function URL streaming to stream tool call results and reasoning to users in real time. For deploying agent backends, the Lambda cold start guide ensures first-token latency stays fast. External reference: Anthropic tool use documentation.

Level up your AI development skills

→ View Course on Udemy — The most comprehensive hands-on course covering every concept in this post with real projects.

→ Building LLM Powered Applications (Amazon) — The definitive book on building production AI systems and agents.

Sponsored links. We may earn a commission at no extra cost to you.

Related on CheatCoders: Amazon Bedrock Agents: tool use, memory, and guardrails, LLM coding agents in Lambda sandboxes, and RAG on AWS: OpenSearch vs Aurora pgvector.

Related: Building AI Agents That Actually Work: The Architecture No Tutorial Shows You · Bedrock Agents Memory: Session Scratchpads Versus Long-Term Stores · Bedrock Agents: Idempotent Tool Calls Against DynamoDB Writes

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.