LLM Agents and Tool Use: A Practical Guide

How to build autonomous LLM agents that can use tools, make decisions, and complete multi-step tasks.

An LLM agent is an LLM that can decide which tools to call, when to call them, and how to combine results to accomplish a goal. This is where LLMs go from “useful assistant” to “autonomous worker.”

The Agent Loop

Every agent follows the same pattern: observe → think → act → observe results → repeat.

defmodule Agent.Runner do
  @max_steps 10

  def run(goal, tools, context \\ %{}) do
    run_loop(goal, tools, context, [], 0)
  end

  defp run_loop(_goal, _tools, context, history, step) when step >= @max_steps do
    {:error, :max_steps_exceeded, history}
  end

  defp run_loop(goal, tools, context, history, step) do
    tool_descriptions = Enum.map(tools, &describe_tool/1)

    response = LLM.complete("""
    Goal: #{goal}
    Available tools: #{Jason.encode!(tool_descriptions)}
    Previous steps: #{format_history(history)}

    Decide your next action. Respond with JSON:
    {"action": "tool_name", "input": {...}} or {"action": "finish", "result": "..."}
    """)

    case Jason.decode!(response) do
      %{"action" => "finish", "result" => result} ->
        {:ok, result, history}

      %{"action" => tool_name, "input" => input} ->
        tool = Enum.find(tools, &(&1.name == tool_name))
        {:ok, output} = tool.execute(input)
        new_history = history ++ [%{tool: tool_name, input: input, output: output}]
        run_loop(goal, tools, context, new_history, step + 1)
    end
  end
end

Defining Tools

Tools are just functions with descriptions the LLM can understand:

defmodule Agent.Tools.WebSearch do
  defstruct name: "web_search",
            description: "Search the web for current information. Input: {\"query\": \"search terms\"}"

  def execute(%{"query" => query}) do
    results = SearchAPI.search(query, limit: 5)
    {:ok, format_results(results)}
  end
end

defmodule Agent.Tools.Database do
  defstruct name: "query_database",
            description: "Query the product database. Input: {\"sql\": \"SELECT ...\"}"

  def execute(%{"sql" => sql}) do
    case validate_sql(sql) do
      :ok -> {:ok, Repo.query!(sql)}
      {:error, reason} -> {:error, reason}
    end
  end

  defp validate_sql(sql) do
    if String.match?(sql, ~r/^SELECT/i), do: :ok, else: {:error, "Only SELECT queries allowed"}
  end
end

The Importance of Tool Descriptions

Your tool descriptions are prompts. Bad descriptions lead to tools being used incorrectly or not at all. Be specific about input format, expected behavior, and when the tool should (and shouldn’t) be used.

Guardrails

Agents without guardrails are dangerous. Always implement:

  • Step limits — Cap the maximum number of tool calls
  • Permission boundaries — Tools should only access what they need
  • Input validation — Sanitize all tool inputs before execution
  • Cost tracking — Monitor token usage per agent run
  • Human-in-the-loop — For high-stakes actions, require approval

Real-World Use Case

We use an agent to handle deployment notifications. When a deploy completes, the agent checks test results, reads the changelog, drafts a summary, posts to Slack, and updates the status page. Five tools, one goal, zero human intervention for routine deploys.

The key insight: agents excel at multi-step tasks where the exact sequence depends on intermediate results. If the steps are always the same, use a pipeline instead.