agentscope-ai / agentscope-ai/agentscope-java

[Feature] 思考模式(thinking)统一开关门面:屏蔽 DeepSeek/GLM/MiniMax 差异,并修复 MiniMax 思考模式响应解析问题

オープン
#1,900 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
area/core/model enhancement
主要言語
Java
スター
5.6k
フォーク
1.3k
平均マージ
4日 12時間
マージ済み PR(30日)
77

説明

## 背景与动机 / Background

在为多模型集成做调研时发现:**走 `OpenAIChatModel` 的几家供应商(DeepSeek、智谱 GLM、MiniMax),在「思考模式(thinking / reasoning)」的开关与深度档位上,请求参数各不相同**,而框架目前没有提供统一的抽象来屏蔽这些差异。开发者必须自己用 `GenerateOptions.additionalBodyParam(...)` 逃生舱手写各家私有参数,且很容易踩坑。

期望框架提供一个**统一的门面(facade)**,让开发者用一致的 API **显式地开启 / 关闭思考、设置思考深度**,把各供应商的适配工作隐藏在框架内部,对外提供与供应商无关的抽象能力。

## 现状调查 / Investigation

### 1. 框架对 OpenAI 兼容系没有「显式的思考开关」

- `GenerateOptions`(`agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java`)中**没有任何思考开关属性**(不存在 `enableThinking` / `disableThinking` 之类的布尔)。与思考相关的仅有两个:
- `thinkingBudget`(Integer):但 `OpenAIChatModel` **并不消费**它——只有 `GeminiChatFormatter`、`DashScopeToolsHelper` / `DashScopeChatModel`、`OllamaOptions` 读取。对 DeepSeek / GLM / MiniMax 是**哑操作**。
- `reasoningEffort`(String):是「深度档位」而非「开关」,且仅部分供应商支持。
- 框架中唯一的显式思考开关 `.enableThinking(Boolean)` 是 **`DashScopeChatModel` 独有**(`agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java`,字段定义见 `:61`,应用见 `:334`),走 `OpenAIChatModel` 的这三家用不到。
- **结论:关闭思考目前只能靠 `GenerateOptions.builder().additionalBodyParam("thinking", Map.of("type","disabled"))` 逃生舱手写,没有一等公民属性。**

### 2. 三家供应商的思考参数差异(均走 `OpenAIChatModel`)

| 供应商 | 开启思考 | 关闭思考 | 深度档位 | 默认 |
|--------|----------|----------|----------|------|
| DeepSeek | `thinking:{type:"enabled"}` | `thinking:{type:"disabled"}` | `reasoning_effort`: high / max | 开 |
| 智谱 GLM | `thinking:{type:"enabled"}` | `thinking:{type:"disabled"}` | `reasoning_effort`: 仅 GLM-5.2+(none/minimal/low/medium/high/xhigh/max,含映射) | 开 |
| MiniMax | `thinking:{type:"adaptive"}` ⚠️ | `thinking:{type:"disabled"}` | 无档位 | 开 |

关键差异:
- **开启值不统一**:DeepSeek / GLM 用 `enabled`,**MiniMax 用 `adaptive`**。
- **关闭值碰巧统一**为 `disabled`,但框架并未把它抽象出来。
- **档位三套不同**:DeepSeek 只有 high/max;GLM 仅 5.2+ 且七档有映射;MiniMax 无档位。
- **硬限制**:MiniMax 仅 M3 能关闭思考;**M2.x 即使传 `disabled` 也无法关闭**。

### 关键文件

