每一个参数背后都是一种权衡

先把后面要反复出现的词理清

这篇里很多参数的解释要用到下面这些概念,先一次性讲清楚,免得后面来回切窗口:

  • token:模型看到的最小单位。中文里常对应 1 个字或 1~2 个字,英文里常对应一个常用单词或前缀/后缀片段。例如 "hello" 是 1 个 token,"reasoning" 可能被切成 "reason" + "ing" 两个 token
  • tokenizer:把字符串切成 token 序列的工具,每个模型自带一套。OpenAI 系列用 tiktoken,开源模型多数自带在 HuggingFace 仓库里
  • logits:模型每生成一个 token 时输出的"原始打分"——一个长度等于词表大小的向量,每个位置是对应 token 的分数。分数越高,被选中的可能越大
  • softmax:把任意一组实数转换成"和为 1 的非负数"(也就是概率分布)的函数:softmax(x_i) = exp(x_i) / Σ exp(x_j)。它把 logits 变成实际可以拿来采样的概率
  • 采样(sampling):从概率分布里抽出一个 token 的过程。最简单的方式是按概率随机抽,复杂方式会先裁剪分布(top-k、top-p)再抽
  • SSE(Server-Sent Events):一种 HTTP 长连接协议,服务器可以持续往客户端推数据。流式输出(stream=True)就是基于它做的
  • JSON Schema:一种描述 JSON 结构的标准(字段类型、必填项、嵌套结构),让程序可以校验一个 JSON 是不是符合预期形状
  • Pydantic:Python 里最流行的数据校验库,用类型注解定义一个数据模型,自动生成 JSON Schema、做反序列化、报详细错误

下面所有参数的"控制什么"都会落到这几个概念上。

起点:一个最简调用里有哪些默认参数

新手第一次调 API 经常写成这样:

from openai import OpenAI

client = OpenAI(api_key="...", base_url="...")

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "你好"}],
)

只传 modelmessages 就能拿到回复。但 chat.completions.create 总共支持 20 多个参数,没传的部分都用默认值。这些默认值常常不是你真正想要的——比如 temperature=1 在抽取任务里会让结果不稳定,没设 max_tokens 时模型可能突然写出 3000 字的长回复,对应的 API 调用成本会显著上升。

下面把这些参数按它们实际控制模型的哪一步分组讲。

模型与上下文:写错就直接报错

model——指定调哪个后端模型。这是字符串完全匹配,DeepSeek 是 "deepseek-chat" / "deepseek-reasoner",OpenAI 是 "gpt-4o" / "gpt-4o-mini" / "o1" 等。写错不会 fallback,直接 404model not found

messages——对话历史的列表。每条消息是 {"role": ..., "content": ...},role 可以是 system(系统提示,定义模型角色)、user(用户输入)、assistant(模型之前的回复)、tool(工具调用结果,下面会讲)。模型把整个列表拼成一个长序列送进去,所以 messages 越长,每次调用计费越贵、首 token 延迟越高。

这两个参数前面主线讲过,不展开。下面是真正影响输出行为的部分。

采样:控制模型怎么从概率分布里挑下一个 token

模型每生成一个 token,本质上是一个三步过程:

  1. 前向传播得到 logits(一个长度 ≈ 50000~200000 的向量,词表多大就多长)
  2. 把 logits 过 softmax 得到概率分布
  3. 从分布里按规则采样一个 token

下面三个参数控制第 2 步和第 3 步。

temperature(OpenAI / DeepSeek 默认 1,范围因供应商而异)

控制采样的"锐度"。它把 logits 在 softmax 之前除以一个温度系数 T:

softmax(logits / T)

T 越小,logits 被放大,softmax 输出越接近 one-hot(one-hot 指只有一个位置接近 1、其余接近 0 的向量),模型几乎总是选概率最高的那个 token——输出确定但单调。T 越大,logits 被压平,分布更接近均匀,低概率 token 也有被选中的机会——输出更发散但容易偏离语义。

数学上 T 还有两个极限:

  • T → 0+——分布退化成 one-hot,等同于直接 argmax(贪心选最高分 token)。T = 0 在实现里通常被特例处理(除以 0 会爆),框架内部直接走贪心
  • T → ∞——分布变成完全均匀,每个 token 概率都是 1/V(V 是词表大小),输出接近随机字符流

直观对应:

  • temperature=0——每次都贪心选最高概率 token(贪心指每一步都选当前最优,不考虑长远)。同一输入基本是同一输出
  • temperature=1——按模型输出的原始概率分布采样,不做缩放
  • temperature>1——拉平分布,低概率 token 选中机会上升

