OpenAI 兼容不等于行为一致:DeepSeek 与 LM Studio 的结构化输出和 Thinking 配置实践
在 .NET 项目中接入 DeepSeek、LM Studio 等 OpenAI-compatible 服务时,一个很常见的误区是:
只要都兼容 OpenAI API,更换
Endpoint和ModelId就能无缝切换。
实际情况并非如此。
OpenAI-compatible 通常只意味着服务采用相似的请求路径和消息结构,并不保证所有参数、枚举值和行为完全一致。response_format、thinking、reasoning_effort 和 reasoning_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_schema和text,不接受json_object。 - 两者都兼容 OpenAI Chat Completions,但支持的协议子集不同。
二、三种响应格式有什么区别
1. Text
如果不设置 response_format,或者显式使用文本模式:
csharp
ResponseFormat = ChatResponseFormat.Text;
模型返回的是普通文本。
可以在提示词中要求:
text
只返回 JSON,不要输出 Markdown 代码块或解释文字。
但这只是提示词约束,服务端不会强制保证输出是合法 JSON。Text 模式兼容性最高,但必须在应用层解析和验证。
2. JsonObject
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 当前支持 text 和 json_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>();
这种实现并不可靠。
原因包括:
- LM Studio 可以部署在任意 IP 或域名。
- 企业网关可能隐藏真实 Provider。
- 同一个 Provider 的不同模型可能支持不同功能。
- Provider 升级后,能力范围可能发生变化。
- OpenAI-compatible 代理可能过滤某些请求字段。
- 同一服务的 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 当前主要支持 high 和 max,并提供以下兼容映射:
| 传入值 | 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_effort 和 reasoning 也可能被静默忽略。
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 参数,还必须验证模型实际行为,而不能只验证请求是否成功。