OpenAI 兼容不等于行为一致:DeepSeek 与 LM Studio 的结构化输出和 Thinking 配置实践

OpenAI 兼容不等于行为一致:DeepSeek 与 LM Studio 的结构化输出和 Thinking 配置实践

在 .NET 项目中接入 DeepSeek、LM Studio 等 OpenAI-compatible 服务时,一个很常见的误区是:

只要都兼容 OpenAI API,更换 EndpointModelId 就能无缝切换。

实际情况并非如此。

OpenAI-compatible 通常只意味着服务采用相似的请求路径和消息结构,并不保证所有参数、枚举值和行为完全一致。response_formatthinkingreasoning_effortreasoning_content 就是最容易踩坑的几个地方。

本文以一个使用 Microsoft.Extensions.AI、Microsoft Agent Framework 和 OpenAI .NET SDK 的项目为例,分析 DeepSeek 与 LM Studio 的差异,以及如何设计更可靠的多 Provider 配置。

一、问题现象

项目原本通过下面的代码要求模型返回严格结构化数据:

csharp 复制代码
ResponseFormat =
    ChatResponseFormat.ForJsonSchema<T>(
        schemaDescription: description);

切换到 DeepSeek 后,服务返回:

text 复制代码
HTTP 400

This response_format type is unavailable now

调用堆栈显示异常发生在:

text 复制代码
AIAgentUsageChatClient.GetResponseAsync

但这并不意味着 usage 中间件存在问题。它只是底层模型请求经过的最后一层包装。真正的错误由模型服务返回。

将格式修改为:

csharp 复制代码
ResponseFormat = ChatResponseFormat.Json;

DeepSeek 可以正常工作,但切换到 LM Studio 后又出现 HTTP 400。

直接请求 LM Studio 得到了更明确的错误:

text 复制代码
'response_format.type' must be 'json_schema' or 'text'

于是问题变得清晰:

  • DeepSeek 接受 json_object,不接受 json_schema
  • 当前 LM Studio 接受 json_schematext,不接受 json_object
  • 两者都兼容 OpenAI Chat Completions,但支持的协议子集不同。

二、三种响应格式有什么区别

1. Text

如果不设置 response_format,或者显式使用文本模式:

csharp 复制代码
ResponseFormat = ChatResponseFormat.Text;

模型返回的是普通文本。

可以在提示词中要求:

text 复制代码
只返回 JSON,不要输出 Markdown 代码块或解释文字。

但这只是提示词约束,服务端不会强制保证输出是合法 JSON。Text 模式兼容性最高,但必须在应用层解析和验证。

2. JsonObject

Microsoft.Extensions.AI 中的:

csharp 复制代码
ChatResponseFormat.Json

通常会转换为:

json 复制代码
{
  "response_format": {
    "type": "json_object"
  }
}

它主要保证模型返回合法 JSON 对象,但不会强制返回内容符合某个 DTO。

例如下面的响应是合法 JSON:

json 复制代码
{
  "summary": 123,
  "unknownField": true
}

但它可能不符合业务期望:

csharp 复制代码
public class MemoryCompressionOutputDto
{
    public string Summary { get; set; } = string.Empty;

    public IReadOnlyList<MemoryFactDto> Facts { get; set; } = [];
}

因此 JsonObject 模式仍需要 DTO 反序列化和业务校验。

DeepSeek 官方 Chat Completions API 当前支持 textjson_object,所以 DeepSeek 应使用 JsonObject。

3. JsonSchema

下面的代码:

csharp 复制代码
ChatResponseFormat.ForJsonSchema<T>()

会根据类型 T 生成 JSON Schema,并构造类似请求:

json 复制代码
{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "MemoryCompressionOutputDto",
      "schema": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "string"
          },
          "facts": {
            "type": "array"
          }
        },
        "required": [
          "summary",
          "facts"
        ],
        "additionalProperties": false
      }
    }
  }
}

JsonSchema 不只是要求返回合法 JSON,还可以约束:

  • 必填字段;
  • 字段类型;
  • 数组元素类型;
  • 枚举值;
  • 嵌套对象;
  • 是否允许额外字段。

LM Studio 支持通过 JSON Schema 在推理阶段约束模型输出,因此更适合使用 JsonSchema。

三、为什么不能根据 Endpoint 自动判断

一种看似简单的实现是:

csharp 复制代码
if (endpoint.Contains("deepseek.com"))
{
    return ChatResponseFormat.Json;
}

return ChatResponseFormat.ForJsonSchema<T>();

这种实现并不可靠。

原因包括:

  1. LM Studio 可以部署在任意 IP 或域名。
  2. 企业网关可能隐藏真实 Provider。
  3. 同一个 Provider 的不同模型可能支持不同功能。
  4. Provider 升级后,能力范围可能发生变化。
  5. OpenAI-compatible 代理可能过滤某些请求字段。
  6. 同一服务的 Chat Completions 和 Responses API 能力也可能不同。

更可靠的做法是显式配置 Provider 能力:

json 复制代码
{
  "OpenAIProvider": {
    "ApiKey": "...",
    "ModelId": "...",
    "Endpoint": "...",
    "StructuredOutputMode": "JsonObject"
  }
}

定义对应枚举:

csharp 复制代码
/// <summary>
/// OpenAI 兼容接口的结构化输出模式。
/// </summary>
public enum StructuredOutputMode
{
    /// <summary>
    /// 使用普通 JSON Object 模式。
    /// </summary>
    JsonObject,

    /// <summary>
    /// 使用严格 JSON Schema 模式。
    /// </summary>
    JsonSchema,

    /// <summary>
    /// 使用文本模式并由应用层解析 JSON。
    /// </summary>
    Text
}

根据配置创建响应格式:

csharp 复制代码
internal static ChatClientAgentRunOptions CreateStructuredOptions<T>(
    AgentRunOptions runOptions,
    StructuredOutputMode mode,
    string description)
{
    return new ChatClientAgentRunOptions(new ChatOptions
    {
        ResponseFormat = mode switch
        {
            StructuredOutputMode.JsonSchema =>
                ChatResponseFormat.ForJsonSchema<T>(
                    schemaDescription: description),

            StructuredOutputMode.Text =>
                ChatResponseFormat.Text,

            _ =>
                ChatResponseFormat.Json
        }
    })
    {
        AdditionalProperties = runOptions.AdditionalProperties
    };
}

四、DeepSeek 的推荐配置

DeepSeek 使用:

json 复制代码
{
  "OpenAIProvider": {
    "ApiKey": "<DeepSeek API Key>",
    "ModelId": "deepseek-v4-pro",
    "Endpoint": "https://api.deepseek.com/",
    "NetworkTimeoutSeconds": 300,
    "StructuredOutputMode": "JsonObject"
  }
}

此时完整校验链路是:

text 复制代码
提示词要求只输出 JSON
    ↓
DeepSeek 保证返回合法 JSON 对象
    ↓
应用 DTO Parser 验证字段结构
    ↓
业务校验验证数据语义

即使启用了 JsonObject,提示词中也应该明确出现 JSON 输出要求:

text 复制代码
请只返回合法 JSON 对象。
不要添加解释、前后缀或 Markdown 代码块。
所有字段必须符合给定的数据结构。

五、LM Studio 的推荐配置

LM Studio 的 OpenAI-compatible Base URL 通常需要包含 /v1

json 复制代码
{
  "OpenAIProvider": {
    "ApiKey": "lm-studio",
    "ModelId": "qwen3.6-27b-uncensored",
    "Endpoint": "http://192.168.100.100:1234/v1",
    "NetworkTimeoutSeconds": 300,
    "StructuredOutputMode": "JsonSchema"
  }
}

SDK 最终请求地址为:

text 复制代码
http://192.168.100.100:1234/v1/chat/completions

注意不要将 Endpoint 配置成:

text 复制代码
http://192.168.100.100:1234

否则 SDK 可能请求:

text 复制代码
http://192.168.100.100:1234/chat/completions

这不是 LM Studio 的 OpenAI-compatible 路径。

此外,LM Studio 中还应开启模型的 Structured Output 设置。这个开关使推理引擎能够依据 JSON Schema 约束 token 生成。

但该开关不会让 LM Studio 自动支持:

json 复制代码
{
  "type": "json_object"
}

因为 response_format.type 的合法性在进入模型推理之前就已经完成校验。

六、Thinking、Reasoning Effort 和 Reasoning Content

结构化输出解决后,另一个常见需求是控制模型的思考模式。

这里必须区分三个概念。

Thinking

Thinking 通常表示是否启用模型的推理过程。

DeepSeek 使用:

json 复制代码
{
  "thinking": {
    "type": "enabled"
  }
}

关闭:

json 复制代码
{
  "thinking": {
    "type": "disabled"
  }
}

Reasoning effort

Reasoning effort 表示模型愿意为推理投入多少计算量。

DeepSeek 使用:

json 复制代码
{
  "reasoning_effort": "high"
}

DeepSeek V4 当前主要支持 highmax,并提供以下兼容映射:

传入值 DeepSeek 实际处理
low high
medium high
high high
xhigh max
max max

因此给 DeepSeek 配置 low 并不意味着它真的以低推理强度运行。

Reasoning content

Reasoning content 是响应中的推理内容,而不是请求控制参数。

典型响应:

json 复制代码
{
  "choices": [
    {
      "message": {
        "reasoning_content": "模型的推理内容",
        "content": "最终答案"
      }
    }
  ]
}

可以简单理解为:

text 复制代码
thinking           是否思考
reasoning_effort   思考多少
reasoning_content  思考了什么
content            最终回答

七、为什么 LM Studio 的 Thinking 控制更复杂

LM Studio 同时提供三套接口:

text 复制代码
/v1/chat/completions
/v1/responses
/api/v1/chat

它们使用不同的 reasoning 参数。

OpenAI Chat Completions

当前项目使用:

text 复制代码
/v1/chat/completions

对当前 LM Studio 模型发送:

json 复制代码
{
  "thinking": {
    "type": "disabled"
  }
}

请求返回 HTTP 200,但响应中仍然包含:

json 复制代码
{
  "reasoning_content": "Thinking Process: ..."
}

这说明 LM Studio 接受了未知字段,却没有执行 DeepSeek 的 thinking 语义。

这是 OpenAI-compatible 服务中非常危险的一种情况:

HTTP 200 只说明请求被接受,不代表所有参数都生效。

类似地,reasoning_effortreasoning 也可能被静默忽略。

LM Studio Responses API

LM Studio 的 /v1/responses 使用 OpenAI Responses 风格:

json 复制代码
{
  "reasoning": {
    "effort": "low"
  }
}

但该能力依赖具体模型。例如官方示例主要使用支持 reasoning effort 的 GPT-OSS 模型。

LM Studio 原生 Chat API

LM Studio 原生接口:

text 复制代码
POST /api/v1/chat

使用更直接的参数:

json 复制代码
{
  "reasoning": "high"
}

可选值包括:

text 复制代码
off
on
low
medium
high

例如:

bash 复制代码
curl http://192.168.100.100:1234/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.6-27b-uncensored",
    "input": "分析这个问题",
    "reasoning": "high"
  }'

如果模型不支持指定模式,LM Studio 原生 API 会返回错误。

但它不能直接替换当前 Agent 使用的 Chat Completions API,因为两者在以下方面不同:

  • 消息结构;
  • 会话状态;
  • 工具定义;
  • 工具调用返回格式;
  • MCP 集成;
  • reasoning 输出格式。

特别是项目中的 ActionPlanner、GameManager 和 NPC Agent 都使用自定义 tools。直接切换到 /api/v1/chat 可能破坏工具调用链路。

八、如何设计跨 Provider 的 Thinking 配置

不要简单定义:

json 复制代码
{
  "ReasoningEffort": "High"
}

然后假设所有 Provider 都会产生相同效果。

更合理的配置需要说明协议:

json 复制代码
{
  "OpenAIProvider": {
    "ReasoningProtocol": "DeepSeekChatCompletions",
    "ThinkingEnabled": true,
    "ReasoningEffort": "High"
  }
}

可能的协议类型包括:

csharp 复制代码
/// <summary>
/// 模型推理控制协议。
/// </summary>
public enum ReasoningProtocol
{
    /// <summary>
    /// 不发送推理控制参数。
    /// </summary>
    None,

    /// <summary>
    /// 使用 OpenAI 推理参数。
    /// </summary>
    OpenAI,

    /// <summary>
    /// 使用 DeepSeek Chat Completions 推理参数。
    /// </summary>
    DeepSeekChatCompletions,

    /// <summary>
    /// 使用 LM Studio 原生 Chat 推理参数。
    /// </summary>
    LmStudioChat,

    /// <summary>
    /// 使用 LM Studio Responses 推理参数。
    /// </summary>
    LmStudioResponses
}

但是协议配置只能解决"如何发送参数"的问题,不能消除接口能力差异。

对于当前项目,更实际的策略是:

DeepSeek