按业务场景的取值经验:

  • 信息抽取、分类、Function Calling:设 0(要求确定性)
  • 代码生成:00.3
  • 日常对话、摘要:0.50.7
  • 创意写作、头脑风暴:0.81.2
  • >1.5 实战中很少用,输出容易偏离语义

各家 API 的实际允许范围并不统一。常见服务商对比:

服务商temperature 范围默认值备注
OpenAI(GPT-3.5 / 4 / 4o)0 ~ 21超 2 直接返回 400 Bad Request
DeepSeek0 ~ 21完全兼容 OpenAI 规范
Qwen / 通义千问0 ~ 21DashScope 也是这个范围
Google Gemini(1.5+ / 2.x)0 ~ 21.0
Mistral La Plateforme0 ~ 1.50.7上限略低
Anthropic Claude0 ~ 11.0设计上拒绝 >1,Anthropic 认为高温对实际应用没意义
OpenAI 推理模型(o1 / o3 系列)不支持推理模型有自己的内部采样策略,外部传 temperature 会报错
HuggingFace transformers(自托管)任意正数1.0研究场景可设到 5、10,但输出基本是噪声

跨供应商切换时要小心——同一段调 OpenAI 的 temperature=1.2 代码搬到 Claude 上会报 temperature must be between 0 and 1,必须按目标 API 的上限做兼容。

temperature=0 在某些实现里仍有微小随机性(GPU 浮点不严格确定),如果完全要可复现,配合下面的 seed,并记录返回的 system_fingerprint(只要 fingerprint 没变,同 seed + 同输入产出基本一致;fingerprint 变了说明后端模型迭代了,结果会变化)。

top_p(0~1,默认 1)

另一种裁剪分布的方式,叫 nucleus sampling(核采样)。top_p=0.9 的意思是:

先把所有 token 按概率从高到低排序,取累积概率达到 0.9 的最小集合,只在这个集合里采样。

也就是说,低概率的"长尾"token 直接被砍掉,只在"高概率核"里采样。

它跟 temperature 是两个独立维度——temperature 调整分布的形状,top_p 切掉分布的尾巴。两者可以同时用,但工程上调其中一个即可。OpenAI 官方文档也建议二选一。多数情况调 temperaturetop_p 留默认 1。

seed(整数,可选)

固定采样的随机种子。同样输入 + 同样 seed 期望产生同样输出。

它不是"严格保证"——GPU 上的浮点运算非确定性、模型版本更新、底层 kernel 优化都可能让同一 seed 跑出微小差异。它的真实定位是"尽力而为的可复现",对调试和单元测试有用,对生产端的强一致性需求别依赖它。

返回结果里会带 system_fingerprint 字段,只要 fingerprint 不变,相同 seed 输出基本一致;fingerprint 变了(后端模型版本更新)意味着结果会变,要重新验证。

长度控制:限制模型最多生成多少

max_tokens / max_completion_tokens

限制单次调用输出的 token 数上限。默认值通常很大(几千到一两万),不设的话有两个风险:

  • 模型被诱导写超长内容,导致单次调用成本失控
  • 触达模型上下文上限被强制截断在奇怪位置

工程上建议总是设一个合理上限。例如抽取任务设 max_tokens=500 通常足够,对话设 2000,长文摘要设 4000

注意:

  • 它限制的是 输出,不是输入。输入太长要看模型的 max_position_embeddings(一般和"context window"对应)
  • 如果输出真的被截断,最后一个 token 可能是不完整的中文字符(utf-8 多字节没切完整)或半句话。要在客户端加防御逻辑——例如检查 finish_reason == "length" 时再追加一次调用让模型续写
  • 较新的 OpenAI o 系列把参数名改成了 max_completion_tokens,语义一样。原因是 o 系列内部有"思考 token",原 max_tokens 的含义模糊了

流式:把输出按 token 推回客户端

stream(布尔,默认 False)

打开后服务端会持续推送 token,而不是等全部生成完再一次性返回。底层是 HTTP 长连接 + SSE 协议。

收到的是一连串 chunk,每个 chunk 含一小段新 token:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "讲个故事"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

关键事实:流式和非流式在计费上完全一致——区别只在用户感知到的延迟和可中断性。生产里如果是聊天界面建议开流式,首字延迟从几秒降到几百毫秒。

stream_options(对象,可选)

最常用的是 {"include_usage": True}——让流式模式下最后一个 chunk 携带 token 用量统计。默认流式不返回 usage,做成本监控时建议加上:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    stream=True,
    stream_options={"include_usage": True},
)

