☰
大模型Tool Use手写实战:三种方案从面试到生产
2026/10/13 13:53:27 网站建设 项目流程

1. 面试现场:为什么“手写 Tool Use”成了高频考题

1.1 从“背八股”到“现场造轮子”的转变

这两年技术面试的风向变了。前几年大家还在背 Transformer 的注意力公式、手推反向传播,现在面试官更倾向于让你现场设计一个能跑起来的小系统。Tool Use(工具调用)就是其中出现频率极高的一个题目。原因不复杂:大模型本身只会生成文本,真正让它产生实际价值的,是它能调用外部函数、查询数据库、操作文件、发起请求。谁能把 Tool Use 这一层写明白,谁就具备了把模型接入真实业务的能力。

面试官让你“手写一个 Tool Use”,考的从来不是你能不能背出某个框架的 API,而是你对整个链路的理解:模型怎么知道有哪些工具可用?模型输出的调用意图怎么解析?参数怎么校验?函数怎么执行?执行结果怎么回填给模型?出错怎么办?多轮怎么处理?这些问题拆开看都不难,但要在白板上或者共享编辑器里现场写出来,就需要你脑子里有一套清晰的架构。

我前后被问过三次这类题,每次的约束条件都不一样。第一次面试官说“不许用任何框架,纯手写”,第二次说“可以用 SDK,但你要说清楚底层发生了什么”,第三次直接给了一个真实场景让我现场调通。三次下来我用了三种不同的方案,各有取舍。这篇文章就把这三种方案完整拆开讲,包括每一步为什么这么设计、参数怎么定、坑在哪里。

1.2 三种方案的适用边界先讲清楚

在展开之前,先把三种方案的定位说明白,方便你对号入座。

第一种是纯手写 JSON Schema 解析方案,不依赖任何框架的工具调用能力,完全靠提示词约束模型输出结构化 JSON,然后自己解析、校验、执行。这种方案最能体现你对底层原理的掌握,面试时如果要求“不许用框架”,这就是标准答案。

第二种是基于官方 SDK 的 Tool 定义方案,用主流模型服务商提供的工具调用接口,把函数签名转成它们要求的格式,让模型直接返回结构化的调用请求。这种方案工程上最稳,也是实际项目里最常用的。

第三种是基于 MCP 协议的通用工具层方案,把工具抽象成独立的服务,通过标准化协议暴露给模型。这种方案适合工具数量多、需要跨进程甚至跨服务复用的场景,面试时如果聊到架构演进,这是加分项。

三种方案不是互斥的,实际项目里往往是组合使用。下面逐个拆解。

2. 方案一:纯手写 JSON Schema 解析,把控制权攥在自己手里

2.1 核心思路:用提示词把模型“逼”成结构化输出机

这个方案的本质是:我不依赖模型服务商的任何工具调用特性,我只用最基础的对话接口,通过精心设计的系统提示词,让模型在需要调用工具时输出一段特定格式的 JSON,然后我在代码里解析这段 JSON 并执行对应函数。

为什么面试官喜欢考这个?因为它把整个链路的每一环都暴露出来了。你得自己定义工具描述格式、自己写解析器、自己处理模型不按格式输出的情况、自己决定什么时候把执行结果塞回对话历史。这些环节在框架里都是黑盒,手写一遍就全明白了。

核心设计有三个部分:工具注册表、提示词模板、解析执行器。

工具注册表就是一个字典,键是工具名,值包含函数本身、参数 schema、描述。参数 schema 我用的是简化版的 JSON Schema,只支持几种基本类型:string、number、boolean、object、array。够用了,太复杂的类型模型也容易出错。

提示词模板是关键。我会在系统提示里明确告诉模型:你有以下工具可用,每个工具的格式是这样的,当你需要调用工具时,必须输出一个特定标记包裹的 JSON,不要输出其他内容。这个标记我通常用<tool_call>和</tool_call>,因为这对标签在模型输出里很少自然出现,解析时不容易误伤。

