☰
手写 Tool Use 全复盘:从提示词正则到原生 Function Calling 的三种实现方案
2026/10/22 10:13:41 网站建设 项目流程

前几天面试,面试官突然甩了一道题:让我手写一个 Tool Use。说实话,第一反应不是紧张,而是觉得这道题特别会挑人——背过 API 示例的人能说出"函数调用就是给模型传 tools 参数",但真让你从 0 写出来,你会发现要处理的全是协议细节、边界情况和小概率失败。Tool Use(工具调用)这两年几乎是 Agent 开发的底座技能,它的核心价值一句话就能讲完:让只会生成文本的大模型,能够"决定"去调用外部函数,并把结果带回来继续推理。这篇文章我会把当天现场写的 3 种方案完整复盘一遍,从最朴素的提示词加正则提取,到结构化输出约束,再到原生工具调用加代理循环,把每种方案的思路、代码、局限和适用场景都摊开讲。不管你是准备 Agent 方向面试,还是要自己从零搭一套工具调用模块,这套"从土到讲究"的演进路线都值得过一遍。

1. Tool Use 到底在解决什么问题

1.1 模型是"参谋",代码才是"手脚"

很多教程把 Tool Use 解释成"让模型能联网、能查数据库、能操作 API",这个说法没错,但只说了一半。Tool Use 真正解决的是两件事:第一,大模型的知识和上下文有边界,训练完成那一刻它的世界就冻结了,它不知道今天的实时天气、不知道某个服务的当前状态,更无法替用户执行下单这种操作;第二,模型本身不是执行者,它是一个意图生成器。它能用自然语言表达"我想查一下订单状态",但真正发起查询、解析响应、把结果整理好再展示给用户的,只能是你写的代码。

所以一个最朴素的类比是:模型像一个"只能动嘴的参谋",它知道该做什么、该调什么工具、该用什么参数,但没有手没有脚;你的代码是"手脚",负责把参谋的指令变成真实动作。Tool Use 就是把这两者连接起来的那套流程和协议。想明白这一点,后面所有方案都很好理解了——你做的所有事情,无非就是三件事:让模型知道有什么工具可用,把模型的调用意图解析出来,然后在你的代码里安全地执行。

1.2 一次完整工具调用的七个环节

把工具调用的完整闭环拆开看,一共是 7 个环节:

  1. 定义工具 Schema:给每个工具写清楚名称、功能描述、参数结构。
  2. 把 Schema 注入消息:让模型"知道"当前任务里有哪些工具可以选。
  3. 模型产出调用意图:模型在回复中返回一个包含工具名和参数的结构化结果。
  4. 解析与校验:检查工具名是否在白名单里、参数是否合法完整。
  5. 执行工具:真正去调用接口、查询数据库、发送指令。
  6. 结果回填:把工具的返回结果以观察消息的形式交回给模型。
  7. 循环直到结束:模型基于观察结果继续推理,或输出最终答案。

面试让手写 Tool Use,基本上就是看你能不能把这 7 步的每一环都落到代码里,而不是背一个接口调用示例。尤其第 4 步和第 6 步,是大多数人容易漏掉的——很多人在演示里只写了"模型返回一个函数名,我执行一下,完事",完全没考虑解析失败怎么办、工具异常怎么办、多轮调用怎么回填,这就是为什么同样的题,有人能拿方案讲一刻钟,有人两分钟就讲完了。

1.3 面试官到底在考察什么

在这道题上,面试官真正想看的不是你记得多少 API 参数名,而是三件事:

第一,你有没有系统的建模思维。工具调用不是"一个接口、一次调用",而是一个有状态的循环,涉及消息历史的维护、回合数的控制、上下文的清理。第二,你有没有失败处理的肌肉记忆。模型输出永远是概率性的,它可能把参数名写错、可能把 JSON 截断、可能调用了不存在的工具,面对这些情况你的程序能不能优雅恢复,这直接反映工程经验。第三,你有没有安全边界意识。工具执行意味着你的代码会被外部输入间接驱动,如果你不假思索地把用户输入拼进命令去执行,那这个功能上线就是事故现场。

我在现场给出的 3 种方案,恰好就是沿着"能力从低到高、可靠性从弱到强"的路线走的,每一种方案都是对前一种的补强。

