调AI接口的姿势

什么,你是说御三家的AI接口参数格式不兼容?

三家官方文档入口

AI 模型和接口更新很快,同一家厂商的不同模型也不一定支持相同参数

阅读本文时可以先收藏下面这些官方入口,实际开发前再用它们确认模型能力、参数范围和弃用状态

厂商 模型与能力对照 请求参数文档 重点查看什么
OpenAI Models 与模型对比 Responses API 模型支持的端点、Streaming、Function Calling、Structured Outputs、输入模态及上下文限制
Anthropic Claude Models Overview Messages API 模型能力、上下文与输出上限,以及 temperature、top_p、top_k 等参数的弃用说明
Google Gemini Models API GenerateContent API supportedGenerationMethods、Token 上限、Thinking,以及模型默认的 temperature、topP、topK

这里需要特别注意「接口支持某个字段」和「你选择的模型支持这个字段」是两回事

例如 Responses API 的参数表里存在 temperature,不代表每一个 OpenAI 推理模型都允许自定义它;Claude 的 Messages API 仍展示采样参数,但新模型已经开始弃用或拒绝非默认值;Gemini 的 topK 是否可用,则可以从 Models API 返回的模型信息中判断

因此最稳妥的检查顺序是

  1. 在模型对照页确认模型 ID、输入模态、上下文长度与功能支持
  2. 在对应端点的 API Reference 中确认字段名称、类型和取值范围
  3. 查看模型说明、弃用公告和迁移文档,确认该参数有没有模型级限制
  4. 上线前用目标模型发送一条最小请求,不要直接把其他模型的参数配置原样复制过来

一次 AI API 请求里到底有什么

不管厂商如何命名,一次文本生成请求通常都可以拆成五层

层次 作用 常见字段
模型层 决定能力、速度、价格和上下文长度 model
上下文层 告诉模型身份、任务和历史消息 system、messages、input、contents
生成层 控制输出长度、随机性和停止条件 max_tokens、temperature、top_p、stop
能力层 让模型调用函数、搜索或读取文件 tools、tool_choice
输出层 约束返回文本、JSON 或固定结构 response_format、response_schema

最小请求并不复杂,以 OpenAI 兼容的 Chat Completions 接口为例

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"model": "MODEL_ID",
"messages": [
{
"role": "system",
"content": "你是一名严谨的技术编辑"
},
{
"role": "user",
"content": "用三点解释什么是向量数据库"
}
]
}

发送请求时还需要在 Header 中设置 API Key 和 Content-Type: application/json,切换兼容平台时通常只需要更换请求地址、密钥和模型 ID

三种主流请求结构

请求发出去之后,三家接口都会返回「模型生成的内容 + 停止原因 + Token 用量 + 请求元数据」

真正需要业务代码读取的,通常不只是回答文字,还包括回答是否完整、有没有触发工具、是否被安全策略拦截,以及本次调用消耗了多少 Token

OpenAI Chat Completions 与兼容接口

大量 OpenAI 兼容接口仍使用 Chat Completions 结构,也就是用 messages 数组承载上下文,每条消息包含 role 和 content

1
2
3
4
5
6
7
8
9
10
11
12
{
"model": "MODEL_ID",
"messages": [
{ "role": "system", "content": "回答要准确、简洁" },
{ "role": "user", "content": "解释 Top-P" }
],
"temperature": 0.7,
"top_p": 0.9,
"max_completion_tokens": 1024,
"stop": ["<END>"],
"stream": false
}

很多国内外平台都兼容这套格式,但「兼容」不代表所有参数都实现一致,尤其是推理参数、结构化输出和工具调用,迁移前仍要查看模型自己的能力表

对应的 Response Body

前面的 OpenAI Chat Completions Payload 通常会得到类似下面的 Response Body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"id": "chatcmpl_abc123",
"object": "chat.completion",
"created": 1788710400,
"model": "MODEL_ID",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Top-P 会从累计概率达到阈值的候选词中进行采样",
"refusal": null,
"tool_calls": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 28,
"completion_tokens": 32,
"total_tokens": 60,
"completion_tokens_details": {
"reasoning_tokens": 0
}
},
"system_fingerprint": "fp_example"
}
字段 作用
id 本次生成结果的唯一标识,排查日志和联系客服时很有用
object 对象类型,非流式 Chat Completions 通常是 chat.completion
created 响应创建时间,使用 Unix 秒级时间戳
model 实际处理请求的模型
choices 候选回答数组,设置 n > 1 时可能返回多条
choices[].index 当前候选回答在数组中的序号
choices[].message 模型生成的消息,普通文本位于 content
choices[].message.refusal 模型拒绝回答时可能出现的拒绝说明
choices[].message.tool_calls 模型要求调用函数或其他工具时返回的调用信息
choices[].logprobs 开启 Logprobs 后返回每个 Token 的概率明细
choices[].finish_reason 模型为什么停止生成
usage 本次请求的 Token 用量
system_fingerprint 服务端模型配置指纹,可辅助判断后端配置是否发生变化

