大模型Tool Use三种实现方案:函数调用、JSON解析与结构化输出
前阵子帮朋友复盘一场大模型应用方向的面试聊到一道让他印象很深的题面试官让他手写一个 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: funknown 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, toolsTOOLS, temperature0.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_asciiFalse) }) 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}, temperature0.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_asciiFalse)}请结合结果给出最终回答。}) 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 跑通再把对比选型想清楚最后把踩过的坑整理成自己的速查表。等真正到了白板上手写的时候你会发现这道题根本不是在考你背代码而是在提供一个机会让你把日积月累的工程能力展示出来。