M3.1-Flash-Preview 首次接入,先用最小请求确认返回结构

文章导读
拿到 M3.1-Flash-Preview 这个模型名之后,先别急着往业务里塞逻辑。模型名只解决“调哪个模型”,解决不了“请求要带哪些字段、写出来的代码落在返回体的哪一层、报错到底是参数写错还是接入配置不对”。建议的顺序是:用一个单文件脚本发一次最小请求,把请求字段和返回结构确认下来,再写解析和业务封装。首次接入时把字段名搞错,比后面调业务逻辑更浪费时间。
📋 目录
  1. 一 在本地准备可运行的最小调用脚本
  2. 二 记录一次成功请求的完整响应体
  3. 三 用错误参数制造一次失败并观察返回
  4. 四 把最小脚本封装成可复用函数
  5. 五 写一个回归检查脚本
A A

拿到 M3.1-Flash-Preview 这个模型名之后,先别急着往业务里塞逻辑。模型名只解决“调哪个模型”,解决不了“请求要带哪些字段、写出来的代码落在返回体的哪一层、报错到底是参数写错还是接入配置不对”。建议的顺序是:用一个单文件脚本发一次最小请求,把请求字段和返回结构确认下来,再写解析和业务封装。首次接入时把字段名搞错,比后面调业务逻辑更浪费时间。

建议把 M3.1-Flash-Preview 的接入拆成两步:先用单文件脚本发一次最小请求,确认请求体字段被接受、返回体里代码内容和结束原因的位置;再基于这份真实返回写封装和解析。4xx 出现时按鉴权、参数、长度三类分别定位,鉴权与参数类错误不要重试;具体字段名以你实际环境的返回为准,不要照抄示例。

在本地准备可运行的最小调用脚本

把模型名、密钥读取和请求体放在同一个文件里,脚本只做一件事:把请求发出去并把原始返回打出来。密钥从环境变量读,不写进代码,这样换环境时只改环境变量。

# minimal_call.py
import json, os, requests

API_KEY = os.environ["M3_API_KEY"]   # 未设置时直接抛 KeyError,避免拿空 key 发请求
BASE_URL = os.environ.get("M3_BASE_URL", "https://your-host/v1/chat/completions")

payload = {
    "model": "M3.1-Flash-Preview",
    "messages": [
        {"role": "user", "content": "用 Python 写一个判断回文的函数,只给代码"}
    ],
    "max_tokens": 256,
    "temperature": 0.2,
}

resp = requests.post(
    BASE_URL,
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json=payload,
    timeout=30,
)

print("HTTP", resp.status_code)
print(json.dumps(resp.json(), ensure_ascii=False, indent=2))

运行方式就是在同一个 shell 里导出环境变量再执行,先确认能收到 200:

export M3_API_KEY=你的密钥
export M3_BASE_URL=https://你的接入地址/v1/chat/completions
python minimal_call.py

预期输出是先打印一行 HTTP 状态码,再打印一段格式化 JSON,里面能看到模型写出的函数体。如果状态码是 400 或 401,就停在这一步,不要带着错误继续写业务代码。请求体字段名以实际接口文档或实际返回为准,如果报 unknown field,就按报错提示逐项删改,而不是一次猜一堆字段。

记录一次成功请求的完整响应体

成功那一次不要把内容截断,整段 JSON 存下来。下面是一种常见的返回结构,字段名和层级请以你实际的返回为准,这里只用来指认“哪一层放什么”。

{
  "id": "req-xxxx",
  "model": "M3.1-Flash-Preview",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "def is_palindrome(s):\n    ..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": <int>,
    "completion_tokens": <int>,
    "total_tokens": <int>
  }
}

逐层看:顶层是请求标识和模型名,用来做日志关联;模型生成的代码在 choices 数组里,choices 是数组而不是对象,所以取内容通常要写 choices[0];真正要入库的代码文本在 choices[0].message.content;这一次为什么停下,看同层的 finish_reason,正常结束一般是一个表示停止的值,被 max_tokens 截断时会是另一个值,这两个值必须先记下来再写判断分支;用量信息在顶层 usage 下,和 choices 平级,不要到 message 里找。

  • 建议落库的字段:请求 id、模型名、finish_reason、content 全文、usage 三个计数、发起时间。
  • 不建议落库的:完整的原始请求体里包含提示词全文,如果要存,先确认业务上是否允许。
  • finish_reason 的值域不要凭记忆硬编码,先把你观察到的几种值列出来,再写映射。

用错误参数制造一次失败并观察返回

主动制造三类错误各一次,把状态码和错误体记下来,后面写业务才能分清该谁负责。

M3.1-Flash-Preview 首次接入,先用最小请求确认返回结构
  • 鉴权失败:把 M3_API_KEY 改成一个明显的错误值,或干脆 unset 让它读不到。返回特征通常是 401 或 403,错误体里会提到身份或密钥无效。处理分支归到配置问题,不要重试,也不要让模型层去兜底,直接让脚本早失败。
  • 参数不合法:把 model 改成不存在的名字,或把 messages 里某个元素删掉 content,或把 max_tokens 写成负数。返回特征通常是 400,错误体会指出具体是哪个字段不合法,这个字段名就是最有价值的信息。处理分支是修请求,不重试。
  • 内容超长:把输入提示词堆到明显过长的程度,或把 max_tokens 调到一个远超上下文窗口的值。返回特征同样是 4xx,错误体里一般会出现长度、上下文窗口、token 上限这类关键词。处理分支是截断输入或调小 max_tokens 后重新发起,属于要改请求才能成功的错误。

三类错误的共同点是“重试没意义”。只有网络超时和服务端 5xx 才值得在代码里退避重试,这一点在下一节封装时直接落进实现。

把最小脚本封装成可复用函数

封装的目标是后续调用只传提示词和少量参数,不再手工拼请求体。签名固定下来之后,重试和超时只在一个地方配置。

def call_m3(prompt, system=None, max_tokens=512, temperature=0.2,
            timeout=30, retries=2):
    """返回 dict:{ok, content, finish_reason, usage, request_id, raw}
    鉴权/参数/长度类 4xx 抛 CallError,不重试;
    超时与 5xx 在函数内部退避重试 retries 次,仍失败则抛出。"""
    messages = []
    if system:
        messages.append({"role": "system", "content": system})
    messages.append({"role": "user", "content": prompt})

    payload = {
        "model": "M3.1-Flash-Preview",
        "messages": messages,
        "max_tokens": max_tokens,
        "temperature": temperature,
    }
    # 超时放在 requests.post(..., timeout=timeout)
    # 重试包在 while attempt <= retries 里,只对超时和 5xx 生效
    # 成功时返回上面约定的 dict;4xx 直接构造 CallError 抛出

返回值约定要写进函数注释里,调用方只认这一份约定。不建议在封装里做“出错就返回空字符串”,那样调用方分不清是模型没输出还是请求失败了。如果业务上更习惯不抛异常,可以在外层再包一层,把 CallError 转成 ok=False 的返回。

写一个回归检查脚本

每次调整 temperature、max_tokens 或换模型地址之后,跑一遍这个脚本,几十秒就能知道调用是否还通、返回结构有没有变。

# check_m3.py
import sys
from minimal_call import call_m3

try:
    r = call_m3("只输出一行:ok", max_tokens=32)
except Exception as e:
    print("调用异常:", type(e).__name__, e)
    sys.exit(2)          # 2 = 配置或网络层问题,不是断言失败

errs = []
if not r.get("content"):
    errs.append("content 为空")
if r.get("finish_reason") not in ("stop", "length"):
    errs.append("finish_reason 值异常")
for k in ("prompt_tokens", "completion_tokens", "total_tokens"):
    if not isinstance(r.get("usage", {}).get(k), int):
        errs.append(f"usage.{k} 缺失或类型不对")
if not r.get("request_id"):
    errs.append("request_id 缺失")

if errs:
    print("失败项:", "; ".join(errs))
    print("原始返回:", str(r.get("raw"))[:500])
    sys.exit(1)          # 1 = 返回结构不符合预期

print("OK", r["finish_reason"])

退出码约定成三档:0 表示通过,1 表示调用通了但返回结构和预期不符,2 表示连调用都没成功。这样挂在提交前检查或定时任务里,失败时从退出码就能判断是先看配置还是先看解析代码。断言项不要写得太细,太细会在接口正常演进时频繁误报;先守住内容非空、结束原因可识别、用量字段存在这三类。