Sitemap

7 Practical Guidelines for Designing AI-Friendly APIs

7 min readJul 2, 2025

--

LLMs are not infallible; they are powerful predictors of language, not execution engines.

The ability of large language models (LLMs) to autonomously select and invoke tools using the ReAct pattern opens the door to truly transformative AI capabilities. These systems evolve from being mere assistants to becoming self-governing agents that interact with the environment — AI agents in the full sense. However, these opportunities also come with the need for thoughtful tool design, ensuring LLMs can use APIs correctly, reliably, and with minimal hallucination.

Traditionally, APIs were developed for use by human programmers. But with LLMs increasingly acting as intermediaries, there’s a growing demand to adapt APIs for machine reasoning. In this guide, we explore practical patterns that help make APIs more “LLM-friendly.”

1. Conditional APIs

It may seem surprising, but conditional logic frequently arises when LLMs invoke tools. As humans, we implicitly understand conditions like “if it’s cold, turn on the heat.” But LLMs, unless paired with external execution environments (e.g., code interpreters), cannot reliably resolve such logic numerically — they reason about conditions in natural language.

Let’s look at an example that could happen in our near future in a smart home.

User: “I’m coming home in 45 minutes. Make the place cozy.”

The AI assistant may reason:

  1. “User arrives at 6:15 PM.”
  2. “Check indoor temperature.”
  3. “If < 21°C, turn on the heating.”
  4. “Dim the lights.”
  5. “Start jazz music.”

Let’s pay attention to the 3rd point and represent it as a code:

temperature = get_indoor_temperature()
if temperature < 21:
set_thermostat(22)

In our case it’s up to LLM to resolve the condition if temperature < 21 . And here is tricky moment whether LLM could manage such conditions properly. Of course, it doesn’t execute it like code. It simulates reasoning about them through language unless explicitly paired with tools that can execute code. LLMs are statistical models of language, so when you write something like: “If the temperature is below 21°C, turn on the heating.” The LLM understands the semantics of the condition due to its training on vast corpora of code, natural language, and logic patterns. But it does not actually evaluate temperature < 21 numerically by itself—unless the values are present in the prompt and it's a simple comparison. For example:

Prompt: The current temperature is 18°C. Should the heating be turned on if it’s below 21°C?

The LLM will very likely say: Yes, because 18°C is below 21°C.

But this is still language-based inference, not true logic execution. This can be a trap, since real-world conditions may vary and can’t always be inferred from language alone. So on one side we could instruct LLM to use tools like python scripting for conditions resolving — OR move the condition inside the tool. Well indeed in intelligent tool-augmented LLM systems, the more we can encapsulate logic and constraints at the tool/API level, the simpler, more robust, and less error-prone the orchestration becomes. And we could replace code above with one line:

set_thermostat(22, threshold=21)

because the condition now is inside the function like:

def set_thermostat(req_temp, threshold=None):
if threshold and get_indoor_temperature() >= threshold:
return # already warm
adjust_thermostat(req_temp)

And it’s really valuable because:

  • LLMs are good at high-level reasoning, not low-level conditional branching.
  • Each branching step increases prompt complexity and token usage.
  • Conditions, fallbacks, and safeguards are handled consistently in code, where debugging is easier and behavior is predictable.
  • This reflects a classic software principle: “The orchestrator should delegate what, not how.”
  • Reduces hallucination risks.

But this approach requires additional thought about the API design and potential conditional use cases, and it is likely that not all conditions can be included in the tool, but perhaps only the more relevant ones.

2. Vectorized APIs

While processing user requests, the AI ​​agent may encounter the need to perform the same operation multiple times in a loop. For example, a user asks the AI ​​agent to “Remove my user from all groups in a social network”, which is equivalent to the code:

user = get_me()
groups = get_groups(user.id)
for group in groups:
delete_group(user.id, group.id)

And with access to the get_groups and delete_group tools, the AI agent will do exactly the same thing, monotonously deleting user from each group one by one. You can sense the problem, right? There’s nothing intelligent about this work, it just wastes tokens, time and money. Its loop can be automated inside the group deletion tool:

user = get_me()
groups = get_groups(user.id)
delete_groups(user.id, groups)

This approach is about batching tools or APIs. Let the LLM act declaratively, and let the tools perform batch operations efficiently. Because otherwise it wastes prompt space and latency. Also many services (internal and external) rate-limit requests. Doing 100 individual updates will likely cause throttling or delays. LLMs are inherently sequential. If it waits for each result to decide the next step (unless pipelined), it’s bottlenecked and it could be extra slow to give LLM ability to think the result of each iteration. And if one fails and retry logic isn’t perfect, you can end up with partial updates.

And the vectorization of API not only helps elegantly solve the LLM management problem, but this design:

  • Handles empty lists gracefully (just no-op).
  • Handles single-element calls seamlessly.
  • Provides bulk feedback in one return.

In that case you will follow the same mindset as vectorized operations in ML libraries NumPy or Pandas: “Don’t loop — operate on arrays.”

