news 2026/10/30 20:11:48

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

作者头像

张小明

前端开发工程师

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

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

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

这两年技术面试的风向变得特别快。前几年大家还在卷 Transformer 的注意力公式怎么推导、BatchNorm 的反向传播怎么算,现在面试官往桌上一靠,直接来一句:“你了解 Tool Use 吗?手写一个我看看。”我第一次听到这个要求的时候,脑子里第一反应是:这东西平时不都是调 API 吗,怎么还手写?

后来面了几轮才明白,面试官考的不是你会不会调某个现成的函数,而是你对大模型如何与外部工具交互这件事的理解深度。Tool Use,说白了就是让语言模型在生成文本的过程中,能够识别出“这个问题我需要查一下天气”“这个计算我需要调用计算器”,然后输出一个结构化的调用请求,外部系统执行完再把结果塞回给模型,模型继续生成最终回答。整个链路涉及意图识别、参数抽取、格式约束、结果回填、多轮编排,每一个环节都能看出一个工程师对系统设计的把控能力。

面试官让你手写,其实是想看三件事:第一,你能不能把非结构化的自然语言转成结构化的函数调用;第二,你懂不懂怎么设计一个稳定的解析和调度机制;第三,你有没有考虑过错误处理、并发、上下文管理这些工程问题。这三件事,恰恰是平时调包时最容易被忽略的部分。

1.2 三种方案的整体思路对比

我当时在面试现场,面试官给了我一个具体的场景:用户输入一段自然语言,系统需要判断是否需要调用工具,如果需要,就调用对应的工具函数,拿到结果后生成最终回复。工具集包括天气查询、计算器、时间查询三个基础工具。

我脑子里快速过了三种方案,并且都手写了出来。这三种方案分别是:基于提示词模板的纯文本解析方案、基于 JSON Schema 的结构化输出方案、基于函数注册与反射的调度方案。它们不是互斥的,而是代表了从简单到复杂、从脆弱到健壮的不同工程阶段。

方案核心思路优点缺点适用场景
提示词模板解析用固定格式让模型输出调用指令,正则解析实现简单,不依赖特定模型能力格式容易漂移,解析脆弱快速原型、Demo
JSON Schema 结构化输出约束模型输出 JSON,按 Schema 校验格式稳定,可校验,易扩展需要模型支持结构化输出生产环境、多工具
函数注册与反射调度工具自动注册,动态路由扩展性极强,解耦彻底实现复杂度高,调试成本大工具数量多、频繁迭代

面试官看完三种方案后,问了我一个问题:“如果线上模型突然不按格式输出了,你怎么办?”这个问题直接把我从“能跑就行”拉到了“生产可用”的层面。下面我把这三种方案的完整实现和踩坑经验逐一拆开讲。

2. 方案一:基于提示词模板的纯文本解析

2.1 核心设计思路

这个方案的核心逻辑特别朴素:我在系统提示词里告诉模型,“如果你需要调用工具,请按照[TOOL:工具名|参数名=参数值]的格式输出”。然后模型生成文本,我用正则表达式去匹配这个格式,提取出工具名和参数,执行工具,再把结果拼回对话历史,让模型生成最终回答。

为什么面试时我第一个写这个方案?因为它最能体现“最小可行”的思路。面试官想看的是你从零搭建的能力,而不是一上来就堆砌复杂架构。这个方案不需要模型支持任何特殊能力,只要它能遵循指令就行,兼容性最好。

但它的脆弱性也很明显。模型有时候会多写一个空格,有时候会把参数值用引号包起来,有时候干脆忘了格式直接输出自然语言。所以这个方案的关键不在于“写出来”,而在于“怎么让它稳定”。

2.2 提示词模板的细节设计

提示词模板不是随便写一句“请按格式输出”就完事了。我当时的模板是这样的:

SYSTEM_PROMPT = """你是一个智能助手,可以使用以下工具: 1. get_weather(city: str) - 查询指定城市的天气 2. calculate(expression: str) - 计算数学表达式 3. get_time() - 获取当前时间 当你需要调用工具时,必须严格按照以下格式输出,不要添加任何其他内容: [TOOL:工具名|参数名1=参数值1|参数名2=参数值2] 例如: 用户:北京天气怎么样? 助手:[TOOL:get_weather|city=北京] 当你不需要调用工具时,直接回答用户问题。 重要规则: - 工具调用格式必须独占一行 - 参数值中不要包含竖线符号 - 如果不需要参数,只写工具名即可 """

