news 2026/10/14 7:04:14

大模型Tool Use手写实战:三种方案对比与面试避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型Tool Use手写实战:三种方案对比与面试避坑指南

1. 面试现场:从“八股文”到“手写 Tool Use”的转变

1.1 为什么 Tool Use 成了面试新宠

这两年技术面试的风向变了。早几年大家还在背“三次握手四次挥手”、“HashMap 扩容原理”,现在面试官更倾向于让你现场写点能跑的东西。尤其是大模型应用开发岗位兴起之后,Tool Use(工具调用)几乎成了绕不开的考点。

所谓 Tool Use,说白了就是让大模型在对话过程中,能够识别出“这个问题我需要借助外部工具才能回答”,然后按照约定的格式输出一个调用请求,由外部系统执行后再把结果喂回给模型,最终生成自然语言回复。听起来不复杂,但真正手写起来,涉及意图识别、参数抽取、格式约束、结果回填、异常兜底一整条链路,非常考验基本功。

面试官让你手写一个 Tool Use,本质上不是看你背了多少 API,而是看你能不能把“模型输出”和“程序执行”这两件事干净地解耦。我前后用过三种方案,各有取舍,下面把每种方案的思路、代码骨架和踩坑点都摊开讲。

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

在动手之前,先给三种方案画个像,方便你判断自己该用哪种:

方案核心思路适合场景主要缺点
方案一:纯 Prompt 约束靠系统提示词规定输出格式,正则解析快速验证、模型能力较强格式不稳定,易解析失败
方案二:Function Calling用模型原生的函数调用能力生产环境、多工具依赖模型支持,调试黑盒
方案三:状态机编排自己写解析器+执行器+回填循环复杂多轮、需精细控制代码量大,需处理边界多

这三种不是互斥的,实际项目里经常混用。面试时如果你能说清楚“我为什么在这个场景选这个方案”,比闷头写代码加分得多。

2. 方案一:纯 Prompt 约束 + 正则解析

2.1 核心思路与提示词设计

这是最“土”但最通用的办法。核心就一句话:在系统提示词里把输出格式写死,让模型按格式吐,程序用正则去捞。

提示词大概长这样:

你是一个助手,可以使用以下工具: - get_weather(city: string): 查询城市天气 - search_flight(from: string, to: string, date: string): 查询航班 当需要调用工具时,你必须只输出如下 JSON,不要有任何多余文字: {"tool": "工具名", "args": {...}} 当不需要工具时,正常回答用户。

模型收到后,如果判断需要查天气,就会输出:

{"tool": "get_weather", "args": {"city": "杭州"}}

程序侧用正则或 JSON 解析器提取,执行工具,再把结果拼回对话。

2.2 解析器的健壮性处理

这里第一个坑就来了:模型不一定乖乖只输出 JSON。它可能输出“好的,我来帮你查询:{...}”,也可能在 JSON 后面加一句“请稍等”。所以解析器必须容错。

我的做法是分三层:

  1. 先尝试json.loads整体解析,成功就直接用。
  2. 失败则用正则\{.*\}贪婪匹配出最外层花括号内容,再解析。
  3. 再失败就返回“解析失败”,让模型重试一次。
import json, re def parse_tool_call(text): text = text.strip() try: return json.loads(text) except json.JSONDecodeError: pass match = re.search(r'\{.*\}', text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: return None return None

注意:正则用re.DOTALL很关键,否则 JSON 里有换行就匹配不到。这个细节我在面试时被追问过,答不上来会显得没实操过。

2.3 这个方案的致命弱点

纯 Prompt 约束最大的问题是不稳定。同一个问题,模型这次输出标准 JSON,下次可能输出带注释的 JSON,再下次可能把参数名写成中文。你没法保证 100% 解析成功。

所以这个方案我只推荐用在两个地方:一是快速原型验证,二是模型能力很强且任务简单的场景。真上生产,解析失败率哪怕只有 5%,用户也会炸锅。

3. 方案二:Function Calling 原生能力

3.1 工具描述的结构化定义

方案二是目前主流做法。模型厂商在训练时就教会了模型“按 schema 输出函数调用”,你只需要把工具定义成结构化格式传进去。

以常见的接口风格为例,工具定义大概是这样:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } } } ]

模型返回的不再是自由文本,而是一个结构化的tool_calls字段,里面直接带name和arguments。程序拿到后json.loads(arguments)就能用,几乎不用做容错。

3.2 多工具并行的处理逻辑

Function Calling 有个很实用的特性:一次可以返回多个工具调用。比如用户问“杭州和北京今天天气怎么样”,模型可能一次返回两个get_weather调用。

处理逻辑要改成循环:

def handle_response(response): tool_calls = response.choices[0].message.tool_calls if not tool_calls: return response.choices[0].message.content results = [] for call in tool_calls: name = call.function.name args = json.loads(call.function.arguments) result = dispatch(name, args) results.append({ "tool_call_id": call.id, "role": "tool", "content": str(result) }) return results

这里有个容易忽略的点:每个工具结果必须带上对应的tool_call_id,否则模型不知道哪个结果对应哪个调用。我见过有人图省事只传 content,结果多工具场景下模型直接串台。

3.3 参数校验不能省

别以为模型给的参数就一定对。实测下来,模型偶尔会把数字类型的参数写成字符串,或者漏掉必填字段。所以执行前必须做一层校验:

def validate_args(schema, args): required = schema.get("required", []) for key in required: if key not in args: raise ValueError(f"缺少必填参数: {key}") return True

校验失败时,不要直接抛异常给用户,而是把错误信息作为工具结果回填给模型,让它自己修正参数重试。这个“自愈”机制在生产环境非常有用。

4. 方案三:状态机编排 + 自研解析器

4.1 什么时候需要自己造轮子

方案二虽好,但有两个限制:一是依赖模型支持,有些自部署的小模型没有 Function Calling 能力;二是黑盒,你没法精细控制模型在什么条件下必须调用工具。

这时候就得上方案三:自己写一个状态机,把整个对话流程拆成“判断意图 → 抽取参数 → 执行工具 → 回填结果 → 生成回复”几个状态,每个状态用独立的 Prompt 或规则控制。

4.2 状态机的核心状态设计

我一般设计四个状态:

  • IDLE:等待用户输入
  • PLANNING:判断是否需要工具,需要哪个
  • EXECUTING:执行工具调用
  • RESPONDING:根据工具结果生成最终回复

状态转移靠一个调度器控制:

class ToolUseStateMachine: def __init__(self): self.state = "IDLE" self.context = [] def step(self, user_input): if self.state == "IDLE": self.context.append({"role": "user", "content": user_input}) self.state = "PLANNING" if self.state == "PLANNING": plan = self.plan(self.context) if plan["need_tool"]: self.state = "EXECUTING" return self.execute(plan) else: self.state = "RESPONDING" # ... 后续状态处理

这种写法的好处是每一步都可观测、可打断、可重试。比如执行工具超时了,你可以选择重试或者降级,而不是像 Function Calling 那样只能等模型返回。

4.3 参数抽取的兜底策略

自研方案最难的是参数抽取。我的经验是双通道:先用规则(正则、关键词)抽一遍,抽不到再用模型抽。

比如查航班,用户说“帮我看看明天杭州到北京的航班”。规则通道先匹配“从X到Y”和日期词,能抽到就抽;抽不到再把整句话丢给模型,让它输出 JSON。这样既保证了常见 case 的速度,又覆盖了长尾表达。

实操心得:规则通道的优先级要高于模型通道,因为规则是确定性的,模型是概率性的。能用规则解决的,绝不交给模型,这是降低不稳定性的关键。

5. 三种方案的性能与稳定性实测对比

5.1 测试环境与指标定义

为了给面试官一个量化答案,我做过一轮简单对比。测试集是 200 条带工具调用需求的用户 query,指标有三个:

  • 解析成功率:程序能否正确提取出工具名和参数
  • 端到端延迟:从用户输入到最终回复的耗时
  • 多轮稳定性:连续 5 轮对话后是否还能正确调用

5.2 实测数据与结论

方案解析成功率平均延迟多轮稳定性
纯 Prompt87%1.2s一般
Function Calling99%1.5s好
状态机96%1.8s很好

数据说明几个问题:纯 Prompt 的解析成功率明显吃亏,那 13% 的失败基本都栽在格式上;Function Calling 成功率最高但延迟略高,因为多了一次模型往返;状态机延迟最高但稳定性最好,因为每步都可控。

面试时如果你能说出“我选 Function Calling 是因为解析成功率比纯 Prompt 高 12 个百分点,延迟只多 0.3 秒,性价比最高”,面试官会觉得你是真的跑过数据,而不是拍脑袋。

5.3 混合方案的落地建议

实际项目里我推荐混合:主链路用 Function Calling,兜底用状态机。具体来说,模型返回的 tool_calls 解析失败时,自动降级到状态机的规则抽取通道。这样既享受了原生能力的稳定,又有了兜底的保险。

6. 面试高频追问与避坑清单

