AI Agent系列(四):工具调用——给 Agent 装上手和眼睛
本文归于合集:AI Agent从入门到工程实战
如果您觉得本系列还不错,麻烦您点个免费的赞让更多人看到,谢谢~
前面我们已经讲过 什么是 AI Agent,也拆过 Agent 的大脑:大语言模型到底是怎么工作的。
上一篇我们进入 ReAct,讲了 Agent 如何“想一步、做一步、看结果,再决定下一步”。
但这里还留着一个关键问题:
Agent 到底怎么“做”?
这一篇我们就拆开讲:工具调用到底是什么,一个工具在工程里长什么样,多工具如何管理,以及工具调用在真实工程里为什么不能只当成一个 Function Calling API。
目录
• LLM 的根本问题:它会说,会编,但不会做 • 工具调用是什么:模型请求,系统执行 • 一个工具在工程里长什么样 • 多工具管理与完整调用链路 • 使用工具调用的常见坑 • 本篇小结 • 下一篇预告
LLM 的根本问题:它会说,会编,不会做
LLM 的根本限制是它默认只能根据上下文和训练数据生成文本,不能天然读取实时网页,访问你的数据库,不能自己真的去发请求、查日志、写文件、创建工单。所以 Agent 得接入外部工具。
工具调用其实是在解决三个问题:
第一,实时信息获取问题。
获取不过时的实时信息才不会瞎编;
第二,无法行动问题。
真的去调用api,调用shell命令,调用脚本做事情,当一个合格的牛马,而不是告诉你方法论提建议,到头来还是自己当牛马;
第三,可信和可追溯问题。
工具调用让 Agent 的过程可以追踪:它调用了什么工具,传了什么参数,返回了什么结果,最后基于什么证据输出结论。
所谓没有实践就没有发言权,在AI这里也是。
形象点说,LLM 是 Agent 的大脑,ReAct 是 Agent 的行动节奏,那么工具调用就是 Agent 的手和眼睛。
工具调用是什么:模型请求,系统执行
工具调用(Tool Calling / Function Calling)用一句话解释就是:程序告诉LLM本系统有什么工具(api/函数/方法),模型需要调用工具的时候输出标准的Json格式向系统请求工具调用,系统执行工具调用。
Function Calling 最容易被误解的一点是:
不是模型真的执行了函数,而是模型生成了一个“我想调用这个函数”的结构化请求。
真正执行函数的是你的应用程序、Agent Runtime、后端服务或者某个工具执行器。
一个最小流程大概是这样:
用户:帮我查一下竞品 A 的最新价格。模型看到可用工具:- web_search(query)- read_webpage(url)- query_internal_plan(product_id)模型输出工具调用请求:{ "tool_name": "web_search", "arguments": { "query": "竞品 A pricing 官网" }}模型负责判断:
• 现在需不需要工具? • 应该用哪个工具? • 参数应该是什么? • 工具结果回来后,下一步怎么处理?
系统程序负责执行:
• 告诉LLM本系统有哪些工具以及工具需要哪些参数 • 这个用户有没有权限使用工具? • 是否命中高风险动作,需要人工确认? • 工具超时怎么办?
LLM输出 JSON 只是行动意图,真实动作必须经过宿主系统接管和执行。

