第三章 模型接入与推理控制:把"黑盒引擎"变成可调、可换、可续的服务

第二章我们把内核看作一个协议网关:外侧对着前端,内侧对着模型。 本章我们走到网关的内侧,仔细看看“对接一个模型”到底意味着什么。 模型不是一个调用一下就返回的函数——它是一个遥远的、有状态幻觉的、会限流会掉线的远程服务;而且市面上同时存在几十个能力各异的模型,每个月还在出新的。 harness 必须回答三类问题:怎么连上(接入)、怎么让它按我想要的方式思考(推理控制)、它出状况时怎么让轮次不断(容错)。


3.1 为什么“填个 API key”远远不够

新手对模型接入的想象是:一个 URL、一把密钥、一个模型名,发请求收回复,完事。

真实场景里,harness 面对的是这样一团乱麻:

  • 模型各不相同。 有的模型支持“思考强度”调节,有的不支持;有的能输出思维链摘要,有的不能;有的上下文窗口 20 万 token,有的 100 万;有的接受图片和音频输入,有的只吃文本;工具调用的并行能力、结构化输出的严格程度、补丁工具的格式……全都因模型而异。
  • 服务商各不相同。 OpenAI 官方、ChatGPT 后端、Amazon Bedrock、本地的 Ollama/LM Studio,甚至用户自己搭的兼容端点——认证方式不同(API key、登录令牌、命令行动态取 token、云厂商签名)、传输能力不同(支不支持 WebSocket)、限流策略不同。
  • 服务随时会出状况。 限流(429)、过载(5xx)、网络中断、流传到一半断掉、凭证过期、服务端安全审查把请求改道到另一个模型、模型退役下架……
  • 同一次会话里模型可能变。 用户中途切换模型、计划模式用另一档模型、子 agent 用更便宜的模型、压缩历史时用专门的压缩模型。

如果让上层的轮次循环(第 1 章)直接面对这些差异,代码会变成一张撒满 if 模型名 == ... 的破网。Codex 的做法是把它们收敛成两层抽象:

  • Provider(服务商):回答“请求往哪发、用什么认证、走什么传输、失败重试几次”——它是线路
  • Model(模型档案):回答“这个模型会什么、支持哪些旋钮、上下文多大”——它是员工档案

轮次循环只认这两层抽象,既不知道也不关心对面是 OpenAI 还是用户自己搭的端点。

graph LR
    subgraph 轮次["轮次循环(只认抽象)"]
        TURN["采样请求:上下文 + 工具 + 推理参数"]
    end
    subgraph 接入层
        MM["模型目录<br/>模型档案(能力卡片)"]
        MC["模型客户端<br/>服务商(线路)"]
    end
    subgraph 远端["模型世界"]
        OAI["OpenAI / ChatGPT 后端"]
        BR["Bedrock"]
        LOCAL["Ollama / LM Studio / 自定义端点"]
    end
    TURN -->|"查能力、冻结快照"| MM
    TURN -->|"发流式请求"| MC
    MC --> OAI
    MC --> BR
    MC --> LOCAL
    MM -.->|"目录本身也来自远端"| OAI

注意最后那条虚线:模型档案本身也是从服务端拉取的。模型目录不是写死在代码里的常量表,而是一个带缓存、带热更新的远程数据集——这是理解本章一切设计的钥匙。


3.2 Provider:一条“线路”的全部参数

Provider 描述的是“怎么到达模型服务”。一个内置或用户自定义的 provider,包含这些信息:

类别 字段 说明
位置 base URL、查询参数 请求发到哪;ChatGPT 登录态和 API key 走不同默认地址
认证 环境变量 key、静态 bearer、命令行取 token、云厂商签名 命令行动态取令牌支持定时刷新;云厂商签名有自己的刷新命令
传输 线协议(只保留 Responses 流)、是否支持 WebSocket、是否支持独立联网搜索端点 旧的 chat 补全线协议已被移除,配置到会直接报错并给出迁移指引
韧性 请求重试次数(默认 4)、流重连次数(默认 5)、流空闲超时(默认 5 分钟)、WebSocket 连接超时(15 秒) 用户可调,但有硬上限(100 次)防止配出无限重试
杂项 附加 HTTP 头(静态值或从环境变量取值)、请求压缩开关 组织 ID、项目 ID 等通过环境变量头注入

