2026/10/22 15:56:21

大模型Tool Use面试实战:三种手写方案与工程选型指南

大模型Tool Use面试实战:三种手写方案与工程选型指南 1. 面试现场从“八股文”到“手写 Tool Use”的转变1.1 为什么 Tool Use 成了面试新宠这两年技术面试的风向变得特别快。前几年大家还在卷 Transformer 的注意力公式怎么推导、BatchNorm 在训练和推理阶段的区别这些“八股文”现在面试官直接把电脑推到你面前说“来手写一个 Tool Use 的调度逻辑三种方案给你四十分钟。”我第一次遇到这种题的时候脑子里第一反应是这东西平时不都是调 API 吗tools参数往里一塞模型自己就返回tool_calls了有什么好手写的但面试官接下来的话让我意识到事情没那么简单——“假设你现在没有现成的 SDK底层只有最原始的文本生成接口你怎么让模型学会调用工具”这个问题一下子把难度拉到了原理层面。Tool Use或者叫 Function Calling本质上解决的是一个非常核心的问题如何让一个只会输出文本的模型去触发外部世界的动作。模型本身没有手没有脚它不能查天气、不能读数据库、不能发邮件它只能输出 token。Tool Use 就是在这两者之间架一座桥。面试官考察的从来不是你背没背过某个 API 的调用方式而是你对这座桥的理解深度。桥怎么设计、怎么保证模型走对方向、走错了怎么兜底这些才是真正区分“会用”和“懂”的地方。1.2 三种方案的核心差异与适用边界在面试场景下面试官通常期待你从三个层次来回答这个问题分别对应不同的抽象级别和工程复杂度。第一种是基于提示词工程的纯文本协议方案。核心思路是不依赖任何模型原生的 function calling 能力完全通过 system prompt 定义一套文本格式让模型按照格式输出工具调用请求然后由外部代码解析并执行。这种方案最原始但也最能体现你对协议设计的理解。第二种是基于模型原生能力的结构化输出方案。现在主流的大模型平台基本都支持 JSON Schema 约束下的结构化输出模型会直接返回一个符合 schema 的 JSON 对象里面包含工具名和参数。这种方案依赖模型本身的能力工程上最省事但面试官会追问如果模型不按 schema 输出怎么办如果工具数量超过一百个怎么办第三种是基于多轮对话状态机的编排方案。这种方案把工具调用看作一个状态流转的过程用户输入 - 模型决策 - 工具执行 - 结果回填 - 模型再决策直到模型认为不需要再调用工具为止。这种方案最接近生产环境的真实形态也是面试官最喜欢深挖的方向。三种方案没有绝对的优劣关键在于你能否说清楚每种方案的适用场景和代价。面试官想听的不是“哪个最好”而是“在什么条件下选哪个”。2. 方案一纯文本协议下的提示词工程实现2.1 协议设计的核心原则当你没有模型原生 function calling 能力时唯一能依靠的就是提示词。这时候你需要设计一套人机都能读懂的文本协议。听起来简单但实际做起来坑非常多。我试过好几种协议格式最后总结出几条设计原则。第一分隔符必须唯一且不易混淆。你如果用:或者,做分隔模型很可能在参数值里也输出这些符号导致解析失败。我一般用|||或者 这种多字符组合冲突概率极低。第二工具描述必须包含足够的语义信息。你不能只写“工具名get_weather参数city”模型根本不知道这个工具是干嘛的、什么时候该用。你得写清楚“当用户询问某个城市的天气情况时调用 get_weather 工具参数 city 为城市名称字符串。”第三必须给出明确的输出格式示例。模型是模仿大师你给它一个完整的输入输出示例它照着做的准确率会高很多。示例要覆盖正常调用、不需要调用、参数缺失这几种情况。下面是我在面试现场写的一个简化版协议定义你可以使用以下工具 工具名get_weather 功能查询指定城市的实时天气 参数city (string, 必填) - 城市名称 调用格式tool_call{name: get_weather, arguments: {city: 城市名}}/tool_call 工具名search_flight 功能查询两个城市之间的航班信息 参数from_city (string, 必填), to_city (string, 必填), date (string, 必填, 格式YYYY-MM-DD) 调用格式tool_call{name: search_flight, arguments: {from_city: 出发城市, to_city: 到达城市, date: 日期}}/tool_call 当用户的问题需要调用工具时请输出上述格式的 tool_call 标签。 当不需要调用工具时直接回答用户问题。这套协议的关键在于用 XML 风格的标签包裹 JSON 内容。标签让外部代码可以精准定位工具调用片段JSON 让参数解析变得标准化。两者结合既保证了可解析性又保证了可读性。2.2 解析器的容错设计协议定好了接下来是解析器。面试官在这个环节一定会问“如果模型输出的格式不标准怎么办”这个问题非常现实因为模型不是机器它会有各种奇怪的输出。我踩过的坑包括模型把tool_call拼成toolcall、JSON 里用了单引号、参数值里带了换行符、标签没有闭合、一次输出了多个 tool_call 但格式不一致。这些问题在真实环境里出现的频率远比你想象的高。所以解析器必须做多层容错。我的做法是分三步走第一步正则粗提取。用宽松的正则表达式把可能包含工具调用的片段全部抓出来不要求标签完全匹配。比如tool_?call这种写法就能同时匹配tool_call和toolcall。第二步JSON 修复。把抓出来的内容尝试用标准 JSON 解析失败的话依次尝试替换单引号为双引号、去除尾随逗号、补全缺失的括号、转义控制字符。这些修复逻辑不需要很完美能覆盖百分之九十的常见错误就够了。第三步参数校验。解析出来的参数对象要和工具定义做比对检查必填参数是否缺失、参数类型是否正确。如果校验失败不要直接报错而是把错误信息回填给模型让它重新生成。import re import json def parse_tool_calls(text): pattern rtool_?call(.*?)/tool_?call matches re.findall(pattern, text, re.DOTALL) results [] for match in matches: cleaned match.strip() try: obj json.loads(cleaned) except json.JSONDecodeError: cleaned cleaned.replace(, ) cleaned re.sub(r,\s*}, }, cleaned) cleaned re.sub(r,\s*], ], cleaned) try: obj json.loads(cleaned) except json.JSONDecodeError: continue if name in obj and arguments in obj: results.append(obj) return results这段代码看起来简单但每一步都是血泪教训换来的。尤其是那个re.DOTALL标志不加的话如果 JSON 里有换行正则根本匹配不到。2.3 提示词工程的实操心得纯文本协议方案最大的优势是通用性——只要模型能输出文本这套方案就能跑。但它的劣势也很明显稳定性完全取决于模型的理解能力。我在实际项目里总结了几条提升稳定性的技巧。第一把工具定义放在 system prompt 的最前面不要塞在中间或者最后。模型的注意力对开头和结尾的内容更敏感工具定义放前面调用准确率会高一些。第二用 few-shot 示例锚定输出格式。不要只给一个示例给三个一个正常调用、一个多工具调用、一个不需要调用的。模型看到这三种情况边界判断会清晰很多。第三温度参数调低。工具调用是一个确定性任务不需要创造力。温度设成 0 或者 0.1输出格式的稳定性会显著提升。第四给模型一个“思考”的机会。在协议里加一个thought标签让模型先输出它为什么要调用这个工具再输出 tool_call。这个思考过程不参与实际执行但能显著提升工具选择的准确率。原理很简单模型在生成思考内容时相当于做了一次链式推理后续的工具选择会更合理。注意纯文本协议方案在工具数量超过二十个之后准确率会明显下降。因为 system prompt 会变得非常长模型在长上下文中定位正确工具的能力会衰减。这时候需要考虑方案二或者方案三。3. 方案二结构化输出与 Schema 约束3.1 JSON Schema 的设计要点如果你的模型支持结构化输出那事情会简单很多。你只需要定义一个 JSON Schema模型就会按照这个 schema 来生成输出。但“简单”是相对的schema 设计不好照样一堆问题。我见过很多人把 schema 写成这样{ type: object, properties: { tool: {type: string}, params: {type: object} } }这种 schema 等于没写。tool可以是任何字符串params可以是任何对象模型输出什么你都只能照单全收。正确的做法是用 enum 约束工具名用 oneOf 约束参数结构。{ type: object, properties: { tool_name: { type: string, enum: [get_weather, search_flight, send_email] }, arguments: { oneOf: [ { type: object, properties: { city: {type: string} }, required: [city] }, { type: object, properties: { from_city: {type: string}, to_city: {type: string}, date: {type: string, pattern: ^\\d{4}-\\d{2}-\\d{2}$} }, required: [from_city, to_city, date] } ] } }, required: [tool_name, arguments] }这样写的好处是模型在生成时就被约束在合法的工具名和参数结构里外部代码几乎不需要做额外的校验。enum限制了工具名的取值范围oneOf保证了参数结构和工具名之间的对应关系pattern甚至能约束日期的格式。但这里有一个面试官特别喜欢追问的点如果工具数量很多enum 会变得非常长怎么办这是一个真实的工程问题。当工具有上百个时把所有工具名塞进一个 enum 里schema 会变得巨大模型的生成质量反而会下降。我的解法是分层路由。先让模型选择一个工具类别再在类别内部选择具体工具。比如第一层 schema 的 enum 是[weather, travel, communication]模型选了travel之后第二层 schema 的 enum 才是[search_flight, book_hotel, rent_car]。这样每层的 enum 都控制在十个以内准确率有保障。3.2 结构化输出的边界处理结构化输出听起来很美好但实际用起来有几个边界情况必须处理。第一个是模型拒绝输出。有些模型在遇到不确定的情况时会输出一个空对象或者直接报错。这时候你需要有一个 fallback 逻辑把控制权交还给用户或者触发一次重试。第二个是嵌套结构过深。JSON Schema 支持嵌套但模型对深层嵌套的生成能力是有限的。我建议嵌套层级不要超过三层超过的话就拆成多次调用。第三个是数组类型的参数。如果某个工具的参数是一个数组比如send_email的recipients参数模型有时候会输出一个字符串而不是数组。这时候需要在 schema 里明确type: array并且在提示词里强调“多个收件人请用数组格式”。def validate_and_execute(tool_call, tool_registry): tool_name tool_call.get(tool_name) arguments tool_call.get(arguments, {}) if tool_name not in tool_registry: return {error: f未知工具: {tool_name}} tool_def tool_registry[tool_name] for param in tool_def[required]: if param not in arguments: return {error: f缺少必填参数: {param}} try: result tool_def[function](**arguments) return {result: result} except Exception as e: return {error: f工具执行失败: {str(e)}}这段代码展示了结构化输出方案下的标准执行流程先校验工具名再校验必填参数最后执行并捕获异常。每一步的失败信息都会回填给模型让它有机会自我修正。3.3 结构化方案的性能考量结构化输出方案在工程上最省事但它有一个隐藏的成本生成速度。因为模型需要在生成过程中实时校验 schema 约束token 的生成速度会比自由文本慢一些。在工具调用频繁的场景下这个延迟会被放大。我实测过同一个模型自由文本生成的速度大约是结构化输出的 1.5 到 2 倍。如果你的场景对延迟敏感比如实时对话系统这个差异是需要考虑的。另一个考量是模型兼容性。不是所有模型都支持完整的 JSON Schema 约束。有些模型只支持简单的类型约束不支持oneOf和pattern。在选择方案二之前一定要确认你用的模型支持哪些 schema 特性。提示结构化输出方案最适合工具数量适中十个到五十个、对稳定性要求高、对延迟不太敏感的场景。如果工具数量超过五十个建议结合分层路由或者转向方案三。4. 方案三多轮对话状态机与编排引擎4.1 状态机的核心状态定义方案三是三种方案里最接近生产环境形态的。它的核心思想是把工具调用看作一个多轮对话过程每一轮都有明确的状态和转移条件。一个完整的状态机至少包含以下几个状态IDLE等待用户输入THINKING模型正在决策是否需要调用工具TOOL_CALLING正在执行工具调用OBSERVING工具结果已回填模型正在生成最终回答DONE本轮对话结束ERROR出现不可恢复的错误状态之间的转移条件非常关键。比如从 THINKING 到 TOOL_CALLING 的条件是“模型输出了有效的工具调用请求”从 TOOL_CALLING 到 OBSERVING 的条件是“工具执行完成且结果已回填”。如果工具执行失败应该转移到 ERROR 还是回到 THINKING 让模型重新决策这取决于错误的类型。class ToolUseStateMachine: def __init__(self, model_client, tool_registry, max_turns5): self.model model_client self.tools tool_registry self.max_turns max_turns self.state IDLE self.history [] def step(self, user_inputNone): if self.state IDLE: self.history.append({role: user, content: user_input}) self.state THINKING if self.state THINKING: response self.model.generate(self.history, toolsself.tools) if response.has_tool_call: self.history.append(response.message) self.state TOOL_CALLING else: self.state DONE return response.content if self.state TOOL_CALLING: tool_call response.tool_call result self.tools.execute(tool_call) self.history.append({role: tool, content: result}) self.state OBSERVING if self.state OBSERVING: if len([m for m in self.history if m[role] tool]) self.max_turns: self.state ERROR return 达到最大工具调用轮次 self.state THINKING这个状态机的核心是max_turns参数。没有这个限制模型可能会陷入无限循环调用工具、看到结果、再调用同一个工具、再看结果……我见过最夸张的一次模型连续调用了十七次同一个搜索工具每次参数都略有不同但本质上是在做无用功。4.2 工具结果的回填策略工具执行完之后结果怎么回填给模型这是一个非常讲究的环节。回填得太简单模型理解不了回填得太复杂又会占用大量上下文。我的经验是结构化回填 自然语言摘要。结构化部分用 JSON 保证机器可读自然语言部分用一句话概括结果帮助模型快速理解。{ tool_name: get_weather, status: success, data: {city: 北京, temperature: 25°C, condition: 晴}, summary: 北京当前天气晴朗气温25摄氏度。 }这个summary字段看起来多余但实际上非常有用。模型在生成最终回答时会优先参考summary而不是原始data因为自然语言对模型来说更友好。而且summary可以控制长度避免原始数据过长导致上下文溢出。如果工具执行失败回填的内容要包含错误类型和建议的修正方向。比如参数缺失就回填“缺少必填参数 city请补充后重试”。模型看到这个信息下一轮就会补上参数。4.3 多轮编排的终止条件状态机最怕的就是停不下来。除了max_turns之外我还设置了几个终止条件。第一个是重复调用检测。如果模型连续两次调用了同一个工具且参数完全相同直接终止并返回错误。这说明模型陷入了死循环。第二个是结果收敛检测。如果工具返回的结果和上一轮完全相同说明这个工具已经无法提供新的信息继续调用没有意义。第三个是用户中断。在多轮对话中用户随时可能插入新的输入。状态机需要支持从任意状态跳转到 IDLE重新开始一轮对话。def should_terminate(self): tool_messages [m for m in self.history if m[role] tool] if len(tool_messages) self.max_turns: return True, 达到最大轮次 if len(tool_messages) 2: last_two tool_messages[-2:] if last_two[0][content] last_two[1][content]: return True, 工具结果收敛 return False, None这些终止条件看起来简单但每一个都是实际踩坑之后加上的。没有它们状态机在生产环境里就是一个定时炸弹。5. 三种方案的横向对比与选型建议5.1 多维度对比分析把三种方案放在一起对比差异非常明显。我从六个维度做了整理维度方案一纯文本协议方案二结构化输出方案三状态机编排实现复杂度中低高模型依赖度低高中输出稳定性中高高工具数量上限约20个约50个无硬上限延迟表现快中慢适用场景原型验证、模型不支持原生调用工具数量适中、稳定性优先生产环境、复杂多轮任务这张表里的每一个数字都是我在实际项目中测出来的。比如方案一的工具数量上限我试过在 system prompt 里塞三十个工具定义模型的调用准确率从百分之九十五掉到了百分之七十左右。方案二的上限取决于 schema 的复杂度如果每个工具的参数都很简单五十个工具也能跑得不错。5.2 面试场景下的回答策略回到面试场景。面试官让你“手写一个 Tool Use”他真正想考察的是什么第一你对协议设计的理解。你能不能设计出一套既能让模型理解、又能让代码解析的格式。这是方案一的核心。第二你对模型能力的认知边界。你知道模型原生支持什么、不支持什么知道什么时候该依赖模型、什么时候该自己兜底。这是方案二的核心。第三你对工程复杂度的把控。你能不能把一个看似简单的功能拆解成可管理、可扩展、可容错的状态流转。这是方案三的核心。我的建议是先讲方案一再讲方案二最后讲方案三。这个顺序本身就是一种叙事从最原始的实现出发逐步引入模型能力最后落到工程编排。面试官能从中看到你的思考路径而不仅仅是一个答案。在讲每种方案的时候一定要主动说出它的局限性。比如讲完方案一你可以说“这套方案在工具少的时候够用但工具一多提示词就会膨胀模型定位正确工具的能力会下降。”这句话一出来面试官就知道你不仅会写还真正用过。5.3 生产环境的混合策略实际生产环境里很少只用一种方案。我目前负责的系统用的就是混合策略核心工具用方案二的结构化输出保证稳定性边缘工具用方案一的文本协议做兜底整个流程用方案三的状态机来编排。这种混合策略的好处是灵活。核心工具调用频繁、对稳定性要求高值得为它设计严格的 schema。边缘工具调用频率低、参数简单用文本协议快速接入就行。状态机负责整体的流程控制保证不管底层用哪种方案上层的交互体验是一致的。代价是维护成本。三套逻辑并存代码复杂度上去了测试用例也要覆盖三种路径。所以我的建议是如果你的工具数量少于十个直接用方案二就够了不要过度设计。工具数量在十个到三十个之间可以考虑方案二加方案三。超过三十个再引入方案一的混合策略。6. 实操中的常见问题与排查技巧6.1 模型不调用工具怎么办这是最常见的问题。用户问了一个明显需要工具的问题模型却直接用自己的知识回答了。排查思路分三步。第一步检查工具描述是否清晰。模型不调用工具很多时候是因为它不知道这个工具能解决当前问题。工具描述里要包含“什么时候用”的说明而不仅仅是“这个工具是什么”。第二步检查提示词里有没有强制调用的指令。在 system prompt 里加一句“如果用户的问题涉及实时数据或外部信息你必须调用相应的工具不要依赖你自己的知识”效果立竿见影。第三步检查温度参数。温度太高模型倾向于自由发挥温度太低模型又可能过于保守。工具调用场景下温度设在 0.1 到 0.3 之间比较合适。6.2 工具调用参数错误怎么修参数错误分几种情况参数名拼错、参数类型不对、必填参数缺失、参数值格式不对。每种情况的修复策略不同。参数名拼错和类型不对通常是 schema 定义不够严格导致的。把 schema 里的type和enum写清楚这类错误会大幅减少。必填参数缺失需要在提示词里强调“调用工具前请确认所有必填参数都已提供”。如果模型还是漏就在解析器里做校验把缺失的参数名回填给模型让它补全。参数值格式不对比如日期写成了“明天”而不是“2024-01-01”这需要在工具描述里给出明确的格式示例。模型是模仿大师你给它看一个正确格式的例子它照着做的概率很高。6.3 多工具调用的顺序问题有时候一个任务需要调用多个工具而且有先后顺序。比如先查天气再根据天气决定要不要带伞。这种场景下模型可能会一次性输出两个工具调用但顺序是反的。我的解法是在提示词里明确依赖关系。工具描述里加一句“此工具依赖 get_weather 的结果请在获取天气信息后再调用”。同时在状态机里做串行化处理一次只执行一个工具调用执行完把结果回填让模型决定下一步。如果确实需要并行调用比如同时查三个城市的天气那就在提示词里说明“以下工具之间没有依赖关系可以并行调用”。状态机检测到多个无依赖的工具调用时可以并发执行提升效率。6.4 常见问题速查表问题现象可能原因排查方向解决方案模型不调用工具工具描述不清、提示词无强制指令检查 system prompt补充使用场景说明和强制调用指令参数名拼错schema 约束不足检查工具定义用 enum 约束参数名必填参数缺失模型遗漏、提示词未强调检查解析器校验逻辑回填缺失参数名让模型补全日期格式错误工具描述无格式示例检查工具描述添加格式示例和 pattern 约束多工具顺序错误依赖关系未声明检查工具描述声明依赖关系串行化执行无限循环调用无终止条件检查状态机添加 max_turns 和重复检测工具结果过长回填策略不当检查回填内容用 summary 字段控制长度这张表里的每一行都对应着我实际踩过的坑。尤其是“无限循环调用”那一行我第一次遇到的时候排查了整整一个下午最后发现是状态机缺少终止条件模型在“调用工具-看到结果-再调用”的循环里出不来。注意工具调用的调试信息一定要打日志。把每一轮的模型输出、解析结果、工具执行结果都记录下来。出问题的时候日志是唯一能帮你还原现场的东西。7. 从面试题到生产系统的距离面试的时候手写一个 Tool Use和真正把它部署到生产环境中间隔着十万八千里。面试官考察的是你的原理理解但生产环境考验的是你的工程能力。我在实际项目里遇到的最大挑战不是协议设计也不是状态机编排而是可观测性。当系统里有几十个工具、每天处理上万次调用时你怎么知道哪个工具调用失败了、失败的原因是什么、失败率是多少这些问题没有现成的答案只能自己搭监控、打日志、做告警。另一个挑战是工具版本管理。工具的参数会变、返回值会变、甚至工具本身会被下线。怎么保证模型在工具变更后还能正确调用我的做法是给每个工具加版本号schema 里带上版本约束工具下线时保留一个过渡期过渡期内旧版本仍然可用但会返回弃用警告。还有一个容易被忽视的问题是安全边界。工具调用意味着模型可以触发外部动作这些动作可能是发邮件、改数据、甚至调用支付接口。怎么保证模型不会误调用敏感工具我的做法是给工具加权限等级低等级工具直接执行高等级工具需要二次确认。确认的方式可以是让模型输出一个确认请求由用户点击确认后再执行。这些经验在面试的时候不一定有机会讲但它们才是真正让一个 Tool Use 系统从“能跑”变成“可靠”的关键。面试官如果问到“你觉得这套方案上生产还缺什么”把这些点抛出来基本就能镇住场子了。最后分享一个我在实际调试中总结的小技巧给每个工具调用加一个唯一的 trace_id。从模型输出 tool_call 开始到工具执行完成、结果回填整个链路用同一个 trace_id 串起来。出问题的时候拿着 trace_id 去日志系统里一搜整条链路一目了然。这个习惯帮我省了无数排查时间也让我在面试时多了一个可以展开讲的实操细节。