为什么要输出 JSON
工具调用通常要求模型输出 JSON 等结构化的格式而非自然语言来进行工具请求,原因是自然语言太难被程序稳定执行,而结构化的json可以被程序轻易解析,从而降低开发成本提高解析成功率。
这个Json格式的工具输出有一套较为标准的 JSON Schema,类似于这样:
{"name":"web_search","description":"搜索公开网页,用于获取实时公开信息。","parameters":{"type":"object","properties":{"query":{"type":"string","description":"搜索关键词,应该尽量具体。"},"limit":{"type":"integer","description":"返回结果数量,范围 1 到 10。","minimum":1,"maximum":10}},"required":["query"]}}一个工具在工程里长什么样
工程里可以把一个工具拆成五部分:
Tool = Metadata + Parameters + Executor + Result + Policy下面逐个看。
ToolMetadata:工具的身份和说明书
ToolMetadata 是工具的元数据,ToolMetadata的核心目标是提高LLM在合适时机调用合适工具的准确性,他的属性至少包括:
• name:工具名。• description:工具描述(最重要,决定调用工具的准确率)。• category:工具类别(第二重要,帮助LLM筛选无关工具,进一步提高调用工具的准确率)。• risk_level:风险等级。• timeout:超时时间。• permissions:需要的权限。• examples:典型调用示例。
不好的工具命名:
searchgetrunquery这些名字太泛,工具一多,模型很容易混淆。
更好的命名:
web.search_public_pagesdocs.read_urlcrm.query_customer_planticket.create_support_issuemail.send_draft这里有几个好处。
第一,带命名空间。web.search_public_pages 一看就是公开网页搜索,crm.query_customer_plan 一看就是查内部 CRM。
第二,动词明确。read_url、query_customer_plan、create_support_issue 比 get 更清楚。
第三,能暴露边界。search_public_pages 暗示它只能搜公开网页,不能搜内部数据库。
比名字更重要的是 description,description写的好不好直接决定了LLM能否在恰当的时机调用恰当的工具。
一个优秀的描述应该包含:
• 这个工具做什么,不该做什么。 • 描述什么场景应该用。 • 场景示例。
例如:
搜索公开互联网网页,用于获取实时公开信息,例如官网定价页、公开文档、新闻公告。ToolParameter:参数不是字段列表,而是约束
参数定义常见写法是 JSON Schema,也可以是其他等价结构。
比如搜索工具:
{"type":"object","properties":{"query":{"type":"string","description":"搜索关键词,必须包含要查询的主体和目标信息。"},"language":{"type":"string","enum":["zh","en","auto"],"description":"搜索语言,默认 auto。"},"limit":{"type":"integer","minimum":1,"maximum":10,"description":"最多返回多少条结果。"}},"required":["query"]}这里有几个细节。
第一,参数名要具体。
第二,能用 enum 就用 enum。
比如 language、region、source_type,枚举能减少模型自由发挥。
第三,边界要写清楚。
limit 如果不限制,模型可能填 1000。对搜索工具来说,这会浪费成本;对数据库查询来说,这可能拖垮服务。
第四,参数说明要写给模型看。
query: 搜索关键词,应该包含目标对象和要查询的信息,例如“竞品 A pricing API limits”。Executor:真正干活的地方
Executor 是工具的执行器。
模型不会直接调用数据库,也不会直接发 HTTP 请求。它只是生成了一个调用意图,Executor 才是真正调工具的程序。
这里最好不要把 Executor 理解成某一个具体函数,比如 execute_xxx_tool()。
更合理的抽象是两层:
• 通用执行管线:负责校验、权限、风险、超时、审计、结果规范化。 • 具体工具处理器:负责真正访问搜索、数据库、邮件、工单、文件等外部系统。
也就是说,不同工具可以有不同 handler,但最好共享同一套执行管线。
classToolExecutor:defexecute(self, tool, tool_call, user_context):# 1. 先按工具自己的参数 Schema 做结构校验。# 这一步只能说明参数类型和必填字段正确。 args = validate_schema( data=tool_call.arguments, schema=tool.parameters, )# 2. 再做业务校验。# 例如搜索 limit 不能太大,SQL 必须只读,邮件收件人不能超过上限。 validate_business_rules( tool=tool, args=args, user_context=user_context, )# 3. 做权限检查。# 同一个工具对不同用户、租户、环境可能权限不同。 check_permission( user_context=user_context, required_permissions=tool.required_permissions, )# 4. 做风险策略判断。# 高风险工具不要直接执行,而是返回一个待确认结果。 decision = evaluate_risk_policy( tool=tool, args=args, user_context=user_context, )if decision.requires_confirmation:return ToolResult.pending_confirmation( tool_call=tool_call, reason=decision.reason, )try:# 5. 真正执行工具。# 这里调用的是工具自己的 handler,所以它可以是搜索、数据库、邮件、工单等任意能力。 raw_result = run_with_timeout( fn=lambda: tool.handler(args, user_context), timeout=tool.timeout, retry_policy=tool.retry_policy, )except TimeoutError:return ToolResult.error( code="TIMEOUT", message=f"工具 {tool.name} 执行超时。", retryable=True, )except Exception as exc:return ToolResult.error( code="EXECUTION_FAILED", message=safe_error_message(exc), retryable=tool.retryable, )# 6. 把原始结果转换成适合模型继续推理的 ToolResult。# 不同工具可以有不同 result_adapter,但输出结构要统一。 result = tool.result_adapter(raw_result)# 7. 做输出裁剪和敏感信息处理。 result = trim_large_payload(result, max_tokens=tool.max_result_tokens) result = redact_sensitive_fields(result, policy=tool.output_policy)# 8. 记录审计日志。 audit_log( user=user_context.user_id, tenant=user_context.tenant_id, tool=tool.name, args=safe_log_args(args), status=result.status, )return result具体工具只需要实现自己的 handler:
defweb_search_handler(args, user_context):return search_provider.query( query=args["query"], limit=args.get("limit", 5), )defquery_customer_plan_handler(args, user_context):return crm_client.get_plan( customer_id=args["customer_id"], tenant_id=user_context.tenant_id, )defcreate_ticket_handler(args, user_context):return ticket_client.create( title=args["title"], body=args["body"], owner=user_context.user_id, )这里最容易被忽略的是:Executor 不只是调用一个 handler。
它还是权限边界、可靠性边界和审计边界。
如果一个工具可以发邮件,Executor 就可能要判断发给谁、内容是什么、是否需要人工确认。
如果一个工具可以执行 SQL,Executor 就可能要限制只读查询、限制表范围、限制扫描行数、设置超时。
如果一个工具可以访问 URL,Executor 就可能要防止访问内网地址、云厂商元数据地址和本地回环地址。
ToolResult:返回给模型的不是原始数据
工具执行完以后,结果通常不能原样塞给模型。
比如网页读取工具可能拿到 200KB HTML,数据库查询可能返回 500 行,日志搜索可能返回几千条堆栈。
这些原始结果有三个问题:
• 太长,浪费 Token。 • 太乱,模型容易抓错重点。 • 可能包含敏感信息。
所以 ToolResult 应该是经过整理的结构化结果。
例如搜索工具可以返回:
{"ok":true,"source":"web.search_public_pages","items":[{"title":"Pricing - Competitor A","url":"https://example.com/pricing","snippet":"Plans include Free, Pro, Business and Enterprise..."}],"truncated":false}网页读取工具可以返回:
{"ok":true,"url":"https://example.com/pricing","content_summary":"页面包含 Free、Pro、Business、Enterprise 四档套餐。","facts":["Free 版限制 3 个成员","Pro 版按月计费","Enterprise 需要联系销售"],"quotes":[],"truncated":true,"next_cursor":"page:pricing:chunk:2"}这里的关键是:结果要能支持下一轮推理。
一个好的 ToolResult 应该满足:
• 足够短。 • 足够客观。 • 有来源。 • 有截断标记。 • 有错误码。 • 能让模型知道下一步怎么补查。
工具调用错误或失败返回的结果也要设计成与工具调用成功一致的结构,避免调用工具错误抛异常导致Agent摆烂。
不好的错误:
Exception: request failed更好的错误:
{"ok":false,"error_code":"TIMEOUT","message":"读取网页超过 5 秒。","retryable":true,"suggestion":"可以减少页面数量,或改用 web.search_public_pages 搜索更具体的页面。"}Policy:工具的安全边界
很多工具设计文章只讲 Metadata、Parameters、Executor、Result,但生产环境还应该显式加一层 Policy。
Policy 负责描述:
• 哪些用户能用。
• 哪些环境能用。 • 需要什么权限。 • 是否需要人工确认。 • 是否允许并发调用。
例如:
{"tool":"mail.send_draft","risk_level":"high","requires_confirmation":true,"allowed_roles":["support_manager","admin"],"max_recipients":5}这样系统才能在模型生成调用后做第二次判断:
这个调用格式是对的,但当前用户、当前任务、当前风险等级,是否真的允许执行?
工具粒度设计:不要把底层 API 原样暴露给 Agent
传统 API 是给确定性程序调用的。
程序员知道接口含义,知道参数从哪里来,也知道失败后怎么处理。所以传统 API 可以很细:
GET /users/{id}GET /orders?user_id=...GET /plans/{id}POST /documents但 Agent 不一样。
Agent 是非确定性的。它会根据上下文选择工具,会自己生成参数,会在工具失败后尝试恢复。你给它的工具越底层,它越需要自己拼流程,也越容易拼错。
比如你想让 Agent 分析一个客户为什么流失。
如果只给底层工具:
crm.get_customerbilling.list_invoicessupport.list_ticketsusage.query_eventsemail.search_messagesAgent 需要自己决定先查什么、怎么关联、如何过滤、如何聚合。它可能查了十次还没抓住重点。
更适合 Agent 的工具可能是:
customer.build_churn_context(customer_id)一个判断标准是:
如果这个工具要求模型连续调用多个底层 API 才能得到一个稳定中间结论,那它可能应该被封装成更高层工具。
当然,高层工具也不能无限膨胀。
太高层的问题是黑盒太大,模型无法控制步骤,结果也难调试。
比较好的粒度是“一个稳定工作单元”:
• 搜索公开网页。 • 读取指定网页。 • 查询客户当前套餐。 • 生成客户上下文摘要。 • 创建待审批邮件草稿。 • 写入一条带来源的研究记录。
每个工具都应该回答一个清晰问题,或完成一个明确动作。
多工具管理与完整调用链路
当工具只有两三个时,你可以把工具定义直接塞给模型。
但只要稍微做点真实应用,工具数量很快就会膨胀。
这时候你需要 Tool Manager,也就是工具管理器。
Tool Manager 可以在内部维护一个 registry,用来保存工具定义,作为多工具系统的管理层负责工具注册、候选工具选择、上下文注入、调用路由、执行委托和调用记录。
Tool Manager 负责什么
一个可用的 Tool Manager 至少负责:
1. 注册工具。 2. 按当前任务过滤工具。 3. 把候选工具注入模型上下文。 4. 接收模型工具调用请求。 5. 路由到正确执行器。 6. 记录调用日志和结果。
伪代码可以这样理解:
classToolManager:def__init__(self, executor):# registry 只是内部存储结构,不代表整个管理器只负责注册。self.registry = {}self.executor = executordefregister(self, tool):# 注册时就检查命名、Schema、权限配置,避免坏工具进入系统。 validate_tool_definition(tool)self.registry[tool.name] = tooldeffilter_tools_for_agent( self, categories: Optional[List[str]] = None, exclude_dangerous: bool = True, max_cost: Optional[float] = None,) -> List[str]:"""根据条件过滤工具""" filtered = []for name, tool inself.registry: metadata = tool.metadata# 类别过滤if categories and metadata.category notin categories:continue# 危险工具过滤if exclude_dangerous and metadata.dangerous:continue# 成本过滤if max_cost isnotNoneand metadata.cost_per_use > max_cost:continue filtered.append(name)return filtereddefexecute(self, tool_call, user_context): tool = self.registry.get(tool_call.name)if tool isNone:return ToolResult.error("UNKNOWN_TOOL", "工具不存在。")# 这里不直接实现所有执行细节,而是委托给通用 ToolExecutor。# ToolManager 负责找到工具和组织流程,ToolExecutor 负责执行管线。 result = self.executor.execute( tool=tool, tool_call=tool_call, user_context=user_context, ) record_tool_trace( tool_call=tool_call, result=result, user_context=user_context, )return result为什么给LLM调用的工具要过滤?
因为工具太多,LLM 会懵。
经测试,给 LLM 20 个工具,它的选择准确率明显下降。给 5 个相关工具,准确率高得多。
这不是 LLM 的问题,是人也会犯的错误——选项太多会导致决策疲劳。
所以要根据任务类型,只暴露相关的工具,任务类型由LLM来出:
# 研究任务:只需要搜索和读取if task_type == "research": tools = registry.filter_tools_for_agent(categories=["search", "web"])# 数据分析:只需要计算和数据库elif task_type == "analysis": tools = registry.filter_tools_for_agent(categories=["calculation", "database"])# 代码任务:只需要文件操作elif task_type == "coding": tools = registry.filter_tools_for_agent(categories=["file", "shell"])使用工具调用的常见坑
第一,把工具调用理解成“模型会自动执行函数”。
这是最常见的误解。
模型只是在输出一个结构化调用请求,真正执行的一定是宿主系统。
第二,工具描述写得太随意。
工具描述不是给人看的注释,而是模型选择工具时的重要依据。
好的工具描述至少要讲清楚三件事:
• 这个工具能做什么。 • 这个工具不能做什么。 • 什么时候应该调用它。
第三,工具粒度太底层。
如果直接把内部 API 原样暴露给模型,模型就会被迫理解大量业务细节。
这不是工具调用,这是把业务编排责任甩给模型。
更好的做法是把工具封装成任务语义明确的能力,例如:
不推荐:get_user、get_order、get_payment、get_refund_policy推荐:summarize_user_order_status底层 API 可以很多,但暴露给 Agent 的工具应该尽量贴近任务本身。
第四,一次性把所有工具都塞给模型。
工具越多,模型越容易选错。
尤其是存在相似工具名、相似参数、相似能力的时候,工具调用准确率明显下降。
你可以按任务类型、风险等级、成本预算做过滤,给LLM更少的选择提升工具调用准确率。
本篇小结
这篇文章其实只讲一件事:Agent 想要从“会说”变成“能做”,必须接入工具。
但工具调用不是简单地把一个函数名丢给模型。
一个能在工程里跑起来的工具系统,至少要包含工具元数据、参数 Schema、执行器、结果结构、权限策略、工具管理、工具过滤、调用链路和审计记录。
下一篇预告
下一篇我们继续沿着“工具接入”往下讲:当工具不再只属于某一个 Agent 应用,而是来自外部系统、多个团队、多个客户端时,为什么会需要一套统一的连接协议MCP。
如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~
商务合作或加技术交流群欢迎联系小助手~

夜雨聆风