文章深入探讨了为 AI Agent 设计工具 API 的架构要点,包括 schema 设计、错误处理与安全模式,以及 MCP 协议的实现细节。
可靠智能体工具使用的架构、模式、错误处理与安全规范
AI 智能体只有在能够完成文本生成以外的工作时才有实际价值。
它们需要搜索数据库、读取文档、调用 API、创建记录、执行代码、发送消息、更新系统,有时还需要从故障中恢复。
也就是说,智能体的能力取决于它能使用的工具。
但这里有一个重要的工程区别:
为人类设计的工具 API 不一定是 AI 智能体的好工具 API。
传统 API 通常围绕可预测的软件客户端设计。客户端开发者了解 API 契约、理解参数名、验证输入、处理错误并编写控制流。
AI 智能体则不同。
模型必须决定是否调用工具、调用哪个工具、提供什么参数,以及如何解释结果。
这使得工具设计成为智能体推理架构的一部分。
现代智能体框架通过结构化模式暴露工具。例如,MCP 工具定义名称、描述、输入模式(input schema),以及可选的输出模式(output schema)。MCP 实现还可以暴露行为提示,如只读或破坏性行为。([Model Context Protocol][1])
所以问题不再是:
"如何将我的 API 暴露给 AI?"
更好的问题是:
"如何设计一个 AI 能可靠推理的 API?"
考虑一个简单的智能体:
User
↓
AI 智能体
↓
模型决定做什么
↓
工具
↓
应用 / 数据库 / API
↓
工具结果
↓
模型继续推理
工具直接位于模型和你的软件之间。
"找到我最新的发票并告诉我它是否已付款。"
智能体可能需要:
选择相关发票。
检查其付款状态。
你可以暴露一个巨大的函数:
get_invoice(
user_id,
invoice_id,
customer_name,
email,
status,
date_from,
date_to,
include_payment,
include_items,
include_customer,
...
)
技术上,这可能可行。
但对于 AI 智能体,它造成了一个困难得多的决策问题。
更好的工具表面可能是:
search_invoices
get_invoice
get_invoice_payment
每个工具有更窄的职责。
这就引出了第一个原则:
围绕决策设计工具,而非围绕数据库操作设计工具
一个工具应该代表智能体能够推理的有意义的能力。
execute_sql
call_api
update_database
search_invoices
create_invoice
cancel_invoice
get_payment_status
第二组给了模型一个语义词汇表。
模型不需要理解你的内部数据库结构。
它只需要理解:
"何时应该使用这个能力?"
开发者有时将工具名视为实现细节。
对于智能体,它们是面向模型的 API 的一部分。
get_data
search_customer_orders
后者立即传达了意图。https://goodoff.co/ 一个好的工具名应该回答:
这个工具提供什么能力?
search_documents
get_document
create_presentation
generate_chart
send_email
schedule_meeting
get_weather
避免需要内部知识的名称:
process_v2
execute_operation
handler_7
data_service
run_query
智能体不应该需要检查你的源代码来理解工具的用途。
这是智能体工程中最容易被忽视的部分之一。
工具描述不仅仅是 API 文档。
它成为模型决策上下文的一部分。
例如,Google 的 Agent Development Kit 文档指出,工具的 Python docstring 会成为模型看到的内容的一部分,并建议清晰地编写,因为它告诉模型何时以及如何使用该工具。([Google GitHub][2])
def search_documents(query: str):
"""Search documents."""
这在技术上是有效的。
但它留下了重要的问题未解答:
query 应该包含什么?
这是语义搜索吗?
智能体应该将其用于精确匹配吗?
智能体何时应该优先使用另一个工具?
更好的描述:
def search_documents(query: str, limit: int = 10):
"""
使用语义和关键词匹配搜索文档库。
当用户询问可能存在于已上传文档内部的信息时使用此工具。
不要将此工具用于精确的文档 ID。
返回匹配的文档及其标题、ID、
相关性分数和简短摘要。
"""
现在模型有了决策指导。
工具应该有严格的输入契约。
{
"name": "search_documents",
"description": "Search the document library for relevant content.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The information or concept to search for."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
}
},
"required": ["query"]
}
}
这比以下好得多:
{
"query": "string"
}
因为模式传达了约束。
现代 MCP 工具使用 JSON Schema 来描述工具输入,也可以定义结构化输出模式。当前的 MCP SDK 文档展示了模式被用于描述工具接受哪些参数,并在处理程序执行前验证这些参数。([MCP TypeScript SDK][3])
这给你提供了一个重要的架构:
Model
↓
Tool schema
↓
Validation
↓
Tool implementation
不要单独依赖模型进行验证。
一个常见错误是向模型暴露每个可能的 API 参数。
{
"customer_id": "...",
"organization_id": "...",
"region": "...",
"locale": "...",
"timezone": "...",
"include_deleted": false,
"include_archived": false,
"include_metadata": true,
"include_permissions": false,
"sort_field": "...",
"sort_direction": "...",
"page": 1,
"page_size": 50,
"cursor": "...",
"debug": false
}
这可能适用于低级后端 API。
但对于面向模型的工具来说通常很差。
模型必须推理太多选择。
相反,创建一个面向智能体的接口:
{
"query": "customer invoices from March",
"limit": 10
}
后端可以将其转换为复杂的内部 API 调用。
这创建了一个有用的分离:
Agent-facing API
↓
Simple tool contract
↓
Adapter layer
↓
Internal APIs
↓
Database
你的内部架构可以保持复杂。
面向模型的接口应该保持可理解。
{
"id": "123"
}
{
"invoice_id": "123"
}
后者更安全,因为语义含义是明确的。
date
start_date
end_date
created_after
created_before
type
document_type
name
customer_name
AI 系统通过语义解释来运作。
减少歧义减少不必要的推理。
假设你的 API 期望一个日期范围。
{
"start_date": "tomorrow",
"end_date": "yesterday"
}
并希望后端处理它,定义显式验证。
{
"type": "object",
"properties": {
"start_date": {
"type": "string",
"format": "date"
},
"end_date": {
"type": "string",
"format": "date"
}
},
"required": ["start_date", "end_date"]
}
然后在后端验证关系:
if start_date > end_date:
raise InvalidDateRange()
模式验证捕获结构性问题。
业务验证捕获语义问题。
工具设计不仅仅关乎输入。
输出同样重要。
假设一个搜索工具返回:
{
"data": [
{
"id": "123",
"text": "...",
"metadata": "..."
}
]
}
模型必须弄清楚每个字段的含义。
{
"results": [
{
"document_id": "123",
"title": "Product Strategy",
"relevance": 0.91,
"excerpt": "The product strategy focuses on..."
}
],
"total_results": 18
}
现在模型有了对下一步有用的信息。
输出应该帮助回答:
智能体下一步应该做什么?
这对于多步骤智能体尤其重要。
假设一个工具返回:
The customer has three unpaid invoices. The oldest
was created on January 12 and is currently overdue.
人类可以理解。
但另一个模型调用必须解释文本。
结构化结果更容易消费:
{
"customer_id": "cus_123",
"unpaid_invoices": 3,
"oldest_invoice": {
"invoice_id": "inv_456",
"created_at": "2026-01-12",
"status": "overdue"
}
}
MCP 目前支持可选的 outputSchema 定义,用于结构化工具结果,其 SDK 可以根据该模式验证结构化内容。([MCP TypeScript SDK][3])
当工具被串联起来时,结构化输出变得尤为重要:
Search customer
↓
Get invoice
↓
Check payment
↓
Generate response
每个步骤都应该产出下一步可以可靠消费的信息。
search_orders 和 cancel_order 之间存在重大差异。
前者读取信息。
后者更改状态。
你的工具接口应该让这个区别一目了然。
get_customer
search_orders
get_invoice
create_customer
update_customer
cancel_order
send_email
delete_document
MCP 工具元数据可以包含行为提示,如 readOnlyHint、destructiveHint 和 idempotentHint。这些是有用的信号,但不应被视为安全控制。([Model Context Protocol][1])
一个实用的架构是:
读取操作
↓
通常可以自动执行
写入操作
↓
验证
↓
检查权限
↓
可能需要审批
↓
执行
destructive 操作
↓
验证
↓
明确确认
↓
执行
工具本身应该强制执行授权。
永远不要假设因为模型选择了一个工具,该操作就是被授权的。
模型会重复操作。
"把报告发送给 Sarah。"
send_report()
服务器处理了它。
响应丢失了。
智能体不知道操作是否成功。
它可能会再次调用该工具。
现在 Sarah 收到了两份报告。
对于有副作用的工具,考虑使用幂等键:
{
"recipient": "sarah@example.com",
"report_id": "report_123",
"idempotency_key": "agent-run-789-send-report"
}
后端可以识别该操作已经执行过。
这将重试从危险行为转变为可管理的风险。
传统 API 通常返回:
{
"error": "Bad Request"
}
这对智能体来说不太有用。
{
"error": {
"code": "INSUFFICIENT_PERMISSION",
"message": "The current user cannot access this project.",
"retryable": false,
"action": "Ask the user to select a project they have access to."
}
}
现在模型有了下次决策所需的信息。
有用的错误类别包括:
INVALID_ARGUMENT
NOT_FOUND
PERMISSION_DENIED
RATE_LIMITED
TEMPORARY_FAILURE
CONFLICT
REQUIRES_CONFIRMATION
模型应该能够区分:
重试
修改请求
询问用户
停止
这种区分在自主系统中变得至关重要。
{
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 10
}
{
"code": "INVALID_CUSTOMER_ID",
"retryable": false
}
智能体然后可以做出更明智的决策。
一个简单的重试策略可能是:
if error.retryable:
retry_with_backoff()
elif error.code == "INVALID_ARGUMENT":
reconsider_arguments()
elif error.code == "PERMISSION_DENIED":
ask_user()
else:
stop_and_report()
这比盲目重试每次失败要安全得多。
一个工具通常应该有一个清晰的职责。
manage_customer
create
read
update
delete
search
merge
archive
restore
这给了模型一个很大的决策表面。
search_customers
get_customer
create_customer
update_customer
archive_customer
每个工具有更精确的含义。
Google 的 MCP 安全指南同样建议保持 MCP 工具专注于单一职责。([Google GitHub][4])
原则很简单:
每个工具的决策越少,智能体的决策通常越清晰。
还有另一种失败模式。
get_user_name
get_user_email
get_user_timezone
get_user_language
get_user_company
get_user_role
get_user_status
现在模型必须发现七个工具才能了解一个用户。
工具粒度应该与有意义的智能体操作相匹配。
智能体会自然地认为这是一个独立的能力吗?
如果是,它可能值得拥有自己的工具。
如果不是,它可能属于另一个操作。
这是一个高级但强大的模式。
Search documents.
Search the document library for information contained
in uploaded documents.
Use this when the answer may exist inside user documents.
Do not use this for general web research.
Do not use this when the user provides an exact document ID.
负面指导减少了工具混淆。
当智能体拥有许多具有重叠能力的工具时,这变得越来越重要。
你现有的后端可能看起来像:
Frontend
↓
REST API
↓
Services
↓
Database
添加智能体并不意味着模型应该不受限制地访问该 REST API。
┌──────────────┐
│ Agent │
└──────┬───────┘
↓
┌─────────────────┐
│ Tool Layer │
│ │
│ search_docs │
│ create_report │
│ send_report │
└────────┬────────┘
↓
┌─────────────────┐
│ Adapter Layer │
└────────┬────────┘
↓
┌─────────────────┐
│ Internal APIs │
└────────┬────────┘
↓
Database
这个适配器层很有价值,因为你的内部 API 可以独立于智能体接口演进。
让我们构建一个简单的 Python 工具。
from typing import TypedDict
class DocumentResult(TypedDict):
document_id: str
title: str
excerpt: str
relevance: float
def search_documents(
query: str,
limit: int = 5
) -> list[DocumentResult]:
"""
Search uploaded documents for information relevant to the query.
Use this when the user asks about information that may exist
inside uploaded documents.
Do not use this for general web research.
Args:
query: Natural-language description of the information to find.
limit: Maximum number of results. Must be between 1 and 10.
Returns:
Matching documents with titles, excerpts, and relevance scores.
"""
if not query.strip():
raise ValueError("query cannot be empty")
if not 1 <= limit <= 10:
raise ValueError("limit must be between 1 and 10")
results = document_search_engine.search(
query=query,
limit=limit
)
return [
{
"document_id": result.id,
"title": result.title,
"excerpt": result.excerpt,
"relevance": result.score
}
for result in results
]
注意这个工具没有暴露什么:
database connection
SQL query
embedding model
vector database
chunk size
index name
internal storage path
这些是实现细节。
智能体需要的是能力,而不是你的基础设施。
一个常见的错误是认为:
"模型在我们应用程序内部,所以工具是可信的。"
模型不是授权系统。
每次工具调用仍然应该通过正常的安全控制。
Agent
↓
Tool
↓
Authenticated identity
↓
Authorization
↓
Validation
↓
Business logic
↓
Database
永远不要让模型生成的参数绕过授权。
def get_customer(customer_id):
return database.get_customer(customer_id)
def get_customer(customer_id, user):
authorize(
user=user,
resource=customer_id,
action="read"
)
return database.get_customer(customer_id)
模型选择它想做什么。
你的应用程序决定它是否被允许这样做。
当智能体做出错误决策时,你需要知道原因。
agent_run_id
tool_name
tool_version
arguments
user_id
timestamp
latency
result_status
error_code
retry_count
对于敏感系统,仔细控制记录了哪些参数和结果。
一个有用的追踪可能看起来像:
Run: agent_9821
09:41:02
Tool: search_documents
Arguments:
query = "Q3 pricing strategy"
09:41:03
Result:
3 documents
09:41:04
Tool: get_document
Arguments:
document_id = "doc_918"
09:41:04
Result:
success
09:41:07
Tool: create_summary
Arguments:
document_id = "doc_918"
09:41:09
Result:
success
现在调试成为可能。
没有工具级的可观测性,智能体故障通常看起来像:
User: 为什么你给了我错误的答案?
Agent: 抱歉。
这不是工程策略。
{
"query": "..."
}
{
"query": "...",
"filters": {}
}
{
"query": "...",
"filters": {},
"ranking": "semantic"
}
改变语义而不考虑现有智能体可能导致微妙的故障。
像对待公共 API 一样对待工具。
search_documents.v1
search_documents.v2
尽可能维护向后兼容的模式。
当多个智能体或外部客户端使用同一工具时,这一点尤为重要。
一个智能体可能因两种截然不同的原因失败:
Model failure
Tool failure
你需要同时测试这两种情况。
def test_search_documents_rejects_empty_query():
with pytest.raises(ValueError):
search_documents("")
def test_search_documents_rejects_invalid_limit():
with pytest.raises(ValueError):
search_documents("pricing", limit=100)
测试模型是否选择了正确的工具:
User:
"Find the pricing information in my uploaded files."
Expected:
search_documents
User:
"Search the internet for today's AI news."
Expected:
web_search
工具选择是智能体行为的一部分,应该按照同样的方式进行评估。
现代智能体开发工作流越来越多地将结构化评估与单元测试和集成测试结合在一起。例如,Google 当前的智能体工具链在开发生命周期中包含了评估数据集和评分工作流。([Google GitHub][2])
在将函数暴露给 AI 智能体之前,先问自己:
工具名称是否无歧义?
它是否描述了一个有意义的能力?
描述是否解释了工具的作用?
是否解释了何时使用它?
是否解释了何时不应使用它?
参数是否具有语义性?
是否定义了必填字段?
是否验证了约束条件?
是否避免了歧义参数?
结果是否结构化?
智能体是否容易理解发生了什么?
输出是否为下一步决策提供了足够的信息?
错误是否机器可读?
智能体能否区分可重试失败和永久性失败?
错误是否解释了智能体接下来可以做什么?
后端是否强制执行授权?
破坏性操作是否有明确标识?
副作用是否显式声明?
确认要求是否在模型外部强制执行?
操作在适当情况下是否幂等?
请求是否可以安全重试?
是否定义了超时?
是否可以追踪每次工具调用?
是否可以识别延迟和失败?
是否可以重现智能体运行?
工具契约是否可以安全变更?
是否有版本控制策略?
生产级智能体不应该长这样:
LLM
↓
Random API calls
↓
Database
更好的架构是:
┌───────────────┐
│ User │
└───────┬───────┘
↓
┌───────────────┐
│ Agent │
└───────┬───────┘
↓
┌─────────────────────┐
│ Tool Registry │
└─────────┬───────────┘
↓
┌────────────────────────┐
│ Agent-Friendly Tool API │
└────────────┬───────────┘
↓
┌────────────────────┐
│ Validation │
│ Authorization │
│ Rate Limits │
│ Idempotency │
│ Observability │
└──────────┬─────────┘
↓
┌──────────────┐
│ Adapter Layer│
└──────┬───────┘
↓
┌──────────────────────┐
│ Internal APIs / DBs │
└──────────────────────┘
模型控制推理循环。
你的应用控制执行边界。
这种分离是根本性的。
智能体工具设计最大的错误,是把工具当作现有函数的简单包装。
工具是一个推理接口。
模型需要理解:
What can I do?
When should I do it?
What information do I need?
What will happen?
What will I get back?
What should I do if it fails?
一个设计良好的工具 API 能回答以上所有六个问题。
这就是为什么工具描述、模式定义、结构化输出、错误契约、权限和可观测性如此重要的原因。
MCP 等协议正在将这些概念正式化。当前的 MCP 工具支持工具描述、输入模式、输出模式和行为注解,而更新的 SDK 还提供了基于模式验证的工具参数和结构化结果。([Model Context Protocol][1])
智能体工程的未来不仅仅是给模型更多工具。
而是给它们更好的接口来推理。
不可靠的智能体与生产级智能体之间的差距,可能小到这种程度:
Bad tool:
"execute_action"
Good tool:
"cancel_subscription"
后者给了模型可以推理的东西。
这就是智能体工具 API 的真正职责。
为 AI 智能体设计工具时:
Design for decisions.
Use explicit schemas.
Keep tools focused.
Write descriptions for the model.
Return structured results.
Make errors recoverable.
Separate reads from side effects.
Enforce authorization outside the model.
Support safe retries.
Instrument every invocation.
Evaluate tool selection.
Version important contracts.
最好的工具 API 不一定是功能最多的那个。
而是给智能体清晰、有约束、可预测操作的工具。
这就是把工具调用从演示功能变成工程系统的关键。