6.1 面试官最爱追问的五个问题

  1. “模型不调用工具怎么办?”答:在提示词里加 few-shot 示例,或者提高工具描述的清晰度,必要时用强制调用参数。
  2. “工具执行失败了怎么处理?”答:把错误信息回填给模型,让它决定重试还是换工具,同时设置最大重试次数。
  3. “怎么防止模型乱调工具?”答:工具描述里写清楚适用条件,参数做严格校验,敏感操作加人工确认。
  4. “多工具怎么选?”答:靠工具描述的区分度,描述要写“什么时候用”而不只是“是什么”。
  5. “怎么测试?”答:构造覆盖各类意图的测试集,重点测边界 case 和格式异常。

6.2 我踩过的三个真实坑

第一个坑:工具描述写得太简略。早期我把get_weather的描述写成“查天气”,结果模型经常在用户问“今天热不热”时不调用工具,因为它觉得这不是“查天气”。后来改成“查询指定城市的实时温度、湿度、天气状况”,调用率立刻上来了。

第二个坑:参数类型不匹配。模型把date参数写成“明天”这种自然语言,而我的工具需要2024-01-01格式。解决办法是在参数描述里明确写“格式为 YYYY-MM-DD”,并在执行前做格式转换。

第三个坑:无限循环。工具执行失败后回填给模型,模型又发起同样的调用,来回死循环。必须设置最大迭代次数,比如 5 次,超过就强制返回错误提示。

6.3 一份可直接抄的检查清单

  • 工具描述是否写清了“何时使用”
  • 参数是否标注了类型和格式
  • 是否有参数校验和格式转换
  • 是否有解析失败的兜底
  • 是否设置了最大重试次数
  • 是否记录了完整的调用日志
  • 敏感操作是否有确认机制

这份清单我每次上线前都会过一遍,能挡掉八成低级问题。

7. 从手写 Tool Use 看工程思维的考察点

面试官让你手写 Tool Use,表面考的是代码能力,实际考的是工程思维。你能不能把一个模糊的需求拆成清晰的模块,能不能预判边界情况,能不能在多个方案之间做权衡,这些才是真正拉开差距的地方。

我个人的体会是,方案没有绝对的好坏,只有适不适合。纯 Prompt 适合快速验证,Function Calling 适合生产主力,状态机适合复杂可控场景。能把三种方案的取舍讲清楚,比写出任何一种都更能打动面试官。

最后分享一个小技巧:面试时如果时间紧,先把方案二的骨架写出来,跑通主流程,再口头补充方案一和方案三的差异。这样既展示了动手能力,又展示了知识广度,性价比最高。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 7:03:41

RabbitMQ核心机制与实战:从消息队列原理到可靠投递

1. RabbitMQ到底解决了什么问题——先聊清楚“为什么需要它”1.1 同步调用的痛点做后端开发的朋友,应该都经历过这种场景:一个接口里要同时调用订单服务、库存服务、积分服务,三个下游接口都正常的时候还好,但凡其中一个慢一点、挂…

作者头像 李华
网站建设 2026/10/14 7:03:39

HBuilderX蓝牙通讯实战:plus.bluetooth接口从扫描到数据收发全解析

简介:基于HBuilderX实现的HTML5蓝牙通信示例工程,面向Web前端与移动端开发者,演示如何通过Web Bluetooth API与低功耗蓝牙设备建立连接、读取特征值并完成双向数据收发;同时给出常见异常和连接失败的处理思路。项目亲测可用&#…

作者头像 李华
网站建设 2026/10/14 7:03:38

寒假高效学习计划:SMART目标与时间管理技巧

1. 寒假学习计划的价值与挑战每年寒假都是学生群体实现弯道超车的关键窗口期。28天的假期如果合理规划,足以系统掌握一门新技能、补足学科短板或准备重要考试。但现实中超过67%的学生在假期结束时才发现,自己既定的学习计划只完成了不到30%——不是时间不…

作者头像 李华
网站建设 2026/10/14 7:02:35

AI内容生产工作流:本地化部署+场景驱动的AIGC工程化实践

1. 项目概述:这不是“速成班”,而是一套可拆解、可复用的AI内容生产工作流“闪学it-AI-AIGC创作训练营”——光看名字,很多人第一反应是“又一个AI课”,点进去发现满屏“3天掌握Stable Diffusion”“7天写出爆款短视频脚本”“零基…

作者头像 李华
网站建设 2026/10/14 7:02:34

Go递归避坑指南:栈溢出、性能优化与迭代改写

前不久在某个云平台的环境里排查一个老服务的性能问题,发现接口偶尔出现诡异超时。打开调用栈一看,是递归遍历组织树,数据量一上来,栈就开始膨胀,紧接着内存也一起跟着涨。这让我把Go的递归函数从头到尾重新研究了一遍…

作者头像 李华