☰
大模型Tool Use三种实现方案:函数调用、JSON解析与结构化输出
2026/10/22 14:54:33 网站建设 项目流程

前阵子帮朋友复盘一场大模型应用方向的面试,聊到一道让他印象很深的题:面试官让他手写一个 Tool Use,不是问概念、不是画架构图,而是直接在白板上从零写一个让大语言模型能够调用外部工具的闭环。三十分钟,写完还要逐行讲原理,讲完还有一堆“如果……怎么办”的追问。他出来之后感叹:这道题看起来不难,但真正想答出深度,背后得有一套完整的工程认知撑着。

Tool Use 说白了就是给大模型接上双手和传感器。模型本身不会主动执行你的代码,它只会在收到用户请求后输出一个“我想调用某某工具、参数是什么”的结构化意图;真正去调用函数、查询数据、把结果回填给模型的,是模型之外的那套调度逻辑。这也是面试官真正想看到的:一个完整的主循环。我这次把三种可落地的方案整理出来:函数调用、提示词加 JSON 解析、结构化输出。从原理到代码,再到线上踩过的坑,一次性讲透。无论你是准备 Agent 方向面试,还是平时在做大模型应用开发,这三个方案应该都能直接用上。

1. 面试前情:Tool Use 到底在考什么

1.1 一句话说清 Tool Use 是什么

先统一一下概念。Tool Use 是让模型能够请求外部工具执行动作的完整交互闭环,它包含五个环节:用户提问、模型判断是否需要工具、模型输出工具名与参数、你的代码执行对应函数、执行结果回填给模型,最后模型基于结果给出最终回答。这个过程如果画出来,是一条有分支的循环线,而不是一条直线。很多人面试时把 Tool Use 和 Function Calling 混为一谈,其实前者是完整的调度系统,后者只是模型服务提供的一种结构化输出能力,两者是“系统”和“零件”的关系。这个区别脑子里清楚,答题方向就不会跑偏。

用生活化的类比来说:大模型像一个只会思考但手脚不便的大脑,它推理能力很强,但看不到实时数据,也动不了任何东西;Tool Use 调度框架就是大脑与外界之间的神经系统,负责把大脑的意图翻译成身体动作,再把眼前看到的信息传回大脑。面试官想看的,正是这条神经回路是否完整、是否能在各种异常情况下继续工作。所以别把这道题当成一个纯 API 调用题,它本质是一道系统设计题。

1.2 面试官设计这道题的真实意图

这道题考察的东西非常密集,至少能看出三个层次的能力。第一个是循环思维。Tool Use 不是一次请求就能结束的:模型第一次输出可能只是一个工具调用请求,你需要把工具的返回结果回填之后再次调用模型,它才能整理出最终答复。这个循环什么时候终止、最大能转几轮、怎么防止模型陷入死循环,都是候选人要主动交代的。只能写出“一次调用直接返回”的人,大概率没写过真实 Agent。

第二个是数据流理解。模型输出的是文本,你要解析成结构化的工具调用指令;工具返回的是 JSON,你要序列化后回填给模型。这两次转换是整个系统最容易出错的地方:解析失败怎么办、工具抛异常怎么办、返回的内容太长导致上下文爆炸怎么办。这些都是隐含考察点,答得越具体越能证明你真写过。

第三个是工程化水平。工具描述怎么组织、参数怎么声明、校验怎么做、错误信息怎么回传给模型,这些都不是 API 文档会告诉你的东西,只能在真实项目里踩过坑才能总结出来。说实话,平时能把这三种方案都完整讲出来的人非常少,大多数候选人是“知道函数调用”这一个答案,一旦面试官说“不许用函数调用能力”,马上就卡住了。这正是我写三种方案的动机:让你从“背答案”变成“懂方案”。

1.3 三种方案的各自定位