读取普通文本时,常见路径是 choices[0].message.content

但生产环境不能只取这一段文字,还要检查 finish_reason

finish_reason 含义 建议处理
stop 模型自然结束,或命中停止序列 正常使用回答
length 达到输出或上下文限制 当作截断结果处理,不要直接视为完整答案
tool_calls 模型希望调用工具 执行工具并把结果送回模型
content_filter 内容被安全系统截断 提示用户调整输入或进入人工处理

usage.prompt_tokens 是输入消耗,usage.completion_tokens 是输出消耗,usage.total_tokens 是二者合计

部分推理模型还会在 completion_tokens_details.reasoning_tokens 中给出推理 Token,它们通常不会直接显示给用户,但同样占用输出预算

Anthropic Messages API

Claude 将系统提示放在顶层 system,对话历史放在 messages 中,并要求显式给出 max_tokens

1
2
3
4
5
6
7
8
9
10
11
12
{
"model": "CLAUDE_MODEL_ID",
"max_tokens": 1024,
"system": "你是一名严谨的技术编辑",
"messages": [
{ "role": "user", "content": "解释 Top-P" }
],
"temperature": 0.7,
"top_p": 0.9,
"stop_sequences": ["<END>"],
"stream": false
}

工具定义使用 input_schema,模型返回 tool_use 内容块,应用执行函数后再把 tool_result 送回模型

对应的 Response Body

Claude 的 content 是内容块数组,而不是一个固定字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"id": "msg_01ABC",
"type": "message",
"role": "assistant",
"model": "CLAUDE_MODEL_ID",
"content": [
{
"type": "text",
"text": "Top-P 会先选出累计概率达到阈值的一组候选词,再从中采样"
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 27,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 35
}
}
字段 作用
id 本次 Message 的唯一标识
type 对象类型,Messages API 中固定为 message
role 返回消息的角色,通常为 assistant
model 实际使用的 Claude 模型
content 内容块数组,可以同时出现文字、思考块或工具调用
content[].type 当前内容块的类型,例如 text 或 tool_use
content[].text text 内容块中的回答正文
stop_reason Claude 停止当前轮次的原因
stop_sequence 命中自定义停止序列时,记录具体序列
usage 输入、缓存和输出 Token 用量

普通文字通常从所有 type = "text" 的内容块中提取,不能假设 content[0] 永远是文字

例如 Claude 决定调用工具时,内容块可能长这样

1
2
3
4
5
6
7
8
{
"type": "tool_use",
"id": "toolu_01ABC",
"name": "get_weather",
"input": {
"city": "上海"
}
}

此时应用应读取 name 和 input 执行函数,再用对应的 tool_use_id 返回 tool_result

Claude 常见的 stop_reason 包括

stop_reason 含义 建议处理
end_turn Claude 自然完成这一轮 正常使用回答
max_tokens 达到最大输出限制 提高上限或继续生成
stop_sequence 命中自定义停止序列 检查 stop_sequence
tool_use Claude 发起工具调用 执行工具并返回结果
pause_turn 服务端工具循环暂停 把当前响应带回下一轮继续
refusal Claude 拒绝继续回答 根据业务策略提示或降级
model_context_window_exceeded 上下文窗口已满 缩短历史消息或重新整理上下文

如果启用了 Prompt Caching,总输入量需要同时考虑 input_tokens、cache_creation_input_tokens 和 cache_read_input_tokens

它们拆开显示,是为了区分普通输入、新写入缓存的输入,以及从缓存中读取的输入

Gemini GenerateContent API

Gemini 使用 contents 和 parts 表示消息,多模态文本、图片和文件都可以放进 parts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"contents": [
{
"role": "user",
"parts": [
{ "text": "解释 Top-P" }
]
}
],
"generationConfig": {
"temperature": 0.7,
"topP": 0.9,
"topK": 40,
"maxOutputTokens": 1024,
"stopSequences": ["<END>"],
"responseMimeType": "text/plain"
}
}

命名虽然不同,但本质仍然是「上下文 + 生成配置 + 输出约束」

这些 Payload 用于展示字段位置,并不代表所有模型都接受相同取值,部分新一代推理模型要求保留默认采样参数,调用前仍要查看具体模型文档

对应的 Response Body

