- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
本指南围绕 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 类型 | 说明 |
|---|---|---|
纯文本reasoningitem | output[].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 standalone
reasoningitem. 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 a
reasoningitem withreasoning_textbefore the corresponding function call and output.
新增测试需要验证两条链路:
- 派生 metadata:从 DeepSeek commentary message 成功派生出隐藏 metadata(而非丢失或渲染);
- 回放顺序:下一次请求中,带
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 未被持久化为隐藏 metadata | commentary 属于后续请求必须携带的无状态历史,不得丢弃 |
| 回放顺序把 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")) }这是本文档的核心回归用例,完整对应三条失败判定:
- 不渲染为可见思考:
reasoningChunks.size == 0且textChunks.size == 0——commentary 文本完全不进入流式输出块; - 持久化为隐藏 metadata:
outputItemMetadataTags.single()成功生成 metadata tag; - 回放顺序正确:回放后
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 )这段代码揭示了回放顺序的核心机制:
- 先恢复
reasoningitem(appendReasoningItemsFromAssistantMessage解码<meta provider="openai:responses_reasoning">载荷,经appendReasoningItemFromMetadata原样写回input); - 再恢复 commentary 转换的 reasoning item(
appendOutputItemsFromAssistantMessage解码<meta provider="openai:responses_output_item">载荷); - 正文转换时移除已回放的思考内容(
removeThinkingContent = reasoningItemReplayed || commentaryMessageReplayed,经ChatUtils.removeThinkingContent剔除<think>,避免可见思考文本再次作为正文提交); - 最后追加
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,可在代码审查或修改后快速自检:
- 可见性:commentary 文本是否出现在
textChunks/reasoningChunks或流式正文输出中?(应当为否,见parseNonStreamingResponse的continue与emitBufferedResponsesMessageItemContent的 commentary 分支) - 持久化:commentary item 是否成功派生
<meta provider="openai:responses_output_item">标记?(对应outputItemMetadataTags.single()断言) - 回放顺序:
input中reasoning(含reasoning_text)是否位于对应function_call与function_call_output之前,且call_id一致?(对应 3.3 / 3.4 用例的input.length()与顺序断言) - 协议隔离:OpenAI Responses 的
encrypted_contentreasoning 是否仍保持原合约,未混入 DeepSeek 的纯文本 metadata?(对应 3.2 用例) - 隐藏标记清理:回放后用户可见的请求文本中是否已通过
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.kt | metadata 编码、解码与 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
相关推荐
Operit 中 DeepSeek Responses 推理重放:Commentary 思考的隐藏元数据捕获与按序回放实现
Operit 中 DeepSeek Responses 推理重放:Commentary 思考的隐藏元数据捕获与按序回放实现 导读 本文剖析 Operit(And
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化如何用MiniCPM4混合思考模式提升3倍推理速度:深思考与非思考模式切换完全指南
如何用MiniCPM4混合思考模式提升3倍推理速度:深思考与非思考模式切换完全指南 MiniCPM4是OpenBMB开源社区推出的超高效端侧大语言模型,通过创新
大模型本地部署模型量化微调LoRA工具调用openBMBAscendGLM-4.5推理模式:思考与非思考双模式
GLM 4.5推理模式:思考与非思考双模式 概述 GLM 4.5作为智谱AI推出的新一代混合推理模型,创新性地引入了 思考模式(Thinking Mode) 和
基础模型大模型人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考