257 lines
9.3 KiB
Markdown
257 lines
9.3 KiB
Markdown
# AI reasoning_content 回传与“继续思考”实验研究报告
|
||
|
||
日期:2026-08-23
|
||
范围:OpenCodeGo / `deepseek-v4-flash`,OpenAI 兼容 `/chat/completions`
|
||
状态:实验性研究,未集成到主项目
|
||
|
||
## 摘要
|
||
|
||
本报告记录了为验证“将模型上一轮 `reasoning_content` 截断/修改后回传,模型是否能够继续合理思考”而执行的一系列实验。
|
||
|
||
结论是:**仅发送 `reasoning_content` 或“上一轮思维链”并不稳定;成功率最高的方案是伪造一段 tool call 历史,并把续写要求、输出格式和需要引用的内容放入 tool 结果中。**
|
||
|
||
与本研究相关的代码在 `AiV2.Tests/Program.cs` 中,默认不会执行。
|
||
|
||
## 目标
|
||
|
||
验证以下问题:
|
||
|
||
1. 模型能否在不追加新的 user 消息时,基于 assistant 的 `reasoning_content` 继续生成最终回答?
|
||
2. 截断后的 `reasoning_content` 是否仍然能被模型读取和引用?
|
||
3. 伪造 `tool_calls` + `tool` 结果历史是否比普通多轮对话更有效?
|
||
4. 工具结果中的措辞是否会显著影响模型对“目标思维链”的定位?
|
||
5. 标记在思维链中的位置是否影响模型的可召回性?
|
||
|
||
## 实验方法与基础设施
|
||
|
||
所有实验使用:
|
||
|
||
- 当前应用配置文件中的 OpenCodeGo / `deepseek-v4-flash`
|
||
- `stream=false` 的一次性 OpenAI 兼容请求
|
||
- 初始问题:
|
||
`A 比 B 高 20%,B 比 C 高 25%,那么 A 比 C 高多少?`
|
||
- 确定性记忆标记:
|
||
`TOKEN_MARK=731942`
|
||
|
||
专用测试位于 `AiV2.Tests/Program.cs`,默认跳过。
|
||
|
||
执行方式:
|
||
|
||
```powershell
|
||
dotnet build .\AiV2.Tests\AiV2.Tests.csproj /p:AiV2TestsBuilding=true
|
||
$env:ARR_AI_E2E = "1"
|
||
$env:ARR_AI_E2E_TOOL = "1"
|
||
$env:ARR_AI_TOOL_REPORT_PATH = "AI_reasoning_continuation_position_sweep_report.md"
|
||
& .\AiV2.Tests\bin\Debug\net461\AiV2.Tests.exe
|
||
```
|
||
|
||
## 尝试过程
|
||
|
||
### 1. 普通多轮:assistant 推理 + 正文再回传
|
||
|
||
消息结构:
|
||
|
||
```text
|
||
system → user → assistant(reasoning_content + content) → user
|
||
```
|
||
|
||
结果:协议层接受 `reasoning_content`,模型通常会重新推导,但无法确认它真正延续了旧思维链。
|
||
|
||
### 2. 只发 assistant 推理,不追加 user
|
||
|
||
消息结构:
|
||
|
||
```text
|
||
system → user → assistant(reasoning_content,content 不发送)
|
||
```
|
||
|
||
结果:OpenCodeGo 接受最后一条 assistant 消息并生成完成,但新 reasoning 往往从题目重新开始。
|
||
|
||
### 3. 伪造 tool call 历史
|
||
|
||
消息结构:
|
||
|
||
```text
|
||
system
|
||
→ user
|
||
→ assistant(content 不发送, reasoning_content, tool_calls)
|
||
→ tool(tool_call_id, 工具结果)
|
||
```
|
||
|
||
请求同时声明 `tools`,并将 `tool_choice` 设置为 `"none"`。
|
||
|
||
工具结果中可以注入:
|
||
|
||
- “继续当前推理”指令
|
||
- 输出格式要求
|
||
- 需要引用的旧思维链内容
|
||
- “如果没有该内容,明确回答不存在”的边界条件
|
||
|
||
这一步开始观察到:**模型可能在部分运行中读取并引用 `reasoning_content`。**
|
||
|
||
### 4. 确定性记忆标记
|
||
|
||
为避免模型从工具结果中照抄标记,标记值只注入到 `reasoning_content`,`tool` 结果中不出现标记值。
|
||
|
||
### 5. 措辞 A/B
|
||
|
||
对比三种措辞:
|
||
|
||
```text
|
||
A:上一轮思维链
|
||
B:当前推理
|
||
C:调用工具之前的思维链
|
||
D:本次 tool_calls 消息中携带的 reasoning_content
|
||
```
|
||
|
||
去掉“上一轮”“截断点”等歧义词后,B/D 的命中率明显更高。
|
||
|
||
### 6. 标记位置扫描
|
||
|
||
使用成功率最高的两种措辞:
|
||
|
||
- B:当前推理
|
||
- D:本次 tool_calls 消息中携带的 reasoning_content
|
||
|
||
标记插入完整思维链的 25%、50%、75%、末尾四个位置,每个组合重复 5 次。
|
||
|
||
## 结果
|
||
|
||
### 措辞 A/B(每个组合 3 次)
|
||
|
||
| 工具提示措辞 | 正确写出标记 | 明确说标记不存在 |
|
||
|---|---:|---:|
|
||
| A:上一轮思维链 | 2 / 3 | 1 / 3 |
|
||
| B:当前推理 | 3 / 3 | 0 / 3 |
|
||
| C:调用工具之前的思维链 | 2 / 3 | 1 / 3 |
|
||
| D:tool_calls 携带的 reasoning_content | 3 / 3 | 0 / 3 |
|
||
|
||
### 位置扫描(每个组合 5 次)
|
||
|
||
| 措辞 | 标记位置 | 正确写出标记 | 明确说没有 | 请求失败 |
|
||
|---|---|---:|---:|---:|
|
||
| B | 25% | 4 / 5 | 1 / 5 | 0 |
|
||
| B | 50% | 5 / 5 | 1 / 5 | 0 |
|
||
| B | 75% | 5 / 5 | 1 / 5 | 0 |
|
||
| B | 末尾 | 3 / 5 | 3 / 5 | 0 |
|
||
| D | 25% | 4 / 5 | 0 / 5 | 1(HTTP 503) |
|
||
| D | 50% | 5 / 5 | 0 / 5 | 0 |
|
||
| D | 75% | 5 / 5 | 2 / 5 | 0 |
|
||
| D | 末尾 | 4 / 5 | 2 / 5 | 0 |
|
||
|
||
## 关键发现
|
||
|
||
1. **tool call 是当前最有效的载体。**
|
||
|
||
把“继续推理、输出格式、引用要求”放进 tool 结果,模型会把这些内容当作任务输入,而不是普通 user 消息。
|
||
|
||
2. **措辞决定模型能否正确定位思维链。**
|
||
|
||
“当前推理”和“本次 tool_calls 消息中携带的 reasoning_content”比“上一轮思维链”更稳定。不要使用“上一轮”“截断点”这些容易引起歧义的词。
|
||
|
||
3. **标记在思维链开头/中部时最容易召回。**
|
||
|
||
50% 和 75% 位置均为 5/5;末尾位置明显下降。这说明不要把关键事实放在 `reasoning_content` 末尾。
|
||
|
||
4. **“正确写出标记”和“明确说没有”不是互斥的。**
|
||
|
||
模型有时先否定、后在同一输出中写出标记。统计时两者可能同时为真,人工审阅必须以完整 reasoning 和正文为准。
|
||
|
||
5. **服务端不稳定。**
|
||
|
||
D/25% 有一次 HTTP 503,属于服务端错误,不是模型失败。OpenCodeGo 未返回 `usage`,因此无法评估成本/token。
|
||
|
||
6. **主项目不应直接启用该机制。**
|
||
|
||
当前实验只能证明“部分情况下有效”,不足以作为生产依赖。
|
||
|
||
## 推荐方案
|
||
|
||
### 推荐消息结构
|
||
|
||
```text
|
||
system
|
||
→ user(原始任务)
|
||
→ assistant(
|
||
content 不发送,
|
||
reasoning_content,
|
||
tool_calls: [{ id, type: "function", function: { name, arguments } }]
|
||
)
|
||
→ tool(
|
||
tool_call_id,
|
||
content: "工具结果:请继续你本次 tool_calls 消息中携带的 reasoning_content。
|
||
请原样写出其中的内部记忆标记;如果没有,请明确回答“标记不存在”。
|
||
请使用 Markdown 输出。"
|
||
)
|
||
```
|
||
|
||
请求级配置:
|
||
|
||
```json
|
||
{
|
||
"tools": [
|
||
{
|
||
"type": "function",
|
||
"function": {
|
||
"name": "analysis_hint",
|
||
"description": "提供继续上一个内部推理的提示",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"instruction": { "type": "string" }
|
||
},
|
||
"required": ["instruction"]
|
||
}
|
||
}
|
||
}
|
||
],
|
||
"tool_choice": "none"
|
||
}
|
||
```
|
||
|
||
### 推荐提示词措辞
|
||
|
||
推荐:
|
||
|
||
```text
|
||
请继续你当前正在进行的内部推理。
|
||
请继续你本次 tool_calls 消息中携带的 reasoning_content。
|
||
```
|
||
|
||
不推荐:
|
||
|
||
```text
|
||
上一轮思维链在这里被截断,请继续。
|
||
```
|
||
|
||
## 对主项目的建议
|
||
|
||
- 保留现有 UI 对 `reasoning_content` 的展示。
|
||
- 不要在生产管线中自动发送/截断/修改 `reasoning_content`。
|
||
- 如未来需要接入,优先使用 tool call 载体,并将关键指令、格式和引用要求放入 tool 结果。
|
||
- 不要假设 `reasoning_content` 的末尾内容一定能被模型召回。
|
||
- 增加原始响应/请求日志,以便区分“模型未看到”和“模型看到了但没引用”。
|
||
|
||
## 主项目集成状态(2026-08-23)
|
||
|
||
- 主项目已落地模型级“推理保护”实验开关,默认关闭,仅在 `IsStream=true` 的模型上生效。
|
||
- 累计推理 token 达到阈值后停止读取当前 SSE 响应,保留部分推理和正文;最多发起一次续写。
|
||
- 续写采用本报告推荐的伪造 tool call 历史:`assistant(reasoning_content + tool_calls)` → `tool(tool_call_id + 收尾指令)`。
|
||
- 首请求不携带 `tools`,只有续写请求注入 `tools` 与 `tool_choice=none`;缓存命中为 best-effort。
|
||
- 保留推理在其末尾依次追加 `[INTERNAL_REASONING_TRUNCATED]` 与中文收尾句;收尾句不插入中间 checkpoint,也不提及“token limit/被截断”等技术细节。
|
||
- 续写工具结果包含“进入收尾阶段、立即停止展开、直接输出最终结果”和“最后一句已经宣告收尾,请立即执行”等强化措辞。
|
||
- UI 在触发推理保护后提供两个可折叠日志:首次请求消息与续写请求完整消息;用户可展开查看完整 `messages`、`tools`、`tool_choice` 和收尾指令。
|
||
- UI 进一步改为阶段化时间线:推理保护事件与诊断在续写思考块之前输出;修订保留旧版本并折叠,最新版本展开;机器可读声明 JSON 与验证结果独立展示。
|
||
- UI 正文在成功提取机器可读声明后会移除 JSON 原文;推理保护诊断显示可读摘要(消息数变化、新增 assistant/tool 消息),完整续写 JSON 作为折叠附件;总览、修订和回查的 user prompt 会以折叠日志展示。
|
||
- 默认测试新增 `ReasoningGuard` 套件后总计 173 项通过;真实 API A/B 仍需要用户手工运行 `ARR_AI_E2E=1` 验证。
|
||
- 可选真实环境测试为 `OpenCodeGoGuardE2e`:设置 `ARR_AI_GUARD_E2E=1` 启用,报告路径可用 `ARR_AI_GUARD_REPORT_PATH` 指定。
|
||
|
||
## 专用测试状态
|
||
|
||
- 测试代码:`AiV2.Tests/Program.cs` 中的 `OpenCodeGoFakeToolCallTests`。
|
||
- 默认行为:不执行。
|
||
- 启用条件:`ARR_AI_E2E=1` 且 `ARR_AI_E2E_TOOL=1`。
|
||
- 输出:Markdown 报告,路径通过 `ARR_AI_TOOL_REPORT_PATH` 指定。
|
||
|
||
主项目中的实验性 `ChatMessage` 扩展、`AiReasoningContinuationSettings`、UI 开关、回传/降级逻辑和 tool call 历史构造均已移除。
|