
1. 面试现场从“八股文”到“手写 Tool Use”的转变面试进行到四十分钟的时候面试官合上了我的简历说了一句让我心里一紧的话“简历上写你做过智能体应用那咱们别背八股了手写一个 Tool Use 吧。”所谓 Tool Use说白了就是让大模型在对话过程中调用外部函数或接口的能力。你问它“今天天气怎么样”它自己不知道但它可以决定去调用一个查天气的函数把结果拿回来再组织成自然语言回答你。这个能力是现在所有智能体产品的核心底座也是面试里区分“只会调 API”和“真正理解智能体运行机制”的分水岭。我当时脑子里快速过了三种方案第一种是基于提示词工程让模型输出结构化 JSON 再手动解析第二种是利用模型厂商原生提供的函数调用能力第三种是自己写一个轻量级的调度器配合正则和状态机做兜底。这三种方案各有各的适用场景也各有各的坑。面试官让我选一种现场写我说我三种都写给你看然后从最简单的开始。这篇文章就是把我当时手写的思路、代码和踩过的坑完整还原出来。不管你是正在准备面试还是实际工作中要落地一个智能体功能这三种方案的取舍逻辑和实现细节都能直接拿去用。我会从最原始的提示词方案讲起一路讲到调度器的设计中间穿插参数计算、异常处理和实测数据。2. 方案一纯提示词驱动的结构化输出2.1 核心思路与适用场景这是最原始也最通用的方案。核心逻辑是在系统提示词里告诉模型“你有以下工具可用”然后要求模型在需要调用工具时输出一段特定格式的文本比如 JSON。前端拿到这段文本后解析执行对应的函数再把结果拼回对话历史里发起下一轮请求。这个方案最大的好处是不依赖任何模型厂商的特殊能力。只要模型能对话就能用。我试过在一些开源模型上跑效果虽然不如原生函数调用稳定但通过精心设计提示词也能达到可用的程度。适合的场景是模型不支持原生函数调用、或者你需要跨多个模型厂商做统一抽象层。但它的缺点也很明显。模型有时候会在 JSON 外面包一层解释性文字有时候会漏掉字段有时候会把参数类型搞错。你需要写大量的解析和容错代码。我当时在面试白板上写了一个简化版面试官看完说“你这解析逻辑比工具本身还复杂”确实是这样。2.2 提示词模板设计与参数约束提示词的设计是这个方案成败的关键。我一般会把工具定义写成类似下面这样的结构放在系统提示词的最前面你可以使用以下工具来帮助回答问题 工具名称get_weather 功能描述查询指定城市的实时天气 参数 - city (string, 必填)城市名称如北京 - unit (string, 可选)温度单位可选值为celsius或fahrenheit默认为celsius 当你需要调用工具时请严格按照以下格式输出不要添加任何其他文字 tool_call {name: 工具名称, arguments: {参数名: 参数值}} /tool_call 当你不需要调用工具时直接正常回答即可。这里有几个细节值得展开。第一我用tool_call标签把 JSON 包起来而不是让模型直接输出裸 JSON。原因是模型在自由生成时很容易在 JSON 前后加“好的我来帮你查询”之类的话用标签包裹后我可以用正则精准提取容错率大幅提升。第二参数描述里我明确写了类型和是否必填这能显著降低模型传错参数的概率。第三我加了一句“不要添加任何其他文字”虽然模型不一定完全遵守但能起到引导作用。实测下来在提示词里把工具描述写得越像一份 API 文档模型的调用准确率越高。我做过一个对比模糊描述“查天气的工具”和详细描述“查询指定城市的实时天气参数包括城市名和温度单位”后者的首次调用成功率从 62% 提升到了 89%。这个数据是我在一个内部测试集上跑的样本量不大但趋势很明显。2.3 解析与容错手写解析器的关键细节解析部分我写了一个函数核心逻辑是先用正则找到tool_call标签之间的内容然后尝试 JSON 解析。如果解析失败再尝试一些修复策略比如把单引号替换成双引号、补全缺失的括号等。import re import json def parse_tool_call(text): pattern rtool_call\s*(.*?)\s*/tool_call match re.search(pattern, text, re.DOTALL) if not match: return None raw match.group(1) try: return json.loads(raw) except json.JSONDecodeError: # 尝试修复常见问题 fixed raw.replace(, ) fixed re.sub(r(\w):, r\1:, fixed) try: return json.loads(fixed) except json.JSONDecodeError: return None这段代码看起来简单但实际跑起来你会发现各种奇葩情况。比如模型输出{name: get_weather, arguments: {city: 北京,}}注意最后那个多余的逗号标准 JSON 解析器会直接报错。我后来加了一个去尾逗号的步骤成功率又提升了一截。还有模型会把参数值写成undefined或者null字符串这些都需要在解析后做二次校验。注意纯提示词方案下永远不要假设模型输出的 JSON 是合法的。你的解析器必须能处理至少五种常见的格式错误否则线上环境分分钟教你做人。2.4 多轮对话中的状态管理这个方案还有一个容易被忽略的问题多轮对话中的工具调用状态怎么管理。比如用户先问“北京天气怎么样”模型调用工具拿到结果后回答了。用户接着问“那上海呢”这时候模型需要知道上一轮调用了什么工具、返回了什么结果才能决定这一轮是否再次调用。我的做法是在对话历史里保留完整的工具调用记录包括请求和响应。具体来说每一轮对话的消息列表里除了 user 和 assistant 的角色我还加了一个 tool 角色专门存放工具返回的结果。这样模型在生成下一轮回复时能看到完整的上下文包括之前调用了哪些工具、拿到了什么数据。这个设计看起来理所当然但我在实际项目中见过有人把工具结果直接拼在 assistant 的消息里导致模型分不清哪些是它自己说的话、哪些是工具返回的数据在多轮场景下经常出现“自己跟自己对话”的诡异情况。所以角色分离这件事从一开始就要做对。3. 方案二原生函数调用能力的实战解析3.1 原生能力的优势与限制第二种方案是利用模型厂商原生提供的函数调用能力。现在主流的大模型 API 基本都支持这个功能你只需要在请求里传入一个 tools 数组模型就会在需要时返回一个结构化的 tool_calls 对象里面包含函数名和参数。你执行完函数后把结果按指定格式传回去模型继续生成。这个方案最大的优势是稳定。模型在训练阶段就专门针对函数调用做了优化输出的 JSON 结构几乎不会出错参数类型也基本正确。我在实际项目里对比过同样的工具定义原生函数调用的首次成功率能到 95% 以上而纯提示词方案只有 85% 左右。而且原生方案不需要你写复杂的解析逻辑SDK 直接帮你处理好了。但它也有明显的限制。第一你被绑定在了特定厂商的生态里换模型就要重写调用逻辑。第二原生函数调用通常有额外的计费虽然单次不贵但量大之后成本差异就出来了。第三有些厂商对 tools 的数量和复杂度有限制比如最多支持 20 个工具参数嵌套层级不能超过三层。这些限制在简单场景下无所谓但在复杂的智能体系统里就会成为瓶颈。3.2 工具定义的标准化写法原生函数调用的工具定义有一套标准格式我一般会写一个辅助函数来生成def build_tool_schema(name, description, parameters): return { type: function, function: { name: name, description: description, parameters: { type: object, properties: parameters, required: [k for k, v in parameters.items() if v.get(required)] } } }这里有个细节required 字段的生成逻辑。很多人在定义参数时会在每个参数里写required: True但原生 API 要求的是在 parameters 层级用一个数组来声明哪些参数必填。如果你搞错了模型可能会把必填参数当成可选的导致调用时缺参数。我踩过这个坑调试了半天才发现是 schema 格式不对。另外工具描述的质量直接影响模型的调用决策。我一般会遵循三个原则描述里包含使用场景、参数说明里包含示例值、避免使用模糊词汇。比如“查询天气”就不如“查询指定城市的实时天气状况返回温度和天气描述”来得清晰。实测下来描述写得越具体模型误调用和漏调用的概率越低。3.3 并行调用与结果回传的处理原生函数调用还有一个强大的特性并行调用。模型可以在一次响应里返回多个 tool_calls比如用户问“北京和上海天气怎么样”模型会同时请求两个 get_weather 调用。这时候你需要并发执行这些函数然后把所有结果一起回传。import asyncio async def execute_tool_calls(tool_calls): tasks [] for call in tool_calls: func_name call.function.name args json.loads(call.function.arguments) tasks.append(execute_single_tool(func_name, args)) results await asyncio.gather(*tasks) return results并行调用能显著降低延迟。我实测过一个场景串行执行三个工具调用平均耗时 2.4 秒并行执行只要 0.9 秒。但要注意不是所有工具都适合并行。如果工具之间有依赖关系比如第二个工具的输入依赖第一个工具的输出那就必须串行。我在调度器里加了一个依赖声明字段模型在调用时会参考这个字段决定是否并行。结果回传的格式也有讲究。每个工具结果需要对应一个 tool_call_id这样模型才能知道哪个结果对应哪个调用。如果 ID 对不上模型会陷入混乱可能重复调用或者忽略结果。这个细节在文档里往往一笔带过但实际开发中一旦搞错排查起来非常痛苦。3.4 成本与延迟的实测对比我做过一组对比测试在同样的任务集上分别用纯提示词方案和原生函数调用方案跑记录成功率和延迟指标纯提示词方案原生函数调用首次调用成功率85%96%平均延迟单工具1.2s0.8s平均延迟三工具并行3.1s1.1s每千次调用额外成本0约 0.5 元跨模型兼容性高低这个数据是基于我自己的测试环境不同模型和任务会有差异但趋势是一致的。原生方案在成功率和延迟上优势明显代价是成本和兼容性。如果你的项目对稳定性要求高、预算充足原生方案是首选。如果是要做多模型适配或者成本敏感纯提示词方案更合适。4. 方案三自建轻量级调度器与兜底策略4.1 为什么还需要调度器前两种方案解决的是“模型怎么表达要调用工具”的问题但实际生产环境里还有一类问题它们解决不了模型该调用工具却没调用、不该调用却调用了、调用了错误的工具、参数传错了。这些问题在演示环境里出现频率低但在真实用户输入千奇百怪的情况下发生率会大幅上升。我自建调度器的核心目的就是做一层兜底和校验。调度器不负责让模型生成工具调用而是负责在模型输出之后、实际执行之前做一轮检查和修正。它有点像机场的安检模型是乘客工具是飞机调度器确保只有合规的乘客才能登机。这个方案在面试里是加分项因为它展示了你对生产环境的理解。面试官后来跟我说前两种方案很多人都会但能想到做调度器兜底的人不多这说明你真正在线上跑过智能体应用。4.2 意图识别与工具路由的实现调度器的第一层是意图识别。我会维护一个关键词到工具的映射表当模型没有返回工具调用但用户输入里包含明显的关键词时调度器会主动触发对应的工具。比如用户说“帮我查一下明天北京的天气”如果模型没调用 get_weather调度器检测到“天气”和“北京”这两个关键词就会强制发起一次调用。INTENT_MAP { get_weather: [天气, 气温, 下雨, 温度], search_flight: [航班, 机票, 起飞], book_hotel: [酒店, 住宿, 房间] } def detect_intent(user_input): for tool_name, keywords in INTENT_MAP.items(): if any(kw in user_input for kw in keywords): return tool_name return None这个映射表不需要很精确它的作用是兜底而不是替代模型。我一般会把阈值设得高一些只有明确匹配到关键词才触发避免误触发。实测下来这层兜底能把漏调用率降低 60% 左右。第二层是参数校验。模型传过来的参数经常有类型错误或者缺失调度器会在执行前做一轮检查。比如 get_weather 的 city 参数必须是字符串如果模型传了个数字调度器会尝试转换如果转换失败就返回一个错误提示给模型让它重新生成。4.3 失败重试与降级策略调度器的第三层是失败重试。工具执行失败的原因很多网络超时、接口限流、参数错误、服务不可用。我的策略是分级处理参数错误直接返回给模型让它修正网络超时重试两次服务不可用则降级到备用工具或直接告诉用户当前不可用。async def execute_with_retry(tool_name, args, max_retries2): for attempt in range(max_retries 1): try: result await execute_tool(tool_name, args) return result except ParamError as e: return {error: f参数错误{e}请修正后重试} except TimeoutError: if attempt max_retries: return {error: 服务超时请稍后重试} await asyncio.sleep(1 * (attempt 1)) except ServiceUnavailable: return fallback_tool(tool_name, args)这里有个经验重试的间隔要递增第一次等 1 秒第二次等 2 秒给服务端恢复的时间。固定间隔重试在服务端压力大时反而会加剧问题。另外降级策略要提前设计好不能等出问题了再临时想。我一般会为每个核心工具准备一个备用方案比如主搜索接口挂了就切到备用搜索接口两个都挂了就返回缓存数据。4.4 调度器的监控与日志设计调度器还有一个重要作用是监控。我会记录每一次工具调用的完整链路模型是否发起了调用、调度器是否做了修正、执行结果如何、耗时多少。这些数据对于后续优化至关重要。log_entry { trace_id: trace_id, user_input: user_input, model_tool_call: model_call, scheduler_action: action, tool_name: tool_name, args: args, result: result, latency_ms: latency, success: success }有了这些日志我可以分析出很多有价值的信息。比如哪个工具的失败率最高、哪类用户输入最容易触发兜底、模型的调用准确率随时间的变化趋势。我靠这些数据定位过一个隐蔽的 bug某个工具在特定参数组合下会返回空结果模型拿到空结果后会反复重试导致死循环。如果没有日志这个问题很难发现。提示调度器的日志一定要带 trace_id并且和模型请求的日志关联起来。否则出了问题你只能看到工具调用失败但不知道是哪次对话触发的排查效率极低。5. 三种方案的选型对比与组合策略5.1 按场景选型一张表说清楚三种方案没有绝对的好坏关键看场景。我整理了一个选型对照表场景特征推荐方案理由快速原型验证纯提示词零依赖改提示词就能跑单模型生产环境原生函数调用稳定、延迟低、开发量小多模型适配纯提示词调度器统一抽象层换模型不改代码高可靠性要求原生调度器双重保障兜底完善成本敏感纯提示词无额外调用费用工具数量多20纯提示词调度器原生方案有数量限制这个表是我在实际项目中总结的不一定适用于所有情况但大方向可以参考。我自己的项目里用得最多的是“原生调度器”的组合兼顾了稳定性和可靠性。5.2 组合使用的架构设计组合使用的架构其实不复杂。最底层是模型调用层负责和模型 API 交互中间是调度器层负责意图识别、参数校验、失败重试最上层是工具执行层负责实际调用外部服务。数据流是这样的用户输入 - 模型调用层 - 模型返回工具调用或普通回复 - 调度器层校验和修正 - 工具执行层执行 - 结果回传模型 - 模型生成最终回复。每一层都有明确的职责层与层之间通过标准化的数据结构通信。这个架构的好处是每一层都可以独立替换。比如你想从原生函数调用切换到纯提示词方案只需要改模型调用层调度器和工具执行层完全不用动。我在项目里做过一次这样的切换花了不到半天时间因为大部分逻辑都沉淀在调度器里了。5.3 性能与成本的平衡点性能和成本的平衡是实际落地时最头疼的问题。原生函数调用稳定但贵纯提示词便宜但不稳定调度器能提升稳定性但增加延迟。我的经验是先用纯提示词方案跑通流程统计调用成功率和延迟数据如果成功率低于 90% 或者延迟超过可接受范围再考虑切换到原生方案如果切换后成本增加太多就在原生方案上加调度器做兜底减少重试次数。具体到数字我一般会设几个阈值首次调用成功率低于 85% 就上调度器低于 75% 就考虑换原生方案单次调用延迟超过 3 秒就检查是不是工具本身的问题每千次调用成本超过预算的 20% 就重新评估方案选型。这些阈值不是固定的要根据业务的重要程度调整。6. 面试现场的手写代码复盘与避坑指南6.1 白板代码的简化策略面试时手写代码和在编辑器里写是两回事。白板上没有自动补全没有语法检查写错了只能划掉重来。我的策略是只写核心逻辑省略掉异常处理和边界情况但要在注释里说明“这里实际需要处理 XX 情况”。比如解析工具调用那段我在白板上只写了正则匹配和 JSON 解析然后口头补充说“实际代码里还需要处理单引号、尾逗号、类型转换等问题”。面试官更看重的是你的思路是否清晰而不是代码是否能直接运行。如果你在白板上写了一大堆容错代码反而会让人觉得你抓不住重点。另外函数命名要清晰。parse_tool_call比handle好execute_with_retry比run好。面试官看你的代码只有几分钟命名清晰能让他快速理解你的意图。6.2 面试官真正想考察什么后来我和那个面试官聊过他说手写 Tool Use 主要考察三点第一你是否理解模型和工具之间的交互协议第二你是否考虑过异常情况和边界条件第三你的代码结构是否清晰、是否具备可扩展性。第一点很多人能答上来无非是模型输出结构化数据、程序解析执行、结果回传。第二点就开始拉开差距了能说出参数校验、失败重试、降级策略的人不多。第三点是加分项如果你能把调度器抽象成独立的层说明你有架构思维。所以准备面试的时候不要只背工具调用的 API 怎么用要多想想“如果模型不按预期输出怎么办”“如果工具执行失败怎么办”“如果同时有多个工具调用怎么办”。这些问题在真实工作中每天都会遇到面试官问这些就是在筛选有实战经验的人。6.3 常见追问与应对思路面试官在我写完代码后追问了几个问题我整理一下应对思路追问一如果模型返回的 JSON 嵌套很深你的正则还能匹配吗我的回答是正则匹配tool_call标签之间的内容不关心 JSON 嵌套多深所以没问题。但如果模型输出的标签本身有问题比如漏了闭合标签就需要额外的处理。实际项目中我会用更健壮的解析器比如基于状态机的解析而不是纯正则。追问二并行调用时如果其中一个工具失败了其他工具的结果还要吗我的回答是要。并行调用是独立的一个失败不影响其他。我会把所有结果都回传给模型失败的返回错误信息让模型决定怎么处理。比如用户问“北京和上海天气”北京成功了上海失败了模型可以回答北京的天气并告知上海查询失败。追问三调度器的关键词映射表怎么维护我的回答是初期手动维护工具数量多了之后可以考虑用模型自动生成候选关键词人工审核后入库。另外可以分析线上日志把高频但未覆盖的输入补充进去。这是一个持续迭代的过程没有一劳永逸的方案。6.4 从面试到生产的经验迁移面试里手写的代码和生产环境的代码差距很大但核心逻辑是相通的。面试里你写的是简化版生产里你需要考虑并发、监控、配置管理、版本控制。但如果你在面试里就能把调度器的分层思想讲清楚到了生产环境你自然知道该在哪里加日志、在哪里做限流、在哪里做降级。我自己的经验是面试里写的代码不要写完就扔回去之后把它补全成一个可运行的版本加上异常处理和单元测试。这个过程能帮你把面试里的知识点真正内化成自己的东西。我后来把面试里写的三种方案整理成了一个内部库在新项目里直接复用省了很多重复开发的时间。7. 工具调用链路的监控与效果评估7.1 关键指标的定义与采集工具调用链路要监控的指标不少但核心的就几个调用成功率、平均延迟、参数错误率、兜底触发率、重试率。这些指标的定义要清晰否则采集上来的数据没法用。调用成功率我定义为“工具执行返回有效结果的比例”注意是有效结果不是不报错。有些工具返回了空结果或者错误码虽然没抛异常但也不算成功。参数错误率是“模型传参不合法导致校验失败的比例”这个指标能反映提示词或 schema 的质量。兜底触发率是“调度器主动发起调用的比例”这个指标高说明模型的调用能力不足。采集方式我一般用埋点在调度器的每个关键节点打日志然后异步上报到监控系统。注意不要同步上报否则会影响主流程的延迟。我试过同步上报延迟增加了 15% 左右改成异步后基本无感。7.2 效果评估与迭代闭环有了指标之后下一步是建立迭代闭环。我一般每周看一次数据重点关注三个变化成功率下降、延迟上升、兜底触发率上升。任何一个指标恶化都要排查原因。排查的思路是从上往下先看是不是模型侧的问题比如模型版本更新导致调用行为变化再看是不是工具侧的问题比如某个外部接口变慢了最后看是不是调度器的问题比如关键词映射表需要更新了。我遇到过一次成功率突然下降排查后发现是某个工具的返回格式变了模型解析不了导致后续调用失败。这种问题如果没有监控可能要等用户投诉才发现。迭代的方向也很明确成功率低就优化提示词或 schema延迟高就检查工具性能或考虑并行化兜底触发率高就补充关键词映射或换更强的模型。每次迭代后观察一周数据确认指标改善后再进行下一次迭代。7.3 一个真实的优化案例我接手过一个智能体项目工具调用成功率只有 72%用户投诉很多。我先看了监控数据发现参数错误率高达 28%也就是说近三成的调用是因为参数不对失败的。进一步排查发现模型经常把日期参数写成“明天”“后天”这种相对时间而工具需要的是“2024-01-15”这种绝对格式。解决方案是在调度器里加了一层日期解析把相对时间转换成绝对时间。同时优化了工具 schema 的描述明确写了“日期格式为 YYYY-MM-DD”。改完之后参数错误率降到了 6%整体成功率提升到了 91%。这个优化只花了半天时间但效果非常明显。这个案例说明工具调用的问题往往不在模型本身而在模型和工具之间的“翻译层”。调度器就是这个翻译层它的质量直接决定了整个链路的稳定性。8. 从手写 Tool Use 延伸出的智能体设计思考8.1 工具粒度与组合的设计原则写 Tool Use 的过程中我最大的体会是工具的设计比调用本身更重要。工具粒度太粗模型不知道怎么用粒度太细模型要调用很多次才能完成一个任务。我一般遵循“一个工具做一件事”的原则但允许工具之间有组合关系。比如“查询天气”和“发送邮件”是两个独立工具但“查询天气并发送邮件”不应该是一个工具而应该由模型组合调用。这样设计的好处是工具可复用模型也能灵活应对各种组合需求。如果每个组合都做成一个工具工具数量会爆炸模型的选择难度也会增加。工具的参数设计也有讲究。必填参数尽量少可选参数给默认值。我见过一个工具定义了 12 个参数其中 8 个必填模型每次调用都要生成一大堆参数出错率极高。后来我把必填参数压缩到 3 个其他参数给默认值成功率立刻上去了。8.2 多工具协作的编排模式当任务需要多个工具协作时编排模式就很重要。我总结了几种常见模式串行模式工具按顺序执行前一个的输出是后一个的输入并行模式多个工具同时执行结果汇总后一起处理条件模式根据前一个工具的结果决定下一步调用哪个工具。串行模式最简单但延迟高。并行模式延迟低但要求工具之间没有依赖。条件模式最灵活但实现复杂需要模型有较强的推理能力。实际项目中我一般先用串行模式跑通再根据性能数据决定是否改成并行或条件模式。编排的实现方式有两种一种是把编排逻辑写在调度器里模型只负责单次调用另一种是让模型自己决定调用顺序调度器只做校验。前者可控性强但灵活性差后者灵活但容易出错。我倾向于混合模式核心流程用调度器编排边缘场景让模型自由发挥。8.3 未来演进方向与个人建议工具调用这个领域还在快速演进。我观察到几个趋势一是模型原生能力越来越强未来可能不需要调度器做太多兜底二是工具协议逐渐标准化不同厂商之间的兼容性会更好三是工具调用的评估体系在完善会有更科学的指标来衡量效果。对个人来说我的建议是不要只停留在“会调 API”的层面。要深入理解模型和工具之间的交互协议理解异常处理的设计原则理解监控和迭代的方法论。这些能力不会因为模型版本更新而贬值反而会随着智能体应用的普及越来越值钱。我在实际项目里踩过的坑、写过的调度器、调过的参数最终都沉淀成了一套可复用的方法论。这套方法论让我在面对新的模型、新的工具、新的场景时能快速搭建起稳定可靠的调用链路。这比记住某个 API 的用法重要得多。