解析执行器的逻辑是:拿到模型输出后,先用正则或者字符串查找定位标记,提取中间的 JSON,用json.loads解析,校验工具名是否存在、参数是否齐全、类型是否匹配,然后调用对应函数,把返回值序列化后作为一条新消息追加到对话历史里,再发起下一轮请求。

2.2 工具描述怎么写才能让模型不犯迷糊

这是整个方案里最容易被低估的部分。很多人觉得工具描述随便写写就行,结果模型要么该调用的时候不调用,要么参数传错。工具描述的质量直接决定了调用准确率。

我的经验是,每个工具的描述要包含四个要素:功能一句话说明、参数逐个解释、什么场景下用、什么场景下不用。最后一条特别重要,因为模型经常在不需要调用的时候乱调用。

举个例子,假设有一个查询天气的工具。差的描述是“查询天气”。好的描述是:“查询指定城市的当前天气。参数 city 为城市名称字符串,必填。当用户明确询问某地天气时使用此工具。如果用户只是闲聊提到天气但没有询问具体信息,不要调用。”

参数描述也要具体。不要写“参数 value”,要写“value:要计算的数学表达式,支持加减乘除和括号,例如 (3+5)*2”。模型对示例的敏感度远高于对类型声明的敏感度。

还有一个技巧:在系统提示里加一条规则,要求模型在调用工具前先输出一句简短的思考,说明为什么要调用这个工具。这样一方面能让调用决策更可解释,另一方面能减少误调用,因为模型在“说出理由”的过程中会自我校验。

2.3 解析器的健壮性设计:模型不听话怎么办

手写方案最大的风险就是模型不按格式输出。我踩过的坑包括:模型在 JSON 外面加了自然语言解释、JSON 里用了单引号、参数值该是数字却给了字符串、一次输出了多个工具调用但格式不统一。

应对策略是分层兜底。第一层,解析前先做清洗,把常见的 markdown 代码块标记去掉,把中文引号替换成英文引号。第二层,json.loads失败时,尝试用正则提取最像 JSON 的片段再解析。第三层,解析成功后做 schema 校验,类型不对就尝试强制转换,比如字符串 "5" 转成数字 5。第四层,如果实在解析不了,就把模型的原始输出当作普通回复返回给用户,同时记录一条日志,不要直接抛异常让整个流程崩掉。

参数校验我写了一个小函数,遍历 schema 里声明的必填字段,检查解析出来的对象里有没有,类型对不对。类型检查用isinstance,但要注意 Python 里bool是int的子类,所以判断数字时要先排除布尔值。这个细节很多人会忽略,导致True被当成数字 1 传进去。

注意:手写方案里,工具执行一定要加超时和异常捕获。模型可能传进来一个让函数跑很久的参数,或者函数本身会抛异常。用try/except包住执行逻辑,把异常信息作为工具结果返回给模型,让模型有机会自我修正,而不是让整个对话中断。

2.4 多轮调用的状态管理

一个完整的 Tool Use 流程往往不是一次调用就结束的。模型可能先查一个信息,拿到结果后再决定查第二个,最后综合两个结果给出回答。所以对话历史的管理很关键。

我的做法是维护一个消息列表,每条消息有 role 和 content。用户消息、模型消息、工具结果消息都往里追加。工具结果消息的 role 我通常设为 "user",content 里注明这是工具返回的结果,并带上工具名。这样模型能清楚区分哪些是用户说的、哪些是工具返回的。

循环控制也要注意。设置一个最大轮次,比如 5 轮,防止模型陷入无限调用。每轮结束后检查模型输出里还有没有工具调用标记,没有就说明它准备给最终答案了,循环结束。

这个方案写下来大概两百行代码,面试时如果时间充裕,完全可以在共享编辑器里现场敲出来。它最大的价值是让你在面试官面前展示出你对整个链路的完整掌控,而不是只会调 API。

3. 方案二:基于官方 SDK 的工具定义,工程上最稳的选择

3.1 SDK 帮你做了什么,你还需要做什么

主流模型服务商的 SDK 都提供了工具调用能力。你只需要按照它们要求的格式定义工具,模型在需要调用时会返回一个结构化的对象,里面包含工具名和参数,不需要你自己解析文本。这比方案一省事太多,也是实际项目里的首选。

但面试时如果只写 SDK 调用,面试官往往会追问“底层发生了什么”。所以你得清楚 SDK 在背后做了哪些事:它把你的工具定义转换成模型能理解的格式塞进请求、它在模型返回时解析出结构化的调用请求、它把调用请求包装成对象给你。本质上方案一里手写的那些解析逻辑,SDK 帮你做了,但原理是一样的。

SDK 方案里你仍然需要自己做三件事:工具函数的实际实现、参数的业务校验、执行结果的组织。SDK 只负责“模型说要调用什么”,不负责“调用得对不对”和“结果怎么用”。

3.2 工具定义的参数设计要点

用 SDK 定义工具时,参数描述同样重要,但格式更规范。一般需要提供工具名、描述、参数对象,参数对象里每个字段有类型、描述、是否必填。

我踩过的一个坑是:参数描述写得太简略,模型经常把可选参数也填上,而且填的是它自己编的值。解决办法是在描述里明确写“如果用户没有提供此信息,不要猜测,留空或使用默认值”。另外,对于枚举类型的参数,把所有可能的值在描述里列出来,模型的选择准确率会明显提升。

还有一个细节:工具名尽量用英文小写加下划线,不要用中文或驼峰。有些 SDK 对工具名有格式要求,而且英文名在日志和调试时更清晰。

参数的数量也要控制。一个工具如果有七八个参数,模型出错的概率会大幅上升。我的经验是单个工具参数不超过五个,超过就考虑拆成多个工具,或者把一些参数合并成对象。

3.3 执行结果的回填格式

SDK 方案里,工具执行完的结果需要按照特定格式回填给模型。不同 SDK 的格式略有差异,但核心都是把结果作为一条特殊角色的消息追加到对话里。

这里有个容易忽略的点:结果的内容要简洁且信息完整。不要把整个数据库查询结果几千行都塞回去,模型处理不了那么长的上下文,而且会浪费 token。我的做法是只返回关键字段,或者对结果做摘要。比如查询返回一百条记录,我只回填前五条加上总数,并注明“共一百条,此处展示前五条”。

如果工具执行失败,也要把失败信息回填,但格式要友好。不要直接抛一个 Python 异常堆栈,而是返回一句“工具执行失败:参数 city 为空,请提供城市名称”。这样模型能理解失败原因并尝试修正。

3.4 并行调用与依赖调用的处理

有些场景下模型会一次性请求调用多个工具,比如同时查天气和查航班。SDK 一般会返回一个调用列表。这时候你可以并行执行这些调用,提升响应速度。但要注意,如果这些调用之间有依赖关系,比如第二个调用的参数来自第一个调用的结果,那就不能并行,必须串行。

判断依赖关系的方法很简单:看第二个调用的参数值是不是引用了第一个调用的输出。如果是,就串行;如果不是,就并行。实际实现时,我通常先全部串行跑通,确认逻辑没问题后再把无依赖的部分改成并行。面试时如果聊到性能优化,这一点是很好的加分项。

提示:并行执行工具时,要注意线程安全和资源竞争。如果工具函数会修改共享状态,并行执行可能出问题。稳妥的做法是只对纯查询类的工具做并行,有副作用的工具一律串行。

4. 方案三:基于 MCP 协议的通用工具层,面向架构演进

4.1 为什么需要把工具从应用里抽出来

前两个方案里,工具函数和调用逻辑都写在同一个应用里。工具少的时候没问题,但工具一多,问题就来了:不同应用想复用同一个工具怎么办?工具需要独立部署、独立升级怎么办?工具的作者和模型应用的使用者不是同一拨人怎么办?