先把定位梳理清楚再展开代码。函数调用方案是模型服务自带的结构化协议,模型的预训练和微调阶段就已经大量对齐过这个交互格式,所以输出稳定、解析成本低,是生产环境的默认选项。提示词加 JSON 解析方案不依赖模型服务的任何特殊能力,只要求模型会生成文本,你通过一段精心设计的话术让模型输出约定好的 JSON,再由代码做解析和分发,它最大的价值是兼容性。

结构化输出方案介于两者之间:模型服务在生成阶段会强制遵守你给出的 JSON Schema,保证输出一定是合法 JSON,可靠性比纯提示词方案高一个数量级,同时也不依赖函数调用那种特定协议。三个方案没有绝对的优劣之分,关键在于你要知道各自的边界在哪里。能把这层讲清楚,面试的深度基本就有了。

2. 方案一:函数调用——最正统的模型原生能力

2.1 为什么面试时先写它

函数调用之所以值得作为第一方案讲,不是说它最复杂,而是它最正统。模型厂商在训练阶段专门对齐过“给定工具定义,输出调用意图”这类样本,所以模型对工具名、参数名、参数类型的输出准确率是最高的,哪怕用户绕了几个弯说“北京热不热”,它也能正确映射到天气查询工具并补全参数。如果换成纯提示词方案,在工具数量超过三个、参数类型嵌套两层以上时,输出质量会肉眼可见地下降。

从工程实现角度看,函数调用的返回结构里自带工具调用 ID、工具名、参数 JSON,代码只需要做解析、分发、回填三个动作,不需要去猜模型的输出边界。解析成本低意味着出错概率低,调试链路短,线上问题好定位。面试时先讲方案一,等于告诉面试官:我知道行业标准路径长什么样,不是只会写花活。我自己做技术选型时,凡是模型服务能力允许的,第一选择永远是它。

2.2 最小可用函数调用闭环:从零到一

先看一个我常用的最小版本:

import json TOOLS = [ { "type": "function", "function": { "name": "query_weather", "description": "查询指定城市当前天气,返回温度、湿度、风力信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京" } }, "required": ["city"] } } } ] def query_weather(city: str) -> dict: # 真实项目里这里会请求天气服务,这里用模拟数据代替 return {"city": city, "temperature": 26, "humidity": 40} def dispatch_tool(name: str, args: dict): if name == "query_weather": return query_weather(**args) return {"error": f"unknown tool: {name}"} def run_with_function_calling(user_input: str, max_rounds: int = 5): messages = [{"role": "user", "content": user_input}] for _ in range(max_rounds): resp = llm_chat(messages, tools=TOOLS, temperature=0.2) # 模型判断不需要工具,直接返回最终回答 if not resp.get("tool_calls"): return resp["content"] # 把模型这次的工具调用请求放回上下文 messages.append(resp) # 逐个执行模型请求的工具 for call in resp["tool_calls"]: fn_name = call["function"]["name"] fn_args = json.loads(call["function"]["arguments"]) result = dispatch_tool(fn_name, fn_args) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False) }) raise TimeoutError("工具调用轮次超过上限")

上面的代码里,llm_chat是我对模型服务的统一抽象,实际对接具体厂商时只需要在这个函数里完成鉴权和协议转换。这段代码虽然短,但主循环的关键要素都在了。第一个要点是把工具声明放在TOOLS里,模型看到的是结构化的工具定义,不是一段自然语言描述;第二个要点是模型返回的resp必须原样追加回messages,因为后续工具结果需要和这次调用请求配对;第三个要点是工具执行结果以role: "tool"的消息回填,并用tool_call_id和模型刚才的请求绑定。

这段代码的运行流程是:用户说“北京今天适合穿短袖吗”,模型返回一个tool_calls,要求执行query_weather,参数是{"city": "北京"};我们的代码执行天气查询,把结果回填;模型看到温度 26 度、湿度 40% 之后,组织一段“北京今天比较热,建议穿短袖”的回答返回。整个过程完整、闭环、可解释。

2.3 主循环的三个关键动作