2. 方案一:提示词注入 + 正则提取(最朴素的原型)

2.1 思路:把工具说明写进提示词,剩下交给解析

第一种方案最直白,也是理解 Tool Use 本质的最佳切入点。思路就是把工具的名称、描述、参数结构用固定格式写进 System Prompt,然后要求模型"当需要调用工具时,只输出一个 JSON 对象"。代码这边做的事情只有两件:从模型回复里把 JSON 抠出来,然后去执行。

这个方案没有任何取巧,完全依赖模型的指令跟随能力和文本生成规律,但它的好处也很明显:不要求模型有原生工具调用能力,任何能输出文字的模型都能跑。所以它特别适合三类场景:快速验证想法的 PoC、内部小工具、或者你手上的模型确实不支持函数调用协议时兜底用。

2.2 最小实现完整代码与解析

先看工具描述部分,我一般把每个工具都整理成一个字典,后续几种方案都能复用:

TOOL_DESCRIPTIONS = [ { "name": "get_weather", "description": "查询某城市的实时天气,返回温度、天气状况和风速", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } }, { "name": "get_stock_price", "description": "查询指定股票的最新成交价格", "parameters": { "type": "object", "properties": { "symbol": {"type": "string", "description": "股票代码"} }, "required": ["symbol"] } }, ] def build_tool_prompt(): lines = [ "你是一个会调用工具的助手。需要调用工具时,只输出一个 JSON,格式如下:", '{"name": "<工具名>", "arguments": {<参数对象>}}', "可用工具:", ] for t in TOOL_DESCRIPTIONS: desc = t["description"] params = json.dumps(t["parameters"], ensure_ascii=False) lines.append(f"- {t['name']}: {desc},参数定义: {params}") return "\n".join(lines)

解析这一步是最容易出问题的。实际运行中,模型返回的内容往往不是干净的 JSON,最常见的两种情况是:把 JSON 包在 markdown 的 ```json 代码块里,以及在 JSON 前后夹带了几句解释。所以解析不能只做一次简单的 json.loads,要做一个容错提取:

def extract_robust(text): # 先把代码块围栏去掉 text = re.sub(r"```(?:json)?", "", text).strip() # 取第一个 { 到最后一个 } 之间的内容,兼容前后有废话 start, end = text.find("{"), text.rfind("}") if start == -1 or end == -1: return None try: return json.loads(text[start:end + 1]) except json.JSONDecodeError: return None

接下来就是主循环了。我当初写的时候没有只做"一次调用就结束",而是允许模型在拿到工具结果后继续,最多循环 3 轮:

def tool_use_naive(user_input: str) -> str: messages = [ {"role": "system", "content": build_tool_prompt()}, {"role": "user", "content": user_input}, ] max_rounds = 3 for _ in range(max_rounds): reply = llm_text(messages, temperature=0) # 工具调用场景温度一定要低 call = extract_robust(reply) if call is None: return reply # 没有工具调用意图,直接当最终答案返回 result = execute_tool(call["name"], call["arguments"]) messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"工具返回:{result}\n请基于结果继续推理并回答。"}) return messages[-1]["content"]

这里有个容易忽略的细节:我把模型上一轮的输出和后端返回结果都追加到 messages 里,让模型能感知对话历史。很多人做简单实现时只把"工具结果"拼成一句话塞进下一轮,短期能跑,但遇到多工具连续调用时,模型会丢失自己之前说过的上下文,很快绕晕。

2.3 这个方案的坑与边界

方案一属于"能用,但你得做好它随时翻车"的阶段。我实际踩过的坑有三个:

第一,模型不按格式输出。你明明说了"只输出 JSON",它偏要先来一段"好的,我来帮您查询",此时 extract_robust 虽然能把 JSON 抠出来,但那些废话会污染下一轮的消息历史。我的对策是约定一个特殊 token 表示"不需要调用工具",比如让模型输出一个固定的 NO_TOOL 字符串,解析时优先处理这种情况,而不是把非 JSON 回复一律当成最终答案。

第二,JSON 参数是嵌套结构时,朴素正则容易误判。上面 extract_robust 用的是"第一个 { 到最后一个 }",对嵌套 JSON 是安全的;但如果你图省事用了匹配最内层花括号的正则,遇到 arguments 里面还有对象时就会拆出碎片。建议直接用起止括号截取,别用贪婪匹配。