内置的 provider 有五类:OpenAI 官方、Amazon Bedrock(两个变体)、Ollama、LM Studio。用户还可以在配置文件里自定义任意 OpenAI 兼容端点。设计上有一个明确的立场:官方不替第三方服务商做背书,内置列表只收录第一方和开源本地端点,其余由用户自行登记。自定义 provider 与内置条目合并时遵循“只扩展、不覆盖”的原则(Bedrock 条目例外,只允许改端点和认证等少数字段)。

认证方式值得多说一句,因为它体现了“harness 不持有秘密”的安全取向:最推荐的是把密钥放在环境变量里,provider 配置只写变量名;静态令牌字段被明确标注为“不推荐”;企业场景下 token 由一个外部命令产出(还能配合刷新命令自动续期),harness 每次请求前调用命令取最新值,密钥永远不落配置文件。

类比:provider 像手机里的“运营商设置”——用哪个基站、什么频段、信号差时重拨几次。你平时完全感知不到它,但出国旅行(换服务商)时全靠它。


3.3 Model:模型不是一个名字,而是一张“能力卡片”

轮次循环真正依赖的,是模型档案里那张内容丰富的能力卡片。每当要发采样请求,harness 都要先回答:这个模型支持什么?卡片上的信息大致分四组:

第一组:思考与输出控制。

  • 支持哪些推理强度档位(none/minimal/low/medium/high/xhigh/max……),默认档是多少;
  • 是否支持思维链摘要参数、默认摘要详细度;
  • 是否支持输出详略(verbosity)控制、默认详略;
  • 支持哪些服务等级(快速通道/弹性通道,见 3.4)。

第二组:上下文与窗口。

  • 上下文窗口大小、允许配置覆盖的最大值;
  • “有效窗口比例”(默认 95%,要给系统指令、工具定义和模型输出预留头部空间);
  • 自动压缩的 token 阈值(缺省按窗口的 90% 派生,且配置值不能超过这个派生值);
  • 工具输出的截断策略(按字节还是按 token、上限多少)——模型读不下无限长的命令输出。

第三组:功能形态。

  • 输入模态:文本、图片、音频;
  • shell 工具的形态、补丁工具是否为自由格式(第 2 章讲过,自由格式工具的参数才能流式预览);
  • 联网搜索工具的形态、是否支持工具搜索、代码模式(code mode)、多 agent 协议版本;
  • 是否启用一种叫 “Responses Lite” 的精简传输格式(3.5 节展开);
  • 实验性工具清单、技能/插件使用说明是否注入。

第四组:文案与身份。

  • 模型自己的基础指令模板(包括个性变量、审批话术、权限说明、多 agent 话术、token 预算提醒文案等);
  • 压缩兼容标识(comp hash,压缩历史时判断模型间是否兼容);
  • 升级建议与退役时间、新模型可用性提示。

第四组尤其反直觉:连系统提示词都是模型档案的一部分。不同模型的行为调校对指令有不同要求,指令跟着模型走,而不是全局一份。harness 自己只内置一份通用基础指令,作为未知模型的兜底。

3.3.1 目录从哪来:四层来源,逐级降级

模型档案不是编译期常量,而是运行时拼出来的,共四层来源:

flowchart TD
    A["① 内置目录 models.json<br/>编译进二进制,开箱即用"] --> B{"需要刷新?"}
    B -->|"在线策略"| C["② 远端 /models 端点<br/>ChatGPT 账号登录且目录有效 → 远端为权威<br/>否则按模型名合并进内置目录"]
    B -->|"离线/缓存策略"| D["③ 磁盘缓存 models_cache.json<br/>5 分钟 TTL,按客户端版本失效"]
    C --> D
    D --> E["④ 兜底档案<br/>未知模型名:保守能力 + 通用指令<br/>打上 fallback 标记"]
    C --> E