第一个关键是上下文轨迹完整性。模型是无状态的,它每一次生成都只能看到你传给它的消息列表。如果你在调用完工具之后没有把 assistant 的tool_calls响应和 tool 的执行结果放回去,模型根本不知道刚才发生了什么,轻则答非所问,重则重复要求同一个调用,直接进入死循环。这个细节写代码时必须一步到位,不能指望后面再补。

第二个关键是工具消息的关联。tool 角色消息需要和 assistant 返回的tool_call_id一一对应,相当于告诉模型“这条结果是你刚才那个调用请求的返回值”。如果对应错了,模型会认为上下文互相矛盾,输出质量直线下降。多工具并行调用的场景里这个对应关系尤其重要,不能只按顺序塞回去。

第三个关键是循环上限。原则上模型在拿到真实结果后不会无限要求调用工具,但天有不测风云,一旦某个工具抛了非预期异常,或者返回内容刚好让模型陷入重复状态,没有max_rounds保护的循环会把整个请求拖到超时。我习惯设 5 到 10 轮的上限,超出后抛出异常给上层,这比让请求一直耗着体面得多。真实生产环境里,我还会把这个超时做成可配置项,因为不同业务愿意等待的时长完全不同。

2.4 踩过函数调用的两个坑

第一个坑是温度参数。工具调用阶段我会把temperature设得很低,比如 0.0 到 0.2。因为模型输出工具参数是一个事实性的选择,不是创意生成,温度越高参数越乱,同一个问题这次传“北京”下次传“beijing”都是可能发生的。等工具结果回来后,最后组织回答的那一次调用可以稍微放开一点,给一点自然语言的空间,但也不要太高,0.5 左右已经够用了。

第二个坑是参数不校验就执行。模型输出的 JSON 即使格式合法,参数内容也可能完全离谱:查询天气的城市传了空字符串,数值类参数传了“abc”。这类问题如果不在分发前拦住,错误会直接发生在外层函数里,排查时非常被动。我的习惯是:分发之前先做一轮参数校验,校验失败就把错误信息直接回传给模型,让它自己纠正。在 Agent 系统里,这叫做“把错误当提示”,模型第二次看到错误信息后通常会给出更合理的参数。

3. 方案二:提示词加 JSON 解析——不依赖函数调用能力的兼容路径

3.1 什么场景下必须走这条路

函数调用虽好用,但并不是所有环境都支持。我在生产里至少遇到过三种情况。第一种是私有化部署的模型服务,接口被内部网关裁剪过,函数调用相关的透传字段全被剥掉了,只留下最普通的文本输入输出。第二种是版本比较旧的开源模型推理框架,函数调用支持不完整,经常出现工具名是对的、参数却是空的情况,这种情况下强行用函数调用反而更不稳定。

第三种是跨厂商迁移的项目,需要同时对多个模型服务,如果每个厂商的函数调用协议格式有差异,统一适配的成本很高。与其维护多套协议,不如全部走一套提示词加 JSON 的通用协议,所有模型只要会聊天就能接入。方案二在这种场景下就是绕不开的兜底能力。它的代价是稳定性会比方案一低,所以配套的提示词设计、解析容错和校验逻辑要写得更加严密。

3.2 提示词模板怎么设计,模型才会乖乖输出 JSON

模板可以拆成四块:角色声明、工具清单、输出协议、示例。角色声明要写清楚“你是一个通过调用工具来回答问题的助手”,让模型从第一句话就开始进入工具调用模式。工具清单要把工具名、用途、每个参数的名称、类型、是否必填说清楚,少一项模型就可能猜错。输出协议要规定“当需要调用工具时,只输出如下 JSON”,并且明确不需要额外解释。示例要给至少一条完整的“用户问题到 JSON”的映射。

这里有一个非常容易漏的点:必须写“如果不需要工具,直接回答用户问题,不要输出 JSON”。如果不加这句,模型会在不需要工具的时候硬凑一个空 JSON,你的代码就得处理“我不想调工具”和“我忘了调工具”两类噪声。我常用的模板长这样,你可以直接抄走:

SYSTEM_PROMPT = """你是一个通过调用工具来回答问题的助手。你有以下工具: 工具名:query_weather 用途:查询指定城市当前天气,返回温度、湿度、风力 参数: - city (string,必填):城市名 工具名:search_news 用途:按关键词搜索最近一周的新闻 参数: - keyword (string,必填):搜索关键词 当用户的问题需要调用工具时,只输出如下JSON,不要输出任何额外文字: {"action": "工具名", "action_params": {...}} 如果不需要工具,直接回答用户问题,不要输出JSON。"""

模板写完后,建议在本地跑二十条不同风格的例子,专门看两类情况:一类是“必须调工具但模型没有调”,另一类是“不需要调工具但模型输出了 JSON”。这两种偏差比例如果超过百分之十,不要急着调模板,先检查是不是工具描述写得不够具体。

3.3 解析容错工程与重试机制

模型不是完美的编译器,输出 JSON 时总会带点小毛病。最常见的三种表现:第一,在 JSON 外面套了 markdown 代码围栏;第二,在 JSON 前后夹杂“好的,结果如下:”之类的自然语言;第三,属性名写错、参数类型写错、甚至少了一个逗号。针对这些问题,解析函数不能只做一次json.loads,我一般写一个多级兜底:

def extract_json_with_fallback(text: str): try: return json.loads(text) except json.JSONDecodeError: pass text = text.strip() if text.startswith("```"): text = text.strip("`").strip() if text.startswith("json"): text = text[4:].strip() start, end = text.find("{"), text.rfind("}") if start >= 0 and end > start: try: return json.loads(text[start:end + 1]) except json.JSONDecodeError: pass return None

依次尝试直接解析、去掉代码围栏后解析、截取第一个左花括号到最后一个右花括号之间的文本再解析,三种都失败就返回None。解析失败之后的处理同样重要:把“模型输出格式不符合要求,请重新输出符合协议的 JSON”作为一条新的 user 消息追加到上下文,让模型重试一次。重试两次仍然失败,就放弃工具调用,直接让模型用已有内容回答问题。这样用户的请求至少能得到一个答复,而不是一个 500 错误。

3.4 一次线上稳定性事故给我的教训

前阵子我维护的一个内部问答机器人走的就是方案二,当时遇到一个很典型的案例。用户问“今天 A 公司股票涨了没”,模型很正确地输出了search_stock的 JSON,但由于关键词被内部数据源映射成了另一个编号,工具返回的结果是空的。正常情况下,应该把“查询无结果”回传给模型,让模型基于这个反馈继续想办法,或者换一个关键词重新查询,或者如实告诉用户查不到。

结果当时代码里把空结果当成最终答案直接返回了,用户收到一句干巴巴的“查询无结果”。这个案例让我意识到:纯提示词方案里,工具返回的内容本身也是模型的输入,它的质量会影响后续推理。工具执行方不能只丢一个空列表,而是应当尽可能把错误原因、空结果的原因描述出来,比如返回{"code": 404, "message": "公司编号未找到,请尝试提供全称"},模型看到这个信息之后就能自主调整。

4. 方案三:结构化输出约束——让模型“只会说 JSON”

4.1 结构化输出和函数调用的区别

结构化输出是指模型服务在生成时遵循一个给定 JSON Schema,保证返回的内容一定是合法 JSON。和函数调用相比,它关注的不是“应该调用哪个工具”,而是“输出必须长成什么样”。函数调用返回的tool_calls是服务级别的结构化字段,由模型服务内部专门处理;结构化输出更像一个通用约束框架,你告诉它“请按这个 Schema 输出对象”,它就会在采样阶段对输出做约束,确保不会跑偏。

