面试官嘲笑:“Agent 换个模型就瞎了,你也敢在简历上写精通?” 我把空读从 64% 干到 0,她急了:“最快多久入职?”
面试日记 第 18 天
今天这场面试,面试官一上来就想把我简历里的“精通 Agent 开发”四个字划掉。
她把电脑转过来,屏幕上是一份生产事故复盘。一个 Agent 原来跑 Claude Opus 4.8,团队换成 GPT-5.6 Sol 后,速度更快、成本更低,结果文件突然不会读了。
三天的生产数据更吓人,52% 到 64% 的文件读取返回空内容。
更离谱的是,工具每次都返回 success: true。Agent 以为自己看完了代码,实际上读了一肚子空气,然后继续一本正经地改项目。
她问我:“换个模型而已,Agent 怎么突然瞎了?”
我当时还想装一下经验丰富,张口就来:“可能是 Prompt 没写清楚,加一句省略未使用的参数就行。”
她笑了一声,把第二页推过来。团队已经试过工具描述、字段提示和 strict 模式,模型依然坚持把 25 个参数全部填满。
本来一次 read 只需要 action 和 file_paths,它连 offset、timeout、siteId 都安排得明明白白。那种热心程度,像你只点了一杯美式,店员顺手给你加了椰果、红豆、芋圆和一勺辣椒油。
问题就出在 offset: 0。工具把这个模型编出来的默认值当成真实意图,最后从文件开头之前读了个寂寞。
面试官拿红笔敲了敲我的简历:“Agent 换个模型就瞎了,你也敢在简历上写精通?”
这句确实有点伤人,我脑子反而清醒了。
既然 Prompt 改不动模型的函数调用习惯,修复点就不该继续放在 Prompt。应该在 Provider 边界加一层 Schema 转换,让模型有办法明确表达“这个参数不用”。
我在白板上写了一个方案:把 OpenAI 侧的可选字段改成“必填但允许为 null”,模型不用的参数统一填 null;工具真正执行前,再把 null 字段剥掉。这样模型看到的是它能稳定生成的 Schema,工具收到的还是原来的干净参数。
她没说话,直接把一份精简测试脚本丢给我:“跑。”
我改完转换逻辑,空文件读取从 64% 掉到了 0%,重复工具调用也跟着少了。刚才还想划我简历的面试官盯着结果看了几秒,把红笔盖上,问我:“最快多久能入职?”
fu
我正准备客气两句,她又把 Offer 往前推了推:“认真问的。”
这时候她才把今天的原题抛出来:
“不同的 LLM Provider 对 Tool Schema 的支持不完全一致,你会怎么处理这种差异?OpenClaw 是怎么做 Schema 适配的?”
面试结束后,我把这场从“你也敢写精通”到“最快多久入职”的反转重新整理了一遍。下面是我的面试复盘。
回答重点
这题可以先给出一句总判断:把标准 Schema 只定义一次,再在 Provider 边界做归一化。
归一化可以理解成统一格式。工具注册时只维护一份标准 JSON Schema,真正调用模型前,系统根据目标 Provider 自动清洗和转换,把 OpenAI、Anthropic、Gemini、xAI 等模型提供商的差异挡在适配层里。
这样一来,上层工具只需要描述自己叫什么、接收哪些参数、参数类型是什么。至于某家不支持 oneOf,某家要求顶层必须有 type: "object",某家使用 input_schema,都交给中间层处理。

