跳转至

第 2 章 模型接口与调用契约

harness 与模型之间只有一层契约:请求里放什么、响应里可能回来什么、失败时怎样区分。这层契约决定了循环能否闭合、工具参数能否安全执行、上下文预算能否算准。本章按"消息 → 参数模式 → 流式 → 结构化输出 → 计量 → 错误 → 路由"的顺序拆开,接口细节以厂商文档为准,机制层面的判据不随模型换代而变。

1. 无状态调用与消息数组

一次请求带三样东西:系统提示、消息数组、可用工具清单。服务端不保存会话,消息数组就是全部真相;换一个进程、换一个厂商,只要数组内容一致,模型看到的历史就一致。这带来两个直接后果:harness 必须自己维护历史(第 3 章),且同一段历史可以在不同模型间迁移。

请求还带一组每次都要显式给的参数:模型名、最大输出长度、温度与随机性设置。这些参数不要依赖服务端默认值——默认值会随版本变化,同一段请求在不同时间跑出不同结果时,先查参数是否漂移。重发完全相同的请求不等于幂等:生成本身有随机性,重试只能保证输入一致,不能保证输出一致。

消息的角色通常分四类:系统(system)给不变量与边界,用户(user)给任务与新输入,助手(assistant)放模型之前的输出,工具(tool)放工具执行结果。工具消息必须带触发它的调用标识,否则并行调用时结果无法与请求配对。角色不是装饰:把工具结果误放进用户消息,模型会把它当成人的指令。

2. 内容块与多模态

消息内容不再只是字符串,而是一组内容块(content block)。常见类型有文本、图像、工具调用、工具结果四类。图像块给引用或内联数据,工具调用块带名称、参数与调用标识,工具结果块用同一标识回填。

工具结果块最少带三个字段:调用标识、内容、是否错误。缺了错误标志,模型只能从文字里猜失败;缺了标识,并行调用时结果会接错。推理类模型的“思考块”通常不回到历史,只影响当轮生成;把思考内容当成可引用的事实写进下一轮,会把未经验证的假设固化成前提。

两类常见错误都出在"块的边界"上:把工具结果与文本拼成一段字符串,模型无法区分哪部分是观测、哪部分是说明;把图像与文本的顺序打乱,模型对"这张图对应哪句话"的指代会漂。块的顺序与类型就是语义,拼接会丢语义。

3. 工具参数的模式

工具声明由名称、描述、参数模式(JSON Schema)三部分组成。参数模式决定模型能生成什么形状的调用,也决定 harness 能在派发前校验到什么程度。最小可用的模式包含类型、属性、必填项与是否允许额外字段;枚举用来收窄取值,默认值减少模型必须填的字段。

{
  "name": "read_file",
  "description": "读取工作区内的文本文件;路径相对工作区根目录",
  "parameters": {
    "type": "object",
    "properties": {
      "path": {"type": "string", "description": "相对路径"},
      "max_bytes": {"type": "integer", "minimum": 1, "maximum": 200000, "default": 20000}
    },
    "required": ["path"],
    "additionalProperties": false
  }
}

描述与模式共同影响工具选择:名称含糊、描述不写边界、参数之间互斥却不表达,模型就会选错工具或填出无效参数。参数校验必须在派发前完成,且失败要分类——字段缺失、类型错误、越界、枚举外取值,模型大多能根据具体错误修正;而"工具不存在"或"参数无法表达用户意图"属于不可恢复,应立即终止或换路径。

3.1 严格模式的代价

部分接口支持严格模式(strict schema),要求所有属性都列入必填、禁止额外字段,并由服务端保证输出符合模式。代价是模式表达力下降:可选字段、复杂联合、条件约束往往无法直接表达。工程上的折中是把"模型必须填的"与"系统可补默认的"分开,模式只约束前者,后者在派发前补齐;不要为了严格模式把一堆默认值硬塞进必填项,那会让模型编造取值。

可选参数用可空类型或枚举里的“未知”项表达,而不是“必填但允许空字符串”——后者会让模型用空字符串应付,把“未指定”与“显式置空”混为一谈。

4. 流式响应与增量解析

流式响应把一次生成拆成事件流(通常基于 Server-Sent Events):开始、增量、结束,工具调用另有独立的事件序列。流式的直接收益是首 token 时延与可中断性,代价是解析变复杂——尤其工具参数。

事件流要当 unreliable:断流、重复事件、结束事件丢失都可能发生。收到结束事件前不得派发工具;流提前关闭时,丢弃未完成的那一轮或按幂等性重试整个请求,不能把半截参数拿去执行。心跳用于区分“模型在思考”与“连接已死”,没有心跳就只能靠超时。

4.1 分片拼接

工具参数可能分成多个增量到达,且每个增量只是字符串的一段,单独解析必然失败。正确做法是先按调用标识累积,收到结束事件后再整体解析与校验。

多个工具调用的增量会交错到达,缓冲区必须按调用标识分开;不能用“当前正在解析的那个调用”这类全局状态。单个参数超过预设上限时直接截断并报错,而不是无限累积——流式解析的内存上限也要算进资源预算。