从使用场景看,结构化输出的覆盖面更广。比如你要让模型输出一段业务分析,里面同时包含结论、理由列表、置信度,以及“是否需要调用工具”的旗标和“工具参数”的对象,函数调用的参数声明也能做,但结构化输出直接多了。在需要模型自主决策的场景里,我通常把结构化输出和工具注册表配合使用,既约束决策格式,又约束最终输出格式,效果非常好。

4.2 如何用 JSON Schema 约束一次工具调度

带约束的版本,核心就是给每次模型调用声明一个输出格式,再在拿到输出后做两层校验。第一层校验格式:content能不能被json.loads解析;第二层校验语义:有没有use_tool字段、tool_name是不是在注册表里、tool_params和对应工具的参数定义是否匹配。两层都通过才真正执行工具,这样能把无效请求拦截在函数执行之前。

我写了一份可以运行的最小骨架:

RESPONSE_SCHEMA = { "type": "object", "properties": { "thought": {"type": "string", "description": "先分析用户需求"}, "use_tool": {"type": "boolean", "description": "是否调用工具"}, "tool_name": {"type": "string", "description": "工具名"}, "tool_params": {"type": "object", "description": "工具参数"} }, "required": ["thought", "use_tool"] } def run_with_schema(user_input: str): messages = [{"role": "user", "content": user_input}] resp = llm_chat( messages, response_format={"type": "json_object", "schema": RESPONSE_SCHEMA}, temperature=0.0 ) data = json.loads(resp["content"]) if not data.get("use_tool"): return data.get("answer", resp["content"]) tool = TOOL_REGISTRY.get(data.get("tool_name")) if tool is None: return "你请求了一个不存在的工具,请修改你的请求" result = tool["handler"](**data.get("tool_params", {})) messages.append({"role": "assistant", "content": resp["content"]}) messages.append({"role": "user", "content": f"工具调用结果:{json.dumps(result, ensure_ascii=False)},请结合结果给出最终回答。"}) final = llm_chat(messages) return final["content"]

注意这里的消息组织比方案一多了一个细节:把 assistant 原始结构化输出作为 assistant 消息放回上下文,再把工具结果以 user 消息形式“告知”模型。虽然角色语义没有函数调用那么严格,但在兼容模式下要的就是通用性,只要能让模型看到“我刚才决定调工具、工具返回了这个结果”就够了。

4.3 多工具场景的注册表设计与调度

说到这里必须展开工具注册表,因为三个方案到多工具场景都会用到同一个思想:把工具抽象成配置和数据,而不是写死在 if-else 里。注册表里每一项至少包含四样东西:name是工具唯一标识;description是模型决定是否调用时的依据,写得好不好直接决定命中率;params_schema用于校验入参;handler是真正执行函数。

TOOL_REGISTRY = { "query_weather": { "description": "查询指定城市当前天气,返回温度、湿度、风力", "params_schema": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] }, "handler": query_weather } }

用注册表重构后,新增一个工具只需要注册一条配置,主循环代码完全不用改。面试官如果看到你在方案三里顺手放了一个注册表结构,基本会认定你写过真实系统,因为只有踩过新增工具需要改一堆 if-else 的痛,才会想到这一层。注册表里还可以放一个冷门但重要的字段:报错模板。例如天气查询工具查不到城市时返回{"code": 404, "message": "城市不存在"},模型读到这个信息后能主动换一种问法或者如实告知用户,而不是硬编一个答案。

5. 三种方案横向对比与选型建议

5.1 一张表看清三个方案的核心差异

对比维度方案一:函数调用方案二:提示词+JSON方案三:结构化输出
依赖模型 API 能力需要原生函数调用支持仅需普通文本生成需要结构化输出支持
输出稳定性高中低,依赖提示词质量较高
解析成本低高,需要多级容错中
上下文血缘有 tool_call_id 严格对应弱,靠消息顺序中,靠消息顺序
多工具扩展方便较繁琐方便
跨厂商兼容差最强中
适用场景生产主力私有化、兼容层自主决策、复杂输出