MCP(Model Context Protocol)就是为解决这类问题设计的。它把工具抽象成独立的服务,通过标准化协议暴露能力。模型应用作为客户端,连接到工具服务,获取工具列表,发起调用。工具服务可以是本地进程,也可以是远程服务,对模型应用来说接口是一样的。

面试时如果聊到这个层面,说明你不只是会写代码,还在思考系统架构。这是从“能实现”到“能规模化”的关键一步。

4.2 协议的核心交互流程

MCP 的交互流程分几个阶段。首先是连接建立,客户端连接到服务端,双方交换能力信息。然后是工具发现,客户端向服务端请求可用工具列表,服务端返回每个工具的名称、描述、参数 schema。接着是调用阶段,客户端发送调用请求,包含工具名和参数,服务端执行后返回结果。

这个流程和方案一、方案二在逻辑上是一样的,区别在于工具的定义和执行被移到了独立进程,通过协议通信。好处是解耦,代价是多了一层通信开销和复杂度。

实现一个最简单的 MCP 服务端,核心就是注册工具、处理列表请求、处理调用请求这三件事。用官方提供的库可以很快搭起来,但面试时如果要求手写,你需要自己实现消息的序列化和反序列化,以及请求路由。

4.3 工具服务的独立部署与版本管理

把工具抽成独立服务后,版本管理就成了新问题。工具的参数 schema 可能会变,老版本的应用可能还在用旧 schema。我的做法是在工具定义里加版本号,服务端同时支持多个版本,客户端请求时指定版本。这样升级工具不会影响正在运行的老应用。

另一个问题是权限控制。不是所有客户端都能调用所有工具。可以在服务端加一层鉴权,根据客户端的身份决定它能看到的工具列表和能调用的工具。这部分在面试时如果主动提到,会显得你考虑得很周全。

4.4 三种方案的对比与选型建议

维度方案一:纯手写方案二:官方 SDK方案三:MCP 协议
实现难度高,需自己解析低,SDK 封装好中,需搭服务
可控性最高中中高
工程稳定性依赖提示词质量高高
工具复用性差差好
适合场景面试、学习原理大多数实际项目多应用共享工具
调试难度高低中

选型建议很直接:面试手写用方案一,实际项目用方案二,工具需要跨应用复用时上方案三。三者可以组合,比如用方案二做应用层,底层工具通过方案三暴露。

5. 面试现场的高频追问与应对

5.1 模型不调用工具怎么办

这是最常见的追问。模型该调用工具的时候不调用,原因通常有三个:工具描述不够清晰、系统提示没有强调调用时机、模型本身能力不足。

排查顺序是:先检查工具描述里有没有明确说明“什么场景下使用”,再检查系统提示里有没有要求模型在需要外部信息时必须调用工具,最后考虑换一个能力更强的模型。如果面试官追问“如果换了模型还是不调用呢”,可以回答:在系统提示里加 few-shot 示例,给一两个“用户问什么、模型应该调用什么工具”的样例,通常能解决。

5.2 参数传错怎么兜底

参数传错分几种:缺必填参数、类型不对、值超出范围。兜底策略是在执行前做校验,校验失败不执行,把错误信息回填给模型让它重试。同时设置重试次数上限,比如三次,超过就返回错误给用户。

还有一种情况是模型传了一个看起来合理但实际不存在的值,比如查询一个不存在的城市。这种只能靠工具函数自己处理,返回“未找到该城市”的结果,让模型据此调整。

5.3 工具执行超时怎么处理

工具执行一定要设超时。我的做法是用信号或者线程池来控制,超过设定时间就中断,返回超时信息给模型。超时时间根据工具类型定,查询类一般五秒,计算类十秒,涉及外部请求的可以放宽到三十秒。

面试时如果被问到“超时后模型会怎么做”,可以回答:模型收到超时信息后,可能会选择重试、换一个工具、或者直接告诉用户当前无法获取信息。具体行为取决于提示词怎么引导。