Gemini 将候选回答放在 candidates 中,文字仍然位于 content.parts 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"candidates": [
{
"content": {
"parts": [
{
"text": "Top-P 会保留累计概率达到阈值的候选词,再从这些候选中抽取下一个词"
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0,
"safetyRatings": [
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
],
"avgLogprobs": -0.18
}
],
"usageMetadata": {
"promptTokenCount": 25,
"candidatesTokenCount": 36,
"totalTokenCount": 61
},
"modelVersion": "MODEL_VERSION",
"responseId": "response_abc123"
}
字段 作用
candidates 模型返回的候选回答数组
candidates[].content 当前候选的内容和角色
candidates[].content.parts 内容块数组,可以包含文字、函数调用等不同数据
candidates[].finishReason 当前候选停止生成的原因
candidates[].safetyRatings 各安全类别的风险判断
candidates[].avgLogprobs 候选回答的平均对数概率
usageMetadata Prompt、候选输出及总 Token 用量
modelVersion 实际生成响应的模型版本
responseId 本次响应的唯一标识
promptFeedback Prompt 被安全策略拦截时返回的反馈

普通文字的常见读取路径是 candidates[0].content.parts[0].text,但实际应用仍应遍历 parts 并根据类型处理

Gemini 常见的 finishReason 包括

finishReason 含义 建议处理
STOP 模型自然结束,或命中停止序列 正常使用回答
MAX_TOKENS 达到最大 Token 数 当作截断处理
SAFETY 候选内容触发安全策略 不要直接展示不完整内容
RECITATION 内容触发引用或复述检测 调整请求并重新生成
MALFORMED_FUNCTION_CALL 模型生成了无效函数调用 校验工具 Schema 后重试
PROHIBITED_CONTENT 检测到禁止内容 按安全策略处理

如果请求本身被拦截,candidates 可能为空,此时应优先检查 promptFeedback.blockReason

三家响应结构怎么记

厂商 回答正文 停止原因 Token 用量
OpenAI Chat Completions choices[].message.content choices[].finish_reason usage
Anthropic Claude content[] 中的 text 块 stop_reason usage
Google Gemini candidates[].content.parts[] 中的 text candidates[].finishReason usageMetadata

无论使用哪一家,都建议把处理逻辑分成四步

  1. 确认候选或内容块确实存在
  2. 根据内容块类型提取文本或处理工具调用
  3. 检查停止原因,识别截断、安全拦截和工具调用
  4. 记录请求 ID、模型版本、Token 用量与耗时

只读取正文而忽略停止原因,是 AI API 接入中最常见也最危险的问题之一

常见参数速查表

通用概念 OpenAI 常见命名 Claude 常见命名 Gemini 常见命名 主要作用
模型 model model URL 或 model 选择具体模型
系统提示 instructions / system 消息 system systemInstruction 设置身份和全局规则
最大输出 max_output_tokens / max_completion_tokens max_tokens maxOutputTokens 限制最多生成多少 Token
随机性 temperature 视模型而定 temperature 调整候选 Token 概率分布
核采样 top_p 视模型而定 topP 按累计概率筛选候选 Token
Top-K 部分模型不提供 视模型而定 topK 只保留概率最高的 K 个候选
停止序列 stop stop_sequences stopSequences 遇到指定文本时停止生成
流式输出 stream stream SDK 流式方法 边生成边返回
工具 tools tools tools 声明模型可以调用的能力
工具选择 tool_choice tool_choice toolConfig 自动、强制或禁止调用工具
结构化输出 response_format / text.format output_config 或严格工具 responseJsonSchema 约束输出结构
推理强度 reasoning.effort 等 effort / thinking 配置 thinkingLevel 调整推理成本与深度

表中的字段会随接口和模型代际变化,最稳妥的做法是先确定模型,再以对应模型的官方参数表为准

Temperature:改变概率分布,而不是直接设置创造力

模型每生成一个 Token,都会先得到一组候选 Token 及其概率,temperature 会在抽样前重新调整这组概率

通俗地说,模型每写下一个词,都像在一个装满候选答案的抽奖箱里抽签

低温度会让原本最有可能的那张签变得更容易抽中,适合需要稳妥答案的任务;高温度则会让冷门选项也获得更多机会,适合需要不同表达和创意的任务

它调整的是模型「怎么选词」,而不是让模型真正变聪明,因此提高温度不会增加知识量,降低温度也不能消除事实错误

可以把它粗略理解为

1
调整后的概率 ∝ exp(logit / temperature)
  • temperature 较低时,高概率候选会更突出,输出通常更稳定、更保守
  • temperature 较高时,候选之间的概率差距会缩小,低概率表达更容易被选中
  • temperature = 0 常被理解为倾向选择最高概率候选,但分布式服务、模型更新和工具执行仍可能让结果不完全可复现
任务 建议思路
信息抽取、分类、生成代码补丁 使用默认值,或在模型支持时尝试较低值
客服回复、摘要、知识问答 从默认值开始,只在输出过于僵硬时小幅提高
标题、文案、故事创作 可以适度提高,并通过多次采样筛选
数学、复杂推理、Agent 优先使用模型默认值和推理参数,不要先动采样参数

新一代推理模型通常针对默认采样设置做过专门优化,某些模型甚至会忽略或拒绝非默认值,所以「越低越准确」已经不是通用规律

如果把同一个问题连续请求十次,低温度下的回答往往比较相似,高温度下则更容易出现不同标题、措辞和思路,这也是观察它是否生效最直观的方法

Top-P:用累计概率划定候选范围

top_p 又叫 Nucleus Sampling,也就是核采样

可以把它想象成点菜时先划定一份「可选菜单」

top_p 越小,菜单上只留下最稳妥的几道招牌菜;top_p 越大,更多小众菜品也能进入候选,但模型最终仍只会从这份菜单里选择

假设下一个 Token 有以下候选

Token 原始概率 累计概率
北京 0.42 0.42
上海 0.28 0.70
广州 0.16 0.86
深圳 0.09 0.95
其他 0.05 1.00

当 top_p = 0.70 时,抽样范围主要保留 北京 和 上海

当 top_p = 0.95 时,广州 和 深圳 也会进入候选集合,输出自然会更丰富

temperature 和 top_p 都在控制随机性,但角度不同

  • temperature 重塑整条概率曲线
  • top_p 直接裁掉累计概率之外的长尾候选

工程上一般不要同时大幅修改两者,否则出现偏差时很难判断是谁造成的,建议保留其中一个默认值,只围绕另一个做实验

简单记忆就是:temperature 调整候选之间的冷热差距,top_p 决定多少候选有资格上桌

Top-K:只看概率最高的 K 个候选

top_k = 20 表示每一步最多从概率最高的 20 个 Token 中继续筛选和抽样

如果说 top_p 是按候选的总可信度划线,top_k 就是更直接地规定「只允许前 K 名参赛」

它与 top_p 的区别在于

  • top_k 限制候选数量,不关心这些候选一共覆盖多少概率
  • top_p 限制累计概率,候选数量会随上下文动态变化

Gemini 等接口较常暴露 topK,OpenAI 兼容接口通常没有这个字段

对于 Gemini 3.x 等新模型,官方建议保留 temperature、topP 和 topK 的默认值,复杂推理任务中手动降低这些参数可能引发重复或性能下降

输出长度与停止条件

max_tokens 与 max_output_tokens

最大输出参数限制的是生成上限,不是保证模型一定写到这个长度

它更像给回答准备一个最大尺寸的纸箱,而不是要求模型必须把纸箱装满

箱子太小,内容可能写到一半被截断;箱子很大,也不代表模型一定会生成很长的回答

需要注意三个问题

  1. 输入 Token 和输出 Token 都会占用上下文窗口
  2. 推理模型可能还会消耗不可见或单独计量的推理 Token
  3. 输出达到上限时可能停在半句话、半段代码甚至半个 JSON 上

因此生产环境不能只解析正文,还要检查 finish_reason、stop_reason 或对应的完成状态

stop

stop 可以是一个或多个停止序列,例如

它相当于提前告诉模型「看到这个标记就停笔」,适合输出边界非常明确的协议

1
2
3
{
"stop": ["<END>", "用户:"]
}

适合固定协议、少样本模板和分隔符明确的生成任务

停止序列通常不会包含在最终结果里,也不要选择正文中可能频繁出现的普通词,否则模型会提前结束

Frequency Penalty 与 Presence Penalty

这两个参数常见于 OpenAI 风格接口,用来降低重复,但含义并不相同

可以把它们理解成两位不同的编辑

presence_penalty 会说「这个词已经讲过了,换个方向吧」,frequency_penalty 则会说「这个词出现太多次了,少用一点吧」

参数 判断方式 直观效果
presence_penalty 某个 Token 是否已经出现过 鼓励模型换一个话题或词汇
frequency_penalty 某个 Token 已经出现了多少次 出现越多,后续惩罚越强

文章反复绕圈时可以小幅提高 frequency_penalty

需要模型覆盖更多角度时可以尝试 presence_penalty

它们不是修复重复问题的万能开关,提示词不清晰、上下文里存在重复模板或采样参数不合适,同样会让模型不断复读

Seed、N、Logprobs 与 Logit Bias

seed

部分接口提供 seed 来提高重复请求之间的一致性,但它通常只代表「尽力而为的可复现」

它类似给随机过程指定同一个抽签起点,有助于复现实验,却不是给回答拍下一张永远不变的照片

例如让模型为咖啡店写一句广告词,即使 Prompt 和参数完全相同,不指定 seed 时也可能得到不同结果

1
2
3
4
5
6
7
8
9
10
11
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "为一家雨天营业的咖啡店写一句广告词"
}
],
"temperature": 0.8,
"seed": 2026
}