这张表是我在实际项目里的体感,每个维度背后的原因我在前面都展开讲过。面试时能把原因讲出来,比只报结论更有说服力。尤其要注意“上下文血缘”这一行:函数调用协议天然把“模型请求”和“工具结果”绑定在一起,方案二和三只能靠消息顺序去衔接,一旦并发执行多工具,必须手动维护好结果和请求的对应关系。

5.2 不同场景怎么选

场景一对公网 Agent 产品,模型统一、接口稳定,直接选方案一。函数调用是模型服务方打磨最多的路径,你不需要自己维护一套解析和校验,风险最低。场景二需要同时对接多个模型厂商,或者跑在客户私有网络的旧版本模型上,方案二是绕不开的兼容层,但需要在提示词工程和解析容错上多投入,不能只写一个简陋版本。场景三模型需要自己决定“要不要调用工具、调哪个、参数是什么”,同时最终输出还希望是一份结构化报告,方案三最合适,它把决策和输出都约束住了。

选型不是非此即彼。一个成熟的工程往往同时使用多个方案:对外入口走函数调用,内部兼容层走提示词 JSON,面向复杂报告的场景走结构化输出。把它们放在一个统一的调度框架里,上层根本不需要关心某次调用具体用了哪种协议。

5.3 面试加分解法:主动聊工具注册表和并发控制

面试官通常不会止步于“能跑通”,他会继续追问:如果新增一个工具,你的代码要改哪里?如果两个工具被并发调用怎么办?这时建议你主动把工具注册表的设计讲出来。轻量注册表就是一个字典,key 是工具名,value 是描述、参数 Schema、执行函数。主循环拿到模型输出后,查找注册表、校验参数、执行、回填结果。新增工具时只需要往字典里加一项,主循环代码一行不动。

再深一层,可以主动提工具调用的超时和并发控制。比如用线程池执行多个独立的工具调用,每个工具单独设置超时;超时不炸整个循环,而是把“工具超时”作为工具结果回传给模型,让模型知道这个工具暂时不可用,可以换一个思路。这种细节才是面试官愿意给加分的点,因为这恰恰是区分“写过 Demo”和“写过系统”的地方。

6. 面试现场实操:从拿到题目到跑通闭环的完整复盘

6.1 动手前先画主循环

三十分钟的手写题,最忌讳提笔就写。我的习惯是先花三到五分钟在白板上画主循环:用户输入进入模型,模型输出分两个分支,一个分支是最终回答直接返回,另一个分支是工具调用请求,箭头指向工具执行,执行结果回填到消息列表,再回到模型。画完这条线,代码结构已经清楚了,剩下的只是填内容。

画图的过程其实也是在和面试官对齐需求。比如工具数量是多少?参数怎么传?需不需要支持流式输出?有没有鉴权?这些问题不确认清楚,埋头写的方向可能就偏了。主动确认需求本身就是一个加分信号,说明你在真实项目里是从需求出发写代码的,而不是从 API 文档出发。

6.2 三十分钟里我先写了哪些核心代码

复盘下来,我当时选的是结构化输出方案。选择它的原因有两个:第一,它不依赖面试环境里模型 API 是否真的支持函数调用,写出来不容易被现场诡异的环境坑到;第二,结构化输出方案能把“输出约束”和“工具决策”同时展示出来,信息密度比只写函数调用更高,更容易让面试官看到全局观。

代码骨架大约是:一个TOOL_REGISTRY字典,里面放一个query_weather作为示例工具;一个RESPONSE_SCHEMA定义模型输出格式;一个主循环读取模型响应、校验工具名、执行工具、回填结果、再调用模型得到最终回答。虽然只有三十行左右,但完整闭环已经出来了。最后我主动补了一句:真实系统里还需要日志、超时、限流、鉴权,这道题的核心是闭环和扩展性,我先不展开。面试官点了点头,这一步就走对了。

6.3 面试官追问环节怎么扛住

