Agent 外部工具怎么封装?
API/函数/插件集成,参数解析与错误处理要点
原题:在开发大模型Agent的过程中,如何定义、封装和集成外部工具(如API、函数、插件)供Agent调用?需要考虑哪些关键因素,例如参数解析、错误处理和可扩展性?
评估与监控 · 美团真题
30 秒回答
- 工具定义的Schema设计(OpenAI格式/自定义格式)
- 参数解析与校验机制
- 错误处理与重试策略
- 工具注册与动态发现机制
回答与解析
答案要点
- 工具定义的Schema设计(OpenAI格式/自定义格式)
- 参数解析与校验机制
- 错误处理与重试策略
- 工具注册与动态发现机制
- 可扩展性架构(插件化/模块化设计)
一、工具定义层:统一Schema
采用OpenAI Function Calling格式或类似JSON Schema:
{
"name": "weather_query",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "format": "date"}
},
"required": ["city"]
}
}
关键设计:描述信息要足够明确,让LLM能准确理解何时调用、参数含义。
二、工具封装层:抽象基类
from abc import ABC, abstractmethod
from typing import Dict, Any
class BaseTool(ABC):
name: str
description: str
parameters: Dict
@abstractmethod
def execute(self, **kwargs) -> Dict[str, Any]:
pass
def validate(self, params: Dict) -> bool:
# JSON Schema校验
pass
核心机制:
- 参数解析:Pydantic自动校验 + 类型转换
- 错误处理:分层捕获(参数错误/业务错误/系统错误),统一包装为LLM可理解的返回格式
- 执行保护:超时控制、熔断降级、限流
三、工具集成层:注册与路由
class ToolRegistry:
def __init__(self):
self._tools: Dict[str, BaseTool] = {}
def register(self, tool: BaseTool):
self._tools[tool.name] = tool
def get_schemas(self) -> List[Dict]: # 供LLM使用
return [{"name": t.name, "description": t.description,
"parameters": t.parameters} for t in self._tools.values()]
async def execute(self, name: str, params: Dict):
tool = self._tools.get(name)
if not tool:
raise ToolNotFoundError()
return await tool.execute(**params)
四、关键考量因素
| 维度 | 设计要点 |
|---|---|
| 可扩展性 | 插件化加载(动态import)、热更新机制 |
| 观测性 | 调用链路追踪、耗时/成功率监控 |
| 安全性 | 参数脱敏、权限校验、沙箱执行 |
| LLM友好 | 错误信息回传、执行结果摘要化 |
五、典型调用流程
LLM输出工具调用 → 解析name+arguments → 参数校验 → 执行工具
→ 结果格式化 → 回传LLM继续推理
注意:工具返回需控制token长度,超长内容建议摘要或截断。
口语版讲法(约4分钟)
- 本质是让LLM安全可靠地调用外部能力
- 统一Schema设计:描述清晰是核心
- 封装层:参数校验、错误处理、超时熔断
- 集成层:注册与动态加载,关注可扩展性和安全性
- 业务场景举例:客服退款工具,风险与取舍
这道题其实问的是,怎么让大模型安全、可靠地调用外部能力,比如查天气、调数据库、发API。核心不是代码怎么写,而是怎么设计一套机制,让LLM能准确理解工具该什么时候用、参数怎么填、出错了怎么兜底。
先说工具定义。我会用统一的Function Calling Schema,比如OpenAI那个格式。关键不是写JSON,而是描述要足够明确。举个例子,一个天气查询工具,city字段描述成"城市名称"没问题,但最好写"比如北京、上海",因为LLM对实例的理解比抽象描述好。参数要标注哪些必填、哪些可选,格式也写清楚,比如日期用YYYY-MM-DD。
再讲封装。我会定义一个抽象基类,每个工具都继承它。这里重点说两个事:参数校验和错误处理。参数校验我直接用Pydantic,自动做类型转换和校验,省心。错误处理要分层:参数错误返回清晰提示,业务错误比如查不到数据返回友好信息,系统错误比如超时则统一包装。错误信息必须让LLM能读懂,不能抛原始异常,得说"查询超时,请稍后重试"这种。另外,每个工具我都加超时控制和熔断,防止一个慢工具拖垮整个Agent。
集成层就是注册中心和路由。我写一个ToolRegistry,启动时扫描目录动态注册工具,这样加新工具不用改代码。对外暴露两个接口:一个给LLM拿Schema列表,一个执行调用。这里有个坑:工具返回内容要控制长度,LLM的上下文窗口有限,超长结果我会摘要或者截断,不然Agent会跑偏。
实际业务场景,比如客服退款系统。用户说"我要退单A,但订单号找不到了",Agent需要调用查订单、查退款政策、生成退款单三个工具。查订单工具得有模糊搜索能力,因为用户可能只记得部分信息;退款政策工具要考虑满减、优惠券叠加这种复杂规则,参数校验就格外重要;生成退款单工具要防重复提交,得做幂等。
落地风险我特别关注两个:一是工具调用失败后的重试策略,不能无限重试,得设次数和退避;二是安全性,比如查订单工具要鉴权,用户只能查自己的订单。可扩展性方面,我会用插件化架构,新工具写一个类放目录就自动注册,热更新通过监听文件变化实现。
还有一个延伸点我简单提一下:当工具数量超过几十个时,LLM选错工具的概率会明显上升。这时候我倾向加一层工具路由,先根据用户意图粗分类,再在子集里做选择,而不是让LLM从海量工具里硬选。
所以整体上,我会把工具集成看成一套契约系统,核心是定义清晰、容错健壮、扩展灵活。
关键一句:工具数量超过几十个时,LLM选错工具概率上升,需要加工具路由做粗分类。
面试官还可能这样问
- 问法 1 · 场景切入
假设我们要做一个智能客服Agent,需要调用天气查询API、订单查询API,你会怎么把这些工具定义好并注册给Agent?比如天气查询的入参和返回格式怎么描述?
- 问法 2 · 层层追问
Agent调用外部工具时,你怎么告诉它有哪些工具可用、每个工具怎么用?……那如果工具返回错误,比如超时或者参数不对,Agent怎么知道并处理?……再进一步,如果以后要加100个新工具,你的设计能方便地扩展吗?
- 问法 3 · 直球架构
设计一个Agent工具集成框架,要求:工具定义用JSON Schema,支持参数校验、错误重试,并且注册机制要方便扩展。你会怎么分层设计?关键类或组件有哪些?