So it gives next profit:

  • Faster execution
  • Lower token usage
  • Cleaner logic
  • Easier error handling
  • More natural for declarative prompting

Where is the fly in the ointment buried? Bulk API are vulnerable to abuse or denial-of-service (DoS). To protect, it should be a trade-off depending on your requirements, where you could make:

  • Configurable Bulk Limit
  • Rate Limit by Identity (Token/IP/User)
  • Authentication & Audit Trails
  • Wrap bulk API for AI over single-entity API, which also gives bonuses:
    1. Enables LLM/agent efficiency.
    2. Preserves backward compatibility.
    3. Keeps your system secure and extensible.

Another problem can arise if get_groups returns too many groups that are not really useful for LLM itself in this case, taking up the context window, since LLM just moves them to the delete_groups tool. So it seems like the best option here is to add conditional logic where groups can be specified externally, otherwise the tool takes all available internally:

user = get_me()
delete_groups(user.id)

def delete_groups(user_id, groups=None):
groups = groups or get_groups(user_id)
for group in groups:
_delete_group(user_id, group.id)

In fact, there is no silver-bullet solution, each sample should be considered and adjusted according to requirements and expectations.

3. Rich Tool Arguments

When you abstract away low-level logic into tools, pass rich objects, not just primitive values.

Instead of:

user = get_me()
groups = get_groups(user.id)
delete_groups(user.id, groups)

Prefer:

user = get_me()
groups = get_groups(user)
delete_groups(user, groups)

Let the tool, but not the LLM, extract internal properties like user.id or group.id. This keeps the orchestration high-level and frees the LLM from managing internals.

Benefits:

  • Cleaner prompt and tool design;
  • Fewer errors from malformed or missing fields;
  • Improved reusability and flexibility of tools;

Remember: “The orchestrator should delegate what, not how.”

It’s like you, as a person who starts the washing machine in the delicate wash mode, without making any fine adjustments to the drum speed or wash time or water temperature, but only doing the common-level operations of adding powder, throwing in the laundry and selecting the wash mode.

4. Counting APIs

Another problem occurs when a user needs to count something, and these questions come up very often. The user wants to know something at a glance, how many unread messages, new emails, upcoming actions he has. We like numbers, we feel in control when we know numbers. And numbers are bad for LLM. It can’t count. To solve this problem, on the one hand, we could provide access to an external tool for calculating, like Python scripts. But on the other hand, we could set up an API.

Example:

User asks: “How many unread emails did I get this morning?” which corresponds to request GET /api/emails?unread=true. And the response returns a paginated list of unread emails. Therefore, to calculate, LLM needs to paginate through all pages. Very suboptimal.

To help with such problems, we can include into response an additional field, a hint to LLM, how many entities in total for the condition in the paginated query. And this value totalCount could be controlled with two options:

  • withCount — If true, injects totalCount into the response alongside paginated data.
  • countOnly — If true, returns only totalCount. Implies withCount=true implicitly.

So if LLM just needs to get the number of unread messages as requested by the user, it will call the API:

GET /api/emails?unread=true&countOnly=true

5. Tools, Not Prompts

Resist the urge to bake all instructions into system prompts. If you have standard responses to queries, like “What can’t you do?”, implement them as tools, not into prompt text.

Why?

  • Prompt guidance can suppress tool usage.
  • System prompts are shared across all use cases.
  • LLMs may misinterpret guidance as behavioral constraints.

Move user-facing responses into tool outputs. System prompts should orchestrate tool usage; outputs should come from tools.

Any content that is not related with guardrailing or instructing should be moved into function outputs, not system prompts. This avoids confusion and prevents LLMs from misinterpreting user-facing guidance as internal behavioral constraints.

6. Rich Descriptions

Define your tools with clear, detailed metadata:

  • Function descriptions
  • Argument types and constraints
  • Example use cases
  • Related tools or intended sequencing

This helps the LLM reason more effectively and reduces misuses. One-time investment, long-term gain.

7. Zero-Trust Prompting

With system prompt of AI agent, prefer style:

  • Minimal system prompt which generalizes agent’s goal and talks how and what tools to use, like in ReAct style.
  • Delegate most logic to tools which decide what to represent in answer. Also LLM as tool could be involved for language processing, like text summarization, generation, sentimental analysis.

Advantages:

  • Prompt is public or non-critical;
  • LLM is a decision helper, not an actor;
  • Tools enforce permission and policy;
  • Behavior is observable, traceable, and externally auditable;

Designing AI-friendly APIs is not just about syntax — it’s about semantics, usability, and robustness in autonomous execution. LLMs are not infallible; they are powerful predictors of language, not execution engines. The more we delegate low-level logic and computation to APIs, the better results we get: less hallucination, faster responses, cleaner orchestration, and a far more powerful agent experience.

Design APIs for the mind of a machine.

--

--

Sergei
Sergei

Written by Sergei

Software Engineer. PhD in Engineering. My interests are IT, High-Tech, coding, debugging, sport, active lifestyle.