几个关键设计:

  • 远端目录是热数据。 启动时和运行中都会按策略刷新;服务端还能通过响应头下发一个 ETag,harness 发现 ETag 变了就后台拉取新目录——新模型上线、旧模型退役,不需要用户升级客户端
  • 缓存保证离线可用。 磁盘缓存带 5 分钟有效期和客户端版本校验(新版客户端可能需要新字段,旧缓存直接作废);网络失败时静默降级到缓存或内置目录,模型发现失败永远不会阻塞用户开工。
  • 模型名匹配是“前缀匹配”。 请求的模型名是 gpt-5.6-codex-20260801 这类带日期后缀的 slug,目录里登记的是 gpt-5.6-codex 这类家族名,按最长前缀匹配;带命名空间的名字(如 custom/gpt-5.3-codex)还会剥掉一段前缀再匹配。
  • 兜底档案宁保守勿冒进。 完全未知的模型名拿到一份“最低能力”卡片:272k 窗口、通用指令、所有高级开关关闭,并打上兜底标记。这个标记会让一轮只警告一次(“未找到模型元数据,使用兜底配置,可能影响表现”),同时关闭依赖元数据准确性的功能(如自动审查、子 agent 的严格模型校验)。原则很清楚:不知道模型会什么,就假设它什么都不擅长。
  • 用户配置可以覆盖,但有钳制。 手动指定的上下文窗口不能超过档案声明的最大值;工具输出截断、自动压缩阈值、基础指令、个性开关都可以在配置层调整。

3.3.2 一张卡片,全会话共享

模型目录由进程级的线程管理器持有(第 1 章说过,认证、目录这类重资源是跨线程共享的)。但每次轮次开始时会重新解析一次模型档案并冻结进轮次快照——这保证了两件事:轮次中途目录热更新不会让同一个轮次“前一半用旧档案、后一半用新档案”;而新轮次自然吃到最新目录。这与第 1 章的步骤快照、第 2 章的“事件即永恒”是同一种思想:边界处冻结,变化只发生在边界之间。


3.4 推理控制的四个旋钮

打开一次采样请求的请求体,能影响模型“怎么想、怎么答”的旋钮有四个。它们全部遵循同一条规则:值从哪来有优先级,发不发要看能力卡片。

3.4.1 推理强度(reasoning effort):思考的“油门”

推理强度控制模型在回答前投入多少内部思考:档位从 none、minimal、low、medium、high、xhigh 到 max,逐级递增。档位越高,思考越充分(复杂推理、多步规划更可靠),但延迟越高、消耗的推理 token 越多——而推理 token 同样计入上下文窗口和账单。

几个耐人寻味的细节:

  • 档位是开放集合,不是封闭枚举。 反序列化时遇到不认识的新档名字符串不会报错,而是存为“自定义值”原样透传。新模型推出新档位时,旧客户端不需要升级就能用——这与第 2 章“协议只增不废”的演化哲学一脉相承。
  • 有一个内部档位 ultra,上线前会被翻译成 max。 ultra 在协议线上不存在,它是 harness 内部的信号:除了“最高思考强度”,还顺带触发“主动式多 agent 编排”模式(第 7 章展开)。对模型服务端来说它就是 max,对 harness 自己来说它是一个行为开关。同一个旋钮,对外是推理参数,对内是编排信号。
  • 有效值有三级回退。 用户显式指定 > 协作模式指定(计划模式可以用不同档位)> 模型卡片的默认档;都没有就不下发,让服务端用自己的默认。
  • 换模型时档位会重映射。 从高档位模型切到只支持 low/medium 的模型时,当前档位不在新模型的支持列表里,harness 不会发一个非法值,而是回落到新模型支持档位的中位数(而不是 silently 用最高档或最低档——中位数是“不算冒险也不算浪费”的折衷)。

3.4.2 思维链摘要(reasoning summary):愿意给人看多少“思路”

模型的思考分两层(第 2 章见过):加密的原始思维链(服务端不希望明文离开,但允许加密回传以保持多轮一致)和可读的思考摘要。摘要参数控制后者:auto / concise / detailed / none。

摘要和原始思维链是两套独立机制,别混淆:

  • 加密思维链通过请求里的 include 字段始终索取reasoning.encrypted_content),harness 只搬运、永远看不到明文,下一轮随上下文回传,保证模型“记得自己想过什么”;
  • 摘要是否下发,要看能力卡片上“这个模型支持摘要参数吗”——不支持就整个字段省略,绝不能把参数发给不认识它的模型。