第三,工具调用的成功率完全依赖模型。模型能力越强越听话,成功率越高;模型能力弱时,你会看到它把参数名改了、把枚举值写错、甚至自己发明一个不存在的工具名。这就是为什么我坚持在 System Prompt 里把参数定义原文用 json.dumps 打出来——让模型照着抄,而不是靠它"理解后转述"。

3. 方案二:结构化输出约束 + 字段级校验(更可靠的手写实现)

3.1 思路升级:从"看运气"到"给模型一条硬轨道"

方案一最大的问题在于,模型输出格式没有被强约束,成功率靠的是提示词威慑。方案二的思路转变很关键:把"允许模型自由发挥再解析"改成"让输出结构变成一个可验证的契约"。

具体做法是定义一层输出契约(Output Contract),它本身也是一个 JSON Schema,但约束的不是工具参数,而是"模型这次回复的整个结构"。比如我让模型必须返回一个包含 thought 和 calls 两个字段的对象。如果模型服务端支持结构化输出或 JSON Mode,就直接把这份契约传给服务端,让解码阶段就保证格式正确;如果不支持,也可以把契约写进提示词,配合字段级校验兜底。

这背后的道理值得多说一句:模型生成文本本质是逐个 token 采样,如果有解码层面的约束,让它只能在合法结构里生成,那错误率会降一个数量级。市面上主流模型服务最近都把这类能力包装成了 JSON Mode 或结构化输出,但底层原理就是约束解码。你自己手写的时候,如果正好用的模型支持这个能力,一定要用,这是性价比最高的可靠性提升手段。

3.2 输出契约 + 字段级校验的代码结构

定义输出契约:

OUTPUT_CONTRACT = { "type": "object", "properties": { "thought": { "type": "string", "description": "解释这一步为什么要调用工具,或说明不需要调用工具的原因" }, "calls": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string", "description": "工具名,必须是可用列表之一"}, "arguments": {"type": "object", "description": "工具参数对象"} }, "required": ["name", "arguments"] } } }, "required": ["thought", "calls"] }

这层包裹的价值在于,它强迫模型先给出推理依据,再给调用计划。我见过很多实现只让模型输出工具调用,结果模型在需要回答"根本不需要工具"的问题时不知所措,或者连续调用多轮工具却不说为什么。加了 thought 字段后,模型的每一步都有迹可循,调试时你能直接看到它的决策链路。

执行和校验那边,我把工具注册表做成一个统一结构,并且加入了字段级校验:

TOOL_REGISTRY = {} def register_tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] = {"func": func, "description": description, "parameters": parameters} return func return decorator def check_args(schema, args): """对参数做基本类型和必填项检查,不依赖外部库""" props = schema.get("properties", {}) required = schema.get("required", []) for key in required: if key not in args: raise ValueError(f"缺少必填参数: {key}") for key, value in args.items(): if key in props: expected = props[key].get("type") if expected == "string" and not isinstance(value, str): raise ValueError(f"参数 {key} 应为字符串") if expected == "number" and not isinstance(value, (int, float)): raise ValueError(f"参数 {key} 应为数字") return True def validate_and_execute(parsed): observations = [] if not isinstance(parsed.get("calls"), list): return ["错误:calls 字段必须是数组"] for i, call in enumerate(parsed["calls"]): name = call.get("name") args = call.get("arguments") or {} if name not in TOOL_REGISTRY: observations.append(f"调用 {i} 失败:未知工具 {name},请从可用列表中选择") continue try: check_args(TOOL_REGISTRY[name]["parameters"], args) result = TOOL_REGISTRY[name]["func"](**args) observations.append(f"调用 {i} 成功:{result}") except Exception as exc: observations.append(f"调用 {i} 失败:{exc}") return observations

这段代码里面,工具名白名单校验和参数类型校验是最核心的两道防线。工具名不在注册表里,绝不能执行;参数类型不对,也绝不能执行。你或许会觉得这是常识,但真正上线的时候,模型偶尔会生成一个合法 JSON 却塞进非法参数,比如一个接受整数枚举的工具被传了字符串,这时候如果没有字段级校验,你的后端函数就会收到一堆莫名其妙的异常。

3.3 让错误观察参与自纠正

方案二比方案一强的地方,不只是解析更稳,而是它把"执行失败"当成正常事件来处理。注意上面 validate_and_execute 返回的 observations 里,失败和成功都被统一转换成了观察文本,这些观察文本会原样回填给模型:

def tool_use_structured(user_input: str) -> str: messages = build_messages(user_input, output_contract=OUTPUT_CONTRACT) for _ in range(4): reply = llm_text(messages, temperature=0, response_format=OUTPUT_CONTRACT) parsed = extract_robust(reply) if not parsed or not parsed.get("calls"): return parsed.get("thought", reply) observations = validate_and_execute(parsed) messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": "\n".join(observations)}) return messages[-1]["content"]

当你把"调用失败:未知工具 xxx,请从可用列表中选择"这类信息回传给模型时,模型下一轮很可能会自我纠正,老老实实选一个合法工具。这个自纠正机制在方案一的朴素循环里也存在,但方案二的失败信息更结构化、更精确,所以纠正成功率明显更高。我在实际测试中观察到,同一个中等能力的模型,方案一的工具选择准确率大概在七成,加上契约约束和错误回传之后能提高到九成上下。

4. 方案三:原生工具调用 + 执行器 + Agent 循环(生产级完整闭环)

4.1 原生 Function Calling 是"借力",不是"甩锅"

前两种方案本质上都是"绕过原生能力、自己造轮子"。但如果你使用的模型服务本身支持原生工具调用(Function Calling),那第三种的正确做法是直接使用它。原生工具调用的最大优势,是服务端在解码阶段就做了约束,模型输出的 tool_calls 是结构化的、可靠的 JSON,而不是让你从自然语言里碰运气抠。

但这里有一个常见的误区:很多人一听说"原生支持",就觉得 Tool Use 不用自己写了。实际上原生工具调用只解决"模型产出结构化调用意图"这一个环节,剩下的解析、校验、执行、结果回填、循环控制、上下文管理,全部还得你自己写。面试的时候如果能把这条边界讲清楚,会比单纯说"我调了一下 tools 参数"高出一个段位。

4.2 主循环代码与消息协议细节

一个最小但完整的原生工具调用主循环长这样:

def run_agent(user_input: str, max_steps: int = 8) -> str: messages = [ {"role": "system", "content": "你是一个能调用工具解决任务的智能助手。"}, {"role": "user", "content": user_input}, ] tools = [TOOL_REGISTRY[name]["schema"] for name in TOOL_REGISTRY] for _ in range(max_steps): resp = llm_chat(messages=messages, tools=tools, tool_choice="auto") msg = resp.choices[0].message if not msg.tool_calls: return msg.content # 关键点 1:assistant 消息必须原样回传,且带上 tool_calls 字段 messages.append({ "role": "assistant", "content": msg.content or "", "tool_calls": [ { "id": call.id, "type": "function", "function": {"name": call.function.name, "arguments": call.function.arguments}, } for call in msg.tool_calls ], }) # 关键点 2:并行调用时,每个工具结果都要用 tool_call_id 对齐 for call in msg.tool_calls: try: args = json.loads(call.function.arguments) except json.JSONDecodeError as exc: args = {} result_text = f"参数解析失败: {exc}" else: result_text = execute_tool(call.function.name, args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result_text), }) return "已达最大推理步数,任务未完成。"

这段代码里有几个协议细节,是只背 API 的人永远不会注意到的:

第一,assistant 消息回传时,必须把它原先输出的 tool_calls 原封不动带上。很多模型服务要求消息历史里,assistant 的那条消息和随后一组 tool 结果消息必须完整成对出现,你要是把 tool_calls 字段丢了,服务端会报错或者模型会混乱。

第二,tool 消息的 tool_call_id 必须对上。尤其是模型一次输出多个并行工具调用时(比如同时查天气和查股票),每个执行结果要挂到它对应的 tool_call_id 上,不能打乱顺序。我见过有人把并行结果合并成一条 tool 消息塞回去,结果模型根本分不清哪个结果对应哪个调用。

第三,execute_tool 内部仍然要保留注册表白名单和参数校验。这一步和方案二完全一致,只是解析来源从"自己抠 JSON"变成了"服务端给结构化字段",但安全防线不能因此省掉。

4.3 生产化三件套:自纠正、上下文压缩与安全护栏

方案三写到这里只是一个能跑的循环,真正上生产还要补三块:

自纠正方面,我在 execute_tool 里统一包装了异常,让所有失败都以文本形式进入观察消息。模型拿到"这个工具调用失败了,原因是 xxx"后,可以自动决定换个工具或修正参数重试。我在一个实际项目中还专门注册了一个叫 report_failure 的调试工具,用来把多次失败后的最终状态汇报给用户,避免 Agent 陷入无限重试。

上下文压缩方面,每轮工具调用的 results 都会被追加进消息历史,一旦工具返回长文本,几轮下来 token 消耗会非常快。我的习惯是:保留 System 提示和最初的用户问题,把中间的 thought 过程保留,但对工具结果做截断(只保留前 N 个字符),尤其是那些工具本身返回 JSON 大对象的,先缩成摘要再回填。这样既保留推理线索,又控制成本。

安全护栏方面,有三条铁律。一是工具白名单,只允许执行注册表里的函数,绝不能动态执行模型生成的代码字符串;二是参数校验,在接线进真实函数前必须过一遍类型和取值范围检查;三是对危险操作做二次确认,比如支付、删除、发送消息这类操作,设计成"模型先输出意图,再由用户确认后才真正执行"。这三条放在 Agent 面试里聊,面试官通常会眼前一亮,因为这说明你想的不是 demo,而是线上事故。

5. 三种方案横向对比与选型决策

5.1 一张表看清差异

把三种方案放在同一张表里对比,各自的定位就很清楚了:

对比维度方案一:提示词+正则方案二:结构化约束+校验方案三:原生调用+Agent循环
实现成本低,两三个函数搞定中,需要输出契约和校验逻辑偏高,协议细节多
依赖模型能力无特殊要求建议支持结构化输出/JSON Mode必须支持原生工具调用
输出可靠性60% - 80%,靠提示词撑85% - 95%,契约+校验双重保障90% - 99%,解码层已有约束
调试友好度差,解析失败原因模糊中,错误信息可回传自纠正好,tool_call_id 全程可追踪
多轮/并行支持需自己处理需自己处理协议原生支持
适用场景PoC、临时脚本、弱模型兜底生产环境、自托管模型、对格式敏感的业务复杂 Agent、多工具协作、对稳定性要求高的业务

从这张表能看出一个结论:方案一适合"先跑通",方案二是"可靠性的分水岭",方案三是"复杂度上去之后的必然选择"。它们不是竞争关系,而是同一件事在不同约束条件下的不同答案。

5.2 我的选型习惯

分享几个实际项目里的选型经验,你可以直接抄作业。

如果项目只是内部工具、demo 或者给非技术同事用的脚本,我会直接上方案一,省事、不依赖任何特殊能力,翻车了大不了重来。如果模型是自托管的开源模型,不支持原生的函数调用协议,那方案二是最稳妥的选择,因为结构化输出通常还能通过提示词勉强控住,配合严格校验,够用。如果用的是主流商用模型服务,并且任务本身需要多轮推理、多个工具配合完成,我毫不犹豫选方案三,因为我只需要把协议细节写好,可靠性由服务端的约束解码来兜底。

还有一条个性化建议:即使最终决定用方案三,也建议先在本地把方案二完整写一遍。原因很简单,方案二强迫你实现校验和错误回传,这两块能力在方案三里同样需要,只是换了数据来源。先把"自己解析"的苦吃过一遍,你才会真正理解原生工具调用帮你省掉了哪些坑,将来出了问题也能快速定位是协议问题、解析问题还是执行问题。

5.3 怎么客观评估一个 Tool Use 模块的好坏

面试官很可能会接着问"你怎么知道你的实现是好的"。我习惯用四层指标来评估:

第一层是工具选择准确率,也就是模型在所有需要调用工具的轮次里,选对工具的比例。第二层是参数提取成功率,工具选对了,但参数填错的错误率要单独统计。第三层是任务级完成率,也就是一个包含多步工具调用的完整任务,最终是否得到了用户满意的答案。第四层是成本指标,包括平均每任务消耗的 token 数和调用轮数。

我自己的做法是准备一个二三十条的小评测集,覆盖单工具、多工具、需纠错、无需工具这四类情况,每条都写清楚标准答案。跑模型时逐条记录,算出一个基线分数。后续每次改提示词、调节点参数,都用同一份评测集回归。这套方法在面试里也可以直接说,它向面试官传递的信息是你的工作方式是有数据支撑的,而不是靠感觉调参。

6. 面试官追问清单:从"能写出来"到"能聊明白"

6.1 六个高频追问与我的回答框架

写完之后,面试官通常会抛出几个追问,这些问题往往比手写代码更关键。我把常见的六个列出来,附上我认为比较有深度的回答方向。

第一个问题:如果模型在 arguments 里传了 schema 里不存在的参数,怎么办?我的回答是两道防线:第一道是注册表里的参数校验,不存在的 key 直接忽略或报错;第二道是把报错信息回传给模型,让它基于错误观察重新生成参数。核心思想是"不执行非法调用、给模型纠错机会"。

第二个问题:多个工具都能满足用户需求,怎么让模型选对?我会把每个工具 description 写得足够具体,尤其是把"什么时候该用、什么时候不该用"直接写进描述里,同时提供一两个典型 few-shot 示例。很多工具被选错,不是模型笨,是描述写得太模糊。

第三个问题:工具执行报错了,要不要把原始错误信息直接返回给模型?我的建议是返回脱敏后的摘要错误,比如把异常类型和简短的错误消息拼进去,但要走白名单和统一格式化,避免把堆栈等敏感信息暴露给模型。

第四个问题:怎么防止模型把用户输入的恶意内容当成工具参数执行?这是一个安全底线问题,回答要强调三层:白名单决定了能调什么,参数校验决定了能不能调,操作确认决定了危险操作要不要真正执行。尤其是涉及执行命令、发请求、改数据之类的工具,必须做二次确认。

第五个问题:连续多轮工具调用把上下文撑爆了怎么办?我会说上下文压缩策略:系统提示和用户原始问题常驻,中间工具结果截断或摘要化,超过阈值后做滑动窗口丢弃最早的历史。还可以设计一个 summarizer 工具,在上下文接近上限时自动总结历史再继续。

第六个问题:怎么评估工具调用模块的准确率?直接对应 5.3 小节的四层指标来讲即可。最好再补一句"我准备了离线评测集,每次改动都会回归",这句在面试里是加分项。

6.2 三处平时不太会写进文档的加分细节

现场写代码容易只顾着主流程,我复盘后觉得有三处细节,如果当时能主动展示出来,会更完整。

第一处是给工具调用加元数据。我在执行结果回填时,除了工具返回的业务内容,还会附带一个形如[status=ok, cost_ms=120]的元信息后缀。别小看这个动作,线上排查问题时,你能一眼看出哪次调用超时、哪次调用失败重试过,而不是靠肉眼看文本里的报错。

第二处是为"不需要工具"的情况预留显式路径。很多实现里,模型只要没输出工具调用,就直接把回复当答案。但实际问题可能是"模型该调工具却忘了调"。我习惯在 System Prompt 里明确写一句"如果可以通过已知信息回答,请直接回答并说明你未调用工具的原因",这样即使用户问题刁钻,模型也不会为了调工具而调工具。

第三处是区分"确定性工具"和"生成性工具"。查询天气、查库存这类结果是确定性的,可以放心执行;但"生成一段文案"、"总结一篇文档"这类本身也是模型生成的工具,执行结果还要过一层质量判断,避免 Agent 自己给自己制造噪音结果。这个意识在复杂 Agent 里特别重要。

6.3 最后说一点我自己实践下来的体会

手写 Tool Use 这件事,最有价值的不是那几行代码,而是你对"模型输出是不可靠的"这个事实有多深的敬畏。第一版方案跑通的时候,我觉得 Tool Use 不过如此;等我真正把错误回传、参数校验、上下文压缩都加上去之后,我才意识到,一个合格的 Tool Use 模块,七成代码都在处理"模型没按你预期干活"的情况。如果你也想在面试前练手,我的建议是别去背框架的源码,静下心把方案二从零写一遍,再对着方案三把消息协议捋一遍,这个过程比看十篇教程都管用。这 3 种方案本身没有高下之分,你能根据场景选对、并且说清楚为什么选它,这才是手写这道题真正想考察的东西。

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

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

立即咨询