拿到 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_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 表示连调用都没成功。这样挂在提交前检查或定时任务里,失败时从退出码就能判断是先看配置还是先看解析代码。断言项不要写得太细,太细会在接口正常演进时频繁误报;先守住内容非空、结束原因可识别、用量字段存在这三类。