摘要还有一个传输优化:开启“并发摘要”特性时,harness 会在流选项里声明 sequential_cutoff 模式,让服务端按段落截止点分批投递摘要,而不是把整段思考憋到最后——配合第 2 章的“段落”事件,UI 能更早渲染出思考过程。

3.4.3 输出详略(verbosity):回答的“篇幅”

verbosity(low/medium/high)控制的是最终答复的详尽程度,不是思考深度——它和推理强度是正交的两个维度:一个模型可以“想得很深、答得很简短”。这个旋钮同样只有能力卡片声明支持时才发送;用户硬配了一个不支持的模型,harness 会打一条警告日志然后忽略,而不是让请求报错。

3.4.4 服务等级(service tier):走哪条“车道”

服务等级是计费/路由层面的选择:priority(快速通道,优先级高、单价高)和 flex(弹性通道,容忍排队换低价)。请求里只会出现模型卡片明确支持的等级,不支持的等级被过滤掉;还有一个特殊的“default”哨兵值,表达“用户显式选择不加等级”,以区别于“没配置、用模型默认”。

graph TD
    subgraph 请求组装
        U["用户/配置/协作模式<br/>指定的期望值"]
        U --> G{"能力卡片<br/>支持吗?"}
        G -->|"支持"| SEND["写入请求体"]
        G -->|"不支持"| DROP["省略字段 / 回落到默认 / 警告"]
    end
    subgraph 四个旋钮
        E["推理强度:思考多深"]
        R["思维链摘要:思路展示多少"]
        V["输出详略:回答多长"]
        T["服务等级:走哪条车道"]
    end
    SEND -.-> E
    SEND -.-> R
    SEND -.-> V
    SEND -.-> T

3.4.5 两个“故意不做”的控制

除了“发什么”,“不发什么”同样说明设计取向:

  • 工具选择永远是 auto 请求体里 tool_choice 恒为 auto——harness 从不命令模型“这一步必须调某个工具”。强制工具调用是一种脆弱的控制:模型为了完成命令会在条件不满足时硬调。harness 选择把“该做什么”完全交给模型判断,把确定性放在结构化输出上:需要模型按固定格式产出时(比如审查结论),用 text.format 挂一个 JSON Schema(还带严格模式开关),模型可以自由思考,但最终答案必须符合 schema。自由给过程,约束给结果。
  • 不设置最大输出长度。 让模型自己决定何时说完(第 2 章的 end_turn 标志),而不是用一个 token 上限把回答拦腰截断。

3.5 Responses Lite:同一语义,两种字节形态

能力卡片里有一个开关特别能代表“模型差异如何被吸收”:use_responses_lite

常规请求里,系统指令放在顶层 instructions 字段、工具清单放在顶层 tools 字段。而开启 Lite 的模型,harness 会把两者改造成输入历史开头的两条 developer 角色条目(一条“附加工具”条目 + 一条指令消息),顶层字段留空;同时:

  • 关闭并行工具调用(该传输格式下不支持);
  • 推理上下文显式声明为“全部轮次”(常规路径下省略此字段,用服务端默认的“当前轮次”);
  • 剥离图片的清晰度细节参数;
  • 工具名按命名空间组织。

为什么要搞两套?因为后端模型的上下文缓存与计费机制对“指令/工具放在哪”有不同的优化路径——某些模型把一切都当作对话条目处理时缓存命中率更高。这纯粹是线上字节布局的差异,语义完全等价:轮次循环组装的还是同一个“上下文 + 工具 + 指令”三元组,差异被封装在请求构建的最后一公里。

这件事的一般教训是:当对接的模型足够多时,“请求长什么样”不应该由调用方决定,而应该由目标模型的能力卡片决定。 轮次循环不需要知道 Lite 的存在;新增一种传输格式,只是在能力卡片上加一个开关、在请求构建处加一个分支。

对第三方 provider 也有类似的“线路卫生”处理:发给非 OpenAI 端点前,harness 会剥除条目上的内部透传元数据和加密函数参数——这些第一方协议里的私房字段,出了 OpenAI 的边界就不该存在。


3.6 让重发历史变便宜:缓存键与增量请求

Agent 循环的每个步骤都要把完整历史重新发给模型(第 1 章)。历史滚到几十轮后,请求体里 95% 以上是和上一次相同的内容。harness 用两套机制压低这笔重复成本,恰好对应第 2 章的两种传输。

