07|模型接口内部:消息、输出项、SSE与工具调用协议
目标:构建不把半截JSON当动作、不把断流当完成的模型适配器。实验:
sse_protocol.py;可选在线适配器:live_readonly.py,本书未运行付费调用。
本章目录
沿着对象与状态理解本章
协议按字节、事件、对象、完成语义逐层处理,不在半份参数上产生副作用。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
字符完整、事件完整、工具参数完整是三道关
为什么看到半段JSON时绝对不能执行?
网络像分批送到的纸条,分割点不遵守汉字或JSON边界。接收端先重组字节和字符,再按流协议重组事件,最后解析结构并确认该输出是否表示完整行动。花括号配平不够,因为字符串内部也可能有花括号。
输入与适用条件:真实编码了一个小SSE文本事件,故意把“爱”的三个UTF-8字节拆成两段。示例并不使用厂商特定的事件命名。
1 / 4 · 收到第一段字节
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
网络片段可以停在一个汉字的UTF-8编码中间。现在只有“爱”的第一个字节,不能把每个网络片段独立按完整字符串解码。
字节缓冲默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
让网络分片方式不影响语义与执行次数。
怎样实现
增量解码→组装事件→解析JSON→检查完成状态→按类型分派。
可能的代价与误解
提前执行、重复事件未去重、调用ID错配,都可能把一次动作变成多次副作用。
如何判断是否做对
把同一字节串在每个切分点拆开,解析后的事件都应一致。
1. 把模型服务视作有状态协议,而不是一个字符串函数
简单接口可以是answer = model(prompt),但真实Agent需要区分文本消息、函数调用、函数结果、推理或压缩状态以及响应终态。某些接口可以由服务保存历史,某些要求客户端回传项目;隐私配置、模型能力与产品接口共同决定可用方式。适配层应保留结构化对象,不应只提取文本后把其余项目全部丢弃。[N01][N06][N17]
例如一个响应可能同时有说明文本和两个工具调用。如果代码只判断“有文本就结束”,工具永远不会执行;若把整个响应转成字符串塞回历史,调用ID、角色和服务需要的状态就失去了原始类型。公开接口要求保留的不透明状态,应原样保存或按文档引用,不能自造摘要代替。
工具调用是模型提出的动作候选。它的名称、参数和ID进入验证器;验证器返回可执行的内部命令;执行器完成实际动作;结果再以匹配的调用ID反馈。名称存在、参数语法正确、权限允许、状态新鲜、执行成功,是五个独立判断。
2. 流不是以完整字符或JSON对象为单位抵达
网络字节流可以把一个中文字符、一个JSON转义序列或SSE边界拆开。正确解析顺序应是:字节增量UTF-8解码,按SSE行规则组装事件,遇到空行形成完整事件,拼接data字段,然后解析该事件JSON,再分派事件类型。[N19]
本书实现支持分块字节、CRLF换行和多data行,并对未结束的最后一事件报截断。测试把中文内容逐字节送入,验证不会因多字节切分损坏文本。不要对每个网络chunk直接json.loads;chunk只是传输边界,不是协议边界。
同理,arguments.delta只是参数的增量,可能尚不闭合。模型先输出{"path":"src/时,不应提前读取一个目录;看到字符串中途的危险片段,也不能简单通过字符串替换“修复”参数。等待约定的输出项完成,再做完整解析与授权。
3. 输出项完成与整个响应完成
本书教学适配器采用保守策略:收集完成的调用项,并在收到响应成功终态后才允许执行;响应失败或截断,禁止执行尚未提交的候选。这是参考实现选择,不意味着所有商业客户端都必须等待相同节点。某些系统可在完整工具项到达后调度,但仍需要处理后续中断和幂等性。[N05][N19]
Calls对象把同一调用ID的增量与完成事件关联。若同一ID被重复用于不同内容,返回冲突;如果收到响应失败,已收集但未执行的调用不会被默默执行。协议层还应区分未知事件(可能向前兼容忽略)与关键缺失(必须失败),不能对所有异常都返回空文本后宣称成功。
生产接入还需要事件大小上限、解析超时、连接闲置超时、取消传播和重连语义。重连不是从头盲目执行所有工具,尤其当上一轮已经产生外部副作用时。
4. Schema验证能保证到哪里
JSON Schema可以约束字段类型、枚举、必填项和额外字段,但不能判断某个订单是否属于当前用户,也不能证明path指向的是用户授权的仓库。结构化输出是协议可靠性的基础,不是业务可信性的终点。[N18]
应把参数转换成类型明确的内部对象,再验证语义。读取文件工具至少要限定根目录、允许的文件类型、最大字节数、符号链接策略和编码失败行为。更新工具还要比较期望版本,以免把模型基于旧代码生成的修改写到新代码上。
异常结果应该具有可行动的含义。not_found可以指导换定位方式;permission_denied表示不能通过换拼写绕过;stale_version要求重新读取;timeout_unknown说明外部动作是否发生尚不确定。把它们都包成“工具失败,请重试”,会诱导错误恢复策略。
5. 工具描述与返回内容如何影响模型
工具名称和描述是模型选择动作的线索。应说明适用场景、关键参数和返回语义,不要把内部服务细节全部暴露给模型。对一组容易混淆的读写工具,强调副作用差异;对搜索工具,返回命中位置、片段、版本和是否截断,比无结构的几万行文本更容易继续使用。[N25]
但“返回更少”不是绝对优化。只返回命中行可能丢失决定分支的前置条件;只返回摘要可能丢失证据。较好的接口支持先定位、再读取连续范围,并明确结果是否完整。模型能逐步扩展证据,比工具擅自把关键信息永久丢掉更稳健。
当前工具Schema也占用上下文。工具很多时可以先提供发现入口,再加载相关定义;只有宿主支持该机制时才这样做。技能文件里写“自动JIT”不代表执行器已经实现按需工具发现。
6. 可选在线适配器的边界
live_readonly.py使用官方Responses接口,要求明确--allow-paid-api、用户指定模型和环境变量中的密钥。只暴露只读文件工具,没有Shell、写入或部署工具;不自动重试网络请求,避免不明确的重复成本。响应输出项按结构保留再回传。
配套结果不包含这个在线入口的实测,因此服务兼容性、模型能力、实际计费与真实任务效果均标为未实测。读者应仅使用人工准备、无秘密的样本目录;文件内容会发送给模型服务。根目录检查不是面向恶意并发文件系统的生产沙箱,不应直接开放整个主目录或企业仓库。
工具参数语法结束后,还需要等待调用结束信号吗
输入与条件。流中先出现参数片段 {"n":1},后面还可能有同一输出项的后续delta或结束事件。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1临时可解析 | 目前字符串恰好是合法JSON | 只说明这个前缀能解析。 |
| 2协议完成 | 等待所用接口定义的调用参数或输出项完成 | 确认没有后续修正或追加。 |
| 3结构/业务检查 | n必须为正整数且在上限内 | 完整语法不代表参数可用。 |
| 4调用一次 | 以稳定call_id登记执行状态 | 避免重连事件重复触发。 |
| 5断线恢复 | 不知道完整结束事件是否到达 | 按协议查询或重建,不任意猜完成。 |
把过程连起来。可解析前缀、完整参数、调用完成和整个响应完成不是必然同一时刻。
边界与反例。不同API的事件名与完成边界不同,教学代码只实现已声明子集。
7. 验收与失败定位
必须通过四个反例:UTF-8在字符中间切分,参数JSON在字符串中间切分,调用项完成但响应失败,收到文本后网络断开。前三者能测试协议状态机,第四项检验“人类可见回答”是否被错误当成任务完成。
若模型反复调用不存在的工具,先比较本轮实际工具Schema和模型返回名称,而不是直接增加Prompt警告。如果工具存在但被权限拦截,应检查身份、宿主配置和调用作用域。模型不应该负责猜测执行器隐藏的配置错误。