这个模板里有几个细节是踩过坑才加上的。第一,必须强调“独占一行”,否则模型可能把工具调用和自然语言混在一起,正则匹配时容易误伤。第二,必须说明参数值中不能包含竖线,因为竖线是分隔符,如果参数值里有竖线,解析就会错位。第三,给出具体示例,模型对示例的遵循程度远高于纯文字描述。

2.3 解析器的实现与容错

解析器的核心是一个正则表达式,但真正让它稳定的是容错逻辑。我当时的实现是这样的:

import re TOOL_PATTERN = re.compile(r'\[TOOL:(\w+)((?:\|\w+=[^\|\]]+)*)\]') def parse_tool_call(text): match = TOOL_PATTERN.search(text) if not match: return None tool_name = match.group(1) params_str = match.group(2) params = {} if params_str: for pair in params_str.strip('|').split('|'): if '=' in pair: key, value = pair.split('=', 1) params[key.strip()] = value.strip() return {"tool": tool_name, "params": params}

这个正则看起来简单,但有几个地方是反复调整过的。[^\|\]]+这个字符类确保了参数值不会跨过竖线和右括号,避免了贪婪匹配导致的错位。split('=', 1)确保参数值里如果包含等号也不会被错误分割。

容错方面,我加了三层保护。第一层是格式清洗:如果模型输出了[TOOL: get_weather | city = 北京 ]这种带空格的格式,先用re.sub把空格去掉再匹配。第二层是工具名模糊匹配:如果模型写的是weather而不是get_weather,用一个别名映射表兜底。第三层是参数缺失检查:如果工具需要参数但解析结果为空,直接返回错误提示让模型重新生成。

注意:这个方案最大的坑是模型“自作聪明”。有一次测试时,模型把工具调用写成了[TOOL:get_weather|city=北京|unit=摄氏度],但我的工具函数只接受city参数,多出来的unit直接导致调用失败。后来我在解析器里加了一个参数白名单校验,只保留工具函数签名中定义的参数。

2.4 这个方案的适用边界

说实话,这个方案我只推荐用在两种场景:一是快速验证想法,二是模型能力受限、不支持结构化输出的时候。一旦工具数量超过五个,或者参数变得复杂(比如嵌套对象、数组),这个方案的维护成本就会急剧上升。面试时我写完这个方案,面试官点了点头说“能跑”,然后紧接着问:“如果工具有一百个呢?”这就是在引导我进入第二个方案。

3. 方案二:基于 JSON Schema 的结构化输出

3.1 从“求模型遵守”到“约束模型输出”

第一个方案的本质是“求”模型遵守格式,模型心情好就遵守,心情不好就乱来。第二个方案的本质是“约束”模型输出,利用模型本身的结构化输出能力,让它在解码阶段就只能生成符合 JSON Schema 的内容。

这个方案的核心变化在于:不再用正则去解析自由文本,而是让模型直接输出一个 JSON 对象,这个对象包含tool_name和parameters两个字段。然后我用 JSON Schema 去校验这个对象,校验通过才执行工具。

为什么这个方案更稳?因为结构化输出是在解码层面做约束的,模型生成每一个 token 时,都会受到 Schema 的限制,不可能生成不符合格式的内容。这就像给模型戴了一个“格式紧箍咒”,它想跑偏都跑不了。

3.2 JSON Schema 的定义与工具描述

Schema 的定义直接决定了模型能否正确理解工具的使用方式。我当时的 Schema 是这样的:

TOOL_CALL_SCHEMA = { "type": "object", "properties": { "tool_name": { "type": "string", "enum": ["get_weather", "calculate", "get_time"], "description": "要调用的工具名称" }, "parameters": { "type": "object", "description": "工具调用参数", "additionalProperties": True }, "reasoning": { "type": "string", "description": "调用该工具的原因简述" } }, "required": ["tool_name", "parameters"] }

这里有几个设计决策值得展开。第一,tool_name用了enum约束,这样模型只能从预定义的工具列表里选,避免了拼写错误或幻觉工具名。第二,parameters用了additionalProperties: True,因为不同工具的参数结构不同,没法用一个统一的 Schema 约束,所以放到运行时再做具体校验。第三,加了reasoning字段,这不是必须的,但它有两个好处:一是让模型的思考过程可追溯,二是相当于让模型“先想再做”,实测能提升工具选择的准确率。