OpenClaw 会在 normalizeToolParameters() 中集中处理 Schema 适配。它接收工具定义、目标 Provider 和模型信息,再返回清洗后的工具定义。上层开发者注册工具时,不需要给每家 Provider 单独写一套参数格式。
这类设计可以概括成八个字:一次定义,按需转换。
今天热点里的 Ploy 事故和 OpenClaw 的实现并非同一套代码,但工程判断一致。Ploy 在 GPT-5.6 的 Provider 边界重写可选参数,并在工具执行前清除 null;OpenClaw 则把不同 Provider 的 Schema 清洗集中到统一入口。两者都没有让每个工具自己背兼容包袱。
扩展知识
为什么不能让每个工具自己适配
假设系统里有 30 个工具,接入 5 家 Provider。如果每个工具都维护 5 份 Schema,马上就会变成 150 份定义。
这还只是第一天。Gemini 增加一个新特性,xAI 调整一个不支持的关键字,某个第三方 Provider 又把 Anthropic 模型套在 OpenAI 兼容协议里,每次变化都要去几十个工具文件中找实现。漏改一处,线上就可能出现只在某个模型下复现的工具调用故障。
把差异收进适配层之后,工具定义保持稳定。Provider 规则变化时,只改对应的清洗函数,再跑一遍兼容性测试,影响范围会小很多。
normalizeToolParameters 做了什么
normalizeToolParameters() 可以看成 Schema 适配层的总入口。处理过程大致分三步。
第一步,根据 Provider 选择清洗策略。Gemini 会进入自己的 Schema 清洗逻辑,xAI 会移除不支持的关键字,其他 Provider 按各自兼容情况处理。
第二步,如果 Schema 已经是标准 object 结构,包含 type 和 properties,系统完成 Provider 清洗后就可以返回。
第三步,如果遇到 anyOf 或 oneOf 这类 union 结构,系统会把多个分支展平成一个 object。各分支的 properties 被合并,同名字段的 enum 也会合在一起。
可以用下面这段简化代码理解:
export function normalizeToolParameters(tool, options) {
const schema = tool.parameters;
function applyProviderCleaning(value) {
if (isGeminiProvider) return cleanSchemaForGemini(value);
if (isXai) return stripXaiUnsupportedKeywords(value);
return value;
}
if (schema.type === "object" && schema.properties && !Array.isArray(schema.anyOf)) {
return { ...tool, parameters: applyProviderCleaning(schema) };
}
const flattenedSchema = flattenSchemaVariants(schema);
return { ...tool, parameters: applyProviderCleaning(flattenedSchema) };
}
展平 union 时,最容易做错的是 required。假设 read 分支要求 filePath,write 分支要求 content,合并后不能把两个字段都粗暴标成必填。OpenClaw 会保留所有分支共同要求的字段,避免某个分支的专属参数误伤其他调用。
同名属性也不能简单覆盖。两个 variant 分别声明 action: "read" 和 action: "write" 时,合并后应该保留为 enum: ["read", "write"],这样展平结构的同时还能留下原来的语义。
外层工具格式也要转换
Schema 内容清洗完,还要处理工具定义外层格式。
| Provider 协议 | 常见工具格式 |
|---|---|
| OpenAI | { type: “function”, function: { name, parameters, description } } |
| Anthropic | { name, input_schema, description } |
麻烦在于,有些第三方 Provider 使用 Anthropic 的模型,API 却实现成 OpenAI 兼容协议。模型是谁、接口长什么样,可能完全对不上。
OpenClaw 会通过兼容 wrapper,在请求发出前拦截工具定义。如果已经是 OpenAI function 格式就直接透传;如果收到 Anthropic 格式,就提取 name、description 和 input_schema,再包装成 OpenAI 需要的结构。
这种中间件式处理很实用。主调用流程不用堆满 Provider 判断,新增兼容逻辑时,只需要在边界再增加一层转换。
三个容易踩中的坑
第一个坑是 union。工具参数用 oneOf 表示多种分支,在 OpenAI 或 Anthropic 上可以正常工作,换到不支持该关键字的 Provider 就会直接报错。适配层需要展平分支,或者改写成目标 Provider 能接受的结构。
第二个坑是顶层类型。有些 Schema 只有 properties,缺少 type: "object"。部分 Provider 能容忍,OpenAI 接口可能直接拒绝。归一化阶段最好自动补齐。
第三个坑是“看起来合理”的默认值。Ploy 的事故最有代表性,GPT-5.6 为不用的参数填入 offset: 0、timeout: 120000 等值。它们格式正确,业务语义却是假的。工具端如果只做类型校验,很可能把这些值当真。
因此,Schema 归一化只负责让模型和 Provider 顺利沟通,工具执行前仍然要做业务校验。文件路径是否存在、命令是否允许、参数组合是否成立,都不能只信模型生成的 JSON。
兼容性要靠测试守住
Provider 的 Schema 支持会变化,今天不认 oneOf,下个版本可能突然支持;原来可以透传的关键字,也可能在接口升级后出问题。
实际项目里可以给每家 Provider 准备一组最小兼容性测试,覆盖 object、array、enum、anyOf、oneOf、nullable、format 等常见能力。测试不仅要看接口有没有报错,还要检查模型是否正确生成参数、工具是否按预期执行、失败时有没有留下可追踪日志。
模型迁移也不能只跑最终成功率。像 Ploy 这次一样,把完整工具轨迹拉出来分类,才能区分模型能力问题、评估框架偏差、Schema 不兼容和缓存配置错误。否则团队很容易花几天调 Prompt,最后才发现故障藏在 Provider 适配层。
面试官追问
追问:Schema 清洗时去掉了 minLength 等约束,会不会让模型生成不合规参数?
回答:会增加出错概率,但工具端本来就不能把 Schema 当成最终校验。Schema 主要帮助模型理解参数结构,模型并不保证百分之百遵守。适配层为了兼容 Provider 删除部分约束后,工具执行前仍要用自己的校验器检查长度、范围、权限和参数组合。模型侧约束负责减少错误,工具侧校验负责拒绝错误,两层都要有。
追问:如果 Gemini 突然开始支持 oneOf,怎么及时更新适配层?
回答:给每个 Provider 维护独立的 Schema 兼容性测试集,并定期调用真实 API 做回归。发现能力变化后,只修改对应 Provider 的清洗模块,再跑该 Provider 和通用工具测试。适配逻辑集中后,更新不需要触碰所有工具定义,也能避免影响其他模型提供商。
追问:为什么不在 Prompt 里直接用自然语言描述工具参数?
回答:工具少时可以勉强使用,工具增加后,Prompt 会迅速膨胀,参数类型、必填关系和枚举值也更容易被模型理解错。结构化 Schema 能稳定表达字段和约束,还方便程序校验。自然语言可以补充业务说明,不能取代 Schema 作为工具调用协议。
追问:如果同一个工具需要同时支持 OpenAI、Anthropic 和 Gemini,你会怎么设计代码结构?
回答:工具层只保留一份标准定义,Provider Adapter 负责 Schema 内容清洗和外层格式转换,调用前统一归一化,执行前统一做参数校验。每家 Provider 的规则放在独立模块,并配套契约测试。这样新增模型时只扩展适配层,工具实现和上层业务都不用跟着改。
那份写着 64% 的事故报告,最后被面试官压在了 Offer 下面。
模型换得越勤,越能看出一个 Agent 系统有没有把 Provider 差异关进笼子里。真正让面试官改口的,也许就是你能指出故障该在哪一层修,以及为什么不能继续靠 Prompt 碰运气。
更多 Tool Calling、Schema 适配和 OpenClaw 工程相关面试题,可以进入面试鸭继续查阅。
更多推荐



所有评论(0)