Agentic Complexity: Low

Tool Use in Go

Register typed tools by name so an agent can dispatch LLM-selected function calls to the right handler without embedding dispatch logic in the agent itself.

The Problem

LLMs can request tool calls by name — but the agent needs to route that name to the right Go function, validate arguments, and return a structured result. Hardcoding a switch on tool names couples the agent to every tool and makes adding new tools a surgery on the core loop.

The Solution

Define a Tool interface with Name(), Description(), and Execute(). A ToolRegistry holds a map[string]Tool and exposes a single Dispatch(call ToolCall) method. The agent remains unaware of concrete tool types — it just passes ToolCall structs from the LLM to the registry. New tools are registered at startup with no changes to the agent.

Structure

Tool Use Pattern
Step 1 of 4

LLM Selects a Tool

The LLM response contains a structured ToolCall with the tool name and JSON arguments. The agent deserializes this into a ToolCall struct and passes it to the registry — no branching required.

Implementation

package main

import "context"

// Tool is a named, self-describing capability the agent can invoke.
type Tool interface {
	Name() string
	Description() string
	Execute(ctx context.Context, args map[string]any) (string, error)
}

// ToolCall is the LLM-selected invocation received from the model.
type ToolCall struct {
	Name string
	Args map[string]any
}

// ToolResult pairs a call with its output.
type ToolResult struct {
	Call   ToolCall
	Output string
	Err    error
}

Real-World Analogy

A company switchboard: callers ask for a department by name, and the operator routes them. The caller doesn’t know the internal extension; the switchboard does. Adding a new department means registering a new extension — the operator’s routing logic stays unchanged.

Pros and Cons

ProsCons
Open/closed — add tools without touching the agentmap[string]any args lose type safety at the boundary
Registry serves as a schema catalog for the LLM promptRequires careful description writing so the LLM selects the right tool
Tools are independently testable unitsDispatch errors are silent if not surfaced back to the LLM
Schemas can be serialized to JSON for the LLM’s function-calling APILarge tool registries can overwhelm the model’s context window

Best Practices

  • Keep tool Description() precise and action-oriented — the LLM uses it verbatim to decide when to call the tool.
  • Validate required args inside Execute() and return descriptive errors so the agent can reason about what went wrong.
  • Expose Schemas() from the registry to auto-generate the function-calling spec for your LLM provider.
  • Use strongly-typed wrapper structs internally and unmarshal from map[string]any at the tool boundary only.
  • Group related tools under a common prefix (e.g., fs_read, fs_write) to help the LLM reason about tool families.

When to Use

  • Any agent that calls external APIs, executes code, reads files, or queries databases.
  • When you want to add or remove capabilities at runtime without redeploying the agent logic.
  • Multi-tenant systems where different tenants have different tool sets.

When NOT to Use

  • Agents with a single, fixed action — the registry adds unnecessary indirection.
  • Deterministic pipelines where tools are always called in a fixed order — use a plain function call.