DeepSeek Harness 报 INVALID_REQUEST:孤立代理项、空内容块与内容审核锁死

故障排查发布于 2026-10-17作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH 400INVALID_REQUESTlone surrogatetoWellFormedthinking 模式Content Exists Risk
DeepSeek Harness 会话每轮请求都 400 且换新会话也复发,多半是历史里混进了非法请求体:孤立代理项、被按码元截断切开的 emoji、空 reasoning 或纯空白文本块、内容审核命中的记录,或消息里出现 dsh-session: 字面量。本文给出各自的定位特征、字段级修复与离线抢救步骤。

DeepSeek Harness(DSH)里最让人误判的一类故障是「每轮都 400」:报错看起来像是模型或网络的问题,开新会话也照样失败,重启也没用——因为毒不在这一次请求里,而在历史里,而历史每轮都会被整段重发。 本批讨论把这一族拆成四种独立的「非法请求体」:非法 Unicode(出站消息含孤立代理项,或 read 工具按 UTF-16 码元截断切开 emoji 造成孤立高代理项);空内容块(模型某次没有推理内容却被持久化成 text: "" 的 reasoning 块,或适配器把 {\"text\":\"\\n\"} 这种纯空白文本块塞进重放);内容审核命中(Content Exists Risk,内容属性 + 每轮重发 = 永久锁死);以及引用解析误命中(消息里出现 dsh-session: 加一串数字,被当成会话引用去解码,失败即整轮抛错)。四者的共同判据是:报错里能找到可搜的字面量,而 JSON.parse 之类的语法检查全部通过(#9089、#8846、#9435、#8830、#9613)。

DeepSeek Harness 非法 Unicode:孤立代理项与「按码元截断」

一句话结论:JavaScript 的字符串操作按 UTF-16 码元计数,一个 emoji 占两个码元,一旦截断点落在中间就会产生孤立代理项;它落盘成合法 JSON(\\ud83d),但被服务端的内容校验判为语义非法,于是整轮请求被拒。 两个来源都真实出现过:外部内容自带坏码元,以及 DSH 自己的 read 工具截断(#9089、#8846)。

DeepSeek Harness 机制:为什么「语法合法」却过不了

  1. JSON.stringify 会把孤立代理项写成 \\ud83d 这样的转义序列——这在 JSON 语法层面完全合法,所以任何 JSON.parse 检查都不会报错;
  2. 服务端的 Messages 端点做的是内容校验(Unicode 合法性),不是语法校验,于是整请求被拒,返回 400 且不带定位信息;
  3. 坏值一旦进入会话历史,就会随历史每轮重发,于是「会话永久失败、开新会话也复发」;
  4. 代码侧缺的是入口校验:llm-deepseek 的 buildMessages 只做结构校验,没有 Unicode 合法性检查;read 工具的截断常量是 2000 个码元(不是码点),用 String.prototype.substring 切分,切在 astral 字符中间就产生孤立高代理项(#8846)。

DSH插件 三步定位:坏值在文件里是「可见的 ASCII」

  1. 在会话文件里粗筛孤立代理项(它们落盘成明文转义,可以直接正则找):
powershell
Select-String -Path "<会话文件>" -Pattern '\\u[dD][89abAB][0-9a-fA-F]{2}' -AllMatches |
  ForEach-Object { $_.Matches } | Group-Object Value | Sort-Object Count -Descending |
  Select-Object -First 10 Count, Name
#   预期:列出可疑的高代理转义,如 \ud83d
  1. 确认它确实是不成对的:高代理项(\\uD800–\\uDBFF)后面必须紧跟一个低代理项(\\uDC00–\\uDFFF)。若后面不是低代理,即为孤立代理项(#9089)。
  2. 确认来源:如果坏值出现在 read 工具的返回里、且位置恰好在第 2000 个码元附近,那基本可以认定是截断切开代理对造成的(#8846)。

DeepSeek Harness 修复:替换坏码元,只重编所在帧

  1. 备份会话文件,然后按「行级」处理:整份复制 → 逐行做 JSON.parse → 扫描不成对的代理码元 → 替换为 U+FFFD → 只重编所在的那一帧、不重新编号(#8846)。
  2. 重编完成后再扫一遍确认没有残留孤立代理项,然后写回。
  3. 代价要如实告知:替换成 U+FFFD 意味着半个 emoji 变成 �,属于有损修复;讨论里也提到社区有离线修复工具可做这类 lone-surrogate 处理(#9089)。
  4. 先止血再修复:如果每轮都有插件在往历史里注入坏内容,得先停掉那个注入源,否则修完立刻复发(#9089)。
  5. 代码侧的正解:用 text.toWellFormed() 在入口做一次规整,或把截断改成码点感知(#9089)。

DeepSeek Harness 空内容块:空 reasoning 与纯空白文本

一句话结论:模型某次回答没有推理内容时,Harness 仍会持久化一个 text 为空的 reasoning 块;而适配器会无条件把每个 reasoning 块序列化成原生 thinking 字段,空块被当成 thinking:"" 发出去就被拒——关掉 thinking 也救不了,因为坏块在历史里。 另一种形态是纯空白文本块({\"type\":\"text\",\"text\":\"\\n\"})夹在两次工具调用之间,让云 API 把消息切开(#9435、#9239、#8836)。

DeepSeek Harness 三种报错形态与对应机制

报错触发条件机制
The content[].thinking in the thinking mode must be passed back to the APIthinking 模式 + 历史里有空 reasoning 块空块被序列化为 thinking:""
thinking type must have non-empty thinking field切到严格的 Anthropic Messages 兼容网关网关对空 thinking 校验更严
tool_use ids were found without tool_result blocks云端端点 + 历史里有纯空白 text 块空白块把消息切开,前一工具的 tool_use 失去紧跟的 tool_result

据社区源级确认,代码侧的关键缺口是「丢弃空块的判定不对称」:interruptedBlocks() 会丢弃空块,而 assembled() 的 keep/drop 判定不会;同时 assistant() 拿不到 thinking 策略,所以把 reasoningEffort 设为 off 也改变不了历史空块的序列化(#9435)。空白文本块那一边则是 serialize.ts 的文本分支没有空块守卫,建议的修法是把判断改成 text.trim().length === 0(#8836)。

DeepSeek Harness 四步修复:块级删除 + 一个必须遵守的不变量

  1. 定位空块:在会话文件里搜空的 thinking / reasoning 内容:
powershell
Select-String -Path "<会话文件>" -Pattern '"thinking":""|"text":""|"text":"\\n"'
#   预期:命中若干条,即为空块
  1. 备份后做块级删除(不需要重新编号,这是它比 Unicode 那类省事的地方)(#9239)。
  2. 同时删掉 replayState.blocks 里同一位置的条目——这是本条最重要的不变量:只删消息块不动索引,assembler 会因为计数不符而丢弃整个 envelope,把签名一起丢掉,故障从「一块不行」升级成「整段不行」(#9239)。
  3. 预防侧:社区插件 @argszero/dsh-empty-block-guard 包裹 llm/stream 的返回值,阻止新的空块落盘:
bash
npm i @argszero/dsh-empty-block-guard

边界:预防插件只能防「新块」,已污染的日志必须离线修。另外注意不要把「丢弃空块」写得过宽——用朴素 || 判断会误伤带工具调用的块(#9435)。

DeepSeek Harness 内容审核锁死:内容属性 + 每轮重发 = 永久失败

一句话结论:Content Exists Risk 是内容属性而不是入口属性,而历史每轮都会整段重发,所以一条过线的记录就能让会话永久失败——连复制会话都带着毒。 讨论里把这个族归纳成「两种机制、三个毒源、三张面孔」,并确认响应侧还有一类 refusal(MALFORMED_RESPONSE)会每轮复现(#8830)。

DeepSeek Harness 三步处置

  1. 先按 turn/end 判定会话死活:能收到 turn/end 说明这一轮是正常结束的(属于响应侧掐流 / 拒答),收不到而只有 400 则是请求侧被拒。这两条路后续处理不同(#8830)。
  2. 唯一已验证的恢复路径是「从毒之前分叉」:从出错前的位置 fork 出新会话,把过线的那段历史留在原会话里不带走(#8830)。
  3. 争取自动恢复:给该 profile 的 retryableCodes 加上对应错误码,或使用把拒绝映射为可重试的社区插件,让请求侧 400 有机会在收窄重发后继续;讨论中也提到有社区插件会逐级收窄重发来续跑(#8830)。

判断要点:内容审核这一类永远不要指望「换入口就好」——换新会话并把历史带过去,等于把毒一起搬走。

DeepSeek Harness 引用解析误命中:消息里出现 dsh-session: 就让整轮失败

一句话结论:会话引用解析器把任何「dsh-session: + 一串字母数字」的裸文本都当成引用去解码,纯数字也是合法形态,解码失败即抛错,而调用点没有 try/catch,于是整轮失败。 甚至连报错原文里带着那个字面量、贴出来求助也会同样失败(#9613)。

据报告者核对源码:@deepseek-ai/dsh-session-reference 的 prepareDirectMessages() 对每条 user 消息的文本块调用 parseSessionReferenceText,正则裸文本分支匹配「scheme + [A-Za-z0-9_-]+」,纯数字合法 → base64url 解码再 JSON.parse 失败即抛 SESSION_REFERENCE_INVALID_REFERENCE;四个版本与 master 逐字未改(#9613)。

处置步骤:

  1. 定位:在消息文本里搜 dsh-session:;命中且后面跟的是普通数字/短串即为误命中。
  2. 短期规避:把该前缀转义或改写(例如插入空格、改用全角冒号),避免被解析器识别。
  3. 缓解插件:社区插件 @argszero/dsh-session-reference-guard 在同一条 waterfall 上以 prepend: true 注册,只重写会被拒的字面量;作者本人标注它是止血而非修复(#9613)。
  4. 正解方向:这一层应当 fail-open——解析失败按普通文本处理,而不是让整轮失败。

DSH插件 排查注意事项

「每轮都 400」的排查口诀是:先把报错文本当关键词去日志里搜,能搜到就直接定位;搜不到再按「换新会话是否复发」判断毒是在历史里还是在当前输入里。 九条要点:

  1. JSON.parse 检查通过 ≠ 内容合法:孤立代理项的转义序列语法完全合法,别用它排除问题(#8846)。
  2. 坏值在文件里通常是可见的 ASCII,正则粗筛比人工翻快得多(#9089)。
  3. 截断与截取要按码点:任何按「长度」切字符串的地方都可能切开代理对,这是本族的制造机(#8846)。
  4. 删空块必须同步删 replayState.blocks:这是本族最贵的一个不变量,忘了它会把小故障放大成整段失败(#9239)。
  5. 关 thinking 治不了历史的病:历史空块照旧被序列化(#9435)。
  6. 内容审核类记住「内容属性」四个字:换入口、复制会话都带毒,只有分叉能救(#8830)。
  7. dsh-session: 是字面量触发,跟会话是否真的存在引用无关(#9613)。
  8. 修复前一律备份:这四类修复都直接改会话文件,备份是唯一回退手段。
  9. 修复状态如实:四类在讨论所指版本里都未修;可用手段是「离线清理已污染日志 + 插件防新块」,属用户侧处置,不是官方修复。

排查这类问题时,常常需要先确认「是不是某个插件注入了坏内容」——尤其空块与引用误命中这两类,注入源往往就是第三方插件。如果你想先在界面上把插件的装卸与日志看清,与其在各处手敲命令,不如用 DSH Plugin Hub 这类可视化插件市场:已安装列表、装卸历史与系统日志集中在一处,定位「谁在往历史里写坏东西」会快不少。

系统日志

来源:Discussion #9089、Discussion #8846、Discussion #9435、Discussion #9239、Discussion #8836、Discussion #8830、Discussion #9613。

常见问题

DeepSeek Harness 会话每轮都报 400 INVALID_REQUEST,开新会话也一样失败,怎么定位是哪条记录坏了?

DeepSeek Harness 这种「每轮都 400」的坏记录,先看报错里有没有可搜的字面量:能搜到就直接定位。如果是孤立代理项这一类,日志里能看到 JSON 里直接写着 \ud83d 这样的转义(它是合法 JSON、语义非法,所以 JSON.parse 检查不出来);可以在会话文件里用正则粗筛不成对的 \uD800-\uDFFF。修复用的是把坏码元替换成 U+FFFD,或者用 text.toWellFormed() 做一次规整,然后只重编所在的那一帧、不重新编号。

DeepSeek Harness 的 thinking 模式反复报 thinking must be passed back,为什么把 thinking 关掉也没用?

DeepSeek Harness 这个报错来自历史里已经落盘的空 reasoning 块,不在当前这次调用。模型某次回答没有推理内容时,Harness 仍会持久化一个 text 为空的 reasoning 块,而适配器会无条件把每个 reasoning 块序列化成原生 thinking 字段,空块被当成 thinking:"" 发出去就被拒。关掉 thinking 只影响新请求,改变不了历史里已存在的空块,所以必须离线清理这些块。

DeepSeek Harness 老会话切到严格 Anthropic Messages 网关后报 thinking must have non-empty field,怎么修?

DeepSeek Harness 报 thinking type must have non-empty thinking field,同样是空 thinking 块,但这类网关校验更严。修复时有一个必须遵守的不变量:删除空 reasoning 块的同时,必须同步删掉 replayState.blocks 里同一位置的条目,否则 assembler 会因为计数不符而丢弃整个 envelope,连签名一起丢。存量会话按块级删除即可,不需要重新编号。

DeepSeek Harness 会话一直报 Content Exists Risk,换新会话复制历史也还是失败,有救吗?

DeepSeek Harness 报 Content Exists Risk 有救,但要接受「从毒点之前分叉」这个前提。内容审核是内容属性而非入口属性,而历史每轮都会整段重发,所以一条过线的记录会让会话永久失败。已验证的恢复路径是从出错之前的位置分叉出新会话;另外可以给该 profile 的 retryableCodes 加上对应的错误码,或使用把拒绝映射为可重试的社区插件来争取自动恢复。

DeepSeek Harness 里消息只写了一句 dsh-session:382,整轮就失败了,为什么?

DeepSeek Harness 的会话引用解析器把任何 dsh-session: 加一串字母数字的裸文本都当成引用去解码。schema 加纯数字也是合法形态,解码后 JSON.parse 失败即抛 SESSION_REFERENCE_INVALID_REFERENCE,而调用点没有 try/catch,于是整轮直接失败。缓解办法是避免在正文里出现该前缀,或用社区插件把会被拒的字面量转义掉;正解是这一层改成 fail-open。

相关术语

孤立代理项(lone surrogate)
孤立代理项是 UTF-16 里没有配对的代理码元(U+D800–U+DFFF 之间单独出现的一个),无法构成合法码点。它在 JSON 里可以合法地写成 \ud83d 这样的转义序列,但语义非法,因此会被服务端的内容校验拒绝,形成 HTTP 400。— https://github.com/deepseek-ai/deepseek-harness/discussions/9089
toWellFormed()
String.prototype.toWellFormed() 是 JavaScript 里把字符串中所有孤立代理项替换为 U+FFFD 替换字符的方法,用来把任意字符串规整为合法 Unicode 序列。它是修复「孤立代理项毒化会话」类问题最直接的工具。— https://github.com/deepseek-ai/deepseek-harness/discussions/9089
replayState.blocks
replayState.blocks 是 DeepSeek Harness 会话日志里用于回放的历史内容块索引,与消息块按位置一一对应。删除消息块时若不同步更新它,计数就会不一致,assembler 会因此丢弃整个 envelope。— https://github.com/deepseek-ai/deepseek-harness/discussions/9239
Content Exists Risk
Content Exists Risk 是服务端内容审核返回的拒绝码,表示请求内容过线。由于它会随历史记录每轮重发,一条过线记录就能让会话永久失败,恢复方式是分叉到毒点之前的会话。— https://github.com/deepseek-ai/deepseek-harness/discussions/8830

来源