AI Agent Tool Design: What Works and What Doesn’t
TL;DR · AI 摘要
AI代理工具设计是影响其可靠性的关键因素,清晰的工具设计能显著减少失败率。
核心要点
- 一个工具应只负责一个明确的操作,避免多行为工具。
- 工具名称和描述应清晰,避免模糊和歧义。
- 结构化的错误处理和参数定义能减少幻觉和不可靠行为。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- AI代理工具设计
- 有效设计
- 单一职责
- 清晰命名
- 结构化错误处理
- 失败模式
- 模糊命名
- 未过滤API暴露
- 静默部分成功
金句 / Highlights
值得收藏与分享的关键句。
一个工具应只负责一个明确的操作,避免多行为工具。
模糊命名、不一致的参数定义和错误处理会增加失败可能性。
结构化的错误处理和参数定义能减少工具边界处的幻觉和不可靠行为。
AI Agent 工具设计:哪些有效,哪些无效
作者:Bala Priya C 发布于:2026年6月15日 分类:人工智能
1
分享
在本文中,你将了解工具设计(而非模型能力)是大多数AI代理失败的根本原因,以及你可以应用哪些具体的模式来解决这个问题。
我们将涵盖的主题包括:
- 提高代理可靠性的工具设计实践,包括单一职责工具、紧密的模式和结构化的错误返回。
- 常见的失败模式,如未经筛选的API暴露、静默部分成功和重叠的工具名称,这些都会破坏实际工作负载。
- 减少工具边界处幻觉和不可靠行为的模式和错误处理。
让我们开始吧。
引言
大多数AI代理的失败看起来像是模型的错误:选择了错误的工具、传递了错误的参数或处理错误的方式不当。但实际上,模型通常只是在使用它所给定的接口。根本问题往往是工具设计本身。
模型只能根据通过工具接口暴露的信息进行推理:工具名称、其描述、参数模式和参数描述。这些细节决定了模型如何解释意图、规划操作和执行任务。当工具设计不清晰、不完整或结构松散时,失败变得可预测而不是偶然。
模糊的命名、模棱两可的指令、不一致的模式、弱参数定义和差的错误处理都会增加失败的可能性。更强的模型可以减少一些错误,但它们无法可靠地弥补有缺陷的接口。本文涵盖:
- 提高可靠性的工具设计实践
- 在演示中看起来正常但在实际工作负载下会失败的失败模式
- 减少工具边界处幻觉的模式和错误设计
每个模式都与它的失败对应项配对,因为理解设计为何失败与知道用什么来替代同样重要。
AI Agent 工具设计中有效的做法
1. 一个工具,一个职责
在大多数代理系统中,一个工具应代表一个明确的操作。当一个工具通过一个操作参数处理多种行为时,模型必须首先确定要调用哪种模式,然后才能解决实际任务。
比较多动作工具与专用单用途工具时,差异变得更加明显:
避免:基于动作的多行为工具
@tool def manage_customer(action: str, customer_id: str | None = None, data: dict | None = None): """ action: create | get | update | delete | suspend """ ...
优选:单一职责工具
@tool def create_customer(data: CustomerInput) -> Customer: """创建一个新的客户记录。""" ...
@tool def get_customer(customer_id: str) -> Customer: """通过ID检索客户。""" ...
@tool def suspend_customer(customer_id: str, reason: str) -> SuspensionResult: """暂停客户账户。""" ...
"创建一个新的客户记录。"
get_customer
"通过ID检索客户。"
suspend_customer
reason
SuspensionResult
"暂停客户账户。"
一个工具,一个职责 单一职责的工具为模型提供了明确的功能,并使你能够更清晰地处理错误和更容易地进行可观测性。
⚠️ 注意:这是一个有用的默认规则,而不是普遍适用的规则。某些领域——例如 shell、文件系统、浏览器或日历工具——可能由于操作空间本身是底层抽象的一部分,因此从受限的多操作接口中受益。
2. 使无效状态不可能的模式
在工具调用代理中,模型通过从你的模式进行推理来构建工具调用参数。
- 松散的模式意味着模型会猜测约束。
- 紧密的模式会编码这些约束,因此无需猜测。
以下是一个示例:
from pydantic import BaseModel, Field from enum import Enum
class Priority(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high"
class CreateTaskInput(BaseModel):
description="简短且可操作的任务标题。使用祈使形式:'Review PR',而不是 'PR Review'。", min_length=5, max_length=100 ) priority: Priority = Field( description="任务优先级。仅在影响其他工作的阻塞任务中使用 HIGH。", default=Priority.MEDIUM ) due_date: str = Field( description="以 ISO 8601 格式表示的截止日期:YYYY-MM-DD。必须是未来的日期。", pattern=r"^\d{4}-\d{2}-\d{2}$" )
Enums 对于具有少量有效值的字段特别有用,因为它们消除了一个可能但无效的输出类别。验证失败会在工具边界处出现,而不是作为难以理解的下游错误。
3. 定义范围而非仅定义目的的描述
工具描述是面向模型的文档。它们需要做两件事:解释何时使用该工具,以及解释何时不使用该工具。大多数描述只做了第一点。
弱:解释了它做了什么,但没有解释何时不使用它
"""在知识库中搜索文档。"""
强:定义了目的、范围和边界
在知识库中搜索文档、政策和参考资料。当用户询问公司流程、产品规格或已记录的工作流程时使用此工具。不要用于实时数据(价格、可用性、当前状态)——请改用 get_live_data()。返回最多 5 个结果,按相关性排序。如果没有返回结果,则说明信息不在知识库中。
弱:解释了它做了什么,但没有解释何时不使用它
"在知识库中搜索文档。"
强:定义了目的、范围和边界
在知识库中搜索文档、政策和参考资料。
当用户询问公司流程、产品规格或已记录的工作流程时使用此工具。
不要用于实时数据(价格、可用性、当前状态)——请改用 get_live_data()。
返回最多 5 个结果,按相关性排序。如果没有返回结果,则说明信息不在知识库中。
如果没有明确区分,模型将仅根据工具名称推断作用域,这在大规模使用时常常是选择错误的可靠来源。一个好的工具定义应包括与其他工具的清晰边界,而不仅仅是使用说明。
4. 结构化、可操作的错误返回
当工具失败时,模型会读取错误并决定下一步该怎么做。未处理的异常或堆栈跟踪会导致模型产生由噪声驱动的后续行为。结构化的错误则为模型提供了分支决策的依据。
结构化错误不仅应报告失败的原因,还应帮助代理决定下一步该怎么做。一个好的错误格式应明确重试行为,并为模型提供清晰的恢复路径:
class ToolError(BaseModel): error_code: str # 机器可读,用于模型分支 message: str # 人类可读的描述 recoverable: bool # 代理是否可以重试? suggested_action: str # 代理下一步应采取的操作 # 记录未找到:可重试 return ToolError( error_code="RECORD_NOT_FOUND", message="未找到ID为'usr_123'的用户记录。", recoverable=True, suggested_action="在调用get_user()之前,使用list_users()获取有效的用户ID。" ) # 额度超出:不可重试 return ToolError( error_code="QUOTA_EXCEEDED", message="今天已达到该工具的API额度。", recoverable=False, suggested_action="通知用户并停止。今天不要重试此工具。" )
ToolError
error_code
机器可读,用于模型分支
message
人类可读的描述
recoverable
bool
代理是否可以重试?
suggested_action
代理下一步应采取的操作
记录未找到:可重试
return
"RECORD_NOT_FOUND"
"未找到ID为'usr_123'的用户记录。"
True
"在调用get_user()之前,使用list_users()获取有效的用户ID。"
额度超出:不可重试
"QUOTA_EXCEEDED"
"今天已达到该工具的API额度。"
False
"通知用户并停止。今天不要重试此工具。"
recoverable标志和suggested_action字段是改变代理行为的关键。没有它们,模型可能会重试不可重试的错误,或者放弃可恢复的错误。
5. 无副作用的幂等操作
每个会改变状态的工具 —— 创建记录、发送消息、转账 —— 必须可以安全地调用两次。实际上,代理可能会重试,网络可能失败,LLM循环可能因为未收到第一次调用的确认而发出第二次调用。
防止重复副作用的一个简单方法是为每个写操作要求一个幂等性密钥:
@tool def send_email( to: str, subject: str, body: str, idempotency_key: str = Field( description="此发送操作的唯一密钥。使用收件人 + 主题 + 时间戳的哈希。 " "重试时使用相同的密钥将返回原始结果,而不会重新发送。" ) ) -> dict: """发送电子邮件。幂等:相同的idempotency_key不会触发第二次发送。""" existing = idempotency_store.get(idempotency_key) if existing: return existing result = email_service.send(to=to, subject=subject, body=body) idempotency_store.set(idempotency_key, result, ttl=86400) return result
send_email
to
subject
body
idempotency_key
"此发送操作的唯一密钥。使用收件人 + 主题 + 时间戳的哈希。 "
"重试时使用相同的密钥将返回原始结果,而不会重新发送。"
"发送电子邮件。幂等:相同的idempotency_key不会触发第二次发送。"
existing
idempotency_store
get
if
result
email_service
send
set
ttl
86400
没有幂等性保证,短暂的故障很容易演变成重复的操作。
AI 代理工具设计中不起作用的方面
1. 对未过滤 API 的薄包装
将代理指向一个 REST API 并将其作为工具暴露出来,是最常见的捷径,也是最常见的生产环境故障来源。为开发者构建的 API 通常暴露了代理实际上并不需要的大量细节。响应中包含了数百个字段,即使只有一小部分是相关的。它们依赖分页,使用缺乏上下文意义的不透明内部 ID,并返回需要深入领域知识才能解释的错误代码。
专门设计的包装器会在内部处理分页,只投影代理需要的字段,并将 API 错误映射到上面讨论的结构化 ToolError 格式。代理从不构造 API 路径或管理页面;它接收到的是可以推理的类型化对象。
然而,过度包装也可能有害。如果每个端点都成为一个单独的、狭窄定义的工具,且没有共享结构,工具界面可能会变得碎片化,使模型更难以导航。目标不是最大程度的抽象,而是保持一致、面向代理的抽象层。
2. 将所有工具加载到每个上下文中
随着工具目录的增大,准确性会下降。2025 年的一项研究 LongFuncEval 对长上下文中工具调用性能进行了研究,发现随着工具目录大小的增加,性能显著下降,即使是在具有 128K 上下文窗口的模型中也是如此。将每个工具加载到每个系统提示中会加剧这一问题,因为这会消耗掉任何任务内容处理之前的令牌预算。
动态工具加载解决了这两个问题。确定哪些工具与当前步骤相关,并只包含这些工具:
STEP_TOOL_MAP = { "research": ["search_documents", "search_web", "get_url_content"], "write": ["create_document", "update_document", "format_text"], "send": ["send_email", "post_to_slack", "create_calendar_event"], } def get_tools_for_step(step_type: str, available_tools: list) -> list: relevant_names = STEP_TOOL_MAP.get(step_type, []) return [t for t in available_tools if t.name in relevant_names]{
"research": [
"search_documents",
"search_web",
"get_url_content"
],
"write": [
"create_document",
"update_document",
"format_text"
],
"send": [
"send_email",
"post_to_slack",
"create_calendar_event"
]
}def get_tools_for_step(step_type: str, available_tools: list) -> list:
relevant_names = STEP_TOOL_MAP.get(step_type, [])
return [t for t in available_tools if t.name in relevant_names]动态工具加载:在每一步中只暴露一小部分相关工具,而不是全部工具,通常可以提高选择的准确性并减少每次调用的令牌成本。
3. 静默部分成功
当工具只完成请求工作的一部分,但返回的响应看起来完全成功时,部分成功就会成为一个问题。代理会继续执行,但基于的是系统状态的不完整或误导性视图。
这通常发生在工具抑制内部失败,并只返回结果中成功部分时:
此版本会无声地误导代理
@tool def bulk_create_tasks(tasks: list) -> dict: created = [] for task in tasks: try: result = task_api.create(task) created.append(result.id) except Exception: pass # 无声失败:这是错误 return {"created": created} # 此版本明确表示部分成功 @tool def bulk_create_tasks(tasks: list) -> BulkCreateResult: created, failed = [], [] for task in tasks: try: created.append(task_api.create(task).id) except TaskCreationError as e: failed.append({"input": task.title, "reason": str(e)}) return BulkCreateResult( created_ids=created, failed_items=failed, success=len(failed) == 0, partial_success=len(created) > 0 and len(failed) > 0 )
此版本会无声地误导代理
bulk_create_tasks
tasks
created
task
try
task_api
create
append
id
except
Exception
pass
无声失败:这是错误
"created"
此版本明确表示部分成功
BulkCreateResult
failed
TaskCreationError
as
e
"input"
"reason"
created_ids
failed_items
success
len
==
0
partial_success
and
部分成功标志为模型提供了一个分支依据:重试失败项、向用户展示部分结果,或中止工作流。
4. 工具名称和描述重叠
当两个工具执行类似的操作时,模型在每次调用时都需要推理使用哪一个工具。这种推理会消耗令牌并引入错误。一些常见的例子包括:
- search_documents 和 find_documents,其目的相同
- get_user 和 fetch_user_profile,其差异不明确
- create_task、add_task 和 new_task,这三个工具用于同一个操作
在这些情况下,仅通过重命名并不能解决问题。每个工具都需要一个可以独立描述的目的,而无需参考其他工具。如果描述需要“与X不同,这个工具……”才能有意义,那这就是一个设计问题。工具蔓延——工具过多且作用范围重叠——是企业部署中不可靠代理行为的来源。
5. 没有确认机制的破坏性操作
任何执行不可逆操作的工具(如删除记录、向真实用户发送消息、执行财务交易)都需要结构化的两步确认,而不是提示中的“确定吗?”一个分阶段的方法引入了明确的确认边界,从而降低了意外或未经授权执行的风险。
最安全的模式是将准备阶段与执行阶段分开,并在两者之间要求一个短暂有效的确认令牌:
@tool def stage_deletion(record_ids: list[str], reason: str) -> StagedDeletion: """准备删除记录。不会删除任何内容。返回一个60秒后过期的确认令牌。使用此令牌调用 confirm_deletion() 以继续操作。""" token = generate_deletion_token(record_ids) staged_deletions[token] = {"ids": record_ids, "expires": now() + 60} return StagedDeletion(token=token, records_to_delete=len(record_ids), expires_in_seconds=60) @tool def confirm_deletion(token: str) -> DeletionResult: """执行准备好的删除操作。不可逆。仅在明确用户批准后确认。""" staged = staged_deletions.get(token) if not staged or staged["expires"] < now(): raise ValueError("令牌无效或已过期。请重新准备删除。") # 继续操作
stage_deletion
record_ids
StagedDeletion
"准备删除记录。不会删除任何内容。
返回一个60秒后过期的确认令牌。
使用此令牌调用 confirm_deletion() 以继续操作。"
token
generate_deletion_token
staged_deletions
"ids"
"expires"
now
+
60
records_to_delete
expires_in_seconds
confirm_deletion
DeletionResult
"执行分阶段删除。不可逆。仅在获得用户明确批准后确认。"
staged
not
or
<
raise
ValueError
"令牌无效或已过期。请重新启动删除阶段。"
proceed
没有确认机制的破坏性操作 两个独立的工具调用意味着模型无法在单个推理步骤中完成破坏性操作,这就是关键所在。
⚠️ 注意:然而,在许多系统中,两步安全流程通常不足以提供充分的保护。即使使用了分阶段和确认机制,还需要额外的保护措施,例如短期、一次性使用的令牌,严格的会话绑定和重放保护,以防止令牌被重复使用、泄露或跨会话执行,从而绕过预期的安全边界。
AI 代理工具设计决策一览
每一行代表 AI 代理工具设计中的一个关键决策:
设计领域
适用情况
不适用情况
工具范围
每个工具只负责单一功能
具有操作参数的工具,如
manage_database(action="create")模式
严格:枚举、验证器、类型字段
宽松:自由字符串、未类型化的字典
描述
包括作用范围边界和何时
不使用的情况
仅描述成功路径
写操作
使用唯一性密钥实现幂等性
一次性发送,没有重试安全机制
错误返回
结构化:
error_coderecoverablesuggested_action未处理的异常或未类型化的字符串
工具数量
按步骤动态加载
每个上下文中都包含所有工具
API 封装
为代理设计的专用封装器,具有代理面向的模式
未过滤的 API 暴露
部分成功
返回中显式包含
partial_success字段
静默异常吞没
破坏性操作
两步分阶段 + 确认
单次调用删除/发送/执行
工具重叠
语义上不同,部署前经过审核
名称和描述相似,相互竞争
为 AI 代理编写有效的工具 —— 使用来自 Anthropic 的 AI 代理是一个有用的工具设计参考。
关于此主题的更多信息
- 实践中的功能扩展:哪些有效,哪些无效
- 构建一个具有错误恢复功能的多工具 Gemma 4 代理
- 在 Weka 中设计并运行你的第一个实验
- 理解卷积神经网络的设计
- 使用 Stable Diffusion 进行室内设计(8 天迷你课程)
- 7 个必须了解的代理 AI 设计模式
/.entry