codecamp

Grok 模型 ID 写法与版本陷阱

把一个「能跑」的请求交给同事接手,最常见的交接方式是「照抄我那段代码,模型名别改」。抄的人照做了,请求也通了。三个月后,同一份代码开始给出不一样的账单,回答风格也变了——代码一行没动,唯一变的是模型字符串背后指向的东西。

这一节只解决一个决策:模型名到底该写死到什么程度。

一、能跑通和可复现是两件事

官方模型页把模型写法分成三档,这不是语法糖,而是三种不同的「未来行为」:

写法 官方给出的行为 适合谁 代价
<modelname> 指向该系列当前稳定版 个人项目、试用 行为与单价随发布漂移
<modelname>-latest 指向该系列当前版本 愿意跟进并自测的团队 漂移更快,回归压力在你这
<modelname>-<date> 钉住某一次发布 线上系统、需要复现结论的场景 新版本能力要手动升

这里有一个极容易读漏的前提:官方原文写的是「部分模型有别名」(Some models have aliases),并且只对 -<date> 这一档承诺「不再更新」。也就是说,三档里只有带日期的写法能兑现可复现;<modelname> 与 -latest 都会随发布移动。

多数人踩的坑不是「用了别名」,而是不知道自己用了别名。教程里写 grok-4,读者抄过来,抄的那一刻它指向某一版;官方发布新版本之后,同一行代码指向另一版。而价格表是按模型分档的,模型换了,单价与上下文窗口会一起换——你以为什么都没改,其实换了个模型在跑。

更需要注意的是:grok-4.7 这种「官方列表里的 ID」并不自动等于钉住版本。它在形态上属于 <modelname> 那一档,该系列是否设了别名、会不会自动迁移,是逐系列的具体事实,不能从命名规则推出来。想拿到「不再更新」的承诺,要用带日期段的发布 ID,例如 grok-4.20-0309-reasoning。

还有一类更安静的变化。官方模型页写明:grok-4.20 及更新的模型不支持 logprobs 与 top_logprobs,设置了也会被静默忽略。如果你的功能依赖这两个字段,升级之后不会报错,只会拿到空数据。这类「能跑通但跑错」比直接失败更难查。

官方对多数用户的建议其实是用别名,好处是自动获得后续能力。本书的取向相反,是因为主线项目要对外承诺成本与行为一致性:一旦账单可复现比「省心」更重要,取舍就会反过来。两种取向都对,差别在约束条件。

二、先给模型字符串定性

下面这个分类器不联网,只做一件事:把项目里出现的模型字符串按官方规则归到五类,并给出该做的动作。

"""模型 ID 分类器:不联网,按官方别名规则判断一个字符串的「可复现性」。"""

import re
from dataclasses import dataclass

## 官方 models 页当前列出的文本模型(核验 2026-11-04);复核时只改这一处
KNOWN_CONTEXT = {
    "grok-4.7": 500_000,
    "grok-4.6": 500_000,
    "grok-4.5": 500_000,
    "grok-4.3": 1_000_000,
    "grok-4.20-0309-reasoning": 1_000_000,
    "grok-4.20-0309-non-reasoning": 1_000_000,
    "grok-4.20-multi-agent-0309": 1_000_000,
    "grok-build-0.1": 256_000,
}

## 官方只有 <modelname>-<date> 承诺「不再更新」;这里按四位日期段识别
DATE_SHAPE = re.compile(r"-\d{4}(?:-|$)")
## 形如 grok-4 的写法:官方定义了别名机制,但某个具体别名是否存在要另查
ALIAS_SHAPE = re.compile(r"^grok-\d+(\.\d+)?$")


@dataclass
class Verdict:
    model_id: str
    kind: str  # pinned / series / float / alias_likely / unknown
    context: int  # 0 表示本次未核到
    action: str


def classify(model_id: str) -> Verdict:
    if model_id in KNOWN_CONTEXT:
        if DATE_SHAPE.search(model_id):
            # 带日期段:官方写明不再更新,是唯一能兑现可复现的一档
            return Verdict(model_id, "pinned", KNOWN_CONTEXT[model_id],
                           "钉住某次发布,升级时显式换 ID")
        # 在列表里但不含日期段:当前可用,是否随别名漂移要逐系列确认
        return Verdict(model_id, "series", KNOWN_CONTEXT[model_id],
                       "当前可用;若该系列设了别名会漂移,须配回归")
    if model_id.endswith("-latest"):
        # 官方别名规则里的浮动档:行为随发布变化
        return Verdict(model_id, "float", 0,
                       "换 pinned ID,或接受漂移并补回归用例")
    if ALIAS_SHAPE.match(model_id):
        # 只说明它「像」系列别名,不替官方断言该别名存在
        return Verdict(model_id, "alias_likely", 0,
                       "到官方 models 页确认该别名,线上改 pinned ID")
    return Verdict(model_id, "unknown", 0,
                   "对照官方 models 页当前列表,别按记忆改拼写")


DEMO = ["grok-4.7", "grok-4.20-0309-reasoning", "grok-4.7-latest",
        "grok-4", "grok-4.5-turbo"]

if __name__ == "__main__":
    print("| 模型字符串 | 类型 | 上下文 | 动作 |")
    print("| --- | --- | --- | --- |")
    for item in DEMO:
        v = classify(item)
        ctx = f"{v.context:,}" if v.context else "未核到"
        print(f"| `{v.model_id}` | {v.kind} | {ctx} | {v.action} |")