usage = None
for chunk in stream:
    if chunk.usage:
        usage = chunk.usage   # 最后一个 chunk 才有
    delta = chunk.choices[0].delta.content if chunk.choices else None
    if delta:
        print(delta, end="", flush=True)

print(f"\n[usage] input={usage.prompt_tokens} output={usage.completion_tokens}")

结构化输出:强制 JSON / 强制 schema

response_format

让模型输出符合特定结构而不是自由文本。

  • {"type": "text"}——默认,纯文本
  • {"type": "json_object"}——保证输出是合法 JSON。但不保证字段结构正确,只是"语法上 parse 得过"。Prompt 里必须出现 "JSON" 这个词,否则模型可能不知道要走 JSON 模式
  • {"type": "json_schema", "json_schema": {...}}(新版 OpenAI 支持)——按提供的 JSON Schema 严格生成。OpenAI 的 SDK 还可以直接传 Pydantic 模型,由 SDK 自动转 schema:
from pydantic import BaseModel
from openai import OpenAI

class Person(BaseModel):
    name: str
    age: int
    email: str | None

client = OpenAI()
resp = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "提取人物信息"},
        {"role": "user", "content": "张三今年 28 岁,邮箱 [email protected]"},
    ],
    response_format=Person,
)
person = resp.choices[0].message.parsed
print(person.name, person.age)   # 张三 28

.parse 是 SDK 的便捷方法,会自动把模型输出反序列化成 Pydantic 实例。出错(不符合 schema)时抛异常。

非 OpenAI 后端(DeepSeek 等)目前多数只支持 json_object,不支持 json_schema。要做严格 schema 校验只能在客户端用 instructor 库做重试 + 验证。

工具调用:让模型决定调哪个函数

tools

把可以被模型调用的函数列表传进去。每个函数声明遵循 JSON Schema:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询某个城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名"},
                },
                "required": ["city"],
            },
        },
    },
]

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "北京现在天气怎么样?"}],
    tools=tools,
)

msg = resp.choices[0].message
if msg.tool_calls:
    call = msg.tool_calls[0]
    print(call.function.name)         # get_weather
    print(call.function.arguments)    # {"city": "北京"}

模型决定调哪个工具、用什么参数。客户端拿到 tool_calls 后实际执行函数,再把结果作为 {"role": "tool", ...} 消息追加回去继续对话。第 07 篇主线展开了完整流程。

tool_choice

控制模型对工具的调用策略:

  • "auto"(默认)——模型自己决定是否调用工具
  • "none"——禁止调用任何工具,强制走纯文本回答
  • "required"——必须调用某个工具
  • {"type": "function", "function": {"name": "xxx"}}——强制调用指定的某个函数

强制某工具的典型场景:让模型每次必须先调用"意图分类工具",根据返回再决定下一步——把 LLM 当作分类器使用。

惩罚与偏置:调整 token 的出现频率

frequency_penalty(-2~2,默认 0)

已经在输出里出现过的 token 按出现次数施加惩罚。每出现一次就累加一次惩罚到 logits 上。正值让模型避免重复(防止"非常非常非常..."这种),负值鼓励重复。

presence_penalty(-2~2,默认 0)

类似 frequency_penalty不按次数累加,只看是否出现过。正值鼓励引入新话题,负值让模型专注已有话题。

工程上:绝大多数场景这两个都留 0。它们对输出的影响微妙、不直观,调整不当反而比默认更差。要防重复优先改 Prompt("避免重复用词")或降 temperature;要让回答更发散直接调 temperature。

logit_bias(字典,可选)

对特定 token 的 logit 加偏置,强制提升或压制某些 token 被生成的概率:

# 假设我们用 tiktoken 拿到了 "抱歉" 和 "对不起" 的 token id
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
ban_ids = enc.encode("抱歉") + enc.encode("对不起")

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "讲一下今天的新闻"}],
    logit_bias={str(tid): -100 for tid in ban_ids},   # -100 约等于禁止
)

+100 约等于强制选中,-100 约等于禁止生成。中间值(±1 到 ±20)是软调节。

实战中很少用——前提是你能拿到 token id(多 token 词要拆开),而且不同模型 tokenizer 不一样、id 各不通用。它的定位是"Prompt 调不动时的最后手段"。

调试用的低层参数

logprobs / top_logprobs

让 API 返回每个生成 token 的对数概率(logprob 是 log probability 的缩写:log(p),p 是概率。因为概率经常很小,取对数后数值更稳定)。