3.6.1 SSE:无状态请求 + 缓存键

HTTP SSE 路径下,harness 刻意把请求做成无状态的:store 标志恒为 false——不要求服务端持久化任何会话,每个请求自带全部上下文,自足可解释。

那服务端缓存靠什么命中?靠请求里的 prompt cache key:它默认就是会话 ID,同一会话的所有请求带着同一个键。服务端据此把“这个键对应的前缀”放进 prompt cache,重复的前缀只收缓存价。

这里能看出一个与前两章的精妙呼应:

  • 第 1 章说“历史只追加、不重写”、插话只在步骤边界注入——这保证了请求的前缀天然稳定:第 N 次请求的输入 = 第 N-1 次的输入 + 尾部新增。如果历史会被改写、重排,缓存键再稳定也命中不了。
  • 缓存键按会话隔离,而会话是一棵 agent 树共享的(第 1 章的 session ID),子 agent 与父 agent 的请求也能共享缓存前缀。

3.6.2 WebSocket:链式增量 + 严格校验

WebSocket 路径更进一步:同一条长连接上,后续请求可以只发新增的条目,用 previous_response_id 把自己链到上一个响应上,服务端凭连接上保存的状态拼出完整上下文。轮次开始前还会发一个 generate=false 的“空预热”请求——不产生任何输出,只为把连接和缓存预热好;会话启动时这个预热就已经在后台 best-effort 进行了(带超时预算,预热不成不影响开工)。

增量发送能省下可观的上行带宽,但它有一个致命风险:如果“我以为服务端记得的前缀”和“服务端实际记得的”不一致,模型就是在错误的上下文上作答。 所以 harness 对“能不能发增量”做了极其严格的校验:

flowchart TD
    A["准备第 N 次请求"] --> B{"非输入字段与上一次<br/>完全一致?<br/>(模型/指令/工具/推理参数/<br/>服务等级/缓存键/text 控制……)"}
    B -->|"任一不同"| F["全量重发"]
    B -->|"一致"| C{"输入是上次输入 +<br/>上次响应产出条目的<br/>严格前缀扩展?"}
    C -->|"不是"| F
    C -->|"是"| D["只发送尾部新增条目<br/>+ previous_response_id"]
    D --> E{"服务端有响应 ID?"}
    E -->|"没有"| F
    E -->|"有"| G["增量请求成立"]

两个校验缺一不可:

  • 非输入字段逐一比对,而且比对代码用的是“穷尽解构”写法——请求体未来新增任何字段,编译器都会强迫开发者显式决定“这个字段变化时是否还允许增量”,不可能悄悄漏判。推理强度、工具集、服务等级任何一个变化,都会改变“这份上下文的含义”,必须全量重发。
  • 输入必须是严格前缀扩展:上一次的输入条目、加上服务端上次响应产出的条目(模型说的话、工具调用也是上下文的一部分),构成基线;当前输入只能在基线上尾部追加。逐条比对不一致就全量重发。

再叠加第 2 章讲的粘性路由令牌(同一轮次钉在同一后端实例上,且严禁跨轮次复用),三层机制共同保证:增量优化只在“服务端状态与本地认知严格一致”时发生,错一点就退回无状态全量。性能收益拿满,正确性不赌运气。


3.7 故障世界:限流、掉线、改道与凭证过期

模型服务是远程的,故障是常态而非例外。第 2 章已经从协议角度讲过“流上的错误是 Err、可重试错误触发重连”,本节从推理控制的角度补齐:哪些错误值得重试、哪些必须立刻认输、限流怎么计量、模型被调包怎么办。

3.7.1 错误的四象限

轮次级的错误分类决定了每种故障的命运:

错误 性质 harness 的反应
上下文超长 致命 不重试;标记窗口已满,交给压缩逻辑(第 4 章)
用量/额度耗尽(硬限额) 致命 不重试;把限流快照带给 UI,错误文案精确区分“你的额度用尽”“工作区额度用尽”“消费上限”等场景,告诉用户该做什么
请求非法 致命 不重试;这是 bug 或不支持的用法,重试一百次也是错
安全策略拦截 致命 不重试;转错误事件
软限流(429 拥塞)、流中断、网络抖动、连接失败、超时、瞬时服务端错误 可重试 退避后重发;重试用的是持久化历史重新组装请求,已完成的工作不丢(第 2 章)

