统一模型客户端怎样屏蔽供应商差异
一句话收获:统一的是调用与错误契约,不是模型能力差异。
这个场景在解决什么问题
这个场景讲的是"统一模型客户端"这个东西——它是一层代码,负责跟不同的模型供应商(OpenAI、Anthropic、各家国产模型等)打交道。目标(goal)是实现五样东西:文本、结构化结果、工具候选、结束原因、错误分类。观察点(observation)是:不管哪家模型的响应进来,最后都装进"同一个结果信封"里。
为什么值得单独学?因为如果你直接在各处散着调不同供应商的 API,一旦换模型,业务代码就得跟着改。这个场景告诉你:统一的是"调用和错误"的契约,而不是把不同模型的能力强行拉平——模型差异依然存在,但上层代码不用为每家的差异各写一套。
先理清这条判断链(文字版)
这个场景的四步是在"归一化"一次模型调用的各种输出,本质是"分离正常结果 → 分离工具候选 → 归一化错误 → 封装成统一信封":
- 先把"普通文本"和"结构化结果"分开表示,让上层知道结果能不能被程序直接使用。
- 再把模型提出的"工具候选"单独解析出来,注意只解析、不执行。
- 接着把截断、限流、超时这些异常,转成稳定的错误类别。
- 最后把所有信息装进一个统一结果信封,上层只读信封里的字段。
这条链的核心逻辑是:让业务代码只面对一个稳定的接口,供应商的差异被"客户端"这层挡在下面。
逐层详解
第 1 步:正常文本与结构化输出
原文:客户端将文本和 Schema 结果分开表示。
白话:客户端把模型返回的"普通文本"和"结构化结果(符合某个 Schema/格式定义的数据)"分开存放。文本是给人读的,结构化结果是给程序用的,两者不能混在一个字段里。
类比:像快递分拣——信封(文本)和包裹(结构化数据)进不同的传送带,因为收件方处理方式完全不同。
举例:你让模型"从一段合同里提取金额和日期",它返回一段自然语言解释(文本),同时返回 {amount: 1000, date: "2026-08-18"}(符合 Schema 的结构化结果)。客户端要把这两个分开,程序只取结构化的那个。
为什么重要:上层需要知道结果能否被程序使用。也就是说,上层代码要能一眼判断"这个结果是能直接进数据库的结构,还是需要人再读一遍的文字"。
怎么验证(证据点):
- 文本字段:文本结果单独存在。
- 结构化字段:结构化结果单独存在。
- Schema 结果:能标明这个结构符合哪个 Schema。
别踩的坑(边界):请求成功不证明结构有效。意思是,请求返回 200、看起来成功了,不代表返回的结构真的符合你定义的 Schema——成功 ≠ 结构合法。
落地检查:结构化结果要独立校验。也就是说,落地时拿到结构化结果后,还要单独跑一次校验,确认它真符合 Schema,不能信"请求成功"。
第 2 步:工具候选
原文:模型提出工具和参数,客户端只解析候选。
白话:模型可能返回"我想调用某个工具、参数是什么"这样的建议。客户端在这一步只做"解析",把这些建议解析成"工具候选",但绝不真的去执行。
类比:像会议里有人提出"我建议联系供应商 A",会议纪要只负责记下这个"建议"(候选),不会立刻去打电话执行。
举例:模型在 Loop 里输出"建议调用 search 工具,参数是关键词'订单状态'",客户端把它解析成一个候选对象 {tool: search, args: {...}},标记为"未执行",然后交出去让 Harness(执行控制器)决定要不要真调用。
为什么重要:执行权限不属于模型客户端。也就是说,客户端只负责"传话",没有资格决定"这个工具该不该真执行"——执行权限在别处(Harness)。
怎么验证(证据点):
- 工具名:候选里工具名明确。
- 参数候选:参数作为候选被解析出来。
- 未执行:明确标记这个候选还没被执行。
别踩的坑(边界):解析成功不代表工具允许调用。意思是,参数解析得再漂亮,也不代表这个工具被允许调用——能不能调还得看白名单和权限。
落地检查:候选交给 Harness 校验。也就是说,落地时解析出的候选必须交给 Harness 去做校验,而不是客户端自己拍板执行。
第 3 步:截断、限流与超时
原文:客户端把结束原因和错误转成稳定类别。
白话:模型调用可能因为各种原因"没正常结束"——输出被截断、触发限流、超时等。客户端要把这些五花八门的异常,统一转成"稳定的错误类别",比如 retryable(可重试)、timeout(超时)等。
类比:像医院分诊台——不管病人是发烧、摔伤还是肚子疼,先按"紧急/一般/观察"分好类,后面才知道走哪个流程。
举例:调用返回了 "rate_limit_exceeded",客户端把它归为 retryable=true 的限流错误;返回 "context_length_exceeded" 则归为"需要裁剪上下文"的类别。上层根据类别决定是重试、停止还是换策略。
为什么重要:错误语义决定重试、停止或查询。也就是说,不同错误该有不同的后续动作,只有先把错误分类,才能决定下一步是重试、还是停下来查、还是直接放弃。
怎么验证(证据点):
- finish reason:能拿到模型的结束原因。
- retryable:能标记"是否可重试"。
- usage:能拿到用量信息。
别踩的坑(边界):所有错误统一重试会制造风险。意思是,如果把所有错误都无脑重试,可能把"已经发生副作用"的操作重复执行,制造大麻烦。
落地检查:传输错误与业务状态分开。也就是说,落地时要区分"传输层面的错误"和"业务层面的状态",两者处理方式完全不同。
第 4 步:统一结果信封
原文:上层只读取状态、内容、候选、用量、结束原因和错误类别。
白话:最后,客户端把前面所有东西——状态、内容、工具候选、用量、结束原因、错误类别——统一装进一个"结果信封"。上层业务代码只读这个信封里的固定字段,不再关心是哪家供应商返回的。
类比:像银行柜台统一了单据格式——不管客户来自哪个支行、哪种业务,最后都填成同一张标准单据,后续流程只看这张单据。
举例:不管是 OpenAI 还是 Anthropic 的返回,业务代码统一读 result.status、result.content、result.toolCandidates、result.finishReason、result.errorCategory,换供应商时业务代码一行都不用改。
为什么重要:模型切换不扩散到业务代码。也就是说,统一的信封把"供应商差异"隔离在客户端这一层,换模型不会波及上层业务逻辑。
怎么验证(证据点):
- 字段稳定:信封字段不随供应商变。
- 供应商原文保留:原始返回被保留下来备查。
- 后续动作明确:能从信封判断下一步该干什么。
别踩的坑(边界):统一信封不能让不同模型输出相同。意思是,信封统一了"格式",但不同模型的能力和输出质量依然不同——不能指望统一信封抹平能力差异。
落地检查:行为差异仍由评估发现。也就是说,落地时模型之间真实的行为差异,还得靠评估(evaluation)去发现,而不是靠信封。
落地可行性小结
本场景涉及的技术栈和落地要点:统一模型客户端、Schema(结构化格式定义)、工具候选(tool candidate)、Harness(执行控制器)、finish reason、错误分类、结果信封(result envelope)。
真实落地时要注意:客户端要定义一套固定的结果字段(状态/内容/候选/用量/结束原因/错误类别),所有供应商的适配都在这一层完成;结构化结果要做独立校验,不能只信"请求成功";工具候选要"只解析不执行",执行权交给 Harness;错误要先分类再决定后续动作。常见坑是:把传输错误和业务状态混在一起处理;以及以为统一了信封就等于统一了模型能力,忽略了评估这一环。
回顾
- 统一客户端屏蔽的是"调用与错误契约"的差异,不是模型能力差异。
- 文本和结构化结果要分开,结构是否有效要独立校验。
- 工具候选只解析不执行,执行权限在 Harness。
- 错误先分类(retryable 等),再决定重试、停止或查询,不能无脑统一重试。
- 统一结果信封让换模型不扩散到业务代码,但能力差异仍靠评估发现。