想象你是一家餐厅的厨具设计师,你的主厨阿金是个天才——脑子里装着一千道菜的菜谱,火候、刀工、调味张口就来。但他有个致命缺陷:他只会“想”,不会“动手”。你让他描述红烧肉的做法,他能讲三小时;你让他去仓库拿五花肉、开火炖上,他双手空空。大脑里的菜谱,够不着一墙之隔的食材仓库。 AI Agent 正是这个主厨。大模型天生只会理解与生成文字,无法查数据库、调接口、操作企业内部系统。要让它的“想法”变成“行动”,就得给它配上专属厨具——这就是自定义工具开发。你的工作,是按餐厅的菜单(业务需求)定制厨具:把内部 API 封装成模型看得懂、选得对、用得稳的工具。 这篇指南会带你走完完整流程:先看工具调用的主干道,再学封装四要素,接着打磨接口与 Schema 说明书,装上校验质检,最后通过测试和版本管理让工具安全上岗。等你学完,就能亲手给 Agent 造出一整套顺手的“厨具”。

岗位关联

掌握自定义工具开发,你就能把企业内部系统和业务能力安全地接入 Agent,让 AI 从“纸上谈兵”变成“亲自下手”——这是 AI 应用落地的核心技能。

目录
  1. 可视化演示
  2. 工具开发总览:主厨为什么需要专属厨具
  3. 工具封装规范:一把菜刀的诞生流程
  4. 接口设计原则:让把手符合手感
  5. Schema定义:给厨具写说明书
  6. 输入校验机制:食材进厨房前的质检
  7. 测试与验证:正式上菜单前的试菜
  8. 版本与兼容:厨具的升级换代
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示自定义工具开发的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

自定义工具开发:自定义工 自定义工具开发总览与封装原则 工具接口设计与Function 核心机制可视化 自定义工具开发整体架构与关键组件

工具开发总览:主厨为什么需要专属厨具

阿金是米其林大厨,脑子里装着上千道菜的菜谱,可他却有个尴尬处境:菜谱只教会他“想”,没教会他“动手”。让他背出红烧肉的做法,他能讲三天三夜;让他真的去仓库拿一块五花肉、开火炖上——他手是空的。厨房里唯一趁手的是一把多功能瑞士军刀,什么都能凑合,什么都不顺手。作为厨具设计师,你学的第一课就是:主厨的创造力 × 顺手的专属厨具 = 真正的出品能力。

在 AI Agent 的世界里,这个“主厨”就是大语言模型。模型本身只负责理解与生成文本,无法直接查询数据库、调用公司内部系统。要让“想法”变成“行动”,就必须给它配上工具。自定义工具开发,就是按业务需求把内部 API、脚本、服务封装成 Agent 能够理解、选择并调用的“专属厨具”。每个工具都是一张“菜谱卡”,告诉模型:这道菜叫什么、需要什么食材、最后端出来什么。

Agent 调用工具的标准路径是一个闭环:模型理解用户的请求 → 从工具清单中选择匹配的工具 → 根据输入 Schema 生成参数 → 工具执行内部逻辑 → 返回结构化结果。整个流程中,工具开发者决定的是中间三个环节的质量:工具有没有被描述清楚、参数有没有被校验、结果有没有被格式化。

定制厨具不是为了“看起来专业”,而是为了让仓库里的食材真正变成餐桌上的菜。当你设计第一个工具时,先问自己:这道菜(业务需求)是什么?主厨拿到这把刀,真的能着手吗?

Agent→理解需求Tool(args)→执行result\displaystyle \text{Agent} \xrightarrow{\text{理解需求}} \text{Tool}(\text{args}) \xrightarrow{\text{执行}} \text{result}
Agent 把一个自然语言问题,转换成一次对工具的调用,拿到结果后继续回答用户。这是工具调用的“主干道”,后面所有设计都围绕这条主干道展开。
  1. 理解需求Agent 先把用户的话拆解为任务意图,例如“查一下 X1001 的出勤”,它知道这需要发起一次查询。
    Agent→理解需求\text{Agent} \xrightarrow{\text{理解需求}}
  2. 选择工具Agent 根据工具的 name 和 description 从注册表里挑最匹配的那一个,类似主厨看菜谱卡选锅。
    Tool\text{Tool}
  3. 填写参数Agent 按输入 Schema 的约束生成结构化参数,例如 {"employee_id": "X1001", "days": 22}。
    Tool(args)\text{Tool}(\text{args})
  4. 执行返回工具执行封装好的内部 API,把结构化结果回传给 Agent,再由 Agent 组织成自然语言回复。
    Tool(args)→执行result\text{Tool}(\text{args}) \xrightarrow{\text{执行}} \text{result}

工具封装规范:一把菜刀的诞生流程

一把好刀不是随手磨出来的。老师傅做菜刀有固定工序:先选钢材定用途,再开刃定锋利度,最后装柄定手感。你给阿金定制厨具也走同样的流程——这就是“封装”:把一段只有你懂的内部代码,包上主厨能看懂的标签、用途说明和使用方法。

工具封装规范,就是把一个内部 API 或函数“包”成 Agent 可调用的格式。业界通行的做法是给每个工具配齐四要素:name(名字)、description(描述)、输入 Schema、输出 Schema,外加一个真正干活的 handler(处理函数)。四要素缺一不可:名字让 Agent 认得出,描述让 Agent 选得对,Schema 让 Agent 填得准,handler 让活儿干得成。

写封装时有三个要点。名字要“见名知义”:query_attendance 比 tool_12 好一万倍,模型没有读代码的能力,它只能靠名字和描述做匹配;描述要“写人话”:直接说明“输入员工 ID 与统计天数,返回该员工当月出勤天数”,而不是写“这是一个查询接口”这种废话;实现要“藏起来”:内部 API 的鉴权、重试、日志都封在 handler 里,对 Agent 只暴露一个简单的可调用接口。

老师傅做好刀,还要在刀柄上刻字——工具名就是刀柄上的刻字。封装得漂亮,主厨阿金才能在几十把刀里一眼摸到对的那把。

Tool≜(name, description, schemain, schemaout, handler)\displaystyle \text{Tool} \triangleq \left( \text{name},\ \text{description},\ \text{schema}_{\text{in}},\ \text{schema}_{\text{out}},\ \text{handler} \right)
一个工具的本质是一个五元组:四个“说明书”字段加上一个“干活”函数。封装就是把内部实现装进这个统一外壳里。
  1. 名称标识name 是工具的身份证,Agent 会在调用结果里用它标记“我用了哪个工具”,因此必须全局唯一且语义清晰。
    name∈Tool\text{name} \in \text{Tool}
  2. 描述字段description 是给模型看的“选购指南”,直接影响选工具准确率,要写具体场景和边界。
    description\text{description}
  3. 双向 Schema输入 Schema 约束参数长什么样,输出 Schema 约定结果的结构,让模型能稳定解析。
    schemain, schemaout\text{schema}_{\text{in}},\ \text{schema}_{\text{out}}
  4. 处理函数handler 是真正执行的那段代码,负责调内部 API、做鉴权、处理异常,模型永远看不到它。
    handler:args↦result\text{handler}: \text{args} \mapsto \text{result}

接口设计原则:让把手符合手感

刀好不好,主厨抓一下把手就知道。把手太粗握不住,太细打滑,位置不对切菜别扭。你给阿金设计工具时,接口就是把手——Agent 不会“用力抓”,它只能按 Schema 往接口里填参数,所以把手要设计得又简单又好握。

接口设计有三个核心原则。第一,单一职责:一把刀只干一件事,查询考勤的工具就别顺带算工资,职责越纯,描述越好写,模型越好选;第二,参数简洁:参数越少越明确,模型填错概率越低,一个工具超过五六个参数就该怀疑是否该拆分了;第三,错误设计:接口要能给出“哪里错了、怎么改”的错误信息,而不是抛一个 500 就完事。

可以从调用成功率的角度理解这三个原则:描述清晰度、参数数量、错误信息可用度,共同决定了模型第一次调用就成功的概率。设计时对每个工具问三个问题:它是否只回答一类问题?参数能否用一句话说清?调用失败时返回的错误是否能让模型自己修正?

给阿金设计的把手,他握上去的瞬间就知道这把刀怎么使。好的接口设计就是让 Agent“摸到就会用”,不需要任何多余的思考成本。

调用成功率∝描述清晰度×错误信息可用度参数数量×职责粒度\displaystyle \text{调用成功率} \propto \frac{\text{描述清晰度} \times \text{错误信息可用度}}{\text{参数数量} \times \text{职责粒度}}
描述越清楚、报错越有用、参数越少、职责越单一,Agent 一次调用就成功的概率就越高。
  1. 描述清晰度description 里写清输入范围、典型示例和边界条件,模型才能准确判断“该不该用我”。
    描述清晰度↑⇒成功率↑\text{描述清晰度} \uparrow \Rightarrow \text{成功率} \uparrow
  2. 错误信息可用度错误要包含字段名、期望值、实际值,让模型能定位并修正参数后重试。
    错误信息可用度↑⇒成功率↑\text{错误信息可用度} \uparrow \Rightarrow \text{成功率} \uparrow
  3. 参数数量每多一个参数,模型就多一点生成错误的概率,因此只保留必要字段。
    参数数量↑⇒成功率↓\text{参数数量} \uparrow \Rightarrow \text{成功率} \downarrow
  4. 职责粒度一个工具对应一个业务动作,避免“大而全”的万能工具导致的描述混乱。
    职责粒度↓⇒成功率↑\text{职责粒度} \downarrow \Rightarrow \text{成功率} \uparrow

Schema定义:给厨具写说明书

新厨具送到厨房,阿金得先看说明书:这个锅能装多少升水?有没有不粘涂层?能进烤箱吗?说明书写得含糊,主厨就不敢用、或用错。Schema 就是厨具的说明书,不过它更硬核——是机器能解析的“结构化说明书”。

JSON Schema 是定义工具输入输出的主流标准。输入 Schema 描述“需要哪些参数、每个参数的类型、是否必填、取值范围、格式要求”;输出 Schema 约定“返回结果的字段结构和类型”。最关键的是每个字段还要配一个 description——就像说明书上的小字注释,告诉模型这个字段到底是什么意思。模型不会猜,它只按 Schema 生成 JSON 参数。

写 Schema 有三条经验。其一,类型必须贴紧真实约束,整数就是整数,别用字符串表示数字;其二,能限定范围就限定,例如“天数”限定 1 到 31,模型就不会生成 100;其三,description 写字段的业务语义而不是字面翻译,例如“员工编号,格式为 X 开头四位数字”,模型才会生成符合格式的值。输出 Schema 同样重要,结构化输出让 Agent 不用在长文本里“大海捞针”。

说明书越清楚,阿金越敢下手。一张写得好的 Schema,等于在工具入口处装了一个“自助引导”,让模型少犯错误、少来回试探。

Schema(prop)=(type, min, max, pattern, description),required⊆properties\displaystyle \text{Schema}(\text{prop}) = \left( \text{type},\ \text{min},\ \text{max},\ \text{pattern},\ \text{description} \right),\quad \text{required} \subseteq \text{properties}
每个参数字段都可以用“类型 + 范围 + 格式 + 说明”来描述,required 数组则列出必填字段。
  1. 字段类型type 声明参数是 string、integer 还是 number,模型据此生成合法 JSON 值的类型。
    type∈{string, integer, number, boolean}\text{type} \in \{ \text{string},\ \text{integer},\ \text{number},\ \text{boolean} \}
  2. 范围格式min/max 约束数值边界,pattern 用正则约束字符串格式,把非法参数挡在生成阶段。
    min≤x≤max,x∼pattern\text{min} \le x \le \text{max},\quad x \sim \text{pattern}
  3. 字段说明description 解释字段的业务含义与示例,是模型理解“该填什么”的关键。
    description(prop)\text{description}(\text{prop})
  4. 必填列表required 数组声明哪些字段必须出现,防止模型漏传导致后端报错。
    required⊆properties\text{required} \subseteq \text{properties}

输入校验机制:食材进厨房前的质检

后厨有条铁律:食材进门先过质检。菜叶发黄的退回,土豆发芽的丢掉,日期过期的直接扔。要是把坏食材直接下锅,整盘菜都毁了。Agent 调用工具也一样——模型生成的参数可能不合法,直接把脏参数打进内部 API,后端会崩溃、数据会写错。

输入校验就是在工具执行前,用 Schema 的规则对参数做一次“过检”:类型对不对、范围合不合理、格式匹不匹配、必填字段齐不齐。校验通过,才把参数交给 handler;校验失败,则返回一个结构化的错误信息,让 Agent 知道问题出在哪个字段、期望是什么,进而自己修正参数、重新调用。这比让 Agent 面对一个笼统的“调用失败”要优雅得多。

校验时机要放在 handler 之外,作为工具的统一入口层——你可以写一个通用的校验器,每个工具注册时自动挂载,而不是在每个业务函数里手写 if-else。错误信息要“对模型友好”:指出字段名、期望类型、实际值,并给出修改建议。注意校验不是安全边界,它只防“模型填错”,不防恶意攻击,鉴权和权限控制仍需在内部 API 侧做。

质检员守在厨房门口,坏食材根本进不了锅。校验让工具系统更健壮,也让模型在一次次“被退货”中学会填对参数——这正是 AI 应用能稳定落地的底气。

validate(x, schema)={pass,x 满足全部字段规则fail→错误原因,x 违反任一规则\displaystyle \text{validate}(x,\ \text{schema}) = \begin{cases} \text{pass}, & x\ \text{满足全部字段规则} \\ \text{fail} \to \text{错误原因}, & x\ \text{违反任一规则} \end{cases}
校验像一个守门员:合法参数放行,非法参数带着“错误报告”打回去,让 Agent 改完再试。
  1. 规则检查逐字段检查类型、范围、格式与必填,任一不满足即判定失败。
    ∀ rule∈schema: x⊨rule\forall\ \text{rule} \in \text{schema}:\ x \vDash \text{rule}
  2. 成功放行所有规则满足时返回 pass,参数正式交给 handler 执行。
    pass⇒handler(x)\text{pass} \Rightarrow \text{handler}(x)
  3. 失败反馈失败时输出字段名、期望、实际值和建议,构造模型能自愈的错误结构。
    fail⇒{field, expected, actual, suggestion}\text{fail} \Rightarrow \{ \text{field},\ \text{expected},\ \text{actual},\ \text{suggestion} \}
  4. 自我修正Agent 收到错误信息后修正参数并发起重试,形成“失败—反馈—重试”闭环。
    retry(修正后 x)\text{retry}(\text{修正后}\ x)

测试与验证:正式上菜单前的试菜

新菜不能第一天就印上菜单。阿金的规矩是:先在试菜厨房做三遍,第一遍按标准菜谱做,第二遍故意用错食材,第三遍把火候调到极限,看这道菜还稳不稳。工具上线前同样要过“试菜”三关,不然上线当天就翻车。

工具测试分三层。第一层是单元测试:单独检验每个工具的 handler 和校验逻辑,给合法参数、非法参数,断言返回结果;第二层是集成测试:把工具装进真实的 Agent 流程里,用自然语言提问,验证“模型选对工具 → 参数填对 → 结果答对”的端到端效果;第三层是边界用例:空字符串、超长文本、负数、特殊字符、并发调用——把能想到的极端输入都先轰一遍。

自动化测试的价值在于“回归保护”:工具改了一行代码,跑一遍测试集就知道有没有弄坏老功能。边界用例尤其重要,模型生成的参数经常出乎意料,比如把 22 填成“二十二”、把必填字段漏掉、传一个超大的 days。覆盖率是常用的度量指标,但它不是万能的——覆盖率高只能说明测得多,不代表测得好,每个断言都要有明确的“期望值”。

试菜厨房里翻车,总好过在正式饭点翻车。工具测试就是你的兜底网——它不保证没有 Bug,但保证你改得起、敢上线。

coverage=已覆盖的边界用例数重要边界用例总数×100%\displaystyle \text{coverage} = \frac{\text{已覆盖的边界用例数}}{\text{重要边界用例总数}} \times 100\%
覆盖率衡量测试的“广度”,提醒你是否把极端情况都想到了;配合断言质量才能衡量测试的“深度”。
  1. 单元测试直接调用 handler 与校验器,对每个工具做独立验证,不依赖外部服务。
    test(tooli)=assert(handler(x), 期望值)\text{test}(\text{tool}_i) = \text{assert}\big( \text{handler}(x),\ \text{期望值} \big)
  2. 集成测试在真实 Agent 流程中提问,验证选择、参数、返回三个环节的衔接。
    Agent(问题)→tool→答案\text{Agent}(\text{问题}) \to \text{tool} \to \text{答案}
  3. 边界用例收集空值、超限、格式错误、并发等极端输入,写入自动化测试集。
    x∈{∅, −∞, +∞, 特殊字符, 并发}x \in \{ \emptyset,\ -\infty,\ +\infty,\ \text{特殊字符},\ \text{并发} \}
  4. 覆盖率统计定期统计边界用例覆盖比例,找出测试盲区并补充用例。
    coverage=NcoveredNtotal\text{coverage} = \frac{N_{\text{covered}}}{N_{\text{total}}}

版本与兼容:厨具的升级换代

用了半年的刀钝了,你给阿金换新刀,但问题来了:老菜单(旧 Agent 配置)里写的是“用旧刀”,新刀外形变了、用法也变了,阿金按老习惯一刀下去,差点切到手。工具的升级换代和换刀一个道理:可以升级,但不能让正在使用的人莫名其妙受伤。

用语义化版本号管理工具:主版本.次版本.修订号。修订号升级:修 Bug,完全兼容;次版本升级:新增可选字段或新工具,向后兼容;主版本升级:删除或改名参数、改变返回结构,不保证兼容。发布策略上,优先保证向后兼容——新增字段而不删旧字段、旧参数给默认值——让已经部署的 Agent 不受影响。实在要破坏性变更,则主版本加一,并附一份迁移指南。

实践中建议工具注册表保留多版本并存:v1 继续服务老 Agent,v2 面向新 Agent 灰度上线,观察调用成功率与报错率稳定后再逐步切流。每次发布都要在工具描述里标注版本号和变更说明,因为模型看到的工具列表来自注册表,描述过时会让模型按旧用法调用新工具。版本管理是“看不见但出大问题”的环节——工具更新后一夜之间所有 Agent 全部报错,这种事在真实系统里并不罕见。

换刀不伤人,升级不崩服务。版本管理就是给厨具升级配上安全气囊:让新增能力的同时,老用户依然能平稳下厨。

v=major⋅minor⋅patch,兼容性={major 不变⇒向后兼容major+1⇒需迁移\displaystyle v = \text{major} \cdot \text{minor} \cdot \text{patch},\qquad \text{兼容性} = \begin{cases} \text{major 不变} \Rightarrow \text{向后兼容} \\ \text{major} + 1 \Rightarrow \text{需迁移} \end{cases}
主版本号没变,老调用方可以放心升级;主版本号变了,必须先评估迁移再发布。
  1. 修订号升级patch +1 只修 Bug,接口和 Schema 完全不动,任何调用方安全升级。
    v→v. .+1: 完全兼容v \to v_{.\,.+1}:\ \text{完全兼容}
  2. 次版本升级minor +1 新增可选能力,旧字段与旧行为保留,老调用方无感知。
    v→v.+1 .: 向后兼容v \to v_{.+1\,.}:\ \text{向后兼容}
  3. 主版本升级major +1 允许破坏性变更,须同步更新 Schema、描述并发布迁移指南。
    v→v+1 . . : 需迁移v \to v_{+1\,.\,.\,}:\ \text{需迁移}
  4. 灰度发布新老版本并存,逐步切流并监控调用成功率,异常时回滚。
    canary:1%→10%→100%\text{canary}: 1\% \to 10\% \to 100\%

AI 应用场景

场景一:企业 HR 考勤查询工具

阶段:工具调用与输入校验

解决的问题:员工经常问“我这个月考勤怎样”,HR 助理要登录多个系统查半天。传统做法是人工查询、手动回复,效率低且口径不一。把考勤接口封装成工具后,Agent 收到提问 → 选择 query_attendance → 校验员工 ID 与天数 → 调内部 API → 用自然语言回复,几秒完成。

具体案例:某公司给内部助手接入了考勤查询工具,员工在群里说“帮我看看 X1001 出勤率”,Agent 自动完成查询并返回“出勤 22 天,出勤率 100%”。非法输入如"abc"会被校验器拦截并引导修正。

场景二:电商客服订单查询工具

阶段:Schema 定义与工具封装

解决的问题:客服机器人只能查标准订单,遇到“查我上个月第三个订单状态”这种个性化问题就答不上。封装订单查询 API 为工具,输入 Schema 定义 user_id、order_time_range 等字段,输出 Schema 返回订单状态列表,Agent 就能拆解用户语义、填对参数、精确回答。

具体案例:大促期间客服机器人接入订单查询工具后,退款咨询的解决率提升 40%,因为 Agent 能直接拿到结构化订单数据,不再让用户反复提供订单号。

场景三:DevOps 运维操作工具

阶段:版本管理与兼容维护

解决的问题:运维同学想让 Agent 帮忙执行部署、查日志、扩容,但直接给 Agent Shell 权限太危险。把运维脚本封装成带权限校验和审计日志的工具,工具版本随脚本迭代,通过版本管理保证线上 Agent 与脚本版本匹配。

具体案例:某团队把“查看服务状态”“滚动重启”封装为工具,接入内部运维助手。工具 v1.0 上线后新增“批量重启”能力时发布 v1.1(向后兼容),老流程不受影响。

Python 演示

python131 行
# -*- coding: utf-8 -*-"""极简自定义工具开发框架:封装内部API -> 定义Schema -> 校验参数 -> 模拟Agent调用""" import re  class SchemaError(Exception):    pass  class ToolSchemaValidator:    """按 Schema 对参数进行类型、范围、格式校验"""     def __init__(self, schema):        self.schema = schema     def validate(self, args):        for key in self.schema.get("required", []):            if key not in args:                raise SchemaError("缺少必填参数: " + key)        for key, rule in self.schema.get("properties", {}).items():            value = args.get(key)            if value is None:                continue            self.check_type(key, value, rule.get("type"))            self.check_range(key, value, rule)            self.check_pattern(key, value, rule)     def check_type(self, key, value, expected):        ok = (expected == "integer" and isinstance(value, int)) \             or (expected == "string" and isinstance(value, str)) \             or (expected == "number" and isinstance(value, (int, float)))        if not ok:            raise SchemaError(key + " 类型应为 " + expected)     def check_range(self, key, value, rule):        if "min" in rule and value < rule["min"]:            raise SchemaError(key + " 不能小于 " + str(rule["min"]))        if "max" in rule and value > rule["max"]:            raise SchemaError(key + " 不能大于 " + str(rule["max"]))     def check_pattern(self, key, value, rule):        pattern = rule.get("pattern")        if pattern and not re.match(pattern, str(value)):            raise SchemaError(key + " 格式不符合 " + pattern)  class Tool:    """工具四要素封装:名字、描述、输入Schema、处理函数"""     def __init__(self, name, description, schema, handler):        self.name = name        self.description = description        self.schema = schema        self.handler = handler        self.call_count = 0     def run(self, args):        ToolSchemaValidator(self.schema).validate(args)        self.call_count += 1        return {"tool": self.name, "call_count": self.call_count,                "result": self.handler(**args)}  def query_attendance(employee_id, days):    """内部API:模拟查询员工出勤"""    table = {"X1001": 22, "X1023": 20, "X1077": 19}    if employee_id not in table:        raise SchemaError("未找到员工 " + employee_id)    ratio = round(table[employee_id] / days, 2)    return {"employee_id": employee_id, "days": days,            "attendance": table[employee_id], "ratio": ratio}  def query_holiday(date_of_month):    """内部API:模拟查询某天是否放假"""    holidays = {1: True, 5: True, 10: False}    return {"date": date_of_month, "is_holiday": holidays.get(date_of_month, False)}  def build_registry():    """把两个内部接口封装成 Agent 可调用的工具注册表"""    attendance_schema = {        "type": "object",        "properties": {            "employee_id": {"type": "string", "pattern": "^X\\d{4}$"},            "days": {"type": "integer", "min": 1, "max": 31},        },        "required": ["employee_id", "days"],    }    holiday_schema = {        "type": "object",        "properties": {            "date_of_month": {"type": "integer", "min": 1, "max": 31},        },        "required": ["date_of_month"],    }    return {        "query_attendance": Tool(            "query_attendance", "查询员工当月出勤天数", attendance_schema, query_attendance),        "query_holiday": Tool(            "query_holiday", "查询某天是否放假", holiday_schema, query_holiday),    }  def agent_call(registry, tool_name, args):    """模拟 Agent:按工具名调用,捕获并返回校验错误"""    tool = registry.get(tool_name)    if tool is None:        return {"error": "工具不存在: " + tool_name}    try:        return tool.run(args)    except SchemaError as err:        return {"error": "参数校验失败: " + str(err)}  def main():    registry = build_registry()    calls = [        ("query_attendance", {"employee_id": "X1001", "days": 22}),        ("query_attendance", {"employee_id": "X1001", "days": 0}),        ("query_attendance", {"employee_id": "abc", "days": 22}),        ("query_holiday", {"date_of_month": 5}),        ("query_holiday", {}),    ]    for name, args in calls:        print(agent_call(registry, name, args))  if __name__ == "__main__":    main()
目的:演示自定义工具开发的完整骨架——如何把内部 API 封装为带 Schema 校验的工具,并模拟 Agent 调用。
  1. ToolSchemaValidator.validate(args)按 Schema 逐字段检查必填、类型、范围、格式,不合格直接抛异常。
  2. check_pattern用正则校验字符串格式,比如员工编号必须匹配 X 加四位数字。
  3. class Tool工具四要素的载体:名字、描述、Schema、handler,一次封装处处复用。
  4. tool.run(args)先校验后执行,并记录调用次数,返回结构化的调用结果。
  5. build_registry()把多个工具放进注册表,Agent 拿着工具名就能查到并调用。
  6. agent_call(...)模拟 Agent 的调用入口,捕获 SchemaError 并转成可读错误信息,让模型能自愈重试。

常见误区

误区很多同学觉得工具描述是给机器看的,随便写一句“查询接口”就行,甚至干脆留空,反正 Schema 已经定义了参数。

正解模型选择工具时不看代码、不看实现的字段名,它主要靠 description 判断“这个工具是干嘛的、适不适合当前问题”。描述写得模糊,模型要么选错工具,要么反复犹豫。把描述当成给一个聪明但没上下文的新同事写交接说明:说清楚输入是什么、输出是什么、典型场景是什么。你的 Schema 定义得再好,模型可能都走不到那一步。

为何常见:开发者习惯了“面向编译器编程”,觉得描述是形式字段;但 Agent 时代,description 是面向模型的“推荐文案”,重要性被严重低估。

误区反正模型很聪明,生成的参数应该都是对的,直接把参数传给内部 API 执行就行了,省得写校验代码。

正解模型生成 JSON 受限于训练数据和 Prompt 质量,类型写错、漏掉必填、超出范围都发生过。设计校验是给工具装“护栏”,不是不信任模型,而是让失败可控、错误可反馈。把脏数据直接透传给内部系统,轻则返回 500,重则写脏数据、触发重复扣款。校验器是工具开发中最不该省的几十行代码。

为何常见:本地调试时模型恰好生成的都是合法参数,让人误以为校验多余;等生产环境大量真实提问涌入,问题才集中爆发。

误区干脆做一个 `do_everything` 工具,接收一个命令字符串,内部用 if-else 分发到所有业务逻辑,这样 Agent 什么都能干。

正解这是接口设计里最典型的反面教材。第一个问题:一个工具描述根本写不清它能干什么,模型选不中;第二个问题:参数五花八门,校验无从下手;第三个问题:没有边界,Agent 可能用一句模糊的话触发危险操作。正确的做法是拆成多个单一职责的小工具:查询归查询、操作归操作,每个工具的输入输出都清晰可控。这就像给主厨一把折叠刀还是十把专业刀——专业厨房永远选后者。

为何常见:把“接入更多能力”理解成“一个接口搞定所有事”,省事心态加上对 Agent 工作方式不了解,就容易设计出这种懒人工具。

误区工具就是一段代码,改了直接上线就行,旧参数删掉就删掉,反正模型会重新读工具列表。

正解线上 Agent 可能缓存了旧工具描述,用户工作流可能写死了旧参数;你删一个字段,整个调用链瞬间崩掉,而且你很难第一时间发现是“工具升级”惹的祸。正确做法:新增不删减,老字段保留并提供默认值;必须破坏性变更时,主版本加一、写迁移文档、灰度上线,新旧版本并存一段时间。工具升级的冲击面往往比想象的广,因为它是被很多 Agent 共享的“公共设施”。

为何常见:开发阶段只有自己调用,觉得“没人用老版本”;上线后才发现工具是公共服务,影响面完全不可控。

面试题

Q1. 自定义工具在 Agent 架构中的核心作用是?

单选题 · easy

  • A.让 Agent 获得调用外部系统的能力← 正确答案
  • B.提高大模型推理与训练速度
  • C.减少模型参数的数量级规模
  • D.替代向量数据库的检索功能

答案:A。自定义工具把内部 API、脚本、服务封装成 Agent 可调用的接口,核心作用是扩展 Agent 的能力边界,让它能查询数据库、操作业务系统。推理速度、参数量、向量库都不是工具要解决的问题,它们分别属于模型优化和知识检索范畴。

Q2. 工具封装规范中最关键的四要素是?

单选题 · easy

  • A.名称、描述、Schema、处理逻辑← 正确答案
  • B.名称、价格、维护人、上线时间
  • C.模型、温度、采样率、最大长度
  • D.数据库、缓存、消息队列、网关

答案:A。一个工具 = name(名字)+ description(描述)+ 输入输出 Schema + handler(处理函数)。价格、维护人属于运营元数据;模型参数属于推理配置;数据库缓存属于基础设施。只有四要素直接决定 Agent 能否“认得出、选得对、填得准、跑得成”。

Q3. Agent 决定调用哪个工具时,最主要的依据是工具的?

单选题 · mid

  • A.描述信息是否具体且无歧义← 正确答案
  • B.代码文件命名是否标准规范
  • C.返回字段的字节数体量大小
  • D.内部函数执行速度的快与慢

答案:A。模型看不到工具的实现代码,只能看到工具列表中的 name 和 description,它靠描述判断“该选谁”。描述具体无歧义,模型才能精准匹配;文件命名、返回体量、执行速度模型都无从感知,也不会作为选择依据。

Q4. 工具参数校验失败后,推荐的处理方式是?

单选题 · mid

  • A.返回错误原因,让 Agent 修正重试← 正确答案
  • B.直接丢弃请求并记录一条日志
  • C.静默返回空字符串作为最终结果
  • D.修改 Schema 绕过校验继续执行

答案:A。校验失败说明参数不合法,最好的处理是返回结构化错误(字段名、期望值、实际值),模型读到后能自我修正并发起重试。直接丢弃会让用户无响应,静默返回空结果会用假数据误导,改 Schema 绕过校验等于拆掉护栏,都是错误的。

Q5. JSON Schema 中 required 字段的作用是?

单选题 · easy

  • A.声明调用方必传的参数名称列表← 正确答案
  • B.规定参数的默认值大小是多少
  • C.声明参数所对应的数据类型
  • D.描述参数的取值范围边界

答案:A。required 是一个数组,列出调用时必须提供的参数名,防止模型漏传。默认值、类型、范围分别由 default、type、min/max 等关键字负责,各司其职。把 required 理解成“必填清单”,就能和“选填字段”区分开。

Q6. 工具从 v1.0 升级到 v2.0 时,正确的是?

单选题 · hard

  • A.更新 Schema 与描述并附迁移说明← 正确答案
  • B.只改内部实现,对外参数保持不变
  • C.新增同名新工具覆盖旧的注册表
  • D.只升级版本号而不更新任何文档

答案:A。主版本升级代表可能的不兼容变更,必须同步更新 Schema、描述并附迁移说明,让旧调用方知道改什么。只改内部、覆盖注册、不更新文档都会导致线上 Agent 按旧描述调用新工具而报错,这是版本管理中最常见的翻车现场。

Q7. 下列属于工具接口设计原则的有?

多选题 · mid

  • A.单一职责,避免大而全的万能工具← 正确答案
  • B.参数简洁,字段越少歧义越少← 正确答案
  • C.错误信息要指出修正方向和方法← 正确答案
  • D.工具数量越多,系统能力就越强

答案:ABC。单一职责、参数简洁、错误可修正都是接口设计的核心原则。工具数量多不等于能力强,堆砌大量同质化工具反而增加模型的选择难度,重点应是每个工具都清晰可用。

Q8. 工具输入校验通常应覆盖哪些检查项?

多选题 · mid

  • A.字段类型是否符合 Schema 声明← 正确答案
  • B.数值范围是否超出 min/max 界限← 正确答案
  • C.字符串格式是否匹配正则规则← 正确答案
  • D.用户是否已经完成系统登录验证

答案:ABC。校验器负责类型、范围、格式、必填等“参数形态”检查。登录验证属于鉴权,应该在内部 API 的服务端完成,而不是工具校验层的事,把两者分开才能各司其职。

Q9. 工具测试中“边界用例”指的是?

单选题 · hard

  • A.空值、超限、特殊字符等极端输入← 正确答案
  • B.只测试最常见最正常的输入场景
  • C.用随机数据进行压测量测试场景
  • D.仅对文档描述正确性进行测试

答案:A。边界用例针对极端和异常输入,如空字符串、负数、超长文本、特殊字符、超出范围的值。只测正常输入会漏掉模型“抽风”时的行为;随机压测属于性能测试;文档测试则是对描述的检验,都不属于边界用例的范畴。

Q10. 关于工具版本兼容维护的正确策略有?

多选题 · hard

  • A.新增字段时尽量保留旧字段不变← 正确答案
  • B.主版本升级时提供迁移指南文档← 正确答案
  • C.新老版本并存并灰度切换流量← 正确答案
  • D.每次发布都删除所有旧的版本

答案:ABC。向后兼容的核心是“加而不删”;破坏性变更要升主版本并附迁移指南;灰度发布让新老版本并存、逐步切流,异常可回滚。每次发布都删旧版本会让回滚无路可走,属于高危操作。