☰
Operit 源码剖析:DeepSeek Responses 思考模式下 Commentary 思考的回归测试覆盖与回放顺序保障
2026/9/27 21:41:03 网站建设 项目流程
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

本指南围绕 Operit 仓库中docs/TODO/deepseek_responses_reasoning_replay_20260907/02_regression_coverage.md展开,讲解 DeepSeek Responses 思考模式下message.phase=commentary这一思考表示如何被持久化为隐藏 metadata、并在下一次工具续接请求中回放为带reasoning_text的reasoningitem。读完本文,你将理解该缺陷的成因(HTTP 400The reasoning_text in the thinking mode must be passed back to the API)、回归测试的四个断言维度,以及DeepseekProvider.kt/OpenAIProvider.kt中对应的编码与回放实现细节,可直接据此在仓库中定位并验证相关代码与测试。

一、背景:为什么需要"Commentary 思考回放"的回归覆盖

1.1 问题根源:思考模式的两种思考表示

从 任务索引文档 可知,DeepSeek Responses 工具续接(tool continuation)可能收到如下 HTTP 400 错误:

The reasoning_text in the thinking mode must be passed back to the API.

原因是:思考模式下,DeepSeek 返回的思考内容存在两种表示,而旧的 DeepSeek adapter 只保留了其中一种:

表示形式输出 item 类型说明
纯文本reasoningitemoutput[].type == "reasoning"用户可见的<think>思考来源,旧实现已能保留
message.phase=commentary消息output[].type == "message"且phase == "commentary"部分思考以"评论性消息"形态下发,旧实现未持久化,导致续接请求丢失这段无状态历史

实现文档 进一步描述了修复前的行为:流式渲染时,message.phase=commentary会被当作reasoningitem 旁边第二个可见思考块输出;同时后续工具续接请求中也缺少这段 commentary 表示。修复目标非常明确——commentary 只应作为隐藏 metadata 持久化,绝不渲染为<think>,且回放时必须以reasoning_text形式插入到相关function_call之前。

1.2 协议层面的约定

DeepSeek Responses 与 Web Search 协议 对该行为给出了权威约定:

思考模式下发生客户端函数调用时,DeepSeek 返回的纯文本reasoningitem 与message.phase=commentary都属于后续请求必须携带的无状态历史。reasoningitem 使用 Responses reasoning 隐藏 metadata 保存,并在下一轮 input 中恢复到对应的 assistant message 与function_call之前;它同时是用户可见的<think>来源。commentarymessage 使用 Responses output item 隐藏 metadata 保存,界面不渲染这段文本;回放到 input 时改写成带reasoning_text的reasoningitem。

同时,协议明确了两条硬性约束:

  • 思考模式里function_call前面必须是reasoning_text:把 commentary 原样写成output_text消息会返回 400;
  • OpenAI Responses 的加密 reasoning 合约保持不变:通用 adapter 只维护encrypted_content格式,纯文本reasoning_text的保存与恢复只属于 DeepSeek 侧(DeepseekResponsesProvider与DeepseekResponsesPayloadAdapter)。

二、回归覆盖的设计:Previous Behavior → Change → Expected Result

02_regression_coverage.md 本身即一份精炼的回归覆盖设计文档,其三段式结构是本文的核心骨架。

2.1 Previous Behavior(既有回归的盲区)

The existing DeepSeek regression only verifies a standalonereasoningitem. It does not cover themessage.phase=commentaryrepresentation observed in the failing request flow.

既有 DeepSeek 回归测试只验证独立的reasoningitem(纯文本思考),完全没有覆盖失败请求流中观察到的message.phase=commentary表示。也就是说,即使reasoningitem 路径一切正常,commentary 路径的损坏依然会绕过测试直接上生产环境。

2.2 Change(新增测试目标)

Add tests that derive metadata from a DeepSeek commentary message and assert that the next request places areasoningitem withreasoning_textbefore the corresponding function call and output.