5.4 多工具冲突怎么决策

当多个工具都能完成同一件事时,模型可能选错。解决办法是在工具描述里明确各自的适用边界。比如一个工具查实时数据,一个工具查缓存数据,描述里就要写清楚“需要最新数据时用前者,可以接受稍旧数据时用后者”。

如果面试官追问“模型还是选错了怎么办”,可以回答:在系统提示里加一条决策规则,明确优先级,或者在工具描述里互相引用,比如“如果你需要实时数据,不要用本工具,请用另一个工具”。

6. 实操复盘:从白板到可运行代码的关键细节

6.1 现场写代码的时间分配

面试手写 Tool Use,时间通常只有二十到三十分钟。我的时间分配是:前五分钟讲架构和思路,中间十五分钟写核心代码,最后五分钟讲测试和边界情况。写代码时不要追求完整,把核心链路写出来就行,工具注册、提示词模板、解析执行这三个部分各写一个函数,能体现思路即可。

如果面试官要求代码能跑,那就用方案二,因为 SDK 调用代码量最少,最容易在现场调通。方案一适合在白板上画流程图加伪代码。

6.2 测试用例怎么设计

写完代码后,面试官通常会让你说怎么测试。我会准备三个用例:正常调用(模型正确调用工具并返回结果)、不调用(用户闲聊,模型直接回答)、异常调用(模型传错参数,系统正确兜底)。这三个用例覆盖了主要路径,能说明你对质量有考虑。

6.3 日志与可观测性

实际项目里,Tool Use 的日志非常重要。每次调用要记录:哪个工具、什么参数、执行耗时、成功还是失败、返回了什么。这些日志在排查问题时是救命稻草。面试时如果主动提到日志设计,会显得你有工程经验,不只是会写 demo。

提示:日志里不要记录敏感参数,比如密码、密钥。如果工具参数可能包含敏感信息,记录前要做脱敏处理。

6.4 我踩过的三个真实坑

第一个坑:工具描述里用了中文标点,模型解析时偶尔出错。后来统一改成英文标点,问题消失。

第二个坑:工具返回的结果里包含了换行符和特殊字符,回填给模型时格式乱了。后来统一做 JSON 序列化,确保格式规整。

第三个坑:并行调用时两个工具同时写同一个文件,导致内容错乱。后来给有副作用的工具加了锁,串行执行。

这三个坑都不复杂,但只有真正写过、跑过、调过的人才会遇到。面试时如果能主动讲出这些,比背十道八股题都有说服力。

7. 从面试题到生产系统:还需要补哪些能力

7.1 工具注册的动态化

面试时工具是写死在代码里的,生产系统里工具需要动态注册和发现。可以做一个工具注册中心,应用启动时从中心拉取工具列表,运行时支持热更新。这样新增工具不需要重启应用。

7.2 调用链路的监控与告警

生产系统需要监控工具调用的成功率、耗时分布、错误类型。设置告警阈值,比如成功率低于百分之九十五就告警。这些数据还能用来优化工具描述和提示词。

7.3 安全与权限

不是所有用户都能调用所有工具。需要在调用前做权限校验,根据用户身份决定哪些工具可用。敏感工具还要加审计日志,记录谁在什么时候调用了什么。

7.4 成本控制

每次工具调用都会消耗 token,工具多了成本会上升。可以通过缓存常用结果、限制单次对话的最大调用次数、对结果做摘要等方式控制成本。

面试时如果聊到这些,说明你不只是把它当一道题,而是当成一个真实系统在设计。这种视角的差异,往往就是通过和不通过的分界线。

我在实际带团队做这类系统时发现,手写一遍 Tool Use 的经历对理解整个链路帮助极大。哪怕平时用 SDK,知道底层发生了什么,排查问题时心里也有底。面试官考这道题,考的从来不是 API 调用,而是你对“模型如何与外部世界交互”这件事的理解深度。把三种方案都吃透,不管面试官怎么追问,你都能接得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询