固定为 seed: 2026 后,多次请求更有机会得到相同或相近的文案,这对于自动化评测很有帮助:当你只修改 System Prompt 时,可以尽量排除随机抽样带来的干扰,更容易判断新 Prompt 到底有没有变好

模型版本、服务端实现、并行计算和系统指纹发生变化时,同一个 seed 仍可能得到不同结果

因此 seed 适合做实验对比和问题复现,不适合用来保证合同、账单等业务结果绝对一致

n 或 candidateCount

一次生成多个候选,适合创意筛选和离线评测,但成本也会接近按候选数增长

例如 n = 3 可以理解为让模型一次交三份答案供你挑选,但 Token 消耗也通常不再只是一份

1
2
3
4
5
6
7
8
9
10
11
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "给一款旅行相册应用起一个中文名字"
}
],
"temperature": 0.9,
"n": 3
}

这次请求可能返回三个候选

  1. 拾光地图
  2. 山海相册
  3. 旅迹

OpenAI 风格接口通常使用 n,Gemini 中类似的字段叫 candidateCount,并非所有模型都支持一次返回多个候选

如果每份回答最多生成 500 个 Token,n = 3 最坏可能产生接近 1500 个输出 Token,所以它不是免费的「多给几个答案」

在线业务一般先生成一个结果,不满足条件再有针对性地重试,比无差别生成多个候选更经济