KNOWN_CONTEXT 是从官方模型页抄录的当前列表,核验日期 2026-11-04。它有两用:判断这个 ID 现在还在不在,以及给后续容量估算提供上下文窗口。复核时只改这一处常量,别把模型名散落到业务代码里。

DATE_SHAPE 与 ALIAS_SHAPE 都只判「形状」。官方说明了别名机制的存在,但某个具体别名是否在售、某个系列有没有设别名,属于列表事实,不是命名规则能推出来的——所以分类器给出的是动作,不是结论。

三、五类字符串的处置口径

五类的处置逻辑不一样:

类型 含义 处置
pinned 在列表中且带日期段 可复现;升级当变更事件处理
series 在列表中、不含日期段 承认它可能浮动,配回归用例
float 以 -latest 结尾 换 pinned ID,或接受漂移并加回归
alias_likely 形如 grok-4,像系列别名 先确认存在性,再决定要不要留
unknown 既不在列表也不像别名 按拼写错或已下架处理

三点值得展开:

  • 别名的代价是双份的。它换来「自动跟进」,代价是行为与单价同时变。内部工具无所谓,它换来的正是省心;但一个要对外报价的系统就不能接受,因为你的成本函数会跟着官方的发布节奏抖动。这也是本书把 series 单独列一类、而不是一律判「可用」的原因。
  • unknown 不等于写错。模型可能刚发布还没进你的常量表,也可能已经下架。把它单独列一类,是为了逼你回去对表,而不是凭记忆改拼写——凭记忆改拼写最常见的产物是 grok-4.5-turbo 这种查不到的名字,或是把 grok-4.20 写成 grok-4-20 这类分隔符错误。
  • 形状判断不能替代对表。grok-4.20-0309-reasoning 带日期段,能拿到「不再更新」的承诺;grok-4.7 形状上只是系列名。两者都在官方列表里,但可复现性不是一回事。

四、预期输出与异常分支

运行 __main__,五组样本应输出:

| 模型字符串 | 类型 | 上下文 | 动作 |
| --- | --- | --- | --- |
| `grok-4.7` | series | 500,000 | 当前可用;若该系列设了别名会漂移,须配回归 |
| `grok-4.20-0309-reasoning` | pinned | 1,000,000 | 钉住某次发布,升级时显式换 ID |
| `grok-4.7-latest` | float | 未核到 | 换 pinned ID,或接受漂移并补回归用例 |
| `grok-4` | alias_likely | 未核到 | 到官方 models 页确认该别名,线上改 pinned ID |
| `grok-4.5-turbo` | unknown | 未核到 | 对照官方 models 页当前列表,别按记忆改拼写 |

真实项目里的失败分支,通常不是分类器报错,而是分类器跑出了你没预期的类型:

现象 原因 处理
升级后回答风格变了 用了浮动别名或会漂移的系列 ID 换 pinned ID,并留一条回归用例
账单比预估高一截 别名指向了另一个模型,单价随模型变 把模型名写进每条账目(见 5.1)
logprobs 有字段但为空 该模型不支持,被静默忽略 升级前查模型页的限制说明
分类器一半是 unknown 常量表没跟上官方列表 定 3 个月复核,或变更时触发

五、判据卡

判断项 该这么做 不该这么做
线上要可复现 用带日期段的发布 ID 假定列表里的系列 ID 钉住了版本
用系列 ID 承认可能漂移,配回归用例 抄一次模型名用一整年
升级模型 显式改 ID,跑一遍回归 等别名自动变好
见到陌生 ID 对官方模型页当前列表 按记忆改拼写或猜后缀
依赖 logprobs 升级前确认该模型是否支持 假定字段总会有值
复核节奏 官方页变动或每 3 个月对表 把核验日期当长期有效

这张卡约束的是升级动作,不是某个 SDK 的写法。换语言、换封装,几条都成立;它唯一不成立的地方是你在做一次性的离线实验——那里用别名反而更省事。

六、来源与核验

  • 官方模型页:https://docs.x.ai/developers/models(核验日期 2026-11-04)。三档别名规则、「部分模型才有别名」的前提、当前文本模型与上下文窗口、logprobs 的静默忽略行为,均来自该页的 Model Aliases 与 Key Information 段。
  • 官方发布说明:https://docs.x.ai/developers/release-notes。模型更迭节奏以该页为准,本节的核验日期不代表长期有效。
  • 旧标识符重定向、点号与连字符写法差异属于第三方说法,本次未在官方页核到,正文不作事实陈述;模型名一律以官方页当前列表为准,价格与可用性以官方定价页和开发者控制台当前显示为准。

七、动手试

  1. 把 KNOWN_CONTEXT 换成你项目实际用到的模型,跑一遍分类器,统计有多少条落在 series / float / alias_likely。若一条 pinned 都没有,说明这个系统目前没有可复现的模型基线。
  2. 给 classify() 加一条 retired 分支:输入一个已知退役的 ID 时返回「不可用,需迁移」。想一想,「弃用」和「已退役」该不该共用一个分支?两者的动作差别在哪?
  3. 如果团队坚持用 -latest 换取自动跟进,你会用什么最小成本的手段,让「行为变了」这件事在 CI 里被发现?把答案写成一句不超过 40 字的判据。
Grok 四个域名别点错
Grok API 成本边界先算清
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }