docs/review/model-reasoning-control-design.md
日期:2026-07-31 状态:Phase 1 已审核并实现 范围:LangBot 主仓库的模型配置、LiteLLM 请求层、Local Agent、Web 管理面板、监控与测试
建议为 LangBot 增加一套与厂商参数解耦的“思考策略”模型,并明确区分三个概念:
现有 remove-think 只属于第 3 类。它会过滤输出,但不会阻止模型思考,也不会降低思考 token、费用或延迟。新能力不应复用或改写这个字段。
推荐实现原则:
provider_default,不向上游增加任何新参数,现有模型行为完全不变。extra_args 保留为高级逃生口,但不能成为主 UI 的思考配置方式。本次结论基于以下可验证来源:
reasoning.effort 的可选值由模型决定,可包括 none、minimal、low、medium、high、xhigh、max;低档位偏向低延迟和低 token,高档位偏向质量。
1.88.1 实现。uv.lock 已锁定该版本,本地缓存中的适配代码可以确认 LangBot 实际依赖所支持的翻译行为。extra_args 会在 LiteLLMRequester._build_completion_args() 中直接合并到 acompletion() 参数。Anthropic、Google 和 LiteLLM 的官方文档域名在本次环境中被浏览器策略禁止访问,因此下表中这些厂商的结论以 LiteLLM 1.88.1 实际适配代码为准。实施前应再用对应厂商官方文档做一次参数范围核验,尤其是模型代际和允许值。
| Provider / 生态 | 可控制能力 | LiteLLM 1.88.1 统一入口 | 关键限制 | 建议支持级别 |
|---|---|---|---|---|
| OpenAI | 思考档位,部分模型支持 none | reasoning_effort | 每个模型支持的档位不同,不能把 none 当成通用能力 | 首批完整支持 |
| Anthropic | 旧模型使用 extended thinking + token budget;新模型可用 adaptive thinking + effort | reasoning_effort 或 thinking | none 表示不发送 thinking;新旧模型的映射不同 | 首批完整支持 |
| Gemini | 2.x 主要映射为 thinkingBudget;3.x 主要映射为 thinkingLevel | reasoning_effort 或 thinking | Gemini 3 的 none 可能只能降到最低档,不能保证真正关闭 | 首批支持,但严格限制关闭语义 |
| DeepSeek | 开启/关闭;当前适配不支持预算档位 | thinking={type: enabled};非 none effort 会映射成开启 | 多轮思考模式要求回传 reasoning_content | 首批开关支持 |
| xAI | 思考档位 | reasoning_effort | 仅 reasoning-capable 模型接受 | 首批完整支持 |
| Ollama | think 布尔值;部分模型接受 low/medium/high | reasoning_effort | 非 gpt-oss 模型的档位可能退化为布尔开关 | 首批支持,按模型能力裁剪 UI |
| OpenRouter | 聚合多厂商的 reasoning 参数 | reasoning_effort、thinking | 实际能力由路由后的模型决定 | 首批支持,能力未知时要求测试 |
| Volcengine / Doubao | thinking.type 支持 enabled/disabled/auto | LiteLLM volcengine 适配器支持 thinking | LangBot 当前 manifest 使用 openai,不会进入该适配器 | 第二批,先修正路由并回归 |
| Bailian / Qwen | 厂商兼容接口有独立思考开关/预算 | LiteLLM dashscope 适配器目前未提供统一 reasoning 映射 | LangBot 当前 manifest 使用 openai,只能通过高级参数透传 | 第二批,实施前核对官方字段 |
| 其他 OpenAI-compatible 网关 | 取决于网关 | 尝试标准 reasoning_effort | 不能仅凭模型名推断完整能力 | 保守支持,默认不自动开启 |
不能把这个功能实现成单一 enable_thinking: bool,原因如下:
LLMModel.extra_args 是 JSON 字段,Web 端已有通用高级参数编辑器。LiteLLMRequester 会按“模型级 extra_args,再调用级 extra_args”的顺序合并参数。reasoning_effort、thinking 和返回的 reasoning_content。LocalAgentRunner 的非流式、流式、工具调用和 fallback 路径都经过 RuntimeProvider.invoke_llm*()。remove-think 已能控制 <think> 或独立 reasoning 内容是否进入展示文本。provider_specific_fields / thought signature 已有保留逻辑和单元测试。extra_args,没有统一语义、能力提示和校验。remove-think 名称容易被误解为关闭模型思考。vision 和 func_call,没有 reasoning 能力。reasoning_content 拼进 <think> 文本后删除原字段,可能损失多轮思考所需的结构化数据。reasoning_content,当前链路不能保证完整保留。openai,导致 LiteLLM 的厂商专用翻译器不会生效。| 层 | 主要文件 | 责任 |
|---|---|---|
| 持久化 | src/langbot/pkg/entity/persistence/model.py、src/langbot/pkg/persistence/alembic/versions/ | 新增 reasoning_config JSON 列和 Alembic 迁移 |
| 模型服务 | src/langbot/pkg/api/http/service/model.py | CRUD 校验、冲突检测、测试模型时使用统一策略 |
| HTTP 控制器 | src/langbot/pkg/api/http/controller/groups/provider/models.py | 继续复用现有模型路由,不新增平行 API |
| 模型管理 | src/langbot/pkg/provider/modelmgr/modelmgr.py | 临时模型、数据库模型与扫描结果加载新字段 |
| 请求抽象 | src/langbot/pkg/provider/modelmgr/requester.py | 定义能力查询和 reasoning 参数构建接口 |
| LiteLLM 适配 | src/langbot/pkg/provider/modelmgr/requesters/litellmchat.py | 能力识别、策略翻译、参数合并、reasoning 返回保留 |
| Provider manifest | src/langbot/pkg/provider/modelmgr/requesters/*.yaml | 必要时修正 Provider 路由;相关变更放到独立阶段 |
| Agent 调用 | src/langbot/pkg/provider/runners/localagent.py | 所有非流式、流式、工具调用、fallback 路径传递统一策略 |
| Pipeline 元数据 | src/langbot/templates/metadata/pipeline/ai.yaml | 第二阶段加入 Pipeline 级覆盖 |
| 输出配置 | src/langbot/templates/metadata/pipeline/output.yaml | 保留键名,澄清 remove-think 只控制展示 |
| Web 类型/API | web/src/app/infra/entities/api/index.ts、web/src/app/infra/http/BackendClient.ts | 增加配置与能力响应类型 |
| 模型 UI | web/src/app/home/components/models-dialog/ | 能力标记、策略控件、校验、模型测试 |
| i18n | web/src/i18n/locales/ | 至少补齐英文、简体中文及项目已有覆盖语言 |
| 测试 | tests/unit_tests/provider/、web/tests/ | 翻译、服务、流式 round-trip、前端状态测试 |
Phase 1 不修改 langbot-plugin-sdk 的公共实体或运行时协议。现有 provider_message.Message.provider_specific_fields 已可承载 Provider 原始 reasoning 数据;只有后续要把 reasoning 升级为跨插件公开实体时,才需要跨仓库 SDK 变更。
新增 ReasoningConfig,保存于 LLM 模型,Pipeline 可提供同结构覆盖。产品层只暴露一个离散档位:
{
"level": "provider_default"
}
字段定义:
| 字段 | 类型 | 含义 |
|---|---|---|
level | provider_default | disabled | enabled | minimal | low | medium | high | xhigh | max | 同时表达开关和思考强度 |
校验规则:
provider_default:不发送任何 reasoning 参数,保持厂商和模型默认行为。disabled:明确关闭;仅当模型可真正关闭时允许保存/运行。enabled:明确开启,但由 Provider 决定具体强度,适用于只有开关的模型。minimal 到 max:明确开启,并指定强度;仅允许选择模型实际支持的档位。auto 统一映射为 provider_default,不再增加一个重复状态。沿用现有 LLMModel.abilities,新增 reasoning 能力标记。同时由后端在 API 返回中计算只读的 reasoning_capabilities:
{
"supported": true,
"controls": ["toggle", "effort"],
"efforts": ["none", "low", "medium", "high"],
"can_disable": true,
"source": "litellm"
}
设计约束:
abilities 仍是用户可编辑的粗粒度能力,符合现有 vision、func_call 模式。reasoning_capabilities 不持久化,优先从 LiteLLM 模型元数据计算,避免模型升级后数据库残留过期能力。supported: null、source: unknown,不猜测。reasoning ability,但未知能力模型必须先通过“测试模型”验证显式策略。在 llm_models 表新增 JSON 列:
reasoning_config JSON NOT NULL DEFAULT {"level":"provider_default"}
使用 Alembic 新迁移,不修改冻结的 legacy migration。
该列作为已实现版本的兼容字段保留;新的模型页不再提供写入口,Local Agent 请求以流水线中按模型 UUID 保存的策略为准。
不建议把内部策略塞进 extra_args,原因是当前 extra_args 会原样发送给 LiteLLM;使用保留键会让内部元数据泄漏到上游,并使高级参数与产品配置难以区分。
Pipeline 当前候选模型策略
↓ 缺少配置时固定为 provider_default
Provider / 模型默认行为
请求参数合并顺序:
基础参数
-> 模型 extra_args
-> 调用级 extra_args
-> 统一 reasoning 策略翻译结果(最后应用)
统一策略最后应用,可以确保流水线行为不受模型页历史设置影响。为了避免用户困惑,保存和测试时要检测 extra_args 中的冲突字段;当 level != provider_default 时,发现以下字段应直接报错:
reasoning_effortthinkingreasoningextra_body 内已知的 thinking、enable_thinking、thinking_budget 等字段当 level == provider_default 时继续允许这些高级参数,保证旧配置兼容。
在 pkg/provider/modelmgr/ 内新增独立的 reasoning 规范化模块,职责是:
ProviderAPIRequester.get_reasoning_capabilities(model)。建议接口:
class ProviderAPIRequester:
def get_reasoning_capabilities(self, model: RuntimeLLMModel) -> ReasoningCapabilities: ...
def build_reasoning_args(
self,
model: RuntimeLLMModel,
config: ReasoningConfig,
) -> dict[str, Any]: ...
LiteLLMRequester 默认优先生成统一参数:
reasoning_effort=<level>thinking={"type":"enabled"} 或 Provider 等价参数reasoning_effort="none"thinking={"type":"enabled","budget_tokens":N}Provider 特例只放在 requester 翻译层,不进入 Pipeline 或平台适配器。
disabled 必须报“不支持关闭,可选择 Provider Default 或最低档”,不能把 none 静默映射成 low/minimal。none 档位最终都只是开启。能力 API 只返回 toggle,UI 不显示档位;多轮必须保存并回传 reasoning_content。thinking.type=enabled/disabled/auto。应先让该 requester 进入 LiteLLM volcengine 适配器,或增加等价的明确翻译,不能依赖模型名。当前 LiteLLMRequester 会读取 reasoning_content,将其拼接成 <think> 文本,再删除原字段。建议改为:
上游 reasoning_content
├─ 原样保存在 Message.provider_specific_fields.reasoning_content
└─ 根据 remove-think 决定是否渲染为 <think>...</think>
流式路径需要在 accumulator 中分别累计 content 与 reasoning_content,最终消息必须携带结构化 reasoning。不能只依赖已经渲染的 <think> 文本反向解析。
这样可以同时满足:
remove-think=true 时用户看不到思考内容,但多轮协议仍能回传必要数据。remove-think=false 时保持当前用户体验。保留数据库和 Pipeline 配置键 remove-think,避免破坏兼容。Web 文案改为更准确的:
向用户展示思考过程Show reasoning processUI 使用正向开关,保存时转换回 remove-think = !showReasoning。文案必须强调它只影响展示,不影响模型是否思考、token 或费用。
模型页只承担能力管理和只读展示:
Reasoning ability 复选框与 Vision、Function Calling 并列,供无法自动识别的自定义模型手动声明能力。在 Local Agent 的主模型和每一个 fallback 模型下分别显示紧凑离散滑杆:
Provider 默认 始终为首个选项;选择它时不向上游增加任何思考参数。Provider 默认 / 关闭 / 开启 / 最低 / 低 / 中 / 高 / 极高 / 最大。Provider 默认 / 关闭 / 开启。关闭;能力未知时只显示不可调的 Provider 默认。流水线配置保持旧格式兼容,并在模型选择对象中增加按 UUID 保存的映射:
{
"model": {
"primary": "primary-model-uuid",
"fallbacks": ["fallback-model-uuid"],
"reasoning": {
"primary-model-uuid": "high"
}
}
}
provider_default 不写入映射;缺少 reasoning 的旧流水线天然等价于全部使用 Provider 默认。
滑杆交互要求:轨道使用现有主色和中性灰,不使用渐变;当前档位同时显示文字;支持键盘方向键和正确的 ARIA value text;窄屏下不溢出。
新增文案至少覆盖 en_US、zh_Hans;ja_JP 在模型面板现有同类字段已覆盖时同步补齐。不要把厂商参数名直接作为用户文案。
模型 CRUD 增加:
reasoning_configreasoning_configreasoning_capabilities模型测试接口必须使用与真实请求完全相同的规范化和翻译逻辑,并在失败时返回可操作错误,例如:
Model gemini-3-... cannot disable reasoning.
Supported controls: effort=[low, medium, high].
可选增加只读调试信息,仅在测试接口返回:
{
"effective_reasoning": {
"level": "low",
"translated_keys": ["reasoning_effort"]
}
}
不得返回 API key、完整请求正文或原始思考内容。
当前 MCP 仅列出模型 Provider,没有完整模型 CRUD 工具。如果本次不新增 agent-accessible HTTP 操作,则无需强行新增 MCP 工具。
如果后续让 Agent 修改模型思考策略,则必须同一提交更新:
src/langbot/pkg/api/mcp/server.pyskills/ 文档控制思考量后,管理员需要判断质量、延迟和成本是否值得。建议第二阶段增加:
reasoning_tokens:从 completion_tokens_details.reasoning_tokens 或 Provider 等价字段提取。effective_reasoning_level:记录规范化后的生效档位,不记录原始思考内容。安全要求:日志、监控、debug API 默认都不得记录 reasoning 原文。思考内容可能包含敏感信息或系统提示,不应因为新增配置而扩大持久化范围。
{"level":"provider_default"}。extra_args 中的 reasoning 参数,避免误判嵌套结构和 Provider 语义。extra_args reasoning 字段时显示“由高级参数控制”,统一策略保持 Provider Default。provider_default 不产生任何新增请求参数。remove-think 的存储键和默认值。litellm_provider,除非该 Provider 在专项回归后单独切换。drop_params 不能用于掩盖显式 reasoning 配置错误;显式策略被丢弃应视为失败。llm_models.reasoning_config。litellm_provider 变更做独立回归,避免把 reasoning 功能和通用请求行为回归混在一起。ReasoningConfig 所有合法/非法组合。provider_default 不产生任何新增参数。extra_args 的顺序。none effort 不伪装成不同档位。reasoning_content 保存到 provider_specific_fields。reasoning_config。extra_args 和 remove-think 行为不变。至少选取以下真实或可控 mock:
none 的 OpenAI reasoning 模型。none 的 reasoning 模型。每个模型比较 Provider Default、最低档、中档、高档或关闭,记录成功率、首 token 延迟、总耗时、总 token 和 reasoning token(若可用)。
| 风险 | 影响 | 控制措施 |
|---|---|---|
| 将“最低思考”误当成“关闭” | 用户以为节省了成本,实际仍在推理 | can_disable 严格校验,不静默降级 |
| 模型能力表过期 | 新模型无法配置或旧模型报错 | 能力未知时保守;允许测试;升级 LiteLLM 时回归 |
| 高 effort 导致延迟/费用陡增 | 用户体验和预算风险 | 默认 Provider Default;UI 提示;后续监控 reasoning token |
extra_args 与统一配置冲突 | 实际生效值不可预测 | 保存/测试时拒绝冲突;统一策略最后应用 |
| reasoning 原文进入日志 | 敏感信息泄露 | 不记录原文,只记录策略和 token |
| 多轮 reasoning 丢失 | 工具调用或后续轮次失败/降质 | 结构化保存并 round-trip;流式专项测试 |
| 修改 Provider 路由造成通用回归 | 非 reasoning 请求也受影响 | 国内 Provider 路由放第二阶段,独立提交和回归 |
remove-think 仅控制展示。extra_args。建议按以上 6 项全部通过,并将 Phase 1 作为一个完整功能单元实施。不要只增加前端开关或只在 extra_args 中写 reasoning_effort;那样虽然改动小,但会继续混淆展示与推理、无法处理 Provider 差异,也无法保证多轮对话正确性。