Files
Artemis-Eapil/docs/llm-stream-diagnostics.md

99 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模型流式参数诊断
小型内置动作的参数流仍受 16384 字符保护。超限时关闭输出流、丢弃不完整参数,不执行该次动作;一次新响应重试和可用备用端点耗尽后,只结束当前任务,不进入共享的模型暂停。批次仍遵循“失败即停止”、人工暂停和取消设置。网络故障、认证错误和最终验收保留各自的恢复规则。
长度保护不是生成进度判断:小于上限但一直没有闭合或终止的参数,仍可能持续到单次请求或外层任务超时。超时、取消和完整性校验共同约束这条路径,未完成的参数不能作为动作执行。
## 请求关联
每个模型请求尝试沿用现有 `llm_diagnostic` 记录,不另建日志数据库。
| 字段 / 请求头 | 含义 |
| --- | --- |
| `request_id` / `X-Client-Request-Id` | 同一次恢复循环内保持稳定的逻辑请求编号;切换备用调用或恢复暂停后可能建立新的逻辑请求 |
| `attempt_id` / `X-Client-Attempt-Id` | 本次客户端尝试编号;重试时变化,`previous_attempt_id` 关联前次尝试 |
| `client_*_id_matches` | 实际发送的请求头是否与本地编号一致;调用方已指定的头保留原值 |
| `server_request_id` | 首个有效的 `X-Oneapi-Request-Id`、`X-Request-Id` 或 `Request-Id`,按此前后顺序选择 |
| `response_x_request_id` | 响应中的原始 `X-Request-Id`,不推定它来自模型上游 |
| `error_request_id` | SDK 异常带回的编号,不覆盖已经从响应头读取的编号 |
| `response_id` | SDK 实际返回的 Responses 响应编号 |
编号只接收有长度限制的可打印白名单字符。请求头不控制会话亲和性,不替换 `thread-id` / `session-id`。网关自身的重试应使用独立编号或序号,不能假装是一次新的客户端尝试。
## 参数指纹 v1
`tool_observation_layer=langchain_tool_call_chunks` 明确表示客户端观测的是 SDK 转换后的参数片段,不是原始 SSE 帧。每工具记录实际可得的 `call_id`、`chunk_index`、工具名,以及:
- `argument_chars`:参数片段的 Unicode 字符数之和。
- `argument_bytes` / `argument_sha256`:按原顺序连接片段后,其 UTF-8 字节长度和 SHA256;不解析、重排参数 JSON,也不规范化空白或 Unicode。
- `argument_deltas`:非空参数片段数,不能直接与网关原始事件数比较。
- `argument_prefixes`:每 4096 字节的累计 SHA256,最多保留前 16 个。可以在分片大小不同、上游预读或中途取消时比较相同字节位置。
- `argument_encoding_valid`:遇到不能编码为 UTF-8 的片段时为 false,当前总字节数和总哈希留空,避免伪造可比结果;诊断本身不改变执行流程。
- `argument_json_whitespace_chars` / `argument_json_whitespace_run` / `argument_json_whitespace_run_max`:实际空格、制表、回车和换行字符的总数、当前尾部连续数和最长连续数。统计的是这四种 JSON 空白码点,也包含字符串内部的实际字符;转义文本 `\\n`、不换行空格不计入。不据此判断 JSON 是否有效。
- `argument_delta_chars_min` / `argument_delta_chars_max`:非空参数增量的最小、最大字符数;尚无非空增量时最小值为空。
- `argument_same_delta_run` / `argument_same_delta_run_max`:连续相同非空增量的当前、最长出现次数,首个增量算一次,空增量不打断。只保留上一增量的 SHA256,不保留正文。相同增量内容不等于重复的协议事件,例如连续空格也可能来自不同的合法事件。
最多记录 64 个工具;超过时设置 `tool_stats_truncated=true`,总参数字符计数仍继续。诊断不保留参数正文。哈希仅证明已收到的字节前缀,不能证明输出完整或动作成功;完整性仍由响应终止状态和解析校验决定。
跨三端优先使用 `response_id + call_id` 关联。LangChain 的 `chunk_index` 可能因文本或推理子块不同于原始 `output_index`,缺失编号时不能按数组位置猜测。正常的 `arguments.done`、`output_item.done`、`response.completed` 快照不重复计入参数;自定义工具转换、非空初始参数或协议异常需要先核对 SDK 映射,不能仅凭跨层哈希不同认定某一端重复发送。
历史日志没有保存的原始帧无法由新增诊断恢复。HTTP 200 只说明响应头已到达;客户端保护后出现的 499 或 Broken pipe 也不能证明最初原因。网关写入本机下游 writer 成功不等于客户端已消费或执行。
## 客户端原始 Responses 观测
`wire_responses` 被动观察 HTTP 客户端实际读取的 SSE 字节,在 SDK 转换之前统计。同一网络读取中可能包含多个事件,因此原始统计可以领先 SDK;SDK 回调阻塞也会扩大两者差距。这一边界不是服务器写出时刻,也不是工具执行时刻。诊断不增加预读任务、队列或重传,不过滤、改写交给 SDK 的字节。
每个响应记录 `response_id`、事件数及 `sequence_number` 的首末值。`sequence_forward_gaps` / `sequence_forward_missing_total` 统计相邻观测间的向前跳变次数及跳过的序号数;乱序事件可能随后补上,不能直接称为永久丢包。`sequence_non_increasing` 统计非递增,`sequence_equal_previous` 统计与前一个序号相等。`sequence_missing_events` 表示事件本身没有有效序号,不表示传输丢失。没有响应编号时不猜测,记录 `unknown_response_events`;标准流中没有编号的后续事件沿用当前响应编号。
每个函数调用记录与 SDK 相同的参数指纹和形态统计,另记原始 `delta_events`、首末参数到达时间,以及两类 done 事件的次数。只累计非空初始 arguments 和 arguments.delta,不重复累计 done 的完整快照。关联优先使用响应编号与实际 `call_id`,`item_id` / `output_index` 用于同一原始流内关联;`identity_unknown`、`identity_conflicts` 表明身份不足或冲突,此时不能据此归责。
每次尝试最多观察 4 条 HTTP 流;每条流最多 16 个响应、64 个工具、128 个索引别名,每工具最多 16 个固定前缀。单个待解析 SSE 帧最多 256KiB,超出后仅跳过该帧的诊断,继续原样交给 SDK。`oversized_frames`、`parse_errors`、`statistics_truncated`、`wire_streams_truncated` 说明覆盖缺口;`incomplete_sse_frame` 与 `body_eof_observed` 区分未闭合帧和正常字节流结束。待解析字节仅临时保留,关闭时释放,日志不保存帧或参数正文。
原始分帧支持 LF / CRLF。HTTP 体声明 gzip 等压缩编码时,标记 `sse_observation=skipped_content_encoding`,只统计读取字节与时间,不把压缩字节误当成 SSE;SDK 仍按原路径解压。这种记录缺少原始事件统计,不能把零计数当成没有收到参数。
## 实际工具约束与耗时
`wire_request_constraints` 检查 HTTP 请求已经序列化的正文,不记录提示、图片或 Schema 正文。逐工具记录 `strict` 为 `omitted`、`false`、`true`,异常值为 `invalid`;不能把省略当成 false,更不能把 Pydantic 本地严格校验当成上游约束。Schema 只保存 SHA256、对象数、非 required 的属性数及未关闭对象数。这是语法统计,不展开引用,也不是上游 Schema 兼容性验证。
当前 `click`、`action_sequence`、`drag` 的真实 SDK 离线请求均省略 strict,保留可选字段。不能只把 strict 开关改为 true:应先按目标接口支持的 Schema 子集处理对象和 required,保留可选值的 nullable 语义,再单独验证兼容性。
请求正文超过 4MiB 时跳过检查并记录 `body_too_large`;流式上传正文不可直接读取时记录 `streaming_body_unavailable`,不会为了诊断消费上传流。最多检查 64 个工具或命名空间节点,每个 Schema 最多统计 1024 个节点,达到上限标记截断。响应事件如果回传 tools,则保存同样的 `returned_tool_constraints`;回显不证明服务端实际执行了约束。
`max_output_tokens` 与 `returned_max_output_tokens` 只表示本层实际观察到的字段。本次核对的 CodexManager 通道会过滤输出 token 上限;短合成探针成功也不能证明上限被执行。不能把设置 max_tokens / max_output_tokens 当成这条链路可靠的止损办法,客户端长度、超时和取消保护仍然必要。
`stream_work` 记录每阶段的调用次数、总耗时和最长耗时:`wire_observe` 为原始帧诊断,`sdk_observe` 为 SDK 片段诊断,`merge` 为聚合,`emit` 为界面回调,`await_sdk_chunk` 为等待 SDK 交出下一个片段。最后一项包含网络读取、原始观测、SDK 转换和 SDK 回调等工作,与其他阶段可能重叠,不能相加或直接当成网络延迟。
聚合每 128 个片段合并一次,降低 SDK 反复解析半截 JSON 的开销;长度保护、诊断和界面输出仍逐片段执行,异步压缩继续使用原异步调用路径。分批合并不修复参数 JSON,最终仍须经过完整性和严格解析校验。
## 取消与关闭
`cancel_observed_at` / `cancel_observed_phase` 记录本次尝试首次在客户端观测到取消的时间与阶段,不等于用户点击停止的时间。`cancel_phase` / `cancel_reason` 沿用恢复层最终记录的取消阶段和来源,可能反映后续重试等待中的停止。首末 SDK 参数时间为 `first_tool_arguments_at` / `last_tool_arguments_at`。原始流内的读取、参数和关闭时间采用 Unix 秒。
SDK 读取中、回调中或两次 yield 之间触发长度保护后,尝试结束路径都会主动关闭已观察的 HTTP 流,不等待垃圾回收。`close_started_at` 记录关闭开始,只有实际成功才写 `closed_at`;失败只记录异常类型,关闭等待最多 1 秒。关闭超时不能覆盖用户停止而变成可重试的请求超时。未能关闭时保留错误证据,不声称连接已成功释放。
## 已核对的上游异常样本(2026-11-02)
一次真实 `action_sequence` 调用在任务超时前持续接收参数,没有完成该工具调用。该样本发生在 `action_sequence` 纳入参数保护之前。按同一 `response_id + call_id` 核对生产诊断,得到以下字符数;请求编号和设备信息不在文档中保留:
| 观测边界 | 参数字符数 |
| --- | ---: |
| CodexManager 收到的官方上游 WebSocket | 40907 |
| CodexManager 本地下游 writer | 40906 |
| NewAPI 收到的上游 SSE | 40904 |
| NewAPI 本地下游 writer | 40904 |
| ARTEMIS 的 SDK 参数片段 | 40619 |
五个边界在 4096 到 36864 字节的九个累计 SHA256 上一致。NewAPI 使用透传模式,收到和写出的完整参数哈希也一致。上游没有终止事件,也没有完成参数快照。该样本证明参数在官方上游返回时就已持续增长,不能归因于 NewAPI 转换、下游重复累计完成快照或 ARTEMIS 最终消息拼接。取消、预读和缓冲会使各层停止计数的位置不同;尾部总长度不同本身不能证明重复发送或字符丢失。
诊断没有参数正文,不能进一步断言是模型生成循环、空白循环、重复描述或官方服务内部的重复事件,也不能排除请求结构触发异常。这个定位只排除已核对样本的下游放大作为最初来源,不能追溯证明没有原始诊断的历史事故。另一次 `click` 异常同样在 NewAPI 接收时已超过四万字符,说明这种现象并非新动作序列独有。
CodexManager 另行修正了普通函数放入 functions 命名空间的兼容性问题。其短上下文、低思考强度合成测试通过,仅能作为兼容性对照,尚不能代替原事故的长上下文、多图、高思考强度复现,也不能称为参数膨胀的根因修复。
本地保护覆盖 `action_sequence`、`drag` 及其他小型内置动作:超限后关闭流、丢弃参数,并按既有策略有限重试;它限制异常对当前任务的影响,不修复上游生成行为,也不保证业务任务一定成功。缩放面板自动隐藏导致的定位失败由动作序列的 `ensure_visible` 准备步骤处理,与参数流异常分别诊断。
## 离线验证
`tests/unit/test_llm_stream_guard.py` 验证保护、有限重试、相同/不同备用端点,以及不会创建或清除其他请求的全局暂停。`tests/unit/test_request_diagnostics.py` 验证请求编号、超时重试、响应头及单次参数计数。`tests/unit/test_argument_fingerprint.py` 验证不同分片、中文和 emoji、固定字节前缀、有界统计与异常编码。这些测试不调用真实模型或设备。
`tests/unit/test_responses_diagnostics.py` 验证原始分帧、序号、done 去重、有界统计和 strict 三态。`tests/unit/test_responses_wire_lifecycle.py` 使用已安装的真实 OpenAI/LangChain SDK 与内存 HTTP 传输,验证小于长度阈值的未闭合流、SDK 回调背压、请求/任务超时、用户停止、超限后主动关闭和关闭失败,以及实际工具 Schema;不发网络请求。`tests/unit/test_chunk_accumulator.py` 对比逐片段聚合结果,验证交错工具、元数据和使用量保持一致,减少半截 JSON 重复解析,异常参数仍由最终校验拒绝。离线通过不代表原业务任务的稳定性已验收。