buffers = {}                       # call_id -> 参数字符串片段
for event in stream:
    if event.type == "tool_args_delta":
        buffers[event.call_id] = buffers.get(event.call_id, "") + event.delta
    elif event.type == "tool_call_end":
        raw = buffers.pop(event.call_id)
        args = json.loads(raw)     # 结束事件之后才解析,途中解析必失败
        validate(tool_schema[event.name], args)

流式还引入一个判定变化:用户中断时,已经生成的文本与已经派发的工具不会自动回滚。harness 要记录"哪些已生效",中断后按已生效状态恢复,而不是把整轮当作没发生(第 9 章)。

5. 结构化输出的两条路

让模型稳定产出结构化数据有两条路:模式约束解码(服务端在生成时限制可选 token,输出保证符合模式)与后校验重试(先生成,再本地校验,不符合则带错误信息重试)。前者一次成功率高、表达力受模式限制;后者表达力强、但要多付调用成本与延迟。

约束解码不是万能的:模式过于复杂时,服务端要么拒绝,要么退化成后校验;模型可能在严格语法下反复输出空对象或直接拒绝生成,这类失败要当成“模式不可用”而不是“模型不行”。后校验重试要限次数,并逐次收紧提示,否则同一错误会稳定重现。

判据:有副作用的动作前必须本地校验,无论服务端是否承诺符合模式。校验失败的重试要带上具体错误(哪个字段、期望什么、实际什么),并设次数上限;把原始输出与错误一起回灌,比只说"格式不对"有效得多。

6. token 计量与窗口

窗口是输入加输出的硬上限,不是"可以塞多少字"的建议值。计量要分三项:输入 token、输出 token、缓存命中部分。工具定义、系统提示、检索内容都计入输入;每轮重发历史,使输入随轮数增长。

本地估算只能用于预算,不能用于对账:中文与代码的 token 比例差别很大,估误差达到两三成很常见。预算阶段按估值留安全边,实际计费以服务端返回的用量为准;两者差距持续扩大时,说明工具定义的序列化方式或语言分布变了。

计量的实现侧分两家:OpenAI 系用 tiktoken,其他模型用 Hugging Face 的 tokenizers(分词器资产从 huggingface_hub 拉);同一段文本换分词器计数可差一截,跨模型比预算时先统一到“同一分词器、同一模板”,否则比较的是分词差异而不是上下文用量。

预算顺序决定截断顺序:先截工具结果(保留首尾与关键行),再压历史(摘要或丢弃中间轮),最后才动系统提示——系统提示承载不变量,截它等于改规则。输出必须预留足够空间,否则结构化输出会在末尾被截断,表现为"JSON 不完整"而不是"超窗"。

7. 错误分类与重试

错误处理的第一步不是重试,而是分类。可按可恢复性分四档:

类别 典型信号 处理
限流 429、并发或配额提示 退避加抖动后重试,限制总时长
瞬态 5xx、连接重置、超时 重试,但必须先确认请求幂等
超窗 上下文长度错误 不重试,先压缩或换更短的工具结果
参数或策略 400、内容过滤、模式校验失败 不盲目重试,修正输入或换策略

7.1 幂等前提

"重试能解决"只对幂等请求成立。读操作与纯生成可以重试;已经产生副作用的写操作重试会重复执行。工程做法是给每个动作一个幂等键,工具侧按键去重;没有幂等键的写操作,重试前必须先查询实际状态。把这条判据前移到工具设计(第 4 章),比在错误处理里补救可靠。

退避要带抖动与总时长上限:固定间隔会让并发请求同时重试,形成新的尖峰;总时长上限防止一个请求占到任务超时。取消与超时要分开处理——用户取消不应触发重试,超时才按错误类别决定是否重试。

8. 多模型路由与降级

多模型路由的动机通常是成本、能力与可用性。路由判据应落在契约差异上:是否支持工具调用、是否支持图像、窗口多大、是否支持结构化输出与缓存。把任务按这些能力分组,比按"模型强弱"分组稳定。

能力矩阵要写成配置而不是代码分支:任务类 → 必需能力 → 候选模型 → 降级顺序。每次路由记录实际选中的模型与原因,同一任务前后两次落到不同模型时,差异才有处可查。

降级链要有明确的语义边界:强模型失败降弱模型时,如果弱模型不支持同等工具模式,降级会静默改变行为。合格的做法是降级前检查能力交集,交集不足时直接失败并说明缺哪项能力;宁可显式失败,也不要让同一任务在不同模型上跑出不同语义。

9. 要点

  • 请求三件套:系统提示、消息数组、工具清单;服务端无状态,历史由 harness 维护。
  • 内容块的类型与顺序就是语义,工具结果必须带调用标识回填。
  • 参数模式既是给模型的约束,也是派发前校验的依据;严格模式要付表达力代价。
  • 流式工具参数按调用标识累积,结束事件后再解析;中断要按已生效状态恢复。
  • 结构化输出两条路各有代价,有副作用的动作前必须本地校验。
  • 错误先分类再重试;写操作重试的前提是幂等键或先查状态。
  • 降级按能力交集判断,交集不足时显式失败,不做静默语义替换。

参考文献与延伸阅读

  • OpenAI. Function Calling 指南. 文档
  • Anthropic. Tool Use 文档. 文档
  • Model Context Protocol. 规范与文档. 规范