top_logprobs=N 进一步返回每一步前 N 个候选 token 及其概率,能看到"模型当时还考虑过哪些选项":

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "今天是星期"}],
    logprobs=True,
    top_logprobs=5,
    max_tokens=3,
)

for token_info in resp.choices[0].logprobs.content:
    print(f"实际生成: {token_info.token!r}  logprob={token_info.logprob:.3f}")
    for alt in token_info.top_logprobs:
        prob = math.exp(alt.logprob) * 100
        print(f"  候选 {alt.token!r}  {prob:.1f}%")

实战价值:

  • 置信度估计:对分类任务,看模型选出 "yes" 的概率有多高(80% 还是 51%)
  • 理解模型为什么这么输出:debug 输出"不对劲"时看候选有没有合理的
  • 模型对比研究:同样 prompt 在不同模型上看分布形状

非研究/调试场景几乎不用,因为它增加返回数据量、还要客户端处理。

n(整数,默认 1)

一次请求让模型生成 N 个独立的回答(采样多次)。常用于"多路采样取最好"——把同一 prompt 跑 5 次,让另一个模型或评分函数挑最优的那个。

注意:输出 token 计费是 N 倍(输入只算 1 倍,因为只发一次)。推理模型(o 系列)不支持 n

stop(字符串或字符串列表)

命中这些字符串时立即停止生成。例:

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "列举 3 个 Python web 框架,每个一行,结束用 END。"}],
    stop=["END"],
)

最多支持 4 个 stop 字符串。

推理模型(o 系列 / DeepSeek-R1)的特殊行为

新一代推理模型在采样和参数上跟普通 chat 模型有几个明显不同:

  • 不支持 temperature / top_p / frequency_penalty / presence_penalty / logprobs——推理模型有自己的内部采样策略,外部参数无效或报错
  • 不支持 n——同上,跟内部多路推理冲突
  • 新增 reasoning_effort"low" / "medium" / "high")——控制模型内部思考链的深度。高档位会推理更久、更准、更贵。简单任务用 low 省钱
  • 输出会包含 reasoning_content 字段(思考过程),但通常不计入最终回复给用户的部分

如果代码里给 o 系列传了 temperature,会直接报错而不是被忽略。生产里如果同时支持普通 chat 模型和推理模型,要做模型判断分别构造参数。

把参数串成场景化的 preset

参数太多,每个调用都从头选很容易不一致。工程上把参数组合按场景固化成 preset 是更稳的做法:

PRESETS = {
    "extract": {
        "temperature": 0,
        "max_tokens": 800,
        "response_format": {"type": "json_object"},
    },
    "chat": {
        "temperature": 0.7,
        "max_tokens": 2000,
        "stream": True,
        "stream_options": {"include_usage": True},
    },
    "creative": {
        "temperature": 1.0,
        "max_tokens": 3000,
        "stream": True,
    },
    "code": {
        "temperature": 0.2,
        "max_tokens": 4000,
    },
    "classify": {
        "temperature": 0,
        "max_tokens": 50,
        "logprobs": True,        # 拿到分类置信度
        "top_logprobs": 5,
    },
}

def chat(messages, scenario: str = "chat", **overrides):
    """按场景挑参数组合,允许临时 override"""
    params = {**PRESETS[scenario], **overrides}
    return client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        **params,
    )

# 用法
chat([{"role": "user", "content": "提取这段里的人名"}], scenario="extract")
chat([{"role": "user", "content": "写首关于秋天的诗"}], scenario="creative")

好处:业务代码里不再到处零散调参数,参数集中管理;调优时改一处影响所有同场景调用。

一个常见误解:参数能完全控制行为吗

不能。参数能调的是采样策略输出长度/格式约束,但模型本身的"判断、推理、知识"不在这里。看到回答不好就调 temperature 是新手常犯的错——多数时候问题在 Prompt、上下文、模型选择,跟参数无关。

排查顺序应该是:

  • 输出乱码 / 不合 JSON 格式 → 检查 response_format 和 Prompt
  • 同样输入结果不稳 → 调 temperature 到 0 + 设 seed
  • 输出太长被截断 → 调 max_tokens 或改 Prompt 让模型简洁
  • 输出太短或答非所问 → 改 Prompt、换模型、加 few-shot 示例
  • 工具调用不准 → 检查 tool description、tool_choice、模型支持度
  • 重复词太多 → 改 Prompt("避免重复");仍未改善再考虑 frequency_penalty

参数是工程上的最后一公里调节,不是模型能力本身的开关。

相关阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:番外 2:API 参数全解,temperature 到 logprobs

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外02-API参数详解/

本文最后一次更新为 天前,文章中的某些内容可能已过时!