Guides

Building AI Agents & Tool Use with open-source tools

Developing production-grade AI agents requires transitioning from basic prompt-response cycles to robust state machines. This guide focuses on implementing a resilient agent loop that handles tool execution, state persistence, and human-in-the-loop validation using a structured orchestration pattern.

4-6 hours5 steps
1

Define Standardized Tool Schemas

Create strict JSON schemas for every tool the agent can access. Ensure parameter descriptions are descriptive but concise, as these serve as the primary instructions for the LLM to decide when and how to call the tool.

tools/inventory.ts
const getInventoryTool = {
  name: "get_inventory_levels",
  description: "Check stock levels for a specific SKU. Use this when users ask about product availability.",
  parameters: {
    type: "object",
    properties: {
      sku: { type: "string", description: "The product SKU, e.g., 'PROD-123'" },
      warehouse_id: { type: "string", enum: ["east", "west"], description: "Optional warehouse location filter" }
    },
    required: ["sku"]
  }
};

⚠ Common Pitfalls

  • Using ambiguous parameter names like 'data' or 'value' which lead to hallucinated inputs.
  • Overloading a single tool with too many responsibilities instead of creating specialized atomic tools.
2

Implement the Agent State Machine

Instead of a simple while-loop, implement a state machine that defines transitions between 'Thinking', 'Acting', and 'Responding'. This allows for better control over the agent's lifecycle and makes the workflow resumable.

workflow/graph.ts
import { StateGraph, END } from "@langchain/langgraph";

const workflow = new StateGraph({ channels: agentStateChannels })
  .addNode("agent", callModel)
  .addNode("action", executeTools)
  .addEdge("agent", "action")
  .addEdge("action", "agent")
  .setEntryPoint("agent");

const app = workflow.compile({ checkpointer: new MemorySaver() });

⚠ Common Pitfalls

  • Failing to implement a 'recursion_limit', which can lead to infinite loops and massive API bills if the agent fails to converge on a solution.
3

Execute Tools with Error Handling

Create a dedicated executor that maps tool calls to local functions. The executor must catch exceptions and format them as tool output so the LLM can attempt to correct its own errors (e.g., 'Invalid SKU format').

workflow/executor.ts
async function executeTools(state) {
  const lastMessage = state.messages[state.messages.length - 1];
  const toolOutputs = await Promise.all(lastMessage.tool_calls.map(async (call) => {
    try {
      const result = await registry[call.name](call.args);
      return { role: "tool", tool_call_id: call.id, content: JSON.stringify(result) };
    } catch (e) {
      return { role: "tool", tool_call_id: call.id, content: `Error: ${e.message}. Please correct the arguments.` };
    }
  }));
  return { messages: toolOutputs };
}

⚠ Common Pitfalls

  • Returning raw stack traces to the LLM, which consumes unnecessary tokens and may confuse the model.
  • Executing tools without timeout limits, causing the entire agent loop to hang.
4

Integrate Human-in-the-Loop Approval

For sensitive tools (e.g., payments, data deletion), implement an interrupt. The agent state is persisted to a database, the execution pauses, and waits for a manual 'approve' or 'reject' signal from an external UI.

workflow/hitl.ts
// Define a node that requires manual intervention
const workflow = new StateGraph(StateSchema)
  .addNode("agent", callModel)
  .addNode("sensitive_tool", executeTransaction)
  .addConditionalEdges("agent", shouldContinue, {
    continue: "sensitive_tool",
    end: END
  });

// Compile with an interrupt before the sensitive node
const app = workflow.compile({ 
  checkpointer: dbSaver,
  interruptBefore: ["sensitive_tool"]
});

⚠ Common Pitfalls

  • Hard-coding approval logic inside the agent prompt rather than enforcing it at the infrastructure/orchestration level.
5

Configure Observability and Trace IDs

Wrap the agent execution in a trace context. Every tool call and LLM interaction must be logged with a unique session ID to allow for post-hoc debugging of complex multi-step failures.

observability/tracing.ts
import { Traceable } from "langsmith/traceable";

const agent = new Traceable({
  name: "OrderProcessingAgent",
  metadata: { userId: "user_123", environment: "production" }
});

const result = await app.invoke(
  { messages: [new HumanMessage("Refund order 55")] },
  { configurable: { thread_id: "session_abc" } }
);

⚠ Common Pitfalls

  • Deploying agents without tracing, making it impossible to determine why an agent entered a specific tool-calling loop in production.

What you built

By moving tool execution into a structured state machine with explicit error handling and human-in-the-loop gates, you create agents that are predictable and safe for production use. Always prioritize observability and strict schema validation to minimize the risks of autonomous LLM behavior.