- `agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java` — 无思考开关属性
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java` — `applyOptions` 不处理 `thinkingBudget`
- `agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java` — 唯一带 `enableThinking` 的实现(可作为门面设计参考)
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/DeepSeekFormatter.java` — DeepSeek 思考 / 工具调用 `reasoning_content` 适配
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/GLMFormatter.java` — GLM 适配

## 期望 / Proposal

提供统一的思考门面,屏蔽供应商差异。建议在 `GenerateOptions` 增加一组**与供应商无关的语义化属性**,由各 `Formatter` 翻译成各家私有参数:

```java
GenerateOptions.builder()
.thinking(ThinkingMode.ENABLED) // ENABLED / DISABLED / AUTO
.thinkingEffort(ThinkingEffort.MAX) // OFF / LOW / MEDIUM / HIGH / MAX(由各 formatter 映射或忽略)
.build();
```

由各 `XxxFormatter` 负责翻译:
- DeepSeek / GLM → `thinking:{type:enabled/disabled}` + `reasoning_effort`
- MiniMax → `thinking:{type:adaptive/disabled}`(注意 `enabled → adaptive` 的映射)

并对**不支持的能力做显式告警或降级**(例如 MiniMax M2.x 无法关闭、GLM 档位需 5.2+),而不是静默无效。

> 实现细节可讨论;核心诉求是:**对外提供一致的、显式的思考开关与档位抽象,把供应商适配收敛进框架内部。**

## 关联问题:MiniMax 思考模式下的响应解析问题 / Related Bug

> 注:本节已于初版后更新。**勘误**——初版误写「开启 `reasoning_split` 时思考在 `reasoning_content` 字段」,正确应为 **`reasoning_details`**;并补充了 `reasoning_split` 的默认行为、`reasoning_details` 的 `type` 兼容性,以及流式约束。

由于框架目前**没有 MiniMax 专用的 formatter / parser**,MiniMax 复用通用的 `OpenAIChatFormatter` + `OpenAIResponseParser`,在 MiniMax 思考模式下会引发响应解析问题。

### MiniMax 思考响应的两种形态

MiniMax 思考内容的返回格式由 `reasoning_split` 参数决定(`extra_body` 顶层布尔,**默认 `false`**,且与 `thinking` 开关**相互独立**——它只控制返回格式,不开关思考):

- **`reasoning_split = false`(默认)**:思考内容直接嵌在 `content` 字段里,用 `...` 标签包裹。
- **`reasoning_split = true`**:思考内容分离到 **`reasoning_details` 字段(数组,每项含 `text`)**。

### 根因(基于源码静态分析)

`OpenAIResponseParser`(`agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIResponseParser.java`)对上述**两种形态都无法正确归入 `ThinkingBlock`**:

1. **默认的 `` 形态**:parser 只识别 `reasoning_content` 字段(非流式 `:162`,流式 `:403`),**完全不解析 `content` 里的 `...`**。于是整段(含标签与思考文本)被当作普通 `TextBlock` 正文(非流式 `:187-189`,流式 `:434-436`),污染最终回复。

2. **`reasoning_split = true` 的 `reasoning_details` 形态**:parser 确实有解析 `reasoning_details` 的分支(`:135` / `:371`),**但要求每项的 `type` ∈ `{reasoning.text, reasoning.summary, reasoning.encrypted}`**(`:149`,这是 OpenRouter / Gemini 的约定)。MiniMax 的 `reasoning_details` 项是否带这个 `type`、取值是否一致**需实测**;若不一致,思考内容会被**静默丢弃**。

> 换言之:**默认配置必然出问题;即便开启 `reasoning_split`,能否正确解析还取决于 `reasoning_details` 的 `type` 是否与框架约定吻合。**

### 预期现象(建议用 MiniMax-M3 复现确认)

1. **SSE 事件流 block 类型错误**:思考被当作正文 `TextBlock` 流式输出,本应触发的 `ThinkingBlock` 事件不会触发,`` 标签混入最终回复文本,使 MiniMax 的输出与其他模型表现不一致。
2. **流式切割问题**:流式下 `content` 按 chunk 增量到达,`` / `` 可能被切到多个 chunk,逐 chunk 当 `TextBlock` 处理时难以识别与剥离。
3. **工具调用后最终回复疑似被截断**:在「工具调用 + 思考」混合场景下(MiniMax 要求完整 assistant 消息含 `tool_calls` 回传以保持思维链连续),结合上述解析问题,怀疑最终回复可能被截断。

> 说明:以上现象**部分基于源码静态分析推断、部分基于使用观察,尚未在本仓库编写复现用例**。如需要,我可以补充最小复现。

### 工程约束:流式无法在用户侧 formatter 简单修复

自定义 Formatter 是 `OpenAIChatModel` 的**共享成员**(`OpenAIChatModel` 持有单个 `formatter` 引用,被所有请求复用),因此**不能在 formatter 里用可变成员维护「当前是否处于 `` 内部」的跨 chunk 状态**(线程不安全)。这意味着流式场景下,仅靠覆盖 `parseResponse` 做后处理**无法可靠剥离被切分的 ``**,更依赖框架内置支持——这也加强了在框架层提供 MiniMax 专用解析的必要性。

### 建议

为 MiniMax 增加专用 formatter / parser;或在 `OpenAIResponseParser` 中:
- (a) 增加对 `content` 内 `...` 的识别与剥离(可配置开关);
- (b) 放宽 `reasoning_details` 的 `type` 兼容(接受 MiniMax 不带标准 `type` 的项),

把思考内容统一归入 `ThinkingBlock`,与 `reasoning_content` 路径对齐。

## 参考链接 / References

官方文档:
- DeepSeek 思考模式:https://api-docs.deepseek.com/zh-cn/guides/thinking_mode
- 智谱 GLM 深度思考:https://docs.bigmodel.cn/cn/guide/capabilities/thinking
- MiniMax(OpenAI 兼容 API,含 thinking 控制 / reasoning_split):https://platform.minimaxi.com/docs/api-reference/text-openai-api

相关源码:
- `agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java`
- `agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java`
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java`
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIResponseParser.java`
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/DeepSeekFormatter.java`
- `agentscope-core/src/main/java/io/agentscope/core/formatter/openai/GLMFormatter.java`

---

环境:agentscope-java(`main` 分支)

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。