Chuyển đến nội dung chính

Designing tools an AI agent can actually use

The first agent I shipped had a tool called query. It took a string and returned rows. The model used it constantly, wrongly, and with growing desperation, because query told it nothing about what could be queried, what the schema was, or what a failure meant.

Renaming it search_orders_by_customer_email and giving the parameter a real description fixed most of the behaviour without touching the model or the prompt. That is the general shape of tool design: the model’s competence with your tools is mostly a function of how well you described them.

The definition is documentation for a reader who cannot ask questions

A tool definition is read once, cold, by something that cannot open your codebase or ping you on Slack. Everything it needs must be in the schema.

{
  "name": "search_orders",
  "description": "Search a customer's orders by email address. Returns at most 50 orders, newest first. Only orders from the last 24 months are indexed — for older orders use fetch_order_archive. Returns an empty list if the customer has no orders; this is not an error.",
  "input_schema": {
    "type": "object",
    "properties": {
      "email": {
        "type": "string",
        "description": "Customer email, exactly as stored. Case-insensitive. Not a partial match — use search_customers first if you only have a name."
      },
      "status": {
        "type": "string",
        "enum": ["pending", "shipped", "delivered", "cancelled"],
        "description": "Optional filter. Omit to return all statuses."
      }
    },
    "required": ["email"]
  }
}

Four things in that definition are doing real work, and each corresponds to a failure I have watched happen:

  • The limit is stated. Without “at most 50”, a model asked to count a customer’s orders will confidently report 50.
  • The boundary is stated. “Last 24 months” plus a pointer to the other tool prevents the model from concluding an old order does not exist.
  • The empty case is stated. Otherwise an empty result gets narrated to the user as a failure.
  • The parameter says what it is not. “Not a partial match” prevents the loop where the model tries "john", gets nothing, tries "john%", gets nothing, and apologises.

Write descriptions for the failure modes, not for the happy path. The happy path is usually inferable from the name.

Granularity: the trade-off nobody warns you about

Given a database, you can expose one tool (run_sql) or forty (get_customer, list_orders, update_shipping_address, …). Both are wrong at the extremes.

ApproachWorks wellFails at
One generic toolFlexible, small definition setModel must know your schema; errors are opaque; no way to constrain what it can do
Many specific toolsClear intent, enforceable permissions, good errorsTool list bloats the context; the model must pick among near-duplicates

My working rule: one tool per user-meaningful action, not per database operation. “Cancel an order” is one tool even if it writes three tables. Meanwhile get_customer_by_id and get_customer_by_email should be one tool with an either/or parameter, because from the model’s perspective they are one intent.

When the tool count passes roughly twenty, the problem shifts from description quality to selection: near-duplicate tools with similar descriptions get confused with each other. At that point either consolidate, or split the agent so each sub-agent sees only its own subset.

Errors are a second chance to teach

The most under-used surface in agent design is the error return. A tool that fails with {"error": "Invalid input"} has wasted a turn. A tool that fails with a correction usually gets it right on the next call.

# Poor
return {"error": "not found"}

# Better
return {
    "error": "no_customer_with_email",
    "message": "No customer found with email 'jon@example.com'. "
               "Check the spelling, or call search_customers with a partial "
               "name to find the correct address.",
    "did_you_mean": ["john@example.com"],
}

Rules I apply to every tool error:

  • Say what was wrong with the input, not what happened internally. “Stack trace” is not actionable.
  • Name the tool that should be called instead, if one exists.
  • Include near-miss data when you cheaply can — the did_you_mean above turns two wasted turns into zero.
  • Distinguish retryable from terminal. A rate limit should say “retry after 5 seconds”; a permissions failure should say “do not retry, tell the user.”

That last point matters more than it sounds. Without it, an agent hitting a permission error will retry indefinitely with small variations, burning tokens and looking broken.

Design for repetition and partial failure

Agents retry. They retry after timeouts they cannot distinguish from failures, and they retry when a previous step’s output was ambiguous.

Make every mutating tool idempotent or explicitly guarded. Accept a client-supplied idempotency key, or return a clear “already done” rather than performing the action twice:

{
  "status": "already_cancelled",
  "message": "Order 1182 was already cancelled at 2026-08-02T11:04Z. No action taken."
}

That response is far better than either silently succeeding or returning an error, because it tells the model the desired end state holds — which is what it actually wanted to know.

For anything genuinely destructive, do not rely on the model’s judgement at all. Return a confirmation token that the tool requires on a second call, so that a human-facing layer can intervene between the two.

Evaluate the tool set, not just the prompt

The uncomfortable part: you cannot tell whether a tool set is good by reading it. Build a small evaluation before you iterate.

  1. Write 20-40 realistic user requests, including ones your tools cannot satisfy.
  2. Record, for each: did the model pick the right tool, fill parameters correctly, recover from errors, and stop when it should?
  3. Read the failures as design feedback, not model feedback.

The impossible-request cases are the ones people skip and the ones that matter most. A good tool set produces “I can’t do that with the tools I have”; a bad one produces a confident wrong tool call. If your agent never says it cannot do something, your evaluation set is too easy.

When a failure appears, the fix is almost always one of: rename the tool, add a sentence to a description, merge two tools, or improve an error message. Prompt changes are the last resort, because they do not transfer when the tool is used by a different agent.

FAQ

Should descriptions include examples?

One example in the description helps for tools with non-obvious formats (date ranges, query syntax). More than one usually means the schema itself should be clearer.

How long can a description be?

Long enough to prevent misuse; every token is context you spend on every request. A few sentences per tool and one per parameter is a reasonable budget.

Do I need MCP for this?

No — MCP is a transport and packaging standard for tools. The design principles here apply whether you expose tools through MCP, a provider’s native function calling, or your own loop.

Should tools return JSON or prose?

Structured data for anything the model must reason over precisely; prose is fine for summaries. Be consistent, because mixed shapes make failures harder to parse.

How do I stop an agent calling a tool in a loop?

Cap the iterations in your loop, and make errors terminal where retrying cannot help. Do not rely on instructions alone.


Tool-definition mechanics, JSON Schema structure and function-calling flows are documented in the provider and specification references linked above. The granularity rule, the error-message checklist, the idempotency guidance and the evaluation approach are my own judgement from building and debugging agent tool sets; model behaviour differs between providers and versions, so validate against the one you deploy.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...