面试官看到结构化输出之后,马上追问了三个问题。第一个:如果工具执行抛异常了怎么办?我的回答是:捕获异常后把错误信息塞进工具结果,让模型基于错误向下推理,而不是让整个请求失败;同时记录一条日志,方便后续排查工具本身的故障。第二个:如果模型判断需要调用两个工具才能回答,你的代码支持吗?我答:支持,但并发处理要谨慎,两个工具参数之间可能有关联,比如先查城市 ID 再用 ID 查天气,这种情况下必须串行;完全独立的两个调用才可以并发。

第三个:怎么防止用户通过提示词注入,让模型调用一个危险工具?我答:第一,注册表里做白名单,模型只能调已注册的工具,不存在的工具一律拒绝;第二,对工具参数做白名单校验,危险参数直接拦截;第三,敏感工具的每一次执行都需要人工二次确认,不默认放行。这三个问题的答案都不是临时想出来的,全是真实项目里踩过坑之后沉淀下来的边界条件。面试官问的问题其实并不可怕,可怕的是你从来没有想过这些边界。

7. 常见问题与排查技巧实录

7.1 高频问题速查表

问题根因解决方案
模型反复调用同一个工具不返回最终回答上下文缺工具结果、模型陷入循环检查回填逻辑,设置循环上限
工具参数解析失败模型输出 JSON 格式不合法去围栏、截取 JSON 文本、重试
工具名不存在提示词或 Schema 描述不清注册表校验,把错误回传模型纠正
工具结果太长导致上下文溢出工具返回内容过大截断或摘要后再回填
不需要工具时也输出空 JSON提示词缺少否定约束在输出协议里加“不需要工具则直接回答”
同一问题多轮调用结果随机temperature 过高工具调用阶段设低温
工具并发执行互相干扰共享变量冲突线程池隔离,有依赖链的调用串行化

7.2 线上系统里最容易翻车的五个细节

翻车点一:把模型响应丢掉。很多人执行完工具就直接把工具结果塞回去,但忘了把“模型刚才的输出”也放回上下文。结果模型完全不知道“自己刚准备查天气”这件事从哪来的,回答自然就飘了。解决方案很简单:在追加 tool 消息之前,先把完整的 assistant 响应 append 到messages。

翻车点二:工具描述写得太敷衍。description写“查询天气”和写“查询指定城市当前天气,返回温度、湿度、风力,适合出行建议场景”,模型对工具的理解完全不同。description 就是模型赖以为生的工具说明书,写得太泛,选错工具的概率会直线上升。

翻车点三:把 JSON 解析做成一次成功。一次成功的解析只适用于演示,真实场景必须要有多级兜底。我在线上系统里至少看到过三种不同的解析失败原因,一个多余的换行符、一个 markdown 围栏、一句多余的解释,都可能让整个请求挂掉。

翻车点四:完全不设工具执行超时。一个外部服务响应很慢时,整个模型请求会被拖慢,超时的坑往往到线上才暴露。正确做法是给工具执行包一层超时控制,超时后把“工具超时”作为结果回传给模型,让模型决定是继续等待还是换一个方案。

翻车点五:不对最终答案做校验。模型结合工具结果生成了最终回答,但回答里可能混入模型自己脑补的内容。在强合规场景,最终回答应该再走一轮规则校验,至少要做人工抽检,不能因为工具结果正确就完全信任模型的转述。

我在实际项目里三个方案都写过、都上线过,最大的体会是:Tool Use 从来不是一个“会调 API 就能写”的工具函数,它是一套完整的调度系统,难点几乎全部集中在边界条件的处理上。面试手写这个题,真正的价值不在于那几十行代码有多漂亮,而在于你能不能把主循环、容错、扩展性这些关键思考一气呵成地讲清楚。如果正在准备这道题,我建议你按这个顺序来:先把三个方案各写一个最小 Demo 跑通,再把对比选型想清楚,最后把踩过的坑整理成自己的速查表。等真正到了白板上手写的时候,你会发现这道题根本不是在考你背代码,而是在提供一个机会,让你把日积月累的工程能力展示出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询