logprobs

logprobs 是 Log Probabilities,也就是对数概率

通俗来说,它是模型输出每一个 Token 时的「选词打分表」

如果说 temperature 和 top_p 是控制模型如何做选择的旋钮,那么 logprobs 就是让模型把部分打分明细展示给开发者看

为什么使用对数概率

普通概率位于 0 到 1 之间,logprob 则是对概率取自然对数 $\ln(p)$

普通概率 Logprob 近似值 如何理解
100% 0 几乎完全确定
50% -0.69 两种选择势均力敌
10% -2.30 可能性较低
1% -4.61 非常冷门

一句话记忆:logprob 通常是 0 或负数,越接近 0 表示模型越倾向选择这个 Token,数值越负表示选择概率越低

计算机使用对数,是因为一段文本的整体概率需要把许多很小的 Token 概率相乘,转换为对数后就能把乘法变成加法,也能减少数值下溢问题

用补全问题举例

假设输入是「中国的首都是」,模型内部的候选分布可能类似

候选 Token Logprob 换算后的近似概率 直观理解
北京 -0.01 99.00% 明显是首选
上海 -4.61 0.99% 可能性很低
南京 -6.91 0.10% 受到历史语境影响的冷门项
广州 -9.21 0.01% 几乎不会选择

如果问题换成「今天晚餐吃」,第一名可能只有 30%,后面的火锅、米线、披萨和炒饭彼此非常接近,这说明模型在选词层面没有明显偏好

在 OpenAI 风格接口中,常见写法是使用 logprobs 开启功能,再用 top_logprobs 指定每个位置额外返回多少个候选,而不是写成 logprobs: 5

1
2
3
4
5
6
7
8
9
10
11
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "只补全答案:中国的首都是"
}
],
"logprobs": true,
"top_logprobs": 5
}

实际开发中有什么用

  1. 观察模型是否犹豫:对比某个位置的候选差距,辅助决定是否需要人工复核
  2. 分类与多选题评测:限制模型只回答固定标签,再比较各标签 Token 的概率
  3. 调试 Prompt:观察修改提示词前后,目标答案的概率是否稳定提高
  4. 分析生成过程:查看模型在关键位置还考虑过哪些表达

需要注意,Token 的选词概率不是答案的事实正确率

模型可能以很高概率复述一条错误常识,也可能因为人名生僻而用较低概率给出正确答案,所以不能简单规定「低于 -5 就是幻觉」

更稳妥的做法是把 logprobs 当成风险信号之一,再结合检索结果、规则校验、模型评测和人工复核共同判断

中文词语还可能被拆成一个或多个 Token,分析「北京」时不能想当然地认为它一定对应单个 Token

