Designing Effective Tools for AI Agents

Designing Effective Tools for AI Agents

An agent’s performance is capped by the quality of its tools — the functions it calls to fetch information or act on the world. Designing them well is not the same as making a long list of functions; it’s about how an LLM perceives, selects, and invokes each one. The guiding principle is human readability: a tool a developer finds easy to understand and use is one an agent uses correctly.

The tool definition

The LLM has nothing to go on but the function signature and docstring. Those are the interface.

Name it unambiguously. The name should state exactly what the tool does.

  • Bad: search(query) — keyword? vector? web?
  • Good: keyword_database_search(query: str) — explicit about method and source.

Write the docstring as an instruction manual. It’s the agent’s primary reference, so cover:

  • Purpose — one sentence on what the tool does.
  • Parameters — every input, its type, and what it means.
  • Return value — the structure and content of the output, especially for complex objects.

Type everything. Explicit types help the agent build valid calls and read results correctly. For complex outputs, define a structure with a dataclass:

from dataclasses import dataclass

@dataclass
class KeywordSearchResult:
    """Data structure for a single keyword search result."""
    document_id: str
    filename: str
    content_snippet: str

def keyword_database_search(query: str) -> list[KeywordSearchResult]:
    """
    Performs a keyword search against the document database.

    Args:
        query (str): The keywords to search for.

    Returns:
        list[KeywordSearchResult]: A list of matching documents, each containing its ID, filename, and a content snippet.
    """
    # ... database search logic ...
    results = search_database(query)
    return results

Tool logic

One tool, one job. Single-purpose tools are chosen correctly; vague or multi-purpose ones get called at the wrong time with the wrong arguments.

Return clean, parsed output. Never hand the agent a raw API dump. Parse the result inside the tool and format only what matters into a clean string:

def _parse_search_results_for_llm(results: list[KeywordSearchResult]) -> str:
    """Parses search results into a clean string for an LLM."""
    if not results:
        return "No results found."

    output_lines = [f"Found {len(results)} documents:"]
    for i, result in enumerate(results):
        output_lines.append(
            f"\nDocument {i+1}:\n"
            f"  ID: {result.document_id}\n"
            f"  Filename: {result.filename}\n"
            f"  Snippet: {result.content_snippet}"
        )
    return "\n".join(output_lines)

# The main tool function would then use this parser before returning:
# return _parse_search_results_for_llm(results)

Guard the context window. Tools that can return many items will drown the agent. Build in limits — a max_results parameter, and sorting options (sort_by='relevance') so the agent can ask for the most relevant items first.

Make errors informative. A failed tool’s message is feedback for the reasoning loop. Generic errors are dead ends; specific ones enable self-correction:

def some_api_tool(api_key: str):
    try:
        # ... logic to call an external API ...
        pass
    except RateLimitError as e:
        raise RuntimeError("API Error: Rate limit exceeded. Wait before retrying the call.")
    except AuthenticationError as e:
        raise ValueError("API Error: Invalid API key provided. Do not try again with the same key.")
    except Exception as e:
        raise RuntimeError(f"An unexpected error occurred: {e}. Check your inputs and try again.")

Notice each message tells the agent what to do next: wait, stop, or check inputs.

Presenting tools in the prompt

How you offer tools matters as much as the tools themselves.

  • Don’t dump every tool. A long catalog makes the right choice harder to find.
  • Offer tools in context. Where possible, expose only the tools relevant to the agent’s current task or state.
  • Separate them clearly. Put tool definitions in their own section with headings or XML tags (<tools>...</tools>) so the agent can focus.

Keep going

Tools are the reach of an agent. Every principle here reduces to one test: could a competent developer, handed only the signature and docstring, call this correctly on the first try? If yes, so can the agent.

This entry was posted in . Bookmark the permalink.