继续使用 /chat/completions

json 复制代码
{
  "ReasoningProtocol": "DeepSeekChatCompletions",
  "ThinkingEnabled": true,
  "ReasoningEffort": "High"
}

这套配置可以可靠生效。

LM Studio 工具型 Agent

继续使用:

text 复制代码
/v1/chat/completions

保留模型默认 thinking 行为。

原因是当前接口下无法证明 DeepSeek 风格的控制参数有效,而切换原生接口会影响 tools。

LM Studio 非工具型任务

对于记忆压缩、每日日志生成等无工具任务,可以单独考虑使用:

text 复制代码
/api/v1/chat

然后通过:

json 复制代码
{
  "reasoning": "off"
}

控制推理。

这意味着项目最终可能需要混合模式:

text 复制代码
工具型 Agent
    → /v1/chat/completions

无工具结构化任务
    → /api/v1/chat

九、为什么仍然需要应用层 Parser

即使使用 JsonSchema,也不应该删除应用层 Parser。

服务端结构化输出仍可能因为以下原因失败:

  • 输出达到 max_tokens 被截断;
  • 模型返回空内容;
  • 推理 token 消耗了全部输出预算;
  • Provider 忽略部分 schema;
  • SDK 转换时丢失某些约束;
  • 本地模型推理引擎不支持复杂 schema;
  • 服务升级导致行为变化。

可靠的处理链路应该是:

text 复制代码
Provider 格式约束
    ↓
JSON 语法解析
    ↓
DTO 类型校验
    ↓
业务规则校验
    ↓
记录失败原因和 Provider 原始响应

尤其对于 reasoning 模型,max_tokens 包含推理 token 和最终答案 token。

实测 LM Studio 请求中,如果设置:

json 复制代码
{
  "max_tokens": 32
}

模型可能把全部 32 个 token 都用于 reasoning_content,最终得到:

json 复制代码
{
  "content": "",
  "finish_reason": "length"
}

因此不能只检查 HTTP 状态码,还需要检查:

  • content 是否为空;
  • finish_reason 是否为 length
  • reasoning token 是否占满输出预算。

十、实践结论

DeepSeek 和 LM Studio 都兼容 OpenAI API,但兼容层级不同。

结构化输出推荐:

text 复制代码
DeepSeek  → JsonObject
LM Studio → JsonSchema
未知服务  → Text

Thinking 控制推荐:

text 复制代码
DeepSeek
  → 在 Chat Completions 中使用 thinking + reasoning_effort

LM Studio Chat Completions
  → 不假设 DeepSeek 参数有效
  → 使用模型默认 reasoning 行为

LM Studio 原生 Chat
  → 使用 reasoning: off/on/low/medium/high

LM Studio Responses
  → 对支持模型使用 reasoning.effort

最重要的工程原则是:

不要把"OpenAI-compatible"理解为"所有 OpenAI 参数完全兼容",更不要把 HTTP 200 理解为所有请求参数已经生效。

多 Provider 项目应该显式描述能力,而不是根据域名猜测;应该保留应用层解析和校验,而不是完全依赖服务端格式约束;对于 reasoning 参数,还必须验证模型实际行为,而不能只验证请求是否成功。

参考资料

相关推荐
MicrosoftReactor1 小时前
技术速递|GitHub Copilot App 入门指南:快速上手
ai·github·copilot·copilot app
城管不管2 小时前
RabbitMQ死信队列
java·分布式·ai·面试·职场和发展·rabbitmq·agent
城管不管2 小时前
rabbitmq如何保证消息不丢失?解决方案又是什么?
开发语言·ai·面试·职场和发展·rabbitmq·php·agent
盖伦发发3 小时前
AIE-AI Engineering三, 四章总结: 如何评估AI应用
人工智能·ai
wangxin2083 小时前
多智能体协作收敛效率关键:大模型幻觉和角色视角
人工智能·ai·多智能体·团队协作·管理学·组织管理·coordclaw
半兽先生4 小时前
2026年RAG系统主流开源文档解析工具选型指南:PaddleOCR、MinerU、LiteParse、HunyuanOCR、Apache Tika 全面对比
人工智能·python·机器学习·ai·开源
看浪的路人16 小时前
第1讲:Agent 到底是什么?和 Chatbot 有什么区别
python·ai
Beginner x_u21 小时前
Codex Rules 与 Skills:项目级和全局级配置一览
ai·codex·rules·skill