调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 返回的模型信息中判断
因此最稳妥的检查顺序是
- 在模型对照页确认模型 ID、输入模态、上下文长度与功能支持
- 在对应端点的 API Reference 中确认字段名称、类型和取值范围
- 查看模型说明、弃用公告和迁移文档,确认该参数有没有模型级限制
- 上线前用目标模型发送一条最小请求,不要直接把其他模型的参数配置原样复制过来
一次 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 | { |
发送请求时还需要在 Header 中设置 API Key 和 Content-Type: application/json,切换兼容平台时通常只需要更换请求地址、密钥和模型 ID
三种主流请求结构
请求发出去之后,三家接口都会返回「模型生成的内容 + 停止原因 + Token 用量 + 请求元数据」
真正需要业务代码读取的,通常不只是回答文字,还包括回答是否完整、有没有触发工具、是否被安全策略拦截,以及本次调用消耗了多少 Token
OpenAI Chat Completions 与兼容接口
大量 OpenAI 兼容接口仍使用 Chat Completions 结构,也就是用 messages 数组承载上下文,每条消息包含 role 和 content
1 | { |
很多国内外平台都兼容这套格式,但「兼容」不代表所有参数都实现一致,尤其是推理参数、结构化输出和工具调用,迁移前仍要查看模型自己的能力表
对应的 Response Body
前面的 OpenAI Chat Completions Payload 通常会得到类似下面的 Response Body
1 | { |
| 字段 | 作用 |
|---|---|
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 | { |
工具定义使用 input_schema,模型返回 tool_use 内容块,应用执行函数后再把 tool_result 送回模型
对应的 Response Body
Claude 的 content 是内容块数组,而不是一个固定字符串
1 | { |
| 字段 | 作用 |
|---|---|
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 | { |
此时应用应读取 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 | { |
命名虽然不同,但本质仍然是「上下文 + 生成配置 + 输出约束」
这些 Payload 用于展示字段位置,并不代表所有模型都接受相同取值,部分新一代推理模型要求保留默认采样参数,调用前仍要查看具体模型文档
对应的 Response Body
Gemini 将候选回答放在 candidates 中,文字仍然位于 content.parts 数组
1 | { |
| 字段 | 作用 |
|---|---|
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 |
无论使用哪一家,都建议把处理逻辑分成四步
- 确认候选或内容块确实存在
- 根据内容块类型提取文本或处理工具调用
- 检查停止原因,识别截断、安全拦截和工具调用
- 记录请求 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
最大输出参数限制的是生成上限,不是保证模型一定写到这个长度
它更像给回答准备一个最大尺寸的纸箱,而不是要求模型必须把纸箱装满
箱子太小,内容可能写到一半被截断;箱子很大,也不代表模型一定会生成很长的回答
需要注意三个问题
- 输入 Token 和输出 Token 都会占用上下文窗口
- 推理模型可能还会消耗不可见或单独计量的推理 Token
- 输出达到上限时可能停在半句话、半段代码甚至半个 JSON 上
因此生产环境不能只解析正文,还要检查 finish_reason、stop_reason 或对应的完成状态
stop
stop 可以是一个或多个停止序列,例如
它相当于提前告诉模型「看到这个标记就停笔」,适合输出边界非常明确的协议
1 | { |
适合固定协议、少样本模板和分隔符明确的生成任务
停止序列通常不会包含在最终结果里,也不要选择正文中可能频繁出现的普通词,否则模型会提前结束
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 | { |
固定为 seed: 2026 后,多次请求更有机会得到相同或相近的文案,这对于自动化评测很有帮助:当你只修改 System Prompt 时,可以尽量排除随机抽样带来的干扰,更容易判断新 Prompt 到底有没有变好
模型版本、服务端实现、并行计算和系统指纹发生变化时,同一个 seed 仍可能得到不同结果
因此 seed 适合做实验对比和问题复现,不适合用来保证合同、账单等业务结果绝对一致
n 或 candidateCount
一次生成多个候选,适合创意筛选和离线评测,但成本也会接近按候选数增长
例如 n = 3 可以理解为让模型一次交三份答案供你挑选,但 Token 消耗也通常不再只是一份
1 | { |
这次请求可能返回三个候选
- 拾光地图
- 山海相册
- 旅迹
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 | { |
实际开发中有什么用
- 观察模型是否犹豫:对比某个位置的候选差距,辅助决定是否需要人工复核
- 分类与多选题评测:限制模型只回答固定标签,再比较各标签 Token 的概率
- 调试 Prompt:观察修改提示词前后,目标答案的概率是否稳定提高
- 分析生成过程:查看模型在关键位置还考虑过哪些表达
需要注意,Token 的选词概率不是答案的事实正确率
模型可能以很高概率复述一条错误常识,也可能因为人名生僻而用较低概率给出正确答案,所以不能简单规定「低于 -5 就是幻觉」
更稳妥的做法是把 logprobs 当成风险信号之一,再结合检索结果、规则校验、模型评测和人工复核共同判断
中文词语还可能被拆成一个或多个 Token,分析「北京」时不能想当然地认为它一定对应单个 Token
logit_bias
按 Token ID 调整某些候选的生成倾向,适合少量硬约束实验,但 Token 切分比肉眼看到的单词复杂,很容易误伤空格、词缀和不同大小写
它像是在模型的候选词表上给某些词加分或扣分,控制力很细,但也因此更难正确使用
假设一个客服分类接口只能回答「退款」或「咨询」,但模型偶尔会先写一句解释,可以尝试提高目标标签对应 Token 的权重,并降低无关 Token 的权重
1 | { |
正值会提高对应 Token 被选中的倾向,负值会降低倾向,接近 -100 的值在部分 OpenAI 风格接口里常用于近似禁止某个 Token,但具体范围仍要以接口文档为准
难点在于 logit_bias 操作的是 Token ID 而不是肉眼看到的完整词语
例如「退款」、前面带空格的「 退款」和不同语言中的同义词可能对应完全不同的 Token,屏蔽一个 ID 并不代表相关表达都会被屏蔽,甚至还可能影响包含该 Token 的其他词
如果目标是限制业务枚举值,JSON Schema 的 enum 往往更可靠
因此 logit_bias 更适合实验和细粒度微调,不应被当成内容安全、敏感词过滤或业务权限控制机制
Streaming:为什么聊天界面能一个字一个字出现
设置 stream: true 后,服务端通常通过 SSE 或类似机制持续返回事件
普通请求像等服务员把整桌菜全部上齐后再开门,流式请求则像菜做好一道就先端一道
因此用户能更早看到第一个字,等待感会明显减轻,但厨房完成整桌菜所需的总时间未必会缩短
1 | { |
真实项目里还需要处理事件边界、增量 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 | { |
type
type 声明数据类型,最常用的有
它解决的是「这个位置究竟应该装什么」的问题,例如价格应当是数字,而不是看起来像数字的字符串
| JSON Schema 类型 | JavaScript 中通常对应 |
|---|---|
object |
普通对象 |
array |
数组 |
string |
字符串 |
number |
任意数字 |
integer |
整数 |
boolean |
布尔值 |
null |
null |
只写 properties 而不写 type: "object" 并不严谨,因为对象专属关键字不会自动把数据类型锁定为对象
properties
properties 定义对象允许出现的字段及各字段约束
可以把它看成对象的字段目录,每一个键都在说明一个字段叫什么、里面允许放什么
它本身不会让字段变成必填,是否必填要由 required 决定
required
required 是必填字段名称数组
需要特别注意,字段出现在 properties 里只代表它被认识,并不代表它必须出现,只有被列入 required 才算必填
1 | { |
在严格结构化输出中,常见做法是把所有业务字段都列入 required,可选值通过联合类型表达,例如
1 | { |
这表示字段必须存在,但值可以是字符串或 null
additionalProperties
默认情况下,JSON Schema 允许出现未在 properties 中声明的字段
这就像一张表格虽然列出了姓名和价格,却没有明确禁止填写其他备注,模型仍可能主动增加你没有设计的字段
1 | { |
这行会关闭额外字段,是 AI 结构化输出里非常重要的约束,也能减少模型自作主张增加解释字段
enum 与 const
enum 把值限制在有限集合中
它类似网页里的下拉选择框,只能从提前准备好的选项中选择,特别适合订单状态、语言代码和分类标签
1 | { |
const 则要求值只能是一个固定值,适合协议版本或事件类型
1 | { |
items
数组中的每一项由 items 约束
数组像一排储物格,items 规定每个格子里应该放同一种什么结构的数据
1 | { |
还可以使用 minItems、maxItems 控制数组长度
description
description 不参与传统 JSON 校验,但对模型理解字段非常重要
如果说其他关键字负责检查格式,description 就负责把业务常识讲给模型听
不要只写「价格」,最好写清单位、来源和缺失时如何处理,例如「订单含税总价,单位为人民币元,无法确认时返回 null」
Schema 只解决格式约束,字段语义仍要靠清晰的命名和描述
$defs 与 $ref
复杂 Schema 可以把重复结构放进 $defs,再使用 $ref 引用
它们很像先定义一个可复用的积木,再在多个位置写明「这里使用那块积木」,能避免相同结构被复制很多遍
1 | { |
不同 AI 平台只支持 JSON Schema 的子集,oneOf、复杂条件、递归引用和部分 format 关键字尤其需要先查兼容范围
在 OpenAI 兼容接口中使用 JSON Schema
支持 Structured Outputs 的 Chat Completions 接口通常使用以下结构
1 | { |
得到的结果应类似
1 | { |
即使启用了严格模式,应用层仍要处理拒绝回答、内容安全拦截、输出被截断和接口错误,不能无条件相信解析一定成功
Tool Calling 与 JSON Schema 的关系
工具调用并不是模型真的执行了函数,而是模型返回一份「我想调用哪个工具,以及参数是什么」的结构化请求
模型更像负责填写申请单的人,真正查天气、写数据库或发送邮件的仍然是你的程序
这一区别非常重要,因为参数校验、权限判断和危险操作确认都必须由应用完成,不能因为请求来自模型就直接信任
一个天气工具可以这样定义
1 | { |
完整流程是
- 应用把工具定义和用户问题一起发给模型
- 模型返回工具名称和符合 Schema 的参数
- 应用校验参数并执行真实函数
- 应用把工具结果连同调用 ID 返回给模型
- 模型根据结果生成最终回答
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-After5xx可以有限重试,并设置总重试预算
工具调用、写数据库、发送消息等有副作用的操作还要使用幂等键,避免请求超时后重复执行
Token、成本与上下文管理
一次调用的成本通常由输入 Token、输出 Token、缓存 Token、推理 Token以及工具费用共同组成
Token 可以粗略理解为模型读写文本时使用的计量单位,它不完全等于汉字数或单词数,因此准确成本应以接口返回的 usage 为准
几个实用习惯
- 不要把整份文档反复塞进每一次请求,优先检索相关片段
- 稳定且重复的系统提示放在前面,利用厂商的 Prompt Cache
- 先让小模型完成分类、路由和简单抽取,再把困难任务交给大模型
- 给输出设置合理上限,避免异常任务无限展开
- 记录模型、延迟、Token、完成原因、重试次数和请求 ID
只记录用户 Prompt 和最终答案远远不够,没有这些元数据,线上效果变差时很难知道是模型、参数、网络还是上下文发生了变化
一套稳妥的参数调优顺序
很多问题并不需要一上来就调 temperature
建议按下面的顺序排查
- 明确任务目标和成功标准
- 改进 System Prompt 与输入数据质量
- 使用 JSON Schema 或工具定义约束结构
- 确认模型是否适合当前任务
- 调整最大输出和推理强度
- 最后再单独实验
temperature或top_p
每次只改一个变量,并准备一组固定测试样本做回归评测
主观感受很容易被一两次漂亮回答欺骗,稳定的 AI 应用最终还是要靠数据集、指标和日志说话
推荐的生产请求模板
1 | { |
生产环境还应在请求之外加入密钥管理、参数校验、超时、有限重试、并发限制、内容安全、链路追踪和成本告警,并在响应中检查完成原因、Token 用量和请求 ID
最后的参数建议
如果暂时不知道该怎么配,可以从这一组原则开始
- 优先保留模型默认采样参数
- 明确设置最大输出 Token
- 需要机器消费结果时使用 JSON Schema,而不是只在 Prompt 里说「返回 JSON」
- 需要外部数据或执行动作时使用 Tool Calling
- 检查完成原因,不把截断结果当成功
- 用评测集调参,不用单次回答调参
- 所有参数都以具体模型的官方文档为准
AI API 的门槛很低,一个 fetch 就能得到回答
真正困难的部分,是把概率性的模型装进确定性的工程系统里,而参数、Schema、工具协议和可观测性,正是连接这两个世界的那层接口