logit_bias

按 Token ID 调整某些候选的生成倾向,适合少量硬约束实验,但 Token 切分比肉眼看到的单词复杂,很容易误伤空格、词缀和不同大小写

它像是在模型的候选词表上给某些词加分或扣分,控制力很细,但也因此更难正确使用

假设一个客服分类接口只能回答「退款」或「咨询」,但模型偶尔会先写一句解释,可以尝试提高目标标签对应 Token 的权重,并降低无关 Token 的权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "将用户问题分类为退款或咨询:我的订单什么时候发货"
}
],
"logit_bias": {
"TOKEN_ID_FOR_退款": 20,
"TOKEN_ID_FOR_咨询": 20,
"TOKEN_ID_FOR_解释性开场": -100
}
}

正值会提高对应 Token 被选中的倾向,负值会降低倾向,接近 -100 的值在部分 OpenAI 风格接口里常用于近似禁止某个 Token,但具体范围仍要以接口文档为准

难点在于 logit_bias 操作的是 Token ID 而不是肉眼看到的完整词语

例如「退款」、前面带空格的「 退款」和不同语言中的同义词可能对应完全不同的 Token,屏蔽一个 ID 并不代表相关表达都会被屏蔽,甚至还可能影响包含该 Token 的其他词

如果目标是限制业务枚举值,JSON Schema 的 enum 往往更可靠

因此 logit_bias 更适合实验和细粒度微调,不应被当成内容安全、敏感词过滤或业务权限控制机制

Streaming:为什么聊天界面能一个字一个字出现

设置 stream: true 后,服务端通常通过 SSE 或类似机制持续返回事件

普通请求像等服务员把整桌菜全部上齐后再开门,流式请求则像菜做好一道就先端一道

因此用户能更早看到第一个字,等待感会明显减轻,但厨房完成整桌菜所需的总时间未必会缩短

1
2
3
4
5
6
7
8
9
10
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "解释什么是流式输出"
}
],
"stream": true
}

真实项目里还需要处理事件边界、增量 JSON、断线、取消请求和最终 usage,优先使用官方 SDK 的流式迭代器会更省心

流式输出降低的是首字等待时间,不会减少模型实际生成的 Token,也不必然降低总耗时

JSON Mode 不等于 JSON Schema

这两个概念非常容易混淆

能力 保证是合法 JSON 保证字段完整 保证字段类型 限制额外字段
普通文本 Prompt 不保证 不保证 不保证 不保证
JSON Mode 通常保证 不保证 不保证 不保证
JSON Schema Structured Outputs 保证 可保证 可保证 可保证

JSON Mode 只是要求输出可以被 JSON.parse,模型仍可能把 price 写成字符串、漏掉 id,或者擅自增加一个 reasoning 字段

JSON Schema 才是在描述数据契约

可以把 JSON Mode 理解为「必须把内容装进一个合法纸箱」,而 JSON Schema 还会规定纸箱里必须有几件商品、每件商品是什么类型,以及能不能夹带额外物品

JSON Schema 核心关键字详解

先看一个适合商品信息抽取的完整 Schema

第一次接触 Schema 时,可以先把它当成一张给 JSON 使用的表格模板:字段名是表头,type 是单元格格式,required 决定哪些格子不能空着,其他关键字继续限制可以填写的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "商品名称"
},
"price": {
"type": "number",
"minimum": 0,
"description": "商品价格,单位为元"
},
"currency": {
"type": "string",
"enum": ["CNY", "USD", "JPY"]
},
"tags": {
"type": "array",
"items": { "type": "string" },
"maxItems": 5
},
"available": {
"type": "boolean"
}
},
"required": ["name", "price", "currency", "tags", "available"],
"additionalProperties": false
}

type

type 声明数据类型,最常用的有

它解决的是「这个位置究竟应该装什么」的问题,例如价格应当是数字,而不是看起来像数字的字符串

JSON Schema 类型 JavaScript 中通常对应
object 普通对象
array 数组
string 字符串
number 任意数字
integer 整数
boolean 布尔值
null null

只写 properties 而不写 type: "object" 并不严谨,因为对象专属关键字不会自动把数据类型锁定为对象

properties

properties 定义对象允许出现的字段及各字段约束

可以把它看成对象的字段目录,每一个键都在说明一个字段叫什么、里面允许放什么

它本身不会让字段变成必填,是否必填要由 required 决定

required

required 是必填字段名称数组

需要特别注意,字段出现在 properties 里只代表它被认识,并不代表它必须出现,只有被列入 required 才算必填

1
2
3
{
"required": ["name", "price"]
}