工具描述部分,我用了类似 OpenAI Function Calling 的格式:

TOOLS = [ { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } }, { "name": "calculate", "description": "计算数学表达式,支持加减乘除和括号", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式"} }, "required": ["expression"] } } ]

3.3 运行时校验与参数绑定

拿到模型输出的 JSON 后,不能直接拿去调用工具,必须做运行时校验。我当时的校验逻辑分三步:

import json def validate_and_execute(raw_output, tools_registry): try: data = json.loads(raw_output) except json.JSONDecodeError: return {"error": "输出不是合法 JSON"} tool_name = data.get("tool_name") if tool_name not in tools_registry: return {"error": f"未知工具: {tool_name}"} tool_def = tools_registry[tool_name] params = data.get("parameters", {}) # 校验必填参数 for required in tool_def["parameters"].get("required", []): if required not in params: return {"error": f"缺少必填参数: {required}"} # 校验参数类型 for key, value in params.items(): expected_type = tool_def["parameters"]["properties"].get(key, {}).get("type") if expected_type == "string" and not isinstance(value, str): return {"error": f"参数 {key} 类型错误,期望 string"} if expected_type == "number" and not isinstance(value, (int, float)): return {"error": f"参数 {key} 类型错误,期望 number"} return tool_def["function"](**params)

这段代码里,类型校验是最容易被忽略但最重要的部分。模型有时候会把数字输出成字符串,比如"expression": "1+1"没问题,但"city": 123就明显不对。如果不做类型校验,直接传给工具函数,轻则报错,重则产生难以排查的 bug。

提示:additionalProperties: True虽然灵活,但也意味着模型可能塞入工具不认识的参数。我在实际执行前会做一次参数过滤,只保留工具函数签名中定义的参数,多余的直接丢弃并记录日志,方便后续优化提示词。

3.4 多工具场景下的路由策略

当工具数量增多时,把所有工具描述都塞进提示词会导致 token 消耗暴涨,而且模型的选择准确率会下降。我当时的优化策略是两阶段路由:第一阶段用一个轻量级的分类模型或关键词匹配,把用户问题路由到某个工具类别;第二阶段只把该类别的工具描述传给模型,让它做精确选择。

比如用户问“北京天气怎么样”,第一阶段识别出“天气”关键词,路由到天气类工具;第二阶段只传get_weather和get_forecast两个工具描述,模型的选择空间从一百个缩小到两个,准确率自然提升。

这个策略的代价是需要维护一个分类映射表,但对于工具数量超过二十个的系统来说,这个代价是值得的。面试时我提到这个优化,面试官追问了一句:“分类错了怎么办?”我的回答是加一个兜底策略:如果模型在第二阶段输出的工具名不在当前类别中,就回退到全量工具列表重新选择一次。

4. 方案三:基于函数注册与反射的调度

4.1 让工具“自己注册自己”

前两个方案都有一个共同问题:工具的定义、描述、执行逻辑是分散的,新增一个工具需要改三四个地方。第三个方案的核心思路是用装饰器实现工具自动注册,让工具函数自己声明自己的元信息,调度器通过反射动态发现和调用。

这个方案在面试时是最加分的,因为它体现了对“开闭原则”的理解——对扩展开放,对修改关闭。新增工具时,只需要写一个函数加一个装饰器,不需要动调度器的任何代码。

TOOL_REGISTRY = {} def tool(name=None, description=None): def decorator(func): tool_name = name or func.__name__ TOOL_REGISTRY[tool_name] = { "name": tool_name, "description": description or func.__doc__ or "", "function": func, "signature": inspect.signature(func) } return func return decorator @tool(description="查询指定城市的实时天气") def get_weather(city: str) -> str: return f"{city}今天晴,气温25度" @tool(description="计算数学表达式") def calculate(expression: str) -> str: return str(eval(expression))

这个装饰器的精妙之处在于,它用inspect.signature自动提取了函数的参数签名,这样在生成工具描述时就不需要手动写 JSON Schema 了。参数名、类型注解、默认值都能自动获取,大大减少了维护成本。

4.2 动态生成工具描述

有了注册表之后,工具描述的生成就可以自动化:

import inspect def generate_tool_prompt(): lines = ["你可以使用以下工具:\n"] for name, info in TOOL_REGISTRY.items(): sig = info["signature"] params = [] for param_name, param in sig.parameters.items(): annotation = param.annotation.__name__ if param.annotation != inspect.Parameter.empty else "any" params.append(f"{param_name}: {annotation}") param_str = ", ".join(params) lines.append(f"- {name}({param_str}): {info['description']}") return "\n".join(lines)

这段代码的输出大概是这样的:

你可以使用以下工具: - get_weather(city: str): 查询指定城市的实时天气 - calculate(expression: str): 计算数学表达式

这样一来,新增工具时只需要写函数和装饰器,提示词会自动更新。面试官看到这里的时候,明显眼睛亮了一下,因为这说明我考虑到了系统的可维护性。

4.3 反射调用的安全边界

反射调用最大的风险是参数注入。如果模型输出了工具函数不认识的参数,直接**params展开会抛TypeError。更危险的是,如果工具函数内部有敏感操作,而模型被诱导调用了不该调用的工具,后果可能很严重。

我的处理方式是加一层参数过滤和权限校验:

def safe_invoke(tool_name, params): if tool_name not in TOOL_REGISTRY: return {"error": f"工具 {tool_name} 未注册"} tool_info = TOOL_REGISTRY[tool_name] sig = tool_info["signature"] # 过滤掉签名中不存在的参数 valid_params = {} for key, value in params.items(): if key in sig.parameters: valid_params[key] = value # 检查必填参数 for param_name, param in sig.parameters.items(): if param.default == inspect.Parameter.empty and param_name not in valid_params: return {"error": f"缺少必填参数: {param_name}"} try: result = tool_info["function"](**valid_params) return {"result": result} except Exception as e: return {"error": f"工具执行失败: {str(e)}"}

这里的关键是参数白名单过滤和异常捕获。白名单过滤防止了意外参数导致的崩溃,异常捕获防止了工具内部错误向上传播导致整个对话中断。另外,对于涉及文件操作、网络请求、数据库写入的工具,我还会加一层权限校验,确保只有特定场景下才能调用。

4.4 三种方案的融合与降级策略

实际生产中,这三种方案不是三选一,而是可以融合的。我的做法是:默认走方案三的注册调度,但保留方案二的 Schema 校验作为中间层,方案一的正则解析作为降级兜底。

具体来说,正常流程是:模型输出 JSON → Schema 校验 → 反射调用。如果模型输出的不是合法 JSON(比如某些模型在特定输入下会退化),就触发降级,用正则去尝试提取工具调用。如果正则也提取不到,就返回一个友好的错误提示,让模型重新生成。

这个降级策略在面试时被面试官评价为“有生产意识”。因为线上环境什么情况都可能发生,模型版本更新、提示词被注入、网络抖动导致输出截断,任何一个环节出问题都需要有兜底方案。

5. 面试官追问环节:那些容易被忽略的工程细节

5.1 多轮对话中的工具调用状态管理

面试官问的第二个问题是:“如果用户连续问了三个问题,第一个需要调天气,第二个需要调计算器,第三个不需要调工具,你怎么管理状态?”

这个问题的核心是对话历史的结构化存储。不能简单地把所有消息拼成一个字符串,而是要把工具调用和工具结果作为独立的消息类型存进去。我当时的做法是维护一个消息列表,每条消息有role和content两个字段,工具调用和工具结果分别用tool_call和tool_result角色标记。

messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": "北京天气怎么样"}, {"role": "tool_call", "content": {"tool": "get_weather", "params": {"city": "北京"}}}, {"role": "tool_result", "content": "北京今天晴,气温25度"}, {"role": "assistant", "content": "北京今天天气晴朗,气温25度,适合外出。"} ]

这样管理的好处是,每一轮工具调用的输入输出都可追溯,方便调试和审计。另外,在生成下一轮回复时,可以把最近几轮的工具调用记录一起传给模型,让它知道“我刚才已经查过天气了”,避免重复调用。

5.2 工具调用失败时的重试与降级

工具调用失败分两种情况:一种是模型输出的格式不对,另一种是工具执行本身出错。这两种情况的处理策略完全不同。

格式不对时,我会把错误信息拼成一条系统消息塞回给模型,让它重新生成。比如:“你上次的输出格式不正确,请严格按照 JSON Schema 输出。”实测下来,模型在收到明确错误提示后,第二次生成的成功率在百分之九十以上。

工具执行出错时,比如天气 API 超时了,我不会让模型重试,而是直接把错误信息作为工具结果返回给模型,让它基于错误信息生成一个友好的回复。比如:“抱歉,天气查询服务暂时不可用,请稍后再试。”这样用户体验比直接报错好得多。

注意:重试次数一定要设上限,我一般设两次。超过两次就放弃工具调用,直接让模型基于已有信息回答。否则模型可能陷入“格式错误→重试→格式错误”的死循环,浪费 token 和时间。

5.3 并发场景下的工具调用编排

如果用户一个问题需要同时调用多个工具,比如“帮我查一下北京和上海的天气,然后算一下温差”,这就涉及并发编排。我的做法是让模型一次性输出多个工具调用,然后用asyncio.gather并发执行。

import asyncio async def execute_tools(tool_calls): tasks = [asyncio.create_task(async_invoke(call)) for call in tool_calls] results = await asyncio.gather(*tasks, return_exceptions=True) return results

这里的关键是异常隔离:一个工具失败不能影响其他工具的执行。return_exceptions=True确保了即使某个任务抛异常,gather也会等所有任务完成,然后把异常作为结果返回。这样我可以在所有结果都拿到之后,统一决定哪些成功、哪些失败,再拼成完整的工具结果返回给模型。

面试时我提到这个点,面试官追问:“如果两个工具之间有依赖关系呢?比如第二个工具的输入是第一个工具的输出。”这就涉及串行编排了。我的回答是:在工具描述里加一个depends_on字段,调度器根据依赖关系构建 DAG,然后按拓扑顺序执行。不过这种场景比较少见,大部分情况下模型会分多轮来调用,而不是一次性输出有依赖的多个调用。

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

6.1 测试环境与评测指标

为了验证三种方案的实际表现,我搭了一个小型的测试环境,用同一套工具集和同一批测试用例,分别跑三种方案。测试用例包括五十条单工具调用、三十条多工具调用、二十条不需要工具调用的纯对话,以及十条边界情况(比如参数缺失、工具名拼写错误、输出格式混乱)。

评测指标有三个:工具选择准确率(选对工具的比例)、参数抽取准确率(参数名和参数值都正确的比例)、端到端成功率(从用户输入到最终回复完全正确的比例)。

6.2 实测数据与结果分析

方案工具选择准确率参数抽取准确率端到端成功率平均延迟
提示词模板解析82%76%71%1.2s
JSON Schema 结构化94%91%89%1.5s
函数注册反射调度93%90%88%1.6s

数据很直观:方案一在简单场景下够用,但一旦涉及多工具或复杂参数,准确率就明显下降。方案二和方案三的表现接近,但方案三在扩展性上完胜。延迟方面,方案二和方案三因为多了 Schema 校验和反射调用,比方案一多了零点几秒,但在可接受范围内。

有意思的是,方案三的端到端成功率比方案二略低一点,排查后发现是因为反射调用时参数过滤太严格,有些模型输出的参数名和函数签名不完全一致(比如模型输出city_name而函数签名是city),导致参数被过滤掉。后来加了一个参数别名映射表,这个问题就解决了。

6.3 什么场景选什么方案

基于实测数据,我的选型建议是这样的:

  • 快速原型、个人项目、模型能力受限:选方案一,够用就行,别过度设计。
  • 生产环境、工具数量中等、需要稳定输出:选方案二,Schema 校验能挡住大部分格式问题。
  • 工具数量多、频繁迭代、团队协作:选方案三,注册机制让新增工具的成本降到最低。
  • 高可用要求、不能接受单点故障:三种方案融合,方案三为主,方案二校验,方案一兜底。

面试时我把这个选型建议说出来,面试官点了点头说:“你不是只会写代码,还知道什么时候不该写代码。”这句话我记了很久,因为它点出了一个工程师的核心能力——技术选型不是选最复杂的,而是选最合适的。

7. 手写 Tool Use 的避坑清单与实操心得

7.1 提示词设计的五个关键点

第一,工具描述要具体。不要写“查询天气”,要写“查询指定城市的实时天气,返回温度和天气状况”。描述越具体,模型选错工具的概率越低。

第二,参数说明要带类型和示例。比如city: str, 例如"北京",这样模型知道该传什么格式的值。

第三,格式约束要放在系统提示词的最后。模型的注意力对末尾内容更敏感,把格式要求放在最后,遵循率更高。

第四,给出正例和反例。正例告诉模型该怎么做,反例告诉模型不该怎么做。实测下来,加了反例之后格式错误率下降了将近一半。

第五,不要一次给太多工具。工具数量超过十个之后,模型的注意力会被分散,选择准确率明显下降。如果工具确实很多,用两阶段路由或者工具分组。

7.2 解析器的容错设计原则

解析器的核心原则是宽进严出。输入侧尽量宽容,各种奇怪的格式都尝试解析;输出侧严格校验,不符合工具签名的参数一律拒绝。

具体来说,输入侧要做四件事:去除多余空白、统一大小写、处理引号包裹、兼容不同的分隔符。输出侧要做三件事:校验工具名是否存在、校验必填参数是否齐全、校验参数类型是否匹配。

还有一个容易被忽略的点:解析失败时要给出明确的错误信息。不要只返回None,要返回“格式错误:未找到工具调用标记”或“参数错误:缺少 city 参数”。这样模型在重试时才知道该改哪里。

7.3 工具函数的编写规范

工具函数本身也有讲究。第一,函数名要语义化,get_weather比gw好,calculate比calc好。第二,参数名要直观,city比c好,expression比expr好。第三,返回值要结构化,尽量返回字典或 JSON 字符串,方便后续处理。第四,异常要捕获,工具函数内部不要抛异常,而是返回错误信息,让调度器统一处理。

第五,也是最重要的一点:工具函数要幂等。同一个请求调用两次,结果应该一样。如果工具有副作用(比如写数据库),要在描述里明确说明,并且加确认机制。

7.4 我踩过的三个真实坑

第一个坑:模型把工具调用写在了思考过程里。有一次模型输出“我需要调用 get_weather 来查询天气 [TOOL:get_weather|city=北京]”,正则匹配到了工具调用,但前面的自然语言也被当成了回复的一部分,导致用户看到了奇怪的中间过程。后来我在解析前先做了一次“思考过程剥离”,把工具调用之前的内容截掉。

第二个坑:参数值里包含特殊字符。用户问“帮我算一下 3*(2+1)”,模型输出[TOOL:calculate|expression=3*(2+1)],但我的正则把括号当成了参数值的边界,导致解析失败。后来把参数值的字符类放宽,允许括号和运算符,只排除竖线和右方括号。

第三个坑:多轮对话中工具结果被覆盖。有一次用户连续问了两个问题,第一个问题的工具结果在第二轮被新的工具结果覆盖了,导致模型在生成最终回复时丢失了上下文。后来我把工具结果和对话历史分开存储,每次生成时按时间顺序拼接,问题就解决了。

这三个坑在面试时我都讲了,面试官的评价是:“能讲出踩坑经历的人,才是真正动手写过的人。”这句话反过来也成立:如果你在面试中能主动讲出踩坑经历,面试官会默认你有实战经验,这比背八股文管用得多。

8. 从手写 Tool Use 延伸到 Agent 编排的思考

8.1 Tool Use 只是 Agent 的冰山一角

手写 Tool Use 的过程中,我逐渐意识到这只是 Agent 系统的一个基础模块。一个完整的 Agent 还需要规划能力(把复杂任务拆成子任务)、记忆能力(记住历史交互和中间结果)、反思能力(评估自己的输出并修正)。Tool Use 解决的是“怎么调用外部能力”,而规划解决的是“什么时候调用什么工具”,记忆解决的是“调用过什么、结果是什么”。

面试时面试官最后问了一个开放性问题:“如果让你设计一个能自动完成复杂任务的 Agent,你会怎么架构?”我的回答是分层架构:最上层是规划器,负责把用户目标拆成步骤;中间层是调度器,负责每一步的工具选择和调用;最下层是执行器,负责实际执行工具并返回结果。三层之间通过结构化消息通信,每一层的输出都经过校验再传给下一层。

8.2 工具生态的标准化趋势

目前工具调用的格式还没有统一标准,不同平台有不同的约定。但我观察到几个趋势:一是Schema 化,工具描述越来越倾向于用 JSON Schema 表达;二是注册化,工具通过注册机制动态发现,而不是硬编码在提示词里;三是权限化,工具调用需要经过权限校验,防止越权操作。

这些趋势的背后逻辑是一致的:让工具调用从“手工作坊”走向“工业化”。手工作坊阶段,每个工具都是手工打磨、手工接入;工业化阶段,工具是标准化组件,即插即用。手写 Tool Use 的意义就在于,你亲手经历过手工作坊的阶段,才能理解工业化阶段每个设计决策的来龙去脉。

8.3 给准备面试的朋友的实操建议

如果你正在准备面试,我建议你至少手写一遍这三种方案。不用写得多完美,但要把核心逻辑跑通。具体来说:

第一,准备一个最小可用的工具集,三个工具就够了:一个无参数的(比如获取时间),一个单参数的(比如查天气),一个多参数的(比如计算器)。

第二,把三种方案的代码都敲一遍,不要复制粘贴,手敲才能发现细节问题。特别是正则表达式和 Schema 校验,手敲一遍比看十遍都管用。

第三,准备一个踩坑故事,面试时主动讲出来。面试官不怕你踩坑,怕的是你踩了坑不知道为什么踩、不知道怎么解决。

第四,想清楚降级策略,这是区分“能跑”和“生产可用”的关键。面试官问“如果模型不按格式输出怎么办”的时候,你要能立刻说出至少两种兜底方案。

第五,了解工具调用的性能瓶颈。工具调用的延迟主要来自模型生成和工具执行两部分,优化方向包括减少工具描述长度、并发执行无依赖的工具、缓存高频调用的结果。

面试手写 Tool Use 这件事,表面考的是编码能力,实际考的是系统设计思维。你能写出三种方案,说明你知道不同方案的优劣;你能讲出踩坑经历,说明你真正动手做过;你能说出降级策略,说明你有生产意识。这三点加起来,比任何八股文都更有说服力。

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

Unity WebGL数据交互:ASP.NET Core Web API连接SqlServer实战详解

我在做一款浏览器端的3D可视化项目时,遇到一个绕不开的需求:玩家的存档记录、操作日志和实时排行榜数据要落到数据库里。项目本身是Unity WebGL打包后运行在浏览器中的,数据后端选定的是ASP.NET Web API,数据库用的是SqlServer。这…

作者头像 李华
网站建设 2026/10/30 20:11:42

从0到1构建PS5状态检测工具:多机型兼容与外设排查实战

开头先交代一下背景:我手上长期有三台不同型号的主机,每次做性能对比、外设适配测试,都要在机器之间来回切换,读个温度、看个固件版本都得进系统设置一层层翻。来回折腾几次之后,我决定写一个能统一读取各型号主机状态…

作者头像 李华
网站建设 2026/10/26 16:50:02

海洋浮标方向谱估计:EMLM、EMEM与BDM算法原理及工程实践

简介:一份面向海洋工程、海洋物理与海洋气象研究者的风浪方向谱计算资源,聚焦EMLM、EMEM、BDM三种方法在波浪谱估计与数值计算中的实现与应用,可帮助解决海上结构设计、船舶航行安全及海洋能开发中的波浪环境分析问题。压缩包共45个文件&…

作者头像 李华
网站建设 2026/10/25 3:16:19

fcoder 需求文档评审技能详解:功能点覆盖性检查与结构化评审输出

后端Web框架 【免费下载链接】fountain 一个用于服务器应用开发的综合工具库。 - 零配置文件 - 环境变量和命令行参数配置 - 约定优于配置 - 深刻利用仓颉语言特性 - 只需要开发动态链接库,fboot负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景 项目地址&am…

作者头像 李华
网站建设 2026/10/25 3:14:47

SpringCloud Gateway流式传输优化实践

1. 问题背景与现象分析最近在基于SpringCloud Gateway构建的微服务架构中接入大模型API时,遇到了一个典型的技术痛点:当后端服务采用流式输出(Streaming)方式返回大模型生成内容时,经过网关层会出现明显的"阻塞&q…

作者头像 李华
网站建设 2026/10/21 0:46:36

GPU推理性能优化:专用Kernel、CUDA Graphs与多卡部署实战

做推理侧算子优化三年多,最深的体会是:训练阶段能用得顺手的 Kernel,放到推理场景里往往不够用;而推理真正需要的 Kernel,又常常长得不像“通用算子库”里会有的样子。前两篇聊完了性能剖析和通用优化手段,…

作者头像 李华