Tool Calling 工具集设计原则
从抽象层次、接口设计、错误处理三方面阐述原则
原题:在构建大模型工具调用(Tool Calling)能力时,工具集的设计应该遵循哪些原则?请从工具抽象层次、接口设计、错误处理等方面进行阐述。
Agent · 字节真题
30 秒回答
- 工具抽象层次:区分原子工具与复合工具,避免过细或过粗
- 接口设计:Schema清晰、参数自描述、支持动态发现
- 错误处理:分级容错、可观测性、优雅降级
- 安全性:权限隔离、输入校验、执行沙箱
回答与解析
答案要点
- 工具抽象层次:区分原子工具与复合工具,避免过细或过粗
- 接口设计:Schema清晰、参数自描述、支持动态发现
- 错误处理:分级容错、可观测性、优雅降级
- 安全性:权限隔离、输入校验、执行沙箱
- 可扩展性:工具注册机制、版本管理、热更新
一、工具抽象层次
核心原则:粒度适中,语义清晰
- 原子工具:单一职责,如
get_weather、send_email,参数明确、无副作用歧义 - 复合工具:高阶封装,如
book_travel内部编排航班+酒店+租车,降低Agent推理负担 - 避免反模式:
- 过细:
get_weather_by_lat+get_weather_by_city+get_weather_forecast→ 应合并为统一get_weather - 过粗:
operate_database包含CRUD所有操作 → 应按操作类型拆分
- 过细:
二、接口设计(Schema)
面向LLM友好的描述
{
"name": "search_product",
"description": "根据关键词搜索商品,返回匹配的商品列表。当用户询问'有什么手机'时使用此工具",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "搜索关键词,如'iPhone 15'"},
"sort_by": {"type": "string", "enum": ["price_asc", "sales_desc"], "description": "排序方式"}
},
"required": ["keyword"]
}
}
关键要点:
description必须包含使用场景触发条件(when to use),减少LLM误调用- 参数类型明确,枚举值给出可选项,复杂对象扁平化处理
- 支持动态工具发现:运行时根据用户权限/场景过滤可用工具列表
三、错误处理机制
分层容错策略
| 层级 | 处理策略 | 示例 |
|---|---|---|
| 参数校验 | 前置拦截,返回结构化错误 | 必填字段缺失 → MISSING_PARAM: "keyword is required" |
| 执行异常 | 重试+降级,Agent可感知 | API超时 → 返回TOOL_ERROR: "weather service unavailable, please try later" |
| 结果异常 | 后处理兜底,保证下游可用 | 返回空数组 → 包装为{"results": [], "suggestion": "try broader keywords"} |
可观测性:每个工具调用生成唯一trace_id,记录输入参数、执行耗时、原始响应,便于问题定位
四、安全与扩展
- 权限隔离:工具绑定角色权限,敏感操作(支付、删除)需二次确认
- 输入净化:参数防注入,特别是SQL、代码执行类工具
- 热更新:工具版本化管理,支持A/B测试和灰度发布
口语版讲法(约4分钟)
- 这道题本质问的是LLM和外部系统怎么安全高效地交互
- 工具粒度要适中,原子和复合工具搭配使用
- Schema设计要面向LLM友好,描述要包含触发条件
- 错误处理要分层,让Agent能感知并降级
- 安全隔离和热更新是上线前必须考虑的
这道题其实在问一个问题:当我们把大模型接入外部系统的时候,怎么让它既能安全高效地调用工具,又不会因为工具设计不好而把整个流程搞崩?核心就是围绕LLM和外部系统之间的契约来设计。
先说工具抽象层次。这里容易走极端,要么把工具拆得太碎,比如一个天气功能拆成按城市查、按经纬度查、查预报三个独立工具,LLM得花好多步才能完成一次查询。要么搞一个大而全的万能工具,比如一个operate database包揽所有CRUD操作,LLM根本搞不清该传什么参数。我的做法是区分原子工具和复合工具。原子工具负责单一职责,比如get weather,参数明确且无歧义。复合工具则是把多个原子工具编排成一个高阶业务操作,比如book travel内部调航班、酒店、租车,这样LLM只需要一次调用,推理负担小很多。真正落地的时候,我会根据业务场景搭配使用:对简单查询用原子工具,对复杂流程用复合工具。举个例子,在客服退款场景里,退款操作本身是原子工具,但处理退货流程可能涉及查询订单、验证商品状态、发起退款、记录备注,我会把这些打包成一个process refund复合工具,LLM一步完成。
再来看接口设计,也就是Schema怎么写给LLM看。这里的关键是描述要包含使用场景的触发条件。比如一个search product工具,它的description不能只写“根据关键词搜索商品”,还要加上“当用户询问‘有什么手机’时使用此工具”。这样LLM才能准确判断何时调用,减少误调用。参数也要自描述,枚举值给出可选项,复杂对象尽量扁平化。另外,我还支持动态工具发现,运行时根据用户权限或当前场景过滤可用工具列表。比如普通用户看不到删除订单的工具,管理员才能看到。
错误处理这块,我把它分成三个层级。第一层是参数校验,前置拦截,LLM传的参数不对,直接返回结构化错误,比如MISSING PARAM: "keyword is required",LLM马上就能补传。第二层是执行异常,比如API超时,我会重试一次,如果还失败就返回TOOL ERROR,并附上降级建议,比如“天气服务暂时不可用,请稍后再试”。第三层是结果异常,比如返回空数组,我会包装成{"results": [], "suggestion": "try broader keywords"},让LLM有改进方向。每个工具调用还会生成唯一trace id,记录输入参数、执行耗时、原始响应,方便问题定位。
安全性和扩展性也是必须考虑的。权限隔离是基础,敏感操作比如支付、删除,需要LLM先确认用户身份,甚至二次确认。输入净化也不能少,特别是SQL或代码执行类工具,防止注入攻击。热更新和版本管理让工具可以灰度发布,先让少量流量试用新工具,没问题再全量上线。
这里我多说一句,工具集设计还有一个容易被忽略的点:工具之间的依赖关系。比如退款工具依赖订单查询工具,如果订单查询挂了,退款工具也跑不了。我目前的做法是在复合工具内部做依赖检查,如果底层工具不可用,复合工具直接返回降级结果,而不是让LLM去猜。但更好的方案可能是让工具集支持动态编排,根据工具健康状态自动调整调用链,这块我还在探索。
所以总的来说,我会把工具集设计看成是LLM和外部系统之间的契约,重点在粒度适中、Schema友好、错误分层、安全隔离这几个方面。而最终取舍上,我更倾向先保证安全,再追求效率。因为工具调用一旦出错,影响的是整个业务,不能为了省几步就把风险放大。
关键一句:工具之间的依赖关系也需要管理,比如复合工具依赖原子工具,底层工具不可用时应该自动降级。
面试官还可能这样问
- 问法 1 · 场景切入
假设你在做一个电商客服机器人,用户说“帮我查一下昨天订单”,你需要调一个工具去查订单。那你的工具集里,是把查订单、查物流、退换货都做成单独的工具,还是合在一起?你设计工具的时候会怎么抽象它们?
- 问法 2 · 层层追问
你做过工具调用吧,你觉得给大模型暴露工具时,接口该怎么设计?……比如参数的描述怎么写能让模型不瞎调?……那如果工具调用出错了,比如超时或者参数不对,你的系统怎么处理?……还有,工具多了以后怎么管理?
- 问法 3 · 直球架构
设计大模型工具调用的工具集,从工具抽象层次、接口设计、错误处理三个方面,你会遵循哪些原则?具体说说怎么避免工具粒度过细或过粗,Schema怎么写才友好,以及错误怎么分级容错。