
1. 从一道面试题说起为什么“手写 Tool Use”成了高频考点“手写一个 Tool Use”——这六个字放在三五年前多数前端或后端岗位的面试里根本不会出现。但从大模型应用开发岗位大量释放开始它几乎成了绕不开的一道坎。我最近帮几个朋友做模拟面试十个人里有七个被问到类似的问题形式各不相同有的让你白板写伪代码有的让你在共享编辑器里跑通一个最小闭环还有的干脆扔给你一段残缺的调度逻辑让你补全。先说清楚 Tool Use 到底是什么。你可以把它理解成大模型和外部世界之间的一根“数据线”。模型本身只会输出文本它不知道今天天气、查不了数据库、发不了邮件。Tool Use 就是让模型在生成回复的过程中主动“喊一声”——我需要调用某个函数参数是这些——然后由我们的程序去执行这个函数再把结果塞回给模型让它继续生成最终答案。整个过程像是一个不会用工具的实习生旁边坐着一个会跑腿的助理实习生说“帮我查一下库存”助理跑一趟回来告诉他结果他再接着写报告。面试官考这个考的不是你会不会调某个现成的 SDK。现成的东西谁都会抄文档。他真正想看的是三件事第一你懂不懂模型输出和函数调用之间的协议长什么样第二你能不能处理多轮调用、参数校验、异常兜底这些脏活第三你在没有框架加持的情况下能不能从零搭出一个能跑的调度循环。这三件事恰好对应了三种不同层次的方案。我前后用过三种写法来应对这类题目从最裸的手动解析到半自动的注册表调度再到基于结构化输出的强约束方案。三种方案没有绝对的优劣关键看面试官想考察什么、你手头的模型能力如何、以及你对“可控性”和“开发效率”的取舍。下面我把这三种方案逐一拆开把每一步的意图、参数、坑点都讲透你照着抄作业就能在面试里稳住。2. 方案一手动解析文本协议最裸但最能体现理解深度2.1 核心思路与适用场景这个方案的核心就一句话让模型按约定格式输出一段特殊文本我们用字符串匹配把它抠出来执行完再把结果拼回去。听起来很土但它恰恰是最能证明你“懂原理”的写法。面试官如果问“如果不用任何框架你怎么实现 Tool Use”你把这套讲清楚基本就稳了一半。适用场景很明确模型本身没有原生函数调用能力或者你用的是自部署的开源模型输出格式完全由提示词控制。这种方案的可控性极高因为解析逻辑全在你手里出问题也好排查。缺点是模型容易“跑偏”格式稍微不对就得靠正则兜底稳定性依赖提示词质量。2.2 协议设计怎么让模型“说人话”又“说机器话”协议设计是这个方案的地基。我一般用类似下面这种结构让模型在需要调用工具时输出一个固定标记包裹的 JSONtool_call {name: get_weather, arguments: {city: 杭州}} /tool_call为什么用 XML 风格的标签而不是直接输出 JSON因为纯 JSON 容易和模型正常回复里的内容混淆加个标签边界正则匹配时误伤概率大幅降低。这个细节很多人不注意实测下来标签包裹的解析成功率比裸 JSON 高出一大截。提示词里要写清楚三件事可用工具列表、每个工具的参数说明、以及“只有在确实需要外部信息时才调用”的约束。工具列表我习惯用 JSON Schema 的简化版描述让模型知道参数类型和是否必填。这里有个经验参数描述里一定要写例子比如city: 城市名称例如杭州模型对例子的敏感度远高于对类型声明的敏感度。2.3 解析与执行正则、异常与多轮循环解析部分我通常写一个函数用正则把tool_call.../tool_call之间的内容抠出来然后json.loads解析。这里必须包一层 try-except因为模型偶尔会输出带注释的 JSON 或者单引号直接解析会炸。解析失败时的兜底策略是把原始输出当作普通回复返回给用户同时记录一条日志而不是直接抛异常中断对话。执行环节要注意参数校验。模型给的参数不一定符合预期比如该传整数的地方传了字符串该传数组的地方传了单个值。我一般写一个轻量的校验层按工具定义检查必填项和类型不通过就构造一条错误信息塞回给模型让它自己修正。这个“让模型自我修正”的机制特别重要面试时主动提出来是加分项。多轮循环是这套方案的灵魂。完整流程是用户提问 → 模型输出可能含 tool_call→ 解析 → 执行 → 把执行结果作为一条新消息追加到对话历史 → 再次调用模型 → 直到模型输出不含 tool_call 的最终回复。循环要有最大轮数限制我一般设 5 轮防止模型陷入死循环反复调用同一个工具。def run_agent(user_input, max_turns5): messages [{role: user, content: user_input}] for _ in range(max_turns): reply call_model(messages) tool_call parse_tool_call(reply) if not tool_call: return reply result execute_tool(tool_call) messages.append({role: assistant, content: reply}) messages.append({role: tool, content: str(result)}) return 达到最大轮数限制注意把工具执行结果追加为tool角色消息时不同模型的角色命名不一样有的用function有的用tool有的干脆用user。这个必须对着你实际用的模型文档来写错了模型会“看不懂”结果。2.4 实操心得与常见坑第一个坑是模型“假装”调用工具。它可能输出一段看起来像 tool_call 的文本但其实是它在解释这个工具怎么用。解决办法是在提示词里强调“只有真正需要执行时才输出标签”同时在解析后加一层判断如果工具名不在注册列表里就当作普通文本处理。第二个坑是参数里的中文和特殊字符。正则匹配时如果用了贪婪模式遇到嵌套内容会匹配过头。我一般用非贪婪的.*?配合re.DOTALL确保跨行也能正确匹配。第三个坑是对话历史膨胀。多轮调用后消息列表会越来越长token 消耗飙升。我的做法是只保留最近 N 轮的工具调用记录更早的用摘要替代。这个优化在面试里提一句面试官会觉得你考虑过生产环境的问题。3. 方案二注册表加装饰器把调度做成可扩展的框架3.1 为什么要从“手动”升级到“注册表”方案一能跑但每加一个工具就要改解析逻辑和提示词工具一多就乱。面试官如果追问“你有二十个工具怎么办”方案一就露怯了。这时候方案二登场用一个注册表把所有工具集中管理用装饰器自动收集工具定义调度器根据模型返回的工具名去注册表里找对应函数执行。这个方案的核心价值在于“可扩展性”。新增工具只需要写一个函数加一个装饰器提示词里的工具列表和执行时的查找逻辑都自动更新。这在真实项目里是刚需也是面试官想看到的工程思维。3.2 工具注册表的设计与实现注册表本质上就是一个字典键是工具名值包含函数引用、参数 schema 和描述。我用装饰器在函数定义时自动注册TOOL_REGISTRY {} def tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] { function: func, description: description, parameters: parameters, } return func return decorator tool( nameget_weather, description查询指定城市的当前天气, parameters{ type: object, properties: {city: {type: string, description: 城市名}}, required: [city], }, ) def get_weather(city): return f{city}今天晴气温 22 度这样设计的好处是工具定义和实现绑在一起不会出现“改了函数忘了改 schema”的情况。生成提示词时遍历注册表把每个工具的 name、description、parameters 拼成一段文本塞进系统提示。执行时用工具名去TOOL_REGISTRY里查查不到就返回错误信息。3.3 调度循环与参数绑定调度循环和方案一类似区别在于执行环节。方案二是根据工具名动态查找函数然后用**arguments把参数字典展开传进去。这里要注意参数名必须和函数签名一致否则会报unexpected keyword argument。我一般在校验层加一步用inspect.signature检查函数接受的参数名和模型给的参数做比对不匹配就提前拦截。import inspect def execute_tool(tool_call): name tool_call[name] args tool_call[arguments] if name not in TOOL_REGISTRY: return {error: f未知工具: {name}} func TOOL_REGISTRY[name][function] valid_params inspect.signature(func).parameters filtered {k: v for k, v in args.items() if k in valid_params} try: return {result: func(**filtered)} except Exception as e: return {error: str(e)}这段代码里有个细节值得说我用filtered过滤掉了模型多给的参数而不是直接报错。因为模型有时候会“自作聪明”加一些额外字段直接报错会让对话中断过滤掉反而更稳。当然必填参数缺失还是要报错的这个交给前面的校验层。3.4 工具描述的质量决定调用准确率这个方案里工具能不能被正确调用八成取决于描述写得好不好。我踩过的坑是两个工具功能相近描述又都写得很笼统模型就经常调错。解决办法是在描述里明确写出“什么时候用这个、什么时候不用”甚至把反例写进去。比如查询订单和查询物流这两个工具描述里我会写“查询订单状态用这个如果用户问的是包裹到哪了用查询物流那个。”这种“排他性描述”能显著降低误调用率。面试时如果你能主动提到这一点说明你真的调过模型不是纸上谈兵。提示工具数量超过十个以后提示词会变得很长模型对靠后工具的关注度会下降。我的经验是把最常用的工具放在列表最前面同时在系统提示里用一句话概括“你有以下工具可用”而不是把全部 schema 都塞进去。4. 方案三结构化输出加原生函数调用最稳但依赖模型能力4.1 结构化输出解决了什么根本问题方案一和方案二都有一个共同的软肋解析靠字符串匹配模型输出格式一抖解析就可能失败。方案三换了个思路——不让模型“自由发挥”输出文本而是用模型原生的结构化输出能力直接返回一个符合 schema 的对象。这样解析环节几乎不会出错因为格式由模型侧保证。现在主流的大模型 API 大多支持某种形式的函数调用或结构化输出。有的是在请求里传tools参数模型返回一个专门的tool_calls字段有的是用 JSON mode 强制输出合法 JSON。不管哪种核心都是把“格式约束”从提示词层面下沉到模型层面。4.2 原生函数调用的请求与响应结构以常见的函数调用接口为例请求时把工具定义放在tools字段里每个工具用 JSON Schema 描述。模型如果决定调用工具响应里会有一个tool_calls数组每个元素包含id、function.name和function.arguments。注意arguments通常是一个 JSON 字符串需要再解析一次。response client.chat.completions.create( modelyour-model, messagesmessages, toolstool_schemas, tool_choiceauto, ) msg response.choices[0].message if msg.tool_calls: for call in msg.tool_calls: name call.function.name args json.loads(call.function.arguments) result execute_tool(name, args) messages.append({ role: tool, tool_call_id: call.id, content: str(result), })这里的关键是tool_call_id。多工具并行调用时模型可能一次返回多个 tool_call每个都有自己的 id。执行完把结果回传时必须带上对应的 id模型才能把结果和调用对应起来。这个 id 机制是方案三独有的面试时能讲清楚说明你真的用过原生接口。4.3 并行调用与结果回传的顺序问题原生函数调用支持一次返回多个工具调用这是方案一和方案二很难优雅实现的。比如用户问“北京和上海今天天气怎么样”模型可以一次返回两个get_weather调用。执行时你可以串行也可以并行但回传结果时必须每个结果单独一条消息且都带上各自的tool_call_id。我踩过的坑是把多个结果合并成一条消息回传模型会“懵”因为它期待的是每个 id 对应一条结果。正确做法是每个结果一条tool角色消息顺序无所谓但 id 必须对得上。这个细节在面试里如果被问到“并行调用怎么处理”答对了就是亮点。4.4 三种方案的横向对比与选型建议对比维度方案一 手动解析方案二 注册表调度方案三 原生函数调用实现难度低中低依赖 API解析稳定性低中高可扩展性差好好对模型要求无无需支持函数调用并行调用支持需自己实现需自己实现原生支持调试难度低全在手里中高黑盒适合面试展示原理理解工程能力实战经验选型建议很直接如果面试官想看你懂不懂底层协议用方案一如果想看你有没有工程化思维用方案二如果岗位偏应用落地、模型也支持用方案三。最稳的回答是三种都讲然后说明你在什么场景下会选哪种这比只讲一种显得全面得多。5. 面试现场怎么讲把三种方案串成一个故事5.1 从需求出发而不是从代码出发很多人一上来就开始写代码面试官还没搞清楚你要解决什么问题你就已经陷进细节了。我的做法是先花一分钟把需求讲清楚模型需要调用外部工具我们要解决的是“模型怎么表达调用意图”和“我们怎么执行并回传结果”这两个问题。然后说“我准备了三种方案从简单到复杂分别对应不同的约束条件”这样面试官会觉得你有全局观。讲的时候要有递进感。先说方案一强调它最能体现对协议的理解然后说“但方案一扩展性差所以有了方案二”自然过渡到注册表最后说“如果模型原生支持函数调用方案三最省事也最稳”。这个叙事线本身就是一次方案选型的演示比干巴巴地罗列代码强太多。5.2 主动暴露权衡而不是只讲优点面试官最怕听到“这个方案完美”。每种方案都有代价主动说出来反而加分。比如方案一解析不稳定方案二需要维护注册表方案三依赖模型能力且调试困难。我会在讲每种方案时都带一句“它的代价是……”然后说“所以我在实际项目里会根据模型能力和团队情况来选”。这种“权衡思维”是资深工程师和初级工程师的分水岭。初级工程师只会说“我用某某框架”资深工程师会说“我为什么不用某某框架”。面试官考手写 Tool Use本质上考的就是后者。5.3 现场编码时的节奏控制如果面试要求现场写代码千万别一上来就写完整实现。我的节奏是先写核心数据结构工具定义和注册表再写解析函数再写调度循环最后补异常处理。每写一段停下来解释一句“这一步是为了解决什么问题”。这样即使时间不够没写完面试官也能看出你的思路是完整的。代码风格上变量名要清晰别用a、b、tmp这种。函数尽量短一个函数只做一件事。注释不用多但关键判断处要写一句为什么。这些细节在面试里都是隐形的加分项。6. 生产环境里那些面试不会问但你必须知道的坑6.1 工具执行超时与熔断面试里写的调度循环通常假设工具秒回但真实环境里工具可能超时、可能报错、可能返回超大结果。我一般给每个工具执行加超时控制超过阈值就返回“工具执行超时”让模型基于这个信息继续。同时要有熔断机制同一个工具连续失败三次就暂时把它从可用列表里摘掉避免模型反复调用一个坏掉的工具。6.2 结果截断与 token 控制工具返回的结果可能非常长比如查数据库返回几百行。直接塞回给模型会撑爆上下文。我的做法是给每个工具的结果设一个长度上限超过就截断并加一句“结果已截断如需更多请细化查询条件”。这个处理在面试里提一句面试官会知道你考虑过生产环境的 token 成本。6.3 安全边界别让模型为所欲为工具调用最大的风险是模型调用了不该调用的工具或者传了危险的参数。比如一个删除数据的工具模型可能因为理解偏差就调了。我的做法是给工具分级只读工具可以直接调写操作工具需要二次确认危险工具直接不暴露给模型。这个安全分层的思路在任何涉及 Tool Use 的岗位面试里都是加分项。注意永远不要假设模型会“理智”地使用工具。提示词里的约束只是软约束真正的安全边界必须写在代码里而不是写在提示词里。6.4 日志与可观测性生产环境里出问题第一件事就是看日志。我一般记录每次工具调用的工具名、参数、执行耗时、返回状态、结果摘要。这些日志在排查“模型为什么调错了工具”时特别有用。面试时如果被问到“怎么调试”把这套日志方案讲出来比说“打 print”专业得多。7. 我个人的一点体会三种方案我都在真实项目里用过也都在面试里讲过。如果只能选一种深入讲我会选方案二因为它最能体现“工程化”的能力既不像方案一那么裸也不像方案三那么依赖外部条件。但真正让我在面试里拿到 offer 的不是某一种方案的代码写得多漂亮而是我能把三种方案的取舍讲清楚并且能说出每种方案在什么场景下会出什么问题、我怎么解决。手写 Tool Use 这件事代码本身不难难的是背后的判断力。模型能力在变框架在变但“理解协议、管理调度、控制边界”这三件事不会变。把这三点吃透不管面试官怎么问你都能接得住。最后分享一个小技巧面试前自己动手把三种方案各写一遍跑通一个最小闭环哪怕只是调用一个查天气的假函数。跑通过和只看过讲出来的感觉完全不一样。