手写Tool Use三阶实战:从字符串模板到生产级注册中心 1. 项目概述当“手写 Tool Use”成为面试新分水岭最近在多个技术社区和求职群看到高频讨论“面试官让我现场手写一个 Tool Use”“Tool Use 已经替代 Promise 成为前端/大模型应用岗的必考题”甚至有某大厂校招面试官在内部分享中明确提到“不考察你调 API 的熟练度而要看你能否在 15 分钟内从零构建一个可被 LLM 理解、可被用户信任、可被系统调度的 Tool”。这不是玄学也不是概念炒作——它直指当前智能体Agent开发中最真实、最落地的一环工具定义与调用协议的工程实现能力。所谓 “Tool Use”本质是让大语言模型具备“动手能力”的桥梁。它不是让模型自己写代码而是让它能准确理解用户意图、自主判断是否需要调用外部能力、精准生成符合规范的调用请求、并安全可靠地解析返回结果。这个过程涉及三重契约人与模型之间的语义契约用户说“查北京天气”模型要懂这是查询类动作、模型与工具之间的结构契约必须输出 JSON 格式且字段名、类型、必填项完全匹配、工具与系统之间的执行契约HTTP 接口或函数调用需满足鉴权、超时、重试等生产级要求。我带过的几位应届生在模拟面试中栽在这一步不是因为不会写 fetch而是卡在“怎么让模型知道该调哪个工具”“怎么让工具返回的结果不被模型胡乱编造”“怎么防止用户一句话触发 5 个工具导致雪崩”这些看似微小却决定成败的细节上。这篇文章不讲抽象理论也不堆砌论文术语。我会以一名经历过 20 场 AI 工程方向技术面试的从业者身份复盘我在真实场景中为解决“手写 Tool Use”这一命题所尝试的三种递进式方案从最轻量的字符串模板法适合 3 分钟快速验证到结构化 Schema 驱动法兼顾可读性与可扩展性再到生产就绪的注册中心 运行时解析法支撑多模型、多工具、多环境的复杂调度。每一种方案我都附上了完整可运行的代码片段、关键参数的设计逻辑、调试时踩过的具体坑点以及它们分别适用于什么面试阶段、什么岗位层级、什么业务场景。如果你正在准备大模型应用开发、智能体架构、AI 产品工程化等方向的面试或者正面临从 Prompt 工程向 Agent 工程转型的实际项目这篇内容就是你打开工具链大门的第一把钥匙——它不教你“背答案”而是帮你建立一套可迁移、可验证、可演进的 Tool Use 设计思维。2. 方案一字符串模板法——3 分钟内跑通的“最小可行体”2.1 为什么从这里开始面试场景下的生存逻辑很多候选人一听到“手写 Tool Use”第一反应是去翻 OpenAI 的 Function Calling 文档然后试图抄一段带tools和tool_choice的完整请求体。这在实际面试中往往适得其反面试官给的时间通常只有 10–15 分钟而你花 8 分钟配好 OpenAI SDK、处理 API Key、写完异步调用逻辑最后只剩 2 分钟写核心逻辑结果连最基本的“识别出用户要查天气”都漏掉了。真正的考察点从来不是你会不会调第三方服务而是你能否在资源极度受限时间、环境、依赖的前提下清晰表达出 Tool Use 的核心契约。字符串模板法就是为此而生。它的核心思想极其朴素把工具调用看作一次受控的字符串拼接而非一次复杂的运行时决策。我们不依赖任何框架、不引入任何外部库、不连接真实 API只用原生 JavaScript或 Python完成三件事1定义工具的描述文本2编写一个能从用户输入中提取关键词并匹配工具的简单规则3按预设格式拼出调用字符串。它牺牲了灵活性但换来了确定性、可预测性和极高的演示成功率。提示面试中当你听到“请手写一个 Tool Use”时先别急着写 async/await。直接问一句“当前环境是否允许联网是否可以使用第三方库” 如果回答是否定的字符串模板法就是你的最优解。它能在白板、本地编辑器、甚至手机备忘录里完成且逻辑一目了然。2.2 具体实现一个可直接粘贴运行的完整示例我们以“查询城市天气”和“获取股票实时价格”两个工具为例目标是让模型在用户输入“北京今天多少度”时输出类似TOOL:weather(city北京)的字符串在用户输入“苹果股票现在多少钱”时输出TOOL:stock(symbolAAPL)。// 定义工具集合每个工具包含名称、描述、参数列表用于后续提示词注入 const tools [ { name: weather, description: 查询指定城市的当前天气情况包括温度、湿度、风速, parameters: [city] }, { name: stock, description: 获取指定股票代码的最新交易价格和涨跌幅, parameters: [symbol] } ]; // 简单关键词匹配函数基于硬编码规则判断用户意图 function matchTool(input) { const lowerInput input.toLowerCase(); if (lowerInput.includes(天气) || lowerInput.includes(度) || lowerInput.includes(湿度)) { // 提取城市名这里用最简策略找“北京”“上海”等常见词实际可替换为更鲁棒的 NER const cities [北京, 上海, 广州, 深圳, 杭州, 成都]; for (const city of cities) { if (lowerInput.includes(city)) { return { name: weather, args: { city } }; } } return { name: weather, args: { city: 北京 } }; // 默认 fallback } if (lowerInput.includes(股票) || lowerInput.includes(股价) || lowerInput.includes(多少钱)) { // 提取股票代码同样用简单关键词映射如“苹果”→“AAPL” const symbolMap { 苹果: AAPL, 特斯拉: TSLA, 微软: MSFT, 谷歌: GOOGL }; for (const [keyword, symbol] of Object.entries(symbolMap)) { if (lowerInput.includes(keyword)) { return { name: stock, args: { symbol } }; } } return { name: stock, args: { symbol: AAPL } }; // 默认 fallback } return null; // 未匹配到任何工具 } // 拼接调用字符串严格遵循预设格式便于后续正则解析 function buildToolCall(toolMatch) { if (!toolMatch) return null; const { name, args } toolMatch; const argStr Object.entries(args) .map(([k, v]) ${k}${v}) .join(, ); return TOOL:${name}(${argStr}); } // 主函数模拟一次完整的 Tool Use 流程 function handleUserInput(input) { console.log([用户输入] ${input}); const toolMatch matchTool(input); if (!toolMatch) { console.log([模型响应] 我暂时无法处理这个请求请换一种说法。); return; } const toolCall buildToolCall(toolMatch); console.log([工具调用] ${toolCall}); // 此处可模拟调用真实接口但面试中只需说明“下一步将解析此字符串并执行” // 例如const response await fetch(/api/${toolMatch.name}, { method: POST, body: JSON.stringify(toolMatch.args) }); } // 测试用例 handleUserInput(北京今天多少度); // 输出[用户输入] 北京今天多少度 // [工具调用] TOOL:weather(city北京) handleUserInput(苹果股票现在多少钱); // 输出[用户输入] 苹果股票现在多少钱 // [工具调用] TOOL:stock(symbolAAPL)这段代码只有 60 行但它完整覆盖了 Tool Use 的四个核心环节工具注册tools数组、意图识别matchTool、参数提取args对象、调用构造buildToolCall。它没有用正则捕获组没有用 AST 解析甚至没用switch语句——因为面试现场手写正则容易出错而if/else链最直观、最容易被面试官快速理解你的设计思路。2.3 实操心得为什么这个方案在面试中“稳如老狗”我带过的一位应届生在某一线大厂终面中被要求“不用任何框架手写一个能调用天气和股票 API 的 Tool”。他没有选择炫技写一个通用解析器而是用了这个模板法并在现场补充了两段关键说明最终拿到了 Offer第一段说明是关于“为什么不用正则”他说“正则虽然简洁但在面试手写时容易漏转义、错括号而且一旦需求变更比如增加‘查询未来三天预报’这个新工具正则规则会变得难以维护。而现在的if/else结构新增一个工具只需要加一个if块和一个tools条目逻辑边界清晰可测试性强。”第二段说明是关于“如何应对模糊输入”他当场改写了matchTool函数增加了对“北京天气怎么样”“北京的天气”“北京今天天气”等多种变体的支持并指出“真实用户不会按教科书语法说话。我的匹配策略优先保证高召回尽量不漏掉有效请求再通过默认参数兜底如城市默认北京避免因匹配失败导致整个流程中断。这比追求 100% 精确但经常返回 null 更符合用户体验。”这两段话比代码本身更能体现他的工程素养。字符串模板法的价值不在于它多高级而在于它让你能把注意力 100% 放在“契约设计”上——工具该怎么描述才能让模型理解参数该怎么命名才能避免歧义失败时该怎么降级才能保持对话流畅这些问题的答案恰恰是高级工程师和初级工程师的核心分水岭。3. 方案二Schema 驱动法——让模型真正“看懂”你的工具3.1 从“人看懂”到“模型看懂”为什么字符串模板不够用了字符串模板法在 3 分钟内能跑通但它有一个致命短板它只解决了“调用端”的问题没解决“理解端”的问题。在真实的大模型应用中我们不是自己写matchTool去判断用户意图而是把工具描述喂给模型让模型自己决定该调哪个、传什么参数。这就要求工具描述必须是一种模型能“读懂”的语言——不是自然语言描述而是结构化的、带约束的 Schema。举个例子如果只告诉模型“这是一个查天气的工具”它可能在用户说“给我北京的天气预报”时正确调用但当用户说“北京现在热不热”“北京体感温度多少”“北京适合穿什么衣服”时它就可能犹豫、错误调用甚至瞎编一个温度值。而如果我们给模型提供这样的 Schema{ name: weather, description: 查询指定城市的当前天气实况返回温度、湿度、风速、天气现象如晴、雨、多云, parameters: { type: object, properties: { city: { type: string, description: 要查询的城市中文全称如北京市、上海市不接受缩写或英文 } }, required: [city] } }模型就能基于这个 JSON Schema 理解1这个工具只处理“当前实况”不处理“未来预报”2city是必填项且必须是中文全称3返回结果中一定会包含temperature字段。这种结构化描述才是模型进行可靠推理的基础。这也是为什么 OpenAI 的functions参数、Anthropic 的tool_use、Google 的function calling都强制要求提供 JSON Schema——它们不是为了好看而是为了让模型的“思考过程”可约束、可验证、可审计。3.2 Schema 驱动法的核心实现一个自包含的运行时解析器方案二的目标是构建一个既能被人类阅读、又能被模型消费、还能被代码执行的统一工具定义体系。它包含三个关键组件Schema 注册中心一个集中管理所有工具 Schema 的对象支持动态增删Prompt 注入器将 Schema 转换为模型能理解的自然语言提示如“你可以使用以下工具1. weather查询城市天气……”调用解析器接收模型返回的 JSON 格式调用请求如{name: weather, arguments: {city: 北京}}校验其合法性并执行对应函数。下面是一个精简但完整的实现仍使用 JavaScript无外部依赖// 1. Schema 注册中心支持添加、获取、列出所有工具 class ToolRegistry { constructor() { this.tools new Map(); } // 添加工具传入 name 和完整 Schema 对象 add(name, schema) { this.tools.set(name, { ...schema, name // 确保 name 字段存在 }); } // 获取指定工具的 Schema get(name) { return this.tools.get(name) || null; } // 获取所有工具 Schema 的数组用于构建系统提示词 list() { return Array.from(this.tools.values()); } } // 2. Prompt 注入器将 Schema 转为模型友好的自然语言描述 function buildSystemPrompt(registry) { const tools registry.list(); if (tools.length 0) return 你不能调用任何外部工具。; let prompt 你是一个强大的 AI 助手可以调用以下工具来帮助用户解决问题。请严格遵守以下规则\n; prompt 1. 只有当用户请求明确需要外部数据时才调用工具\n; prompt 2. 调用工具前必须确认所有必填参数都已获得\n; prompt 3. 如果参数缺失或模糊先向用户提问不要猜测。\n\n; prompt 可用工具列表\n; tools.forEach((tool, index) { prompt ${index 1}. ${tool.name}${tool.description}\n; prompt 参数${JSON.stringify(tool.parameters)}\n; }); return prompt; } // 3. 调用解析器校验并执行模型返回的工具调用 class ToolExecutor { constructor(registry) { this.registry registry; } // 校验模型返回的调用请求是否合法 validateCall(call) { if (!call || typeof call ! object) { return { valid: false, error: 调用格式不是 JSON 对象 }; } const { name, arguments: args } call; if (!name || typeof name ! string) { return { valid: false, error: 缺少 name 字段或类型错误 }; } const toolSchema this.registry.get(name); if (!toolSchema) { return { valid: false, error: 工具 ${name} 未注册 }; } // 校验参数 if (!args || typeof args ! object) { return { valid: false, error: arguments 字段缺失或不是对象 }; } const { properties, required [] } toolSchema.parameters; if (!properties || typeof properties ! object) { return { valid: false, error: 工具 Schema 中缺少 properties 定义 }; } // 检查必填参数是否齐全 for (const req of required) { if (!(req in args)) { return { valid: false, error: 缺少必填参数 ${req} }; } } // 检查参数类型简化版只检查 string/number/boolean for (const [key, value] of Object.entries(args)) { const propDef properties[key]; if (!propDef) continue; const expectedType propDef.type; const actualType typeof value; if (expectedType string actualType ! string) { return { valid: false, error: 参数 ${key} 应为字符串但得到 ${actualType} }; } if (expectedType number actualType ! number) { return { valid: false, error: 参数 ${key} 应为数字但得到 ${actualType} }; } if (expectedType boolean actualType ! boolean) { return { valid: false, error: 参数 ${key} 应为布尔值但得到 ${actualType} }; } } return { valid: true, toolSchema, args }; } // 执行已校验的调用此处用模拟函数代替真实 API async execute(call) { const validationResult this.validateCall(call); if (!validationResult.valid) { throw new Error(工具调用校验失败${validationResult.error}); } const { toolSchema, args } validationResult; // 模拟真实工具函数根据工具名分发 switch (toolSchema.name) { case weather: return this._simulateWeather(args.city); case stock: return this._simulateStock(args.symbol); default: throw new Error(未知工具${toolSchema.name}); } } // 模拟天气查询返回固定 JSON _simulateWeather(city) { return { city: city, temperature: Math.floor(Math.random() * 10) 20, humidity: Math.floor(Math.random() * 30) 40, condition: [晴, 多云, 小雨, 阴][Math.floor(Math.random() * 4)] }; } // 模拟股票查询 _simulateStock(symbol) { const basePrice { AAPL: 190, TSLA: 240, MSFT: 330, GOOGL: 135 }; const change (Math.random() - 0.5) * 2; return { symbol: symbol, price: (basePrice[symbol] || 100) change, changePercent: change.toFixed(2) }; } } // 使用示例 const registry new ToolRegistry(); // 注册天气工具使用标准 JSON Schema registry.add(weather, { description: 查询指定城市的当前天气实况返回温度、湿度、风速、天气现象, parameters: { type: object, properties: { city: { type: string, description: 要查询的城市中文全称如北京市、上海市 } }, required: [city] } }); // 注册股票工具 registry.add(stock, { description: 获取指定股票代码的最新交易价格和涨跌幅, parameters: { type: object, properties: { symbol: { type: string, description: 股票代码如AAPL、TSLA } }, required: [symbol] } }); // 构建系统提示词可直接喂给模型 console.log( 系统提示词 ); console.log(buildSystemPrompt(registry)); // 创建执行器 const executor new ToolExecutor(registry); // 模拟模型返回的调用请求这是模型在 function calling 模式下输出的 JSON const modelOutput { name: weather, arguments: { city: 北京 } }; // 执行调用 executor.execute(modelOutput) .then(result console.log( 工具返回结果 , result)) .catch(err console.error(执行失败, err));这段代码约 200 行但它实现了真正的 Schema 驱动工具定义即 SchemaSchema 即文档文档即执行依据。它不再依赖关键词硬匹配而是让模型基于语义理解做决策它不再拼接字符串而是用标准 JSON 通信它不再忽略参数校验而是内置了类型检查和必填项验证。这已经非常接近生产环境中LangChain或LlamaIndex的底层工具管理逻辑。3.3 关键参数设计原理为什么required和type必须显式声明在上面的 Schema 中required: [city]和type: string看似简单但它们背后是大量线上事故换来的经验required字段是防止“空参数调用”的保险丝。我曾参与过一个电商客服 Agent 项目初期 Schema 没设required结果模型在用户说“查一下订单”时自作主张传了{order_id: }导致后端接口返回 500 错误整个对话流中断。加上required后模型会主动追问“请问您的订单号是多少”把问题拦截在前端。type字段是防止“类型错乱”的防火墙。另一个案例是金融类 Agent天气工具的city是字符串而股票工具的symbol也是字符串但模型有时会把symbol当成数字传{symbol: 12345}。后端解析时类型不匹配直接崩溃。显式声明type: string后我们的解析器能在执行前就报错而不是让错误穿透到下游服务。这些细节在面试中说出来远比写一百行花哨的装饰器更有说服力。Schema 驱动法教会你的不是怎么写代码而是怎么用代码定义契约、用契约约束行为、用行为保障稳定。4. 方案三注册中心 运行时解析法——面向生产的 Tool Use 架构4.1 当“手写”变成“可运维”为什么需要第三种方案前两种方案解决了“能不能跑”和“能不能懂”的问题但还没解决“能不能管”的问题。想象这样一个场景你所在的团队同时维护着 15 个 Agent 应用分别对接天气、股票、日历、邮件、数据库、内部 CRM 等 20 多个工具。每个工具由不同小组开发API 版本不一鉴权方式各异有的用 API Key有的用 OAuth2有的走公司统一网关。此时如果每个 Agent 都各自维护一份ToolRegistry就会出现配置漂移CRM 工具升级了A 组更新了 SchemaB 组忘了同步结果 B 组的 Agent 调用失败重复造轮子每个组都写自己的ToolExecutor但鉴权逻辑、重试策略、熔断机制几乎一模一样监控盲区没人知道过去 24 小时哪个工具被调用了多少次、平均耗时多少、错误率多少。这就是方案三要解决的问题把 Tool Use 从一个“手写函数”升级为一个“可注册、可发现、可治理”的运行时服务。它的核心思想是“关注点分离”——工具定义What、工具执行How、工具调度When/Where彻底解耦。这不再是面试白板上的几行代码而是你在简历中可以写“主导设计并落地了公司级 Tool Registry 中心支撑 15 Agent 日均 200 万次工具调用”的硬核项目。4.2 架构全景图三个核心模块及其协作关系方案三的架构由三个松耦合模块组成它们通过标准接口通信不依赖特定语言或框架模块职责关键能力面试中可强调的亮点Tool Registry注册中心统一存储和管理所有工具的元数据Schema、Endpoint、Auth Config、Owner、SLA支持 REST API 查询、Web UI 管理、GitOps 配置同步、变更审计日志“我设计了基于 Git 的 Schema 版本管理每次 Schema 变更自动触发 CI 测试确保向前兼容”Tool Executor执行器作为独立服务部署接收标准化调用请求负责鉴权、路由、重试、熔断、日志、指标上报内置多种鉴权插件API Key/OAuth2/JWT、可配置重试策略指数退避、Prometheus 指标暴露“我实现了基于令牌桶的 QPS 限流防止某个 Agent 故障导致下游工具雪崩”Agent Runtime运行时每个 Agent 应用内嵌的轻量客户端负责从 Registry 发现工具、构建调用请求、发送给 Executor、解析响应自动缓存 Registry 数据、支持 fallback 工具、调用链路追踪Trace ID 透传“我给 Runtime 加了本地 Schema 缓存即使 Registry 临时不可用Agent 也能降级运行 5 分钟”这三个模块之间只通过 HTTP 和 JSON 交互意味着你可以用 Python 写 Registry用 Go 写 Executor用 TypeScript 写 Runtime完全不影响集成。这种设计哲学正是高级工程师区别于“写代码的人”的关键——他们思考的不是“怎么实现”而是“怎么组织”。4.3 核心环节实现一个可落地的注册中心原型我们聚焦最关键的Tool Registry模块用 Node.js Express 实现一个最小可行原型。它不追求功能完备但必须体现生产级设计的关键考量版本控制、权限隔离、变更可追溯。// registry-server.js const express require(express); const fs require(fs).promises; const path require(path); const app express(); app.use(express.json()); // 工具存储目录模拟数据库 const TOOLS_DIR path.join(__dirname, tools); // 初始化工具目录 async function initToolsDir() { try { await fs.access(TOOLS_DIR); } catch { await fs.mkdir(TOOLS_DIR, { recursive: true }); } } // 读取所有工具支持版本过滤 app.get(/tools, async (req, res) { try { const { version } req.query; const files await fs.readdir(TOOLS_DIR); const tools []; for (const file of files) { if (!file.endsWith(.json)) continue; const content await fs.readFile(path.join(TOOLS_DIR, file), utf8); const tool JSON.parse(content); // 如果指定了 version只返回匹配的版本 if (version tool.version ! version) continue; // 注入元数据文件名即 ID修改时间即 lastModified tool.id file.replace(.json, ); tool.lastModified (await fs.stat(path.join(TOOLS_DIR, file))).mtime.toISOString(); tools.push(tool); } res.json({ success: true, data: tools }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); // 注册新工具带版本和 Owner 信息 app.post(/tools, async (req, res) { try { const { name, version, description, parameters, endpoint, auth, owner } req.body; // 强制校验必须有 name、version、description、parameters if (!name || !version || !description || !parameters) { return res.status(400).json({ success: false, error: 缺少必需字段name, version, description, parameters }); } // 生成唯一文件名name-version.json const filename ${name}-${version}.json; const filepath path.join(TOOLS_DIR, filename); // 构建完整工具对象加入审计字段 const tool { name, version, description, parameters, endpoint: endpoint || , auth: auth || { type: none }, owner: owner || unknown, createdAt: new Date().toISOString(), createdBy: req.headers[x-user-id] || system }; await fs.writeFile(filepath, JSON.stringify(tool, null, 2), utf8); res.status(201).json({ success: true, message: 工具 ${name} v${version} 注册成功, id: filename.replace(.json, ) }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); // 获取单个工具详情含历史版本 app.get(/tools/:id, async (req, res) { try { const { id } req.params; const files await fs.readdir(TOOLS_DIR); const versions []; for (const file of files) { if (file.startsWith(${id}-) file.endsWith(.json)) { const content await fs.readFile(path.join(TOOLS_DIR, file), utf8); const tool JSON.parse(content); tool.id file.replace(.json, ); tool.lastModified (await fs.stat(path.join(TOOLS_DIR, file))).mtime.toISOString(); versions.push(tool); } } if (versions.length 0) { return res.status(404).json({ success: false, error: 工具未找到 }); } // 返回最新版本为主数据其他版本为 history versions.sort((a, b) b.version.localeCompare(a.version)); const latest versions[0]; const history versions.slice(1); res.json({ success: true, data: { latest, history } }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); // 启动服务器 const PORT process.env.PORT || 3000; initToolsDir() .then(() { app.listen(PORT, () { console.log(Tool Registry 服务启动于 http://localhost:${PORT}); console.log(工具目录${TOOLS_DIR}); }); }) .catch(console.error);这个注册中心只有 120 行代码但它包含了生产系统的关键基因版本化管理name-version.json的文件命名规则天然支持多版本共存和灰度发布审计追踪createdAt、createdBy、lastModified字段让每一次变更都有据可查权限预留x-user-id请求头的设计为后续接入公司统一认证体系留出接口强 Schema 校验在注册入口就拦截缺失字段而不是等到运行时才发现。你可以把它部署在任意一台服务器上然后让所有 Agent 应用通过GET /tools获取最新工具列表。这才是真正意义上的“手写 Tool Use”——你写的不是一行代码而是一套可持续演进的基础设施。4.4 生产级避坑指南那些只有踩过才懂的细节在真实项目中落地这套架构我总结了三条血泪教训每一条都值得在面试中展开讲永远不要相信模型传来的arguments即使 Schema 写得再严谨模型也可能会返回{city: null}或{city: 123}。我们的ToolExecutor必须在调用前做二次清洗对字符串参数 trim 空格对数字参数 parseInt对布尔参数 Boolean() 转换。有一次我们没做 trim用户输入“北京 ”末尾有空格导致天气 API 返回 404而日志里只显示“城市不存在”排查了 2 小时才发现是空格问题。熔断阈值不是拍脑袋定的初始我们给所有工具设了统一的 50% 错误率熔断结果天气工具因为第三方服务不稳定频繁熔断而内部 CRM 工具却因错误率低一直不熔断导致故障扩散。后来我们改为按工具 SLA 动态设置对 P99 200ms 的工具熔断阈值设为 5%对 P99 2s 的工具设为 20%。这个细节体现了你对 SRE 思维的理解。Schema 变更必须双写兼容当我们要给天气工具增加unit参数支持摄氏/华氏时不能直接改旧 Schema。而是先发布weather-v2让新 Agent 用 v2旧 Agent 继续用 v1同时在 Executor 里写兼容逻辑如果收到 v1 请求自动补unit: celsius。等所有 Agent 迁移完成再下线 v1。这种渐进式演进能力是区分玩具项目和工业级系统的试金石。5. 常见问题与排查技巧实录从面试现场到线上故障5.1 面试高频问题速查表面试官最爱问的 7 个问题问题我的标准回答要点为什么这样答底层逻辑Q1为什么不用正则匹配而要用 if/else“正则在手写时易出错且难以维护。if/else结构清晰新增工具只需加一个分支逻辑边界明确可测试性强。”展示工程权衡意识不盲目追求“酷”而选择“稳”和“可演进”。Q2模型返回的 JSON 格式错了怎么办“在ToolExecutor.validateCall()中做三层校验1. JSON 结构是否合法2.name是否在注册中心存在3.arguments是否符合 Schema。任一失败都抛出明确错误绝不静默失败。”强调防御性编程生产环境里90% 的故障源于对异常的忽视。Q3如何防止工具调用死循环“在 Agent Runtime 层设置最大调用深度如 3 层并在每次调用时透传trace_id。Executor 记录调用链发现同一 trace_id 在 1 秒内出现 3 次相同工具调用自动拒绝。”体现系统观单点优化不如全局