在严格结构化输出中,常见做法是把所有业务字段都列入 required,可选值通过联合类型表达,例如

1
2
3
{
"type": ["string", "null"]
}

这表示字段必须存在,但值可以是字符串或 null

additionalProperties

默认情况下,JSON Schema 允许出现未在 properties 中声明的字段

这就像一张表格虽然列出了姓名和价格,却没有明确禁止填写其他备注,模型仍可能主动增加你没有设计的字段

1
2
3
{
"additionalProperties": false
}

这行会关闭额外字段,是 AI 结构化输出里非常重要的约束,也能减少模型自作主张增加解释字段

enum 与 const

enum 把值限制在有限集合中

它类似网页里的下拉选择框,只能从提前准备好的选项中选择,特别适合订单状态、语言代码和分类标签

1
2
3
4
{
"type": "string",
"enum": ["pending", "paid", "cancelled"]
}

const 则要求值只能是一个固定值,适合协议版本或事件类型

1
2
3
{
"const": "order.created"
}

items

数组中的每一项由 items 约束

数组像一排储物格,items 规定每个格子里应该放同一种什么结构的数据

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"score": { "type": "number", "minimum": 0, "maximum": 1 }
},
"required": ["id", "score"],
"additionalProperties": false
}
}

还可以使用 minItems、maxItems 控制数组长度

description

description 不参与传统 JSON 校验,但对模型理解字段非常重要

如果说其他关键字负责检查格式,description 就负责把业务常识讲给模型听

不要只写「价格」,最好写清单位、来源和缺失时如何处理,例如「订单含税总价,单位为人民币元,无法确认时返回 null」

Schema 只解决格式约束,字段语义仍要靠清晰的命名和描述

$defs 与 $ref

复杂 Schema 可以把重复结构放进 $defs,再使用 $ref 引用

它们很像先定义一个可复用的积木,再在多个位置写明「这里使用那块积木」,能避免相同结构被复制很多遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"$defs": {
"address": {
"type": "object",
"properties": {
"city": { "type": "string" },
"detail": { "type": "string" }
},
"required": ["city", "detail"],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"shippingAddress": { "$ref": "#/$defs/address" }
},
"required": ["shippingAddress"],
"additionalProperties": false
}

不同 AI 平台只支持 JSON Schema 的子集,oneOf、复杂条件、递归引用和部分 format 关键字尤其需要先查兼容范围

在 OpenAI 兼容接口中使用 JSON Schema

支持 Structured Outputs 的 Chat Completions 接口通常使用以下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"model": "MODEL_ID",
"messages": [
{
"role": "user",
"content": "从这句话提取订单:Layton 买了两杯咖啡,共 58 元"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "order",
"strict": true,
"schema": {
"type": "object",
"properties": {
"customer": { "type": "string" },
"product": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 },
"totalPrice": { "type": "number", "minimum": 0 }
},
"required": ["customer", "product", "quantity", "totalPrice"],
"additionalProperties": false
}
}
}
}

得到的结果应类似

1
2
3
4
5
6
{
"customer": "Layton",
"product": "咖啡",
"quantity": 2,
"totalPrice": 58
}

即使启用了严格模式,应用层仍要处理拒绝回答、内容安全拦截、输出被截断和接口错误,不能无条件相信解析一定成功

Tool Calling 与 JSON Schema 的关系

工具调用并不是模型真的执行了函数,而是模型返回一份「我想调用哪个工具,以及参数是什么」的结构化请求

模型更像负责填写申请单的人,真正查天气、写数据库或发送邮件的仍然是你的程序

这一区别非常重要,因为参数校验、权限判断和危险操作确认都必须由应用完成,不能因为请求来自模型就直接信任

一个天气工具可以这样定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city", "unit"],
"additionalProperties": false
}
}
}

完整流程是

  1. 应用把工具定义和用户问题一起发给模型
  2. 模型返回工具名称和符合 Schema 的参数
  3. 应用校验参数并执行真实函数
  4. 应用把工具结果连同调用 ID 返回给模型
  5. 模型根据结果生成最终回答

JSON Schema 在这里约束的是「函数入参」,结构化输出约束的则是「最终回答」,两者用途相似但所处阶段不同

System、User 与 Assistant 消息怎么分工

角色 放什么内容 不建议放什么
System / Instructions 身份、边界、格式、长期规则 每轮都会变化的临时问题
User 当前任务、业务数据、用户要求 服务端密钥和内部机密
Assistant 历史回答、工具调用 伪造的权威结论
Tool 程序真实执行结果 未经校验的用户指令

系统提示优先写稳定规则,动态数据放在用户消息或工具结果里,这样既容易缓存,也能降低用户输入覆盖系统规则的风险