新增测试需要验证两条链路:

  1. 派生 metadata:从 DeepSeek commentary message 成功派生出隐藏 metadata(而非丢失或渲染);
  2. 回放顺序:下一次请求中,带reasoning_text的reasoningitem 必须出现在对应的function_call与function_call_output之前。

2.3 Expected Result(失败判定标准)

The test fails if commentary thought is rendered as visible thinking, if it is not persisted as hidden metadata, or if replay order separates a function call from its output.

测试在以下三种情况下必须失败,这是回归测试的"护栏"语义:

失败条件违反的协议约定
commentary thought 被渲染为可见思考可见思考只应来自reasoningitem,commentary 不得成为第二个 thinking 块
commentary 未被持久化为隐藏 metadatacommentary 属于后续请求必须携带的无状态历史,不得丢弃
回放顺序把 function call 与它的 output 分离function_call与对应function_call_output必须相邻且由call_id绑定

这三条失败判定与后续的四个测试用例一一对应,构成了完整的回归闭环。

三、测试用例逐条解析:断言维度与关键代码

回归测试集中在 DeepseekResponsesPayloadAdapterTest.kt,共四个用例,覆盖"纯文本思考"与"commentary 思考"两大路径。

3.1 纯文本 reasoning 保留并在 function call 前回放

