资讯动态

大模型Tool Use手写实战:三种实现方案与面试核心考点解析

发布时间:2026/10/16 11:13:19 来源:尧图企业网站定制
1. 面试现场从“八股文”到“手写 Tool Use”的转向1.1 为什么“手写 Tool Use”成了新的高频考点过去两年大模型应用层的面试题经历了明显的迭代。早期面试官喜欢问“Transformer 的注意力机制怎么算”“LoRA 微调的秩怎么选”这些偏底层和训练侧的问题。但从去年下半年开始我陆续参与和旁听了几场面试发现一个很明显的转向面试官越来越关注候选人能不能把大模型真正“接”到外部系统上也就是所谓的Tool Use工具调用能力。这个转向背后有很现实的业务逻辑。模型本身的知识是静态的训练数据截止之后就一无所知而真实业务里需要查数据库、调接口、算实时数据、操作文件系统。一个只会聊天的模型价值有限一个能自主决定“我现在该调用哪个工具、传什么参数、拿到结果后怎么继续推理”的模型才是能落地的智能体雏形。所以面试官让你手写一个 Tool Use本质上是在考察三件事你对函数调用协议的理解深度、你对多轮编排的控制能力、以及你在没有现成框架时能不能从零搭出可用的东西。我那次面试面试官的原话是“别用现成的 Agent 框架就给我手写一个能跑通的 Tool Use最好能体现不同的实现思路。”这句话其实信息量很大——“不同的实现思路”意味着他不满足于单一方案想看你对这个问题的认知广度。我当时脑子里快速过了三种方案从最裸的手动解析到基于模型原生能力的结构化调用再到带编排循环的自主智能体。下面我把这三种方案完整拆开讲包括每种方案的代码骨架、适用场景、踩过的坑以及面试时怎么讲才能加分。1.2 先搞清楚 Tool Use 到底在解决什么问题在动手写之前得先把问题定义清楚。Tool Use 的核心链路其实就四步意图识别 → 参数抽取 → 工具执行 → 结果回填。模型收到用户问题后判断这个问题是否需要外部工具如果需要从问题里抽取出调用工具所需的参数然后由外部代码真正执行工具最后把执行结果塞回上下文让模型基于结果生成最终回答。听起来简单但魔鬼在细节里。比如意图识别模型怎么知道该调用哪个工具参数抽取时用户说“帮我查一下北京明天的天气”模型要能抽出城市是北京、时间是明天。工具执行可能失败失败了怎么让模型知道并重试结果回填时如果工具返回的是一大坨 JSON怎么压缩成模型能消化的形式这些问题在三种方案里的处理方式完全不同也是面试官真正想听你展开的地方。我见过不少候选人一上来就写openai.function_call代码是能跑但面试官追问“如果模型不支持原生函数调用怎么办”“如果工具执行超时了你的循环怎么退出”就答不上来了。所以手写 Tool Use 的关键不在于调通 API而在于你能不能说清楚每种方案的边界和取舍。2. 方案一纯提示词驱动的文本解析方案2.1 核心思路与适用场景第一种方案是最“土”但最通用的完全依赖提示词工程让模型输出一段约定格式的文本然后用正则或字符串解析把工具名和参数抠出来。这个方案不依赖任何模型厂商的特殊能力只要模型能遵循指令输出文本就行所以兼容性最好本地部署的小模型也能用。它的工作流程是这样的在系统提示词里定义好所有可用工具的说明包括工具名、功能描述、参数列表和输出格式要求。然后要求模型在需要调用工具时输出类似Action: get_weather和Action Input: {city: 北京, date: 明天}这样的结构化文本。外部代码用正则匹配这两行解析出 JSON执行工具再把结果以Observation: ...的形式拼回对话历史再次请求模型直到模型输出Final Answer:为止。这个方案特别适合几种场景一是模型能力较弱、不支持原生函数调用二是需要跨多个模型厂商做统一抽象三是教学和原型验证阶段逻辑透明方便调试。缺点是解析稳定性完全依赖模型的指令遵循能力模型稍微“发挥”一下格式就崩了。2.2 提示词模板与解析代码骨架提示词的设计是这个方案的重中之重。我一般会把工具定义写成一段清晰的清单并且用 few-shot 示例告诉模型期望的输出格式。下面是我常用的模板结构你是一个可以使用工具的助手。可用工具如下 工具名get_weather 功能查询指定城市指定日期的天气 参数city字符串城市名、date字符串日期如明天 工具名calculator 功能执行数学计算 参数expression字符串合法的数学表达式 当你需要调用工具时严格按以下格式输出 Action: 工具名 Action Input: {参数名: 参数值} 当你已经获得足够信息可以回答用户时输出 Final Answer: 你的最终回答 用户问题{question}解析代码的核心就是两个正则。我用 Python 写因为生态最顺手import re import json ACTION_RE re.compile(rAction:\s*(\w)) INPUT_RE re.compile(rAction Input:\s*(\{.*?\}), re.DOTALL) FINAL_RE re.compile(rFinal Answer:\s*(.*), re.DOTALL) def parse_model_output(text): final FINAL_RE.search(text) if final: return {type: final, content: final.group(1).strip()} action ACTION_RE.search(text) action_input INPUT_RE.search(text) if action and action_input: try: params json.loads(action_input.group(1)) except json.JSONDecodeError: return {type: error, content: 参数 JSON 解析失败} return {type: action, tool: action.group(1), params: params} return {type: error, content: 无法解析模型输出}这里有个细节值得展开INPUT_RE用了非贪婪匹配.*?配合DOTALL是为了防止模型在 JSON 后面又输出一堆解释文字导致匹配越界。但非贪婪也有风险如果 JSON 内部嵌套了花括号比如参数值本身是个对象正则就会提前截断。所以更稳妥的做法是用括号计数的方式手动扫描或者干脆要求模型把参数写成扁平结构。我在面试时就主动提了这一点面试官明显眼睛亮了一下因为这属于真正踩过坑才知道的细节。2.3 这个方案的致命弱点与补救纯文本解析最大的问题是脆弱性。模型可能把Action Input写成ActionInput可能用单引号代替双引号可能在 JSON 后面加个句号这些都会让解析失败。我实测下来一个 7B 级别的模型在复杂工具描述下格式遵循率大概只有七成左右剩下三成需要靠重试和纠错提示来兜底。补救手段有几个。第一是输出约束在提示词里反复强调“只输出规定格式不要有任何额外文字”并且用 stop 参数在生成到Observation:时截断。第二是解析容错除了标准正则再准备一套宽松匹配比如把单引号替换成双引号、去掉尾随标点后再尝试解析。第三是失败重试解析失败时把错误信息作为 Observation 回填让模型自己纠正格式通常第二次就能对。注意重试一定要设上限我一般设 3 次。否则模型可能陷入“格式错→重试→还是错”的死循环白白烧 token。超过上限就直接返回兜底话术别硬撑。这个方案虽然土但它是理解 Tool Use 本质的最好起点。面试时如果你能把这个方案的流程、代码和缺陷都讲清楚面试官会认为你基础扎实而不是只会调库。3. 方案二基于原生函数调用能力的结构化方案3.1 原生 Function Calling 的协议细节第二种方案是现在主流模型都支持的原生函数调用。它的核心区别在于工具的定义不再写在提示词里让模型“读”而是通过 API 的一个独立字段通常叫tools或functions传给模型模型返回的也不再是自由文本而是一个结构化的对象里面明确包含函数名和参数 JSON。这就把方案一里最脆弱的“文本解析”环节彻底消除了。以常见的对话补全接口为例工具定义大概长这样tools [ { type: function, function: { name: get_weather, description: 查询指定城市指定日期的天气, parameters: { type: object, properties: { city: {type: string, description: 城市名}, date: {type: string, description: 日期如明天} }, required: [city, date] } } } ]模型返回的tool_calls字段是一个数组每个元素包含id、function.name和function.arguments。注意arguments是一个 JSON 字符串需要再解析一次。拿到之后执行工具然后把结果以role: tool的消息、带上对应的tool_call_id回填再次请求模型。这个id很关键它保证了多工具并行调用时结果能正确对应。3.2 多工具并行调用的处理逻辑原生方案一个很大的优势是支持并行工具调用。比如用户问“北京和上海明天天气怎么样”模型可能一次性返回两个 tool_call分别查两个城市。这时候你的执行层要能并发处理并且把两个结果都回填。我一般用线程池或者异步来跑from concurrent.futures import ThreadPoolExecutor def execute_tool_calls(tool_calls): results [] with ThreadPoolExecutor(max_workerslen(tool_calls)) as executor: futures { executor.submit(dispatch_tool, tc.function.name, json.loads(tc.function.arguments)): tc for tc in tool_calls } for future in futures: tc futures[future] try: output future.result(timeout10) except Exception as e: output f工具执行失败{e} results.append({ role: tool, tool_call_id: tc.id, content: str(output) }) return results这里有个容易忽略的点工具执行一定要设超时。外部接口可能卡住如果不设超时整个对话就挂死了。我设的是 10 秒超过就返回失败信息让模型决定是重试还是换方案。另外回填的content必须是字符串如果工具返回的是字典要先序列化否则接口会报错。3.3 结构化方案的边界与成本考量原生方案虽然稳定但也不是银弹。首先它绑定模型厂商不同厂商的字段名和返回格式有差异做多模型适配时要写一层抽象。其次工具定义会占用上下文 token工具多了之后开销不小我实测过20 个工具的定义大概要吃掉 1500 到 2000 token这在长对话里是实打实的成本。还有一个隐蔽的坑模型有时候会“幻觉”出一个不存在的工具名或者参数类型不对比如该传字符串的传了数字。所以执行前一定要做参数校验用 JSON Schema 校验一遍不通过就返回错误让模型重来。我在面试时特意强调了这一点因为很多人以为原生方案就万事大吉其实校验层不能省。提示如果你的工具数量超过 30 个建议做工具检索先用一个轻量模型或向量检索筛出最相关的几个工具再传给主模型否则上下文会被工具定义撑爆模型的选择准确率也会下降。4. 方案三带编排循环的自主智能体方案4.1 ReAct 循环的完整实现第三种方案是把 Tool Use 升级成一个自主循环也就是常说的 ReAct推理行动模式。前两种方案本质上都是“一问一答式”的调用模型调一次工具拿到结果就回答。而自主智能体方案里模型可以连续调用多个工具根据上一步的结果决定下一步做什么直到它自己判断任务完成。这个循环的骨架是这样的把用户问题放进消息列表进入一个 while 循环。每一轮请求模型如果模型返回工具调用就执行并把结果追加到消息列表继续下一轮如果模型返回的是普通文本没有工具调用就认为任务结束输出结果。循环要设最大轮数防止无限循环。def run_agent(user_query, max_steps8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_query} ] for step in range(max_steps): response call_model(messages, toolstools) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content tool_results execute_tool_calls(msg.tool_calls) messages.extend(tool_results) return 任务步骤过多已终止这个方案和方案二的区别在于控制权。方案二是模型调一次工具就交回控制权方案三是模型自己掌握“要不要继续”的判断权。这就带来了更强的任务完成能力比如“帮我查一下这周北京的天气然后推荐适合穿的衣服”模型会先查天气再根据天气结果调用推荐工具两步串起来。4.2 循环控制、状态管理与终止条件自主循环最容易出问题的地方就是终止条件。我踩过的坑包括模型反复调用同一个工具、模型在两个工具之间来回横跳、模型一直说“我需要更多信息”但就是不结束。所以除了最大轮数还要加几个保险。第一是重复调用检测。记录每一步的工具名和参数如果连续两步完全一样就强制中断把当前已有信息汇总让模型给结论。第二是状态压缩。循环轮数多了之后消息列表会很长每轮都全量传进去成本很高。我的做法是保留系统提示、原始问题、以及最近几轮的工具交互中间过时的工具结果做摘要压缩。第三是显式终止指令在系统提示里明确告诉模型“当你已经能回答用户问题时直接输出答案不要再调用工具”。状态管理还有个细节工具执行的结果可能很长比如返回一个几百行的表格。直接塞进上下文会挤占空间我一般会做截断加摘要只保留前若干行和关键统计信息并告诉模型“完整数据已省略”。这个处理在面试时是加分项因为它体现了你对上下文窗口的实际管理意识。4.3 三种方案的横向对比与选型建议把三种方案放一起对比选型逻辑就清晰了。我整理了一张表面试时如果被问到“你怎么选”直接按这个思路答维度方案一 文本解析方案二 原生调用方案三 自主循环实现复杂度低中高解析稳定性差依赖模型遵循高结构化返回高同方案二模型兼容性最好任何模型需模型支持需模型支持多步任务能力弱中强Token 开销中中高调试难度低逻辑透明中高链路长适用场景原型、弱模型生产环境单轮调用复杂任务编排我的选型建议是如果只是做单轮工具调用方案二足够稳定又省心如果模型不支持原生调用或者要做多厂商统一抽象方案一兜底如果是需要多步推理的复杂任务比如数据分析、多源信息整合那必须上方案三但要接受更高的成本和更复杂的调试。5. 面试加分项那些容易被追问的细节5.1 工具描述怎么写才能让模型选对面试官很可能会追问“你怎么保证模型选对工具”这其实是个提示词工程问题。我的经验是工具描述要满足三个原则。第一描述要写“什么时候用”而不只是“是什么”。比如不要只写“查询天气”而要写“当用户询问某地某时的天气状况、温度、是否下雨时使用”。第二工具之间要有清晰的边界。如果两个工具功能重叠模型就会犹豫要么合并要么在描述里明确区分。第三参数描述要给出示例值比如date参数写“如 2024-01-01 或 明天”模型抽取时准确率会明显提升。我还做过一个对比实验同一组工具描述写得含糊时模型选错工具的概率大概 25%把描述改成“场景示例”的写法后错误率降到 8% 以下。这个数据在面试时抛出来比空谈“要写好描述”有说服力得多。5.2 工具执行失败后的兜底策略工具不可能永远成功接口超时、参数非法、权限不足都会发生。关键是失败之后怎么办。我的处理分三层第一层是重试对于网络抖动这类瞬时错误自动重试一到两次用指数退避。第二层是降级如果某个工具挂了看有没有替代工具比如主搜索接口失败就切备用接口。第三层是告知模型把失败原因作为工具结果回填让模型决定是换个方式问、还是直接告诉用户暂时查不到。这里有个反直觉的点不要把错误信息藏起来。有些开发者怕模型“看到错误会慌”就把失败包装成空结果。结果模型以为工具返回了空数据基于错误前提继续推理最后给出离谱答案。正确的做法是如实回填“工具执行失败超时”模型反而能做出合理应对比如提示用户稍后重试。5.3 安全边界工具权限与输入校验这是最容易被忽视但面试官很看重的一点。Tool Use 意味着模型能触发真实操作如果工具是“删除文件”“发送邮件”“执行 SQL”那风险就大了。我的原则是最小权限 强制校验。每个工具只开放它必需的能力比如查询工具就用只读账号绝不共用管理员权限。所有参数在执行前都要校验尤其是涉及路径、SQL、命令拼接的必须做白名单或转义防止模型被诱导生成危险输入。注意永远不要相信模型输出的参数是安全的。哪怕模型看起来很“聪明”它也可能因为提示词注入而生成恶意参数。校验层是最后一道防线不能省。6. 我实际踩过的坑与排查经验6.1 常见问题速查表下面这张表是我在实际项目和面试准备中整理的高频问题基本覆盖了手写 Tool Use 时 80% 的故障现象可能原因排查方向解决手段模型不调用工具直接回答工具描述不清或提示词没强调检查系统提示是否说明可用工具强化描述加 few-shot 示例解析报 JSON 错误模型输出格式不规范打印原始输出看格式加容错解析和重试工具名对不上模型幻觉或大小写不一致对比返回名和注册名执行前做名称映射校验循环不终止缺最大轮数或终止指令看是否反复调同一工具加轮数上限和重复检测结果回填后模型答非所问工具结果太长或格式乱检查回填内容截断摘要统一格式并行调用结果错位tool_call_id 没对应检查 id 匹配逻辑严格按 id 回填6.2 调试 Tool Use 的实用技巧调试这类系统最有效的手段是把完整的消息列表打出来。很多人只打印模型最终输出结果出了问题根本不知道中间发生了什么。我习惯在每一轮循环后把 messages 完整 dump 成 JSON 存文件出问题时回放一遍一眼就能看出是哪一步的工具结果导致了模型跑偏。另一个技巧是用 mock 工具做隔离测试。真实工具涉及网络和外部依赖调试时不稳定。我会先写一批返回固定值的假工具把整个编排逻辑跑通确认循环、解析、回填都没问题再换成真实工具。这样能把“编排 bug”和“工具 bug”分开排查效率高很多。还有个经验给每次请求打上 trace id。多轮循环里一次用户请求可能触发五六次模型调用日志混在一起很难看。带上 trace id 后一次请求的所有调用能串起来定位问题快得多。这个习惯在生产环境尤其重要。6.3 面试时怎么讲才能脱颖而出回到面试场景。手写 Tool Use 这道题面试官真正想看的不是你能背出 API而是你的工程判断力。我的建议是先花一分钟把三种方案的取舍讲清楚说明你为什么在某个场景选某个方案然后再动手写。写的时候不要只写 happy path主动把错误处理、超时、循环上限这些边界补上边写边解释“这里为什么要这样”。如果时间允许可以主动提一句“如果工具数量继续增长我会引入工具检索层”或者“如果要做多租户工具权限要按用户隔离”。这些延展能体现你有生产环境的思维而不是只会写 demo。我那次面试最后就是靠主动聊工具检索和权限隔离把话题引到了系统设计层面面试官的反馈明显更积极了。说到底Tool Use 这个能力写出来是一回事写好是另一回事。三种方案没有绝对优劣关键是你能不能根据模型能力、任务复杂度、成本预算做出合理选择并且把每个方案的坑都提前想到。这套思路不光面试有用真到了做智能体产品的时候也是实打实的基本功。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