面试日记 第 18 天

今天这场面试,面试官一上来就想把我简历里的“精通 Agent 开发”四个字划掉。

她把电脑转过来,屏幕上是一份生产事故复盘。一个 Agent 原来跑 Claude Opus 4.8,团队换成 GPT-5.6 Sol 后,速度更快、成本更低,结果文件突然不会读了。

三天的生产数据更吓人,52% 到 64% 的文件读取返回空内容。

更离谱的是,工具每次都返回 success: true。Agent 以为自己看完了代码,实际上读了一肚子空气,然后继续一本正经地改项目。

她问我:“换个模型而已,Agent 怎么突然瞎了?”

我当时还想装一下经验丰富,张口就来:“可能是 Prompt 没写清楚,加一句省略未使用的参数就行。”

她笑了一声,把第二页推过来。团队已经试过工具描述、字段提示和 strict 模式,模型依然坚持把 25 个参数全部填满。

本来一次 read 只需要 actionfile_paths,它连 offsettimeoutsiteId 都安排得明明白白。那种热心程度,像你只点了一杯美式,店员顺手给你加了椰果、红豆、芋圆和一勺辣椒油。

问题就出在 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,都交给中间层处理。

不同 Provider 对 JSON 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 结构,包含 typeproperties,系统完成 Provider 清洗后就可以返回。

第三步,如果遇到 anyOfoneOf 这类 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 分支要求 filePathwrite 分支要求 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 格式,就提取 namedescriptioninput_schema,再包装成 OpenAI 需要的结构。

这种中间件式处理很实用。主调用流程不用堆满 Provider 判断,新增兼容逻辑时,只需要在边界再增加一层转换。

三个容易踩中的坑

第一个坑是 union。工具参数用 oneOf 表示多种分支,在 OpenAI 或 Anthropic 上可以正常工作,换到不支持该关键字的 Provider 就会直接报错。适配层需要展平分支,或者改写成目标 Provider 能接受的结构。

第二个坑是顶层类型。有些 Schema 只有 properties,缺少 type: "object"。部分 Provider 能容忍,OpenAI 接口可能直接拒绝。归一化阶段最好自动补齐。

第三个坑是“看起来合理”的默认值。Ploy 的事故最有代表性,GPT-5.6 为不用的参数填入 offset: 0timeout: 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 工程相关面试题,可以进入面试鸭继续查阅。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