@Test fun `plaintext reasoning is preserved and replayed before function calls`() { val reasoningItem = JSONObject() .put("type", "reasoning") .put("id", "rs_plain_1") .put("content", reasoningContent) // type=reasoning_text val metadataTag = DeepseekResponsesPayloadAdapter.parseNonStreamingResponse( JSONObject("""{"output":[$reasoningItem]}""") ).reasoningMetadataTags.single() // ... 构造带 metadataTag 的续接请求后调用 toResponsesRequest assertEquals("reasoning", input.getJSONObject(0).getString("type")) assertEquals("rs_plain_1", input.getJSONObject(0).getString("id")) assertFalse(input.getJSONObject(0).has("encrypted_content")) assertFalse(input.getJSONObject(0).has("summary")) assertEquals("message", input.getJSONObject(1).getString("type")) assertEquals("function_call", input.getJSONObject(2).getString("type")) assertEquals("function_call_output", input.getJSONObject(3).getString("type")) }

该用例验证:纯文本reasoningitem 经解析后生成reasoningMetadataTags(隐藏 metadata),回放时input顺序为reasoning → message → function_call → function_call_output,且原始content(含reasoning_text与 item IDrs_plain_1)被原样保留,不携带encrypted_content与summary——即保持纯文本合约。

3.2 加密 reasoning 不进入 DeepSeek metadata

@Test fun `encrypted reasoning is not emitted as deepseek reasoning metadata`() { val reasoningItem = JSONObject() .put("type", "reasoning") .put("id", "rs_encrypted_1") .put("encrypted_content", "encrypted-reasoning") .put("summary", JSONArray()) val parsed = DeepseekResponsesPayloadAdapter.parseNonStreamingResponse(...) assertEquals(0, parsed.reasoningMetadataTags.size) }

该用例守卫 OpenAI Responses 的加密 reasoning 合约:encrypted_content形式的 reasoning item 不得被当作 DeepSeek 纯文本 metadata 处理(reasoningMetadataTags必须为空),确保两条协议互不串扰。

3.3 commentary 思考在相关 function call 之前被保留(核心回归)

@Test fun `commentary thinking is preserved before the related function call`() { val commentaryItem = JSONObject() .put("type", "message") .put("id", "msg_commentary_1") .put("role", "assistant") .put("phase", "commentary") .put("content", commentaryContent) // type=output_text val parsed = DeepseekResponsesPayloadAdapter.parseNonStreamingResponse(...) assertEquals(0, parsed.reasoningChunks.size) assertEquals(0, parsed.textChunks.size) val metadataTag = parsed.outputItemMetadataTags.single() // ... val input = DeepseekResponsesPayloadAdapter.toResponsesRequest(chatStyleRequest) .getJSONArray("input") assertEquals(3, input.length()) assertEquals("reasoning", input.getJSONObject(0).getString("type")) assertEquals("msg_commentary_1", input.getJSONObject(0).getString("id")) val replayedContent = input.getJSONObject(0).getJSONArray("content") assertEquals("reasoning_text", replayedContent.getJSONObject(0).getString("type")) assertEquals("I need to activate the package before calling its tool.", replayedContent.getJSONObject(0).getString("text")) assertEquals("function_call", input.getJSONObject(1).getString("type")) assertEquals("call_commentary_1", input.getJSONObject(1).getString("call_id")) assertEquals("function_call_output", input.getJSONObject(2).getString("type")) assertEquals("call_commentary_1", input.getJSONObject(2).getString("call_id")) }

这是本文档的核心回归用例,完整对应三条失败判定:

  1. 不渲染为可见思考:reasoningChunks.size == 0且textChunks.size == 0——commentary 文本完全不进入流式输出块;
  2. 持久化为隐藏 metadata:outputItemMetadataTags.single()成功生成 metadata tag;
  3. 回放顺序正确:回放后input长度恰为 3,顺序严格为reasoning(携带msg_commentary_1的 ID 与reasoning_text)→function_call→function_call_output,且function_call与function_call_output通过相同call_id(call_commentary_1)绑定,绝不被其他 message 隔开。

3.4 流式缓冲:completed item 无内容时仍保留 commentary 文本

@Test fun `buffered commentary thinking is preserved when the completed item has no content`() { val commentaryItem = JSONObject() .put("type", "message") .put("role", "assistant") .put("phase", "commentary") // 注意:无 id、无 content val commentaryText = "I need to wait for the command before deciding the next action." val metadataTag = DeepseekResponsesPayloadAdapter.createStreamingCommentaryMetadataTag( commentaryItem, commentaryText ) ?: throw AssertionError("Expected commentary metadata") // ... assertEquals(3, input.length()) assertEquals("reasoning", input.getJSONObject(0).getString("type")) assertEquals("reasoning_text", replayedContent.getJSONObject(0).getString("type")) assertEquals(commentaryText, replayedContent.getJSONObject(0).getString("text")) assertEquals("function_call", input.getJSONObject(1).getString("type")) assertEquals("function_call_output", input.getJSONObject(2).getString("type")) }

该用例覆盖流式场景:当output_item.done时 commentary item 本身已不含content(正文 delta 早已通过response.output_text.delta增量到达),此时必须依赖createStreamingCommentaryMetadataTag用缓冲的 commentary 文本生成 metadata,保证即使 completed item 为空,思考文本也不丢失。

3.5 测试的运行方式

该测试属于 JVM 单元测试(位于app/src/test/java,使用 JUnit 4 +org.json),可在仓库根目录通过 Gradle 单元测试任务运行,例如:

./gradlew :app:testDebugUnitTest --tests "com.ai.assistance.operit.api.chat.llmprovider.DeepseekResponsesPayloadAdapterTest"

测试中辅助构造的singleToolContinuationRequest负责生成"assistant 消息 + tool_calls + tool 结果"的 Chat 风格请求体,再交由DeepseekResponsesPayloadAdapter.toResponsesRequest转换为 Responsesinput数组进行断言,完整复刻了真实工具续接的请求形态。

四、源码级原理:metadata 编码与回放顺序的实现

4.1 解析阶段:commentary 与 reasoning 的分流

在 DeepseekProvider.kt 的parseNonStreamingResponse中,遍历output数组时对type == "message"的 item 先做 commentary 判定:

val isCommentaryMessage = item.optString("phase", "").trim().equals("commentary", ignoreCase = true) if (isCommentaryMessage) { // Commentary is continuation state for the next request, not a second think block. createCommentaryMetadataTag(item)?.let { metadataTag -> outputItemMetadataTags.add(metadataTag) reasoningObserved = true } continue // 不进入 textChunks / reasoningChunks,即不产生可见输出 }

而type == "reasoning"的 item 则走createReasoningMetadataTag生成reasoningMetadataTags(同时仍会提取reasoningChunks用于可见<think>渲染)。由此可见:commentary 与 reasoning 在同一轮输出中分别落盘为两类隐藏 metadata,可见思考只来自 reasoning item。

4.2 编码:Base64 JSON payload +<meta>协议标记

两类 metadata 的编码方式高度一致,定义在 ChatMarkupRegex.kt:

fun openAiResponsesReasoningMetaTag(payloadBase64: String): String { return """<meta provider="openai:responses_reasoning">$payloadBase64</meta>""" } fun openAiResponsesOutputItemMetaTag(payloadBase64: String): String { return """<meta provider="openai:responses_output_item">$payloadBase64</meta>""" }
  • createReasoningMetadataTag(DeepseekProvider.kt#L708-L726):要求type == "reasoning"、id非空且content含reasoning_text,payload 保存reasoning_id与原始content;
  • createCommentaryMetadataTag(DeepseekProvider.kt#L728-L753):要求type == "message"且phase == "commentary",payload 保存type、role、id(可选)与content,并附注释"DeepSeek emits some thinking as a commentary message instead of a reasoning item";
  • createStreamingCommentaryMetadataTag(DeepseekProvider.kt#L755-L781):流式场景下基于缓冲文本重建content(type=output_text),同样编码为<meta provider="openai:responses_output_item">标记。

这些<meta>标记属于隐藏协议标记:Android 与 Web 渲染层、复制文本清理(removeOpenAiResponsesReasoningMeta/removeOpenAiResponsesOutputItemMeta/removeOpenAiResponsesProtocolMeta,见 ChatMarkupRegex.kt#L296-L306)均不展示它们,从而保证"隐藏 metadata 始终不出现在用户可见请求内容中"。

4.3 回放阶段:toResponsesRequest的 input 重建顺序

在 DeepseekProvider.kt 的 input 构建逻辑 中,遍历历史messages时对 assistant 消息依次执行:

val reasoningItemReplayed = appendReasoningItemsFromAssistantMessage(message, input) val commentaryMessageReplayed = appendOutputItemsFromAssistantMessage(message, input) val convertedContent = convertMessageContentForResponses( content = message.opt("content"), removeThinkingContent = reasoningItemReplayed || commentaryMessageReplayed )

这段代码揭示了回放顺序的核心机制:

  1. 先恢复reasoningitem(appendReasoningItemsFromAssistantMessage解码<meta provider="openai:responses_reasoning">载荷,经appendReasoningItemFromMetadata原样写回input);
  2. 再恢复 commentary 转换的 reasoning item(appendOutputItemsFromAssistantMessage解码<meta provider="openai:responses_output_item">载荷);
  3. 正文转换时移除已回放的思考内容(removeThinkingContent = reasoningItemReplayed || commentaryMessageReplayed,经ChatUtils.removeThinkingContent剔除<think>,避免可见思考文本再次作为正文提交);
  4. 最后追加function_callitems(保留call_id绑定)。

appendOutputItemFromMetadata(DeepseekProvider.kt#L1170-L1206)是 commentary 回放的关键:metadatatype == "message"且role == "assistant"时,调用convertCommentaryContentToReasoningContent把output_text/text/reasoning_text统一转换为reasoning_text,再构造type=reasoning的 item 插入input。源码注释直接点明了 400 的根因:

// Thinking-mode function calls require reasoning_text. Commentary is that thought // in a message envelope; replaying it as output_text makes DeepSeek return 400.

由此,input最终呈现的稳定顺序为:reasoning(含reasoning_text)→message→function_call→function_call_output,与协议"思考模式里function_call前面必须是reasoning_text"完全吻合。

4.4 流式路径:OpenAIProvider 的 provider 扩展点

OpenAIProvider.kt 为 DeepSeek 提供了一组protected open扩展点,其中createResponsesMessageMetadataTag默认返回null,由DeepseekResponsesProvider覆写:

override fun createResponsesMessageMetadataTag(item: JSONObject, bufferedText: String): String? { return DeepseekResponsesPayloadAdapter.createStreamingCommentaryMetadataTag(item, bufferedText) }

流式事件处理在 OpenAIProvider.kt#L2644-L2729 中:response.output_item.done到达时,若该 message item 是 commentary,则调用扩展点生成 metadata tag 并通过emitter.emitMetadataTag落盘(这正是索引文档所述"在完成的 Responses message item 边界调用 provider extension hook")。同时:

  • DeepseekResponsesProvider声明useResponsesApi = true、bufferResponsesOutputTextUntilItemDone = true(DeepseekProvider.kt#L1290-L1291);
  • isResponsesCommentaryMessage覆写为按phase == "commentary"判定(DeepseekProvider.kt#L1293-L1295);
  • emitBufferedResponsesMessageItemContent(OpenAIProvider.kt#L3007-L3030)在补发缓冲正文时再次检查 commentary——若是,只标记reasoningObserved = true而不调用processResponsesRegularContentDelta,注释写明"Emitting it as think would show a second thinking block next to the reasoning item"。

非流式路径同理:parseResponsesNonStreamingResponse返回的reasoningMetadataTags与outputItemMetadataTags在 OpenAIProvider.kt#L3367-L3372 被逐一emitMetadataTag。至此,流式与非流式两条路径都满足"commentary 只进隐藏 metadata、绝不进入界面"。

五、回归护栏自查清单

结合02_regression_coverage.md的 Expected Result,可在代码审查或修改后快速自检:

  1. 可见性:commentary 文本是否出现在textChunks/reasoningChunks或流式正文输出中?(应当为否,见parseNonStreamingResponse的continue与emitBufferedResponsesMessageItemContent的 commentary 分支)
  2. 持久化:commentary item 是否成功派生<meta provider="openai:responses_output_item">标记?(对应outputItemMetadataTags.single()断言)
  3. 回放顺序:input中reasoning(含reasoning_text)是否位于对应function_call与function_call_output之前,且call_id一致?(对应 3.3 / 3.4 用例的input.length()与顺序断言)
  4. 协议隔离:OpenAI Responses 的encrypted_contentreasoning 是否仍保持原合约,未混入 DeepSeek 的纯文本 metadata?(对应 3.2 用例)
  5. 隐藏标记清理:回放后用户可见的请求文本中是否已通过removeThinkingContent与stripOpenAiResponsesProtocolMarkup移除<think>与<meta>标记?

六、相关文件速查

文件作用
02_regression_coverage.md本文主体:回归覆盖设计与失败判定标准
01_capture_commentary_reasoning.md实现方案:commentary 捕获与回放
index.md任务范围:DeepseekProvider.kt、OpenAIProvider.kt、测试与协议文档
deepseek_responses_web_search.md协议所有权:reasoning_text保存/恢复归属与 400 约束
DeepseekResponsesPayloadAdapterTest.kt四个回归测试用例
DeepseekProvider.ktmetadata 编码、解码与 input 重建
OpenAIProvider.kt流式事件处理与 provider 扩展点
ChatMarkupRegex.kt<meta>协议标记的生成、提取与清理

综上,02_regression_coverage.md所定义的回归覆盖,通过"可见性、持久化、顺序、协议隔离"四个断言维度,将 DeepSeek Responses 思考模式下的 commentary 回放行为固化为可执行的测试护栏;而源码实现则通过<meta provider="openai:responses_output_item">隐藏标记与reasoning_text转换,确保每一次工具续接都完整携带所有持久化的思考表示,且严格保持原始顺序。

  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:FitGirl游戏启动器完全指南:3分钟打造你的专属游戏库
下一篇:FitGirl游戏启动器终极指南:3步打造个人游戏库的完整方法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询