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.