把业务迁到 DeepSeek-V4.1-Flash-1017 之前,先用一条只有一句提示的请求跑通:确认地址能连通、鉴权头被接受、模型名在服务方的模型列表里存在、返回结构能被你的代码解析。这一步跑不通,后面加多少提示词都是白费;这一步跑通了,再往上加业务提示词时,出错就能定位到具体某一层,而不是在“接入问题”和“提示词问题”之间来回猜。
适用场景:首次接入 DeepSeek-V4.1-Flash-1017,或从其他模型平移过来。操作动作:只发一条单句提示,不带系统提示、不带示例、不带业务上下文;把地址、密钥、模型名放进环境变量;把返回体原样打出来确认字段路径。验证方式:看 HTTP 状态码、看 choices 里的 content 是否非空、看 finish_reason 与 usage 是否能取到。风险边界:具体端点路径、字段名和模型 ID 以服务方控制台和模型列表为准,下面的骨架是通用形态,需要结合环境替换后再验证。
写一条只有一句提示的最小请求
最小请求的作用是排除业务提示词带来的干扰。请求体里只放一句用户消息,不调 temperature,不加 system,不加 few-shot,不加历史。这样返回异常时,可能的原因只剩网络、鉴权、模型名、请求体格式四类,排查范围是收敛的。
下面是通用接入骨架,花括号里的内容是占位符,路径和字段需要按服务方接入说明替换:
POST {BASE_URL}/chat/completions
Authorization: Bearer ${API_KEY}
Content-Type: application/json
{
"model": "${MODEL_ID}",
"messages": [
{"role": "user", "content": "用一句话说明你现在可以接收请求。"}
]
}
鉴权头一般是 Bearer 加密钥,也有服务方用自定义 header 名,以控制台给出的示例为准。模型名不要凭记忆写:先把 DeepSeek-V4.1-Flash-1017 当作候选值,到模型列表页或接入说明里核对一次大小写和分隔符,再写进配置。模型 ID 写错时,报错通常长得像“模型不存在”,和密钥失效的报错不是一回事,值得区分开。
预期返回结构只需记住形状,不要记具体数值(下面的 0 和省略号都是占位):
{
"id": "...",
"object": "...",
"model": "...",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "..."},
"finish_reason": "stop"
}
],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}
用 curl 或任意 HTTP 客户端发一次,先看状态码再看待响应体。2xx 说明连通、鉴权、模型名三项都过了;401 或 403 回去查密钥是否带上了、是否多了空格;404 或提示模型不存在,查模型名与端点路径;429 看额度与限流;超时则先怀疑网络出口和超时设置,不要急着改提示词。
把鉴权与模型名放进环境变量
地址、密钥、模型名这三项一旦在代码、脚本、配置文件里各写一份,后面改一处忘一处,就会出现“测试环境能跑、部署后 401”这类漂移问题。建议统一从环境变量读,其他地方只引用变量名。
export DS_BASE_URL="https://{host}/v1"
export DS_API_KEY="{key}"
export DS_MODEL="DeepSeek-V4.1-Flash-1017"
读取时给缺失直接报错,不要静默用默认值兜住——默认值会让“变量没生效”这个故障藏起来:
import os
def need(name):
v = os.environ.get(name)
if not v:
raise SystemExit(f"缺少环境变量 {name},请先 export 后再启动")
return v
BASE_URL = need("DS_BASE_URL")
API_KEY = need("DS_API_KEY")
MODEL = need("DS_MODEL")
如果配置文件(yaml、json、.env)里也保留了 model 字段,启动时打一行日志输出实际生效的模型名和端点,和配置里的值对一遍。日志不要打印完整密钥,打印长度或首尾几位即可。这行日志在后续排查里很省事:至少能确认跑的是哪个模型,而不是凭印象判断。
确认返回结构里的字段路径
代码里迟早要取三样东西:正文、结束原因、用量。先在最小请求的响应里找到它们各自的位置,写成路径占位符,再落到代码里。
- 正文:
resp["choices"][0]["message"]["content"] - 结束原因:
resp["choices"][0]["finish_reason"] - 用量:
resp["usage"]["total_tokens"],以及 prompt / completion 两项
这些路径是按常见对话补全结构写的,实际名称要对照一次真实返回。做法很直接:把最小请求的响应体原样打印(脱敏后),照着实测字段抄路径,比凭经验猜可靠。取不到时给兜底,但兜底不等于吞错:
def pick(d, path, default=None):
cur = d
for k in path:
if isinstance(cur, dict) and k in cur:
cur = cur[k]
elif isinstance(cur, list) and isinstance(k, int) and 0 <= k < len(cur):
cur = cur[k]
else:
return default
return cur
text = pick(resp, ["choices", 0, "message", "content"], "")
reason = pick(resp, ["choices", 0, "finish_reason"], "unknown")
usage = pick(resp, ["usage"], {})
if not text:
# 记录原始响应与 reason,别只留一句空字符串
log.warning("空正文 reason=%s raw=%s", reason, resp)
这样写的好处是失败时能看到原因:finish_reason 为 length 说明输出被长度限制截断,为其他值则要对照服务方定义确认语义;正文为空但结构正常,多半是提示词或长度参数的问题,不是接入问题。分清这两类,后面的调整才有方向。
按业务提示词逐层加回
最小请求跑通后,不要一次性搬入整套业务逻辑。按“系统提示 → 少量示例 → 长输入”的顺序一层层加,每层只改一个变量,发一次请求,保留请求体快照(把 messages 存成文件),出问题就回退到上一层。
[
{"role": "system", "content": "{角色与输出格式约束}"},
{"role": "user", "content": "{真实业务输入}"}
]
- 第一层:只加系统提示。验证请求就是最小请求加上 system message。观察点:返回结构没变、字段仍能取到、输出开始遵守角色和格式约束。这一步异常,问题在系统提示的写法或与服务方的兼容性上。
- 第二层:加一到两条示例。验证请求是 system + 两条示例问答 + 真实输入。观察点:结构不变,usage 里的输入 token 明显上升,示例是否挤占了你预期的输出空间;格式是否真的被示例带稳。
- 第三层:换成真实长输入。验证请求是完整业务请求。观察点:是否触及上下文上限,finish_reason 是否变成截断,延迟和费用是否在你可接受的范围内,输出在真实噪声数据下是否还稳定。
每一层的判断标准都相同:结构不变、字段能取、输出符合预期格式、结束原因正常。哪一层开始出现偏差,就把那一层新加的内容单独拎出来重发一次,确认是可复现的问题而不是偶发。这样迁移到 DeepSeek-V4.1-Flash-1017 的过程才有分明的是非线:接入是接入,提示词是提示词,长度和上下文限制是另一回事。