go-patterns
esc

Tool Use

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.

Not read yet ~3 min · 3 files · go1.24
family Agentic complexity Low in the wild High example src/code/agentic/tool-use

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

| Pros | Cons | |------|------| | Open/closed — add tools without touching the agent | map[string]any args lose type safety at the boundary | | Registry serves as a schema catalog for the LLM prompt | Requires careful description writing so the LLM selects the right tool | | Tools are independently testable units | Dispatch errors are silent if not surfaced back to the LLM | | Schemas can be serialized to JSON for the LLM’s function-calling API | Large 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.