可以把 System 看成长期有效的岗位说明书,User 是本次交办事项,Assistant 是模型的工作记录,Tool 则是外部系统送回的真实结果

不要把所有历史消息无限追加,长对话可以采用摘要、结构化状态或服务端会话能力控制上下文成本

推理参数:不要和输出长度混为一谈

推理模型通常还会提供 reasoning_effort、reasoning.effort、thinkingLevel、effort 或思考预算一类参数

这些参数控制模型在回答前投入多少推理资源,和最终答案写多长不是一回事

它更像决定模型在落笔前可以使用多少草稿纸,而最大输出 Token 决定最终答卷最多能写多长

场景 推理强度建议
分类、改写、简单抽取 低
常规代码、分析、工具选择 中
复杂调试、数学证明、长链路 Agent 高

提高推理强度通常意味着更高延迟和更多 Token 成本,不应默认拉满

Timeout、重试与并发

timeout 通常是 SDK 或 HTTP 客户端参数,不是模型生成参数

它控制的是应用愿意等多久,而不是要求模型在多少秒内思考完

生产环境建议区分三类超时

  • 连接超时,连接服务端花费的最长时间
  • 首字超时,等待第一个流式事件的最长时间
  • 总超时,整次生成允许占用的最长时间

遇到 429、部分 5xx 和短暂网络错误时,可以使用指数退避并加入随机抖动

例如将第一次重试延迟设为 1 秒,之后依次增加到 2 秒、4 秒、8 秒,并将最大延迟限制在 30 秒,再叠加一小段随机时间避免大量请求同时重试

不要盲目重试所有错误

  • 400 多半是参数或 Schema 错误,重试不会变好
  • 401 和 403 应检查密钥、权限和项目配置
  • 429 需要遵循速率限制及 Retry-After
  • 5xx 可以有限重试,并设置总重试预算

工具调用、写数据库、发送消息等有副作用的操作还要使用幂等键,避免请求超时后重复执行

Token、成本与上下文管理

一次调用的成本通常由输入 Token、输出 Token、缓存 Token、推理 Token以及工具费用共同组成

Token 可以粗略理解为模型读写文本时使用的计量单位,它不完全等于汉字数或单词数,因此准确成本应以接口返回的 usage 为准

几个实用习惯

  1. 不要把整份文档反复塞进每一次请求,优先检索相关片段
  2. 稳定且重复的系统提示放在前面,利用厂商的 Prompt Cache
  3. 先让小模型完成分类、路由和简单抽取,再把困难任务交给大模型
  4. 给输出设置合理上限,避免异常任务无限展开
  5. 记录模型、延迟、Token、完成原因、重试次数和请求 ID

只记录用户 Prompt 和最终答案远远不够,没有这些元数据,线上效果变差时很难知道是模型、参数、网络还是上下文发生了变化

一套稳妥的参数调优顺序

很多问题并不需要一上来就调 temperature

建议按下面的顺序排查

  1. 明确任务目标和成功标准
  2. 改进 System Prompt 与输入数据质量
  3. 使用 JSON Schema 或工具定义约束结构
  4. 确认模型是否适合当前任务
  5. 调整最大输出和推理强度
  6. 最后再单独实验 temperature 或 top_p

每次只改一个变量,并准备一组固定测试样本做回归评测

主观感受很容易被一两次漂亮回答欺骗,稳定的 AI 应用最终还是要靠数据集、指标和日志说话

推荐的生产请求模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"model": "MODEL_ID",
"messages": [
{
"role": "system",
"content": "回答准确、简洁,不确定时明确说明"
},
{
"role": "user",
"content": "USER_INPUT"
}
],
"max_completion_tokens": 1200,
"stream": false
}

生产环境还应在请求之外加入密钥管理、参数校验、超时、有限重试、并发限制、内容安全、链路追踪和成本告警,并在响应中检查完成原因、Token 用量和请求 ID

最后的参数建议

如果暂时不知道该怎么配,可以从这一组原则开始

  • 优先保留模型默认采样参数
  • 明确设置最大输出 Token
  • 需要机器消费结果时使用 JSON Schema,而不是只在 Prompt 里说「返回 JSON」
  • 需要外部数据或执行动作时使用 Tool Calling
  • 检查完成原因,不把截断结果当成功
  • 用评测集调参,不用单次回答调参
  • 所有参数都以具体模型的官方文档为准

AI API 的门槛很低,一个 fetch 就能得到回答

真正困难的部分,是把概率性的模型装进确定性的工程系统里,而参数、Schema、工具协议和可观测性,正是连接这两个世界的那层接口

参考资料