注意“限流”被劈成了两半:瞬时拥塞(你太快了,等会儿再来)是可重试的,服务端还会通过 Retry-After 告诉客户端等多久——harness 优先采用这个值,没有才用自己的指数退避(200ms 起步、翻倍、±10% 随机抖动防重试风暴);额度耗尽(你没钱了/没配额了)是致命的,重试毫无意义,需要的是可操作的提示。同样是 429,语义天差地别。

3.7.2 两级重试与传输降级

重试分两级,数字都可以按 provider 调整:

  • 请求级(连接还没建立就失败):默认 4 次,退避重试;5xx 和传输错误重试,软 429 不在这一级处理;
  • 流级(流传到一半断了):默认 5 次;前端收到咨询性的“重连中…… 2/5”事件(发布版会刻意压低第一次 WebSocket 重连的提示音量,避免瞬断刷屏)。

两级都失败后还有一张底牌:WebSocket → HTTP SSE 降级。一旦降级,本会话剩余时间都 stick 在 HTTP 上(这是会话级的粘性决策,反复横跳没有意义),并明确告知用户“已从 WebSocket 回退到 HTTPS 传输”。握手阶段收到“需要升级协议”之外的拒绝也会立即触发降级。此外有一个特性开关:纯网络连接失败时(非内部任务、非 Bedrock),可以无限期重连,退避从 5 秒封顶到 60 秒,提示“重连中……等待网络”——笔记本合盖、地铁断网这类场景,轮次可以一直等到网络回来。

3.7.3 凭证过期:自己先治,治不好再报错

还有一类高频故障:令牌过期(401)。它走专门的认证恢复路径:识别出可恢复的认证错误后,harness 调用认证管理器刷新凭证(命令行取 token 的 provider 此刻重新执行取 token 命令),然后用新凭证重试原请求;刷新失败才认输。整个过程对轮次透明,用户最多感知到一次短暂停顿。

3.7.4 模型改道与安全缓冲:服务端“偷偷”做的事

服务端并不总是按你点的菜上菜:

  • 安全路由改道。 高风险请求可能被后端路由到另一个模型处理。响应头里会带回“实际服务的模型名”,harness 比对发现与请求不符,就发一个警告事件——每轮最多一次(用原子标志位去重,防止刷屏)。它只是警告不是错误:轮次继续,用户知情即可。
  • 安全缓冲。 服务端安全审查期间,输出可能被缓冲延迟。流里会带上缓冲原因、适用场景,以及“要不要换个更快的模型重试”的建议;UI 显示“审查中”。
  • 限流快照作为旁路数据。 每次响应头里的配额信息(主/副两个时间窗口的剩余量、积分余额)被解析成快照,攒到响应结束随用量事件一起发给前端——用户在状态栏看到“本周额度还剩 30%“靠的就是它。软限流时它是提示,硬限额时它是致命错误的配图。
  • 目录热更新。 如 3.3 所述,响应头里的目录 ETag 会触发后台刷新模型目录。

3.7.5 模型不只陪用户聊天

最后一个容易被忽略的事实:模型客户端是 harness 内部的公共设施。除了主对话,它还服务于:

  • 历史压缩:专门的压缩端点(也可复用普通采样),且压缩时如果“历史里记录的旧模型”已经不可用,会自动用当前模型重试一次(压缩模型兜底);
  • 记忆巩固:后台把长期记忆总结成摘要的内部任务;
  • 自动审查:高风险操作前的模型审查,还可以指定专用的审查模型。

这些内部调用复用同一套客户端、同一套重试与认证恢复逻辑,只是推理参数(比如摘要关闭、强度调低)和遥测标记不同。对话、压缩、审查、记忆,在 harness 眼里都是“带着不同参数的一次采样”。


3.8 模型的选择与切换

把上面的零件串起来,看一次会话中模型决策的完整时间线:

sequenceDiagram
    participant U as 用户/前端
    participant S as 会话
    participant MM as 模型目录
    participant M as 模型服务

    M-->>MM: 启动/ETag 变化:拉取 /models
    U->>S: 开轮次(可携带模型/档位覆盖)
    S->>MM: 按模型名解析能力卡片(前缀匹配,未知则兜底)
    MM-->>S: 冻结模型档案进轮次快照
    S->>M: 采样请求(按卡片裁剪参数)
    Note over S,M: 轮次中:重试/降级/认证恢复对用户透明
    U->>S: 中途切换模型
    Note over S: 下个轮次重新解析卡片<br/>档位重映射到新模型支持区间
    S->>MM: 解析新模型卡片
    MM-->>S: 新快照
    S->>M: 用新模型继续(历史不变)

几个要点:

  • 切换模型不切换历史。 历史是会话的财产(第 4 章),模型只是处理历史的引擎。换模型后下一轮次用新卡片重新组装请求;推理档位如 3.4.1 所述重映射,其余旋钮同样按新卡片重新门控。
  • 子 agent 用模更严格。 spawn 子 agent 时(第 7 章),harness 会校验子模型确实在目录里、请求的服务等级和推理档位被该模型支持;未知模型上的严格校验会被跳过(兜底卡片已经降级处理)。
  • 目录决定 UI。 前端的模型选择器列表直接来自目录:可见性、排序优先级、是否默认、是否仅 API key 模式可用(ChatGPT 登录和 API key 用户看到的模型集不同)、升级建议与退役倒计时,都是目录数据的投影。模型退役不是“哪天突然不能用”,而是目录里先带上升级建议和退役日期,引导用户迁移。

3.9 小结:面向“不确定远端”的四条设计原则

  1. 能力是数据,不是代码。 模型支持什么、不支持什么,全部沉淀在远程可更新的能力卡片里;每个推理参数发出前都过一道“这个模型支持吗”的门控,不支持就省略或回落,绝不把请求发成错误。未知模型拿到保守兜底档案并降级相关功能。新增模型、新增档位、新增传输格式,大部分时候不需要改 harness 代码。

  2. 状态归属 harness,缓存交给服务。 harness 坚持无状态请求(store=false)、历史只追加、每次请求自足可解释;同时用稳定的缓存键(会话 ID)、WebSocket 增量链和粘性路由配合服务端缓存,并以“非输入字段全等 + 输入严格前缀扩展”的双重校验保证增量优化永远不会在错误的上下文上运行。

  3. 按语义区分故障,连续性是默认值。 瞬时故障(断流、软限流、过载、令牌过期)重试或自愈,重试对轮次状态透明、对用户可见;永久故障(超长、没钱、非法请求、安全拦截)快速认输并给出可操作的信息。重试穷尽还有传输降级兜底。轮次要么向前推进,要么响亮地报告,绝不静默卡死。

  4. 把变化冻结在边界上。 模型档案轮次级冻结、参数步骤级快照、目录热更新只影响下一轮次;同一轮次内“看到的工具、使用的参数、执行的能力”严格一致(呼应第 1 章的步骤快照)。模型会变、目录会变、服务端会改道,但这些变化都被挡在轮次边界之外。

留给读者思考的几个问题

  • harness 一边坚持 store=false 的无状态请求,一边又用缓存键和 previous_response_id 深度依赖服务端状态——“无状态”的边界到底划在哪里?为什么这个边界比“全靠服务端记忆”或“完全不要服务端缓存”都好?(→ 第 4、9 章)
  • 兜底能力卡片选择“什么高级功能都关”,而不是“什么都假设支持”。反过来做会怎样?(提示:一次失败的工具调用和一次缺失的能力,哪个更容易被发现?)
  • 增量请求要求“非输入字段完全一致”——为什么推理档位变了就算上下文前缀没变也必须全量重发?这和第 1 章“插话只在步骤边界注入”是同一种什么约束?
  • 软限流(429 拥塞)在请求级不重试、在流级重试,硬限额(额度耗尽)在哪一级都不重试——如果把硬限额也做成退避重试,用户会看到什么?
  • 服务端把请求改道到另一个模型时,harness 只警告不阻止。什么情况下“警告”就够了,什么情况下必须“拒绝”?这与第 8 章的安全策略边界有什么关系?

下一章我们进入上下文与指令管理:每次请求里那份越来越长的“历史”到底是怎么组装的——消息如何增量累积、超长时如何压缩、环境信息以什么形式注入、系统指令又分哪几层。

分类:Agent Harness标签:#agent #harness #codex