DeepSeek V4.1-Flash-1017 Python 实测:工具调用、JSON 与推理控制

DeepSeek V4.1-Flash-1017 Python 教程:用 requests 通过 SandBase 调 deepseek/deepseek-v4.1-flash,含流式、工具调用、JSON Schema 输出、推理预算控制和错误处理,每段代码都真实跑过。

DeepSeek V4.1-Flash-1017 Python 教程封面;编辑插画

我用 DeepSeek V4.1-Flash-1017 学到的第一个有用教训,是一个空答案。我用 max_tokens=300 发了一道有点难的计数题,拿回 HTTP 200、finish_reason: "length",而 content: ""。usage 里写明了原因:300 个 completion token 全是我根本看不到的 reasoning token,而且每一个都计了费。这个模型是先推理再回答,这一个行为几乎决定了下面所有代码。

这是一篇面向”想要能跑的代码、而不是功能清单”的 DeepSeek V4.1-Flash-1017 Python 教程。它调用的是 deepseek/deepseek-v4.1-flash——这个 id 现在指向 10 月的 1017 checkpoint——走 SandBase 的 OpenAI 兼容 Chat Completions 接口,用的是最朴素的 requests。每段代码都在 2026-10-17 对着线上模型真实跑过,每个费用都来自 GET /v1/tasks/<id>/cost,不是我自己算的。

先说结论

  • 把任意 OpenAI 兼容客户端指向 https://api.sandbase.ai/v1,模型 id 用 deepseek/deepseek-v4.1-flash。流式、并行工具调用、response_format JSON Schema 都是一次就通。
  • 隐藏的 reasoning token 和可见回答共用 max_tokens 预算,并按输出价计费。设成 300 时模型返回空答案却被计了 300 个输出 token;设成 16000 时用少得多的 token 答对了。
  • reasoning_effort 确实改变行为:在一道难题上各跑 3 次,reasoning token 中位数分别是 1,678(low)、1,853(high)、2,737(不设),全部答对。传非法值会返回 HTTP 503,所以发送前要校验。
  • 不带 messages 的请求返回的是 HTTP 503 而不是 400——“遇到 5xx 就重试”的粗暴逻辑会去重试一个客户端 bug。重试前先看错误。
  • 整个测试过程共 15 次计费调用,花了 $0.0176,有效单价接近每百万 token $0.22 / $0.90(截图时在 $0.30 / $1.20 标价上有 25% 上线折扣)。

开始之前

你需要 Python 3.10+、放在 SANDBASE_API_KEY 里的 SandBase key,以及 requests 包。下面的代码用裸 HTTP,是为了把机制露出来;openai SDK 同样可用,文末会讲。

python3 -m venv .venv && source .venv/bin/activate
pip install "requests==2.32.5"
export SANDBASE_API_KEY="your-key"

有两个事实决定了代码怎么写。第一是上限:SandBase 模型页 显示 1M 上下文窗口和 384K 最大输出,所以 max_tokens 有真正的余量。第二是价格:模型标价每百万 token 输入 0.30 美元、输出 1.20 美元,截图时 25% 的上线折扣把它压到接近 0.22 / 0.90。reasoning token 按输出计费。

SandBase 的 DeepSeek V4.1 Flash 模型页,显示 id deepseek/deepseek-v4.1-flash、25% off 标签、0.22 与 0.90 美元的 token 价、1M 上下文窗口、384K 最大输出和 live 生产路由 模型页列出了每段代码用到的确切 id、下面费用背后的折扣价,以及约束 max_tokens 的 384K 最大输出(截图于 2026-10-17)。

第 1 步:用 requests 发一个请求

这是其余每一步的基本形状。

import os
import requests

BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}

resp = requests.post(
    f"{BASE}/chat/completions",
    headers=HEADERS,
    json={
        "model": "deepseek/deepseek-v4.1-flash",
        "messages": [
            {"role": "system", "content": "Answer in one sentence."},
            {"role": "user", "content": "What does HTTP status 429 mean for an API client?"},
        ],
        "max_tokens": 1024,  # 这个预算同时覆盖隐藏推理 token 和可见回答
    },
    timeout=120,
)
resp.raise_for_status()
body = resp.json()
choice = body["choices"][0]
print("id:", body["id"])
print("finish_reason:", choice["finish_reason"])
print("answer:", choice["message"]["content"])
print("usage:", body["usage"])

cost = requests.get(f"{BASE}/tasks/{body['id']}/cost", headers=HEADERS, timeout=30).json()
print("billed:", cost["cost"], cost["currency"])

真实输出,只留关键字段:

id: b552c90b-4e94-4e96-b1cb-62bee2f39485
finish_reason: stop
answer: HTTP 429 means the API client has sent too many requests in a given amount of
  time and is being rate-limited, so it should back off and retry later ...
usage: {'completion_tokens': 82, 'completion_tokens_details': {'reasoning_tokens': 36},
  'prompt_tokens': 47, 'total_tokens': 129}
billed: 0.000084 USD

注意:一句话的回答也带了 36 个 reasoning token。/cost 响应里还能看到折扣:original_cost 是 $0.000113,叠加 25% 模型折扣后结算为 $0.000084。记好这个 id,它是你之后对账的唯一凭据。

第 2 步:流式,并在末尾拿到 usage

设 stream: true,读 Server-Sent Events,并用 stream_options 要一个末尾的 usage chunk。

import os, json, time, requests

BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}

start = time.monotonic(); ttft = None; parts = []; usage = None; cid = None
with requests.post(f"{BASE}/chat/completions", headers=HEADERS, stream=True, timeout=180, json={
    "model": "deepseek/deepseek-v4.1-flash",
    "messages": [{"role": "user", "content": "Write a 4-line poem about rate limits."}],
    "max_tokens": 2048, "stream": True, "stream_options": {"include_usage": True},
}) as s:
    for line in s.iter_lines():
        if not line or not line.decode().startswith("data: "):
            continue
        payload = line.decode()[6:]
        if payload == "[DONE]":
            break
        chunk = json.loads(payload)
        cid = cid or chunk.get("id")
        if chunk.get("usage"):
            usage = chunk["usage"]
        for ch in chunk.get("choices", []):
            piece = ch.get("delta", {}).get("content")
            if piece:
                if ttft is None:
                    ttft = time.monotonic() - start
                parts.append(piece)

task_id = cid.removeprefix("chatcmpl-")  # 流式返回的 id 带 chatcmpl- 前缀
print("first token:", round(ttft, 1), "s | total:", round(time.monotonic() - start, 1), "s")
print("usage:", usage)
print(requests.get(f"{BASE}/tasks/{task_id}/cost", headers=HEADERS, timeout=30).json()["cost"])

我这次跑,第一个可见 token 在 4.9 秒后出现,流在 5.0 秒结束,40 个 prompt token、209 个 completion token(其中 173 个是推理)。第一个 token 慢,是模型在落笔前先思考;这是我这台机器的一次观测,不是延迟基准。计费 $0.000197。

一个值得知道的坑:流式 chunk 的 id 形如 chatcmpl-e3857df2-...,但 GET /v1/tasks/chatcmpl-.../cost 会返回 404。先去掉前缀——task_id = cid.removeprefix("chatcmpl-")——费用查询就通了。

SandBase Chat Completions 文档,展示 OpenAI 兼容接口、用 base_url https://api.sandbase.ai/v1 的 Python 客户端,以及流式、工具、结构化输出等小节 SandBase 的 Chat Completions 指南记录了 base URL、这里用到的 stream_options.include_usage,以及第 3、4 步用到的工具与结构化输出能力(截图于 2026-10-17)。

第 3 步:工具调用的完整往返

传 tools,执行模型要求的调用,为每个调用追加一条带对应 tool_call_id 的 tool 消息,再问一次。这里的工具是本地字典,结果可核对。

import os, json, requests

BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
MODEL = "deepseek/deepseek-v4.1-flash"

ORDERS = {"A-1001": {"status": "shipped", "carrier": "DHL", "eta": "2026-10-19"},
          "A-1002": {"status": "processing", "carrier": None, "eta": None}}

def get_order_status(order_id: str) -> dict:
    return ORDERS.get(order_id, {"error": f"unknown order {order_id}"})

TOOLS = [{"type": "function", "function": {
    "name": "get_order_status",
    "description": "Look up shipping status for one order id.",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string"}},
                   "required": ["order_id"]}}}]

messages = [{"role": "user", "content": "Where are orders A-1001 and A-1002? One line each."}]

for turn in range(5):  # 硬上限,防止模型糊涂时无限循环
    r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=120,
                      json={"model": MODEL, "messages": messages, "tools": TOOLS, "max_tokens": 2048})
    msg = r.json()["choices"][0]["message"]
    calls = msg.get("tool_calls") or []
    print(f"turn {turn}: tool_calls={len(calls)}")
    if not calls:
        print("answer:", msg["content"]); break
    messages.append({k: v for k, v in msg.items() if v is not None})
    for call in calls:
        args = json.loads(call["function"]["arguments"] or "{}")
        result = get_order_status(**args)
        messages.append({"role": "tool", "tool_call_id": call["id"], "content": json.dumps(result)})
else:
    raise RuntimeError("tool loop did not finish in 5 turns")

第 0 轮,模型在一次响应里同时要了两个订单(两个并行 tool_calls,102 个 completion token,$0.000162)。第 1 轮,它没有为那条没有承运商的订单编造信息:

- A-1001: shipped via DHL, ETA 2026-10-19.
- A-1002: still processing — no carrier or ETA yet.

第 1 轮只计费 $0.000079,usage 里能看出原因:它的 256 个 prompt token 是缓存命中。对话前缀被复用,所以第二轮比它的 token 数看起来要便宜。在更长的 agent 里,这种 prompt 缓存行为,就是账单线性增长还是二次增长的区别。

第 4 步:用 response_format 做结构化输出

import os, json, requests

BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}

SCHEMA = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "invoice_number": {"type": "string"},
        "total_usd": {"type": "number"},
        "line_items": {"type": "array", "items": {
            "type": "object",
            "properties": {"description": {"type": "string"}, "amount_usd": {"type": "number"}},
            "required": ["description", "amount_usd"], "additionalProperties": False}},
    },
    "required": ["vendor", "invoice_number", "total_usd", "line_items"],
    "additionalProperties": False,
}
TEXT = ("Invoice INV-2291 from Northwind Hosting. Dedicated server October: $1,200.00. "
        "Backup storage 2 TB: $80.00. Support plan: $150.00. Amount due: $1,430.00.")

r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=120, json={
    "model": "deepseek/deepseek-v4.1-flash",
    "messages": [{"role": "user", "content": f"Extract the invoice fields.\n\n{TEXT}"}],
    "response_format": {"type": "json_schema",
                        "json_schema": {"name": "invoice", "strict": True, "schema": SCHEMA}},
    "max_tokens": 2048})
choice = r.json()["choices"][0]
if choice["finish_reason"] != "stop":
    raise RuntimeError(f"incomplete JSON risk: finish_reason={choice['finish_reason']}")
data = json.loads(choice["message"]["content"])  # 若返回非 JSON 会抛错
items_sum = round(sum(i["amount_usd"] for i in data["line_items"]), 2)
print(json.dumps(data, indent=2))
print("line items sum:", items_sum, "| matches total:", items_sum == data["total_usd"])

模型返回了纯 JSON,三条明细齐全,total_usd: 1430.0,求和校验通过。它用了 157 个 completion token(78 个推理)对应 228 个 prompt token,计费 $0.000193。即便开了严格 JSON 模式,也要保留 finish_reason 检查和必填字段解析——被截断的响应仍然是非法 JSON,这个失败模式不会因为你要了 schema 就消失。

第 5 步:推理预算与 max_tokens

回到那个空答案。这个模型先推理再回答,推理 token 计入 completion_tokens,而 max_tokens 把两者一起卡住。一个对一词回答看起来很宽裕的上限,可能全被思考花掉。下面这道难题是”有多少个 7 位正整数的各位数字之和恰好等于 30?“——本地暴力算出的正确答案是 453,438。

import os, requests

BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
PROMPT = "How many 7-digit positive integers have a digit sum of exactly 30? Reply with the number only."

def ask(prompt, max_tokens, retry_budget=16000):
    r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=180,
                      json={"model": "deepseek/deepseek-v4.1-flash",
                            "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens})
    c = r.json()["choices"][0]; u = r.json()["usage"]
    print(f"max_tokens={max_tokens}: finish={c['finish_reason']} content={c['message']['content']!r} "
          f"reasoning={u['completion_tokens_details']['reasoning_tokens']}")
    if c["finish_reason"] == "length":
        if max_tokens >= retry_budget:
            raise RuntimeError("still truncated at the retry budget; shorten the task")
        return ask(prompt, retry_budget, retry_budget)  # 推理和回答共用这个预算
    return c["message"]["content"]

print("final:", ask(PROMPT, max_tokens=300))

max_tokens=300 时,调用以 finish_reason: length 停下,内容为空,300 个全是 reasoning token——为一个我根本没拿到的答案计费 $0.000282。重试到 16000 时返回了 453438。由此有三条规则:对每个响应都检查 finish_reason;把”空内容 + length”理解为”预算太小”而不是”模型拒答”;不要为了省钱把 reasoning 模型的 max_tokens 调得很紧——设成够用,然后用 prompt 控成本。

配套的旋钮是 reasoning_effort。在这道难题上各跑 3 次:

reasoning_effort每次的推理 token中位数答对
不设2,206 / 2,737 / 2,8662,7373/3
"low"1,678 / 1,306 / 1,9141,6783/3
"high"1,256 / 2,450 / 1,8531,8533/3

每个设置都答对了,low 的推理 token 中位数最少——在中低难度任务上这是一个实在的成本杠杆。但区间有重叠、样本只有 3,所以把它当方向而非保证,拿你自己的 prompt 去测。还有一个发现:传 reasoning_effort: "banana" 返回了 HTTP 503,而不是干净的参数校验错误,所以只用 low / medium / high,别指望非法值会安全失败。那 9 次难题调用是测试里最大的一笔,合计 $0.0166。

第 6 步:错误处理与重试

我用裸 requests 发了几个常见错误,看真实状态码:

错误HTTP响应体(截断)
key 错误401{"error":"invalid API key"}
deepseek/deepseek-v4.1-flash-preview404type: not_found_error,“model not found”
缺 messages 字段503type: server_error,“Input required: specify … messages”

有两点要在设计时考虑。401 的响应体是 error 下的纯字符串,而其它是 OpenAI 风格对象,所以读错误要留防御;以及缺 messages 这个 bug 返回的是 503,大多数通用重试策略会当成临时故障——于是”重试所有 5xx”的粗暴逻辑会去重试一个永远不可能成功的请求。本地校验 messages,只重试 429/500/502/503/504,并且绝不自动重试一个副作用可能已经发生的工具轮次。

改用 openai SDK

上面的一切都能通过 openai SDK 跑,只要改两处——base URL 和模型 id——因为 SandBase 是 OpenAI 兼容的。我没有把每一步都用 SDK 重跑一遍,所以不会引用 SDK 专属的输出,但客户端的写法有文档:

from openai import OpenAI
client = OpenAI(base_url="https://api.sandbase.ai/v1", api_key=os.environ["SANDBASE_API_KEY"])
resp = client.chat.completions.create(
    model="deepseek/deepseek-v4.1-flash",
    messages=[{"role": "user", "content": "Hello!"}], max_tokens=1024)

DeepSeek API 更新日志,确认 deepseek-flash 现在由 DeepSeek-V4.1-Flash-1017 提供服务,调用方式不变 DeepSeek 更新日志确认 deepseek-flash 在上游现在指向 1017 checkpoint;在 SandBase 上对应的 id 是 deepseek/deepseek-v4.1-flash(截图于 2026-10-17)。

整个教程花了多少钱

步骤计费调用数费用
第 1–4 步(基础、流式、工具、JSON)5$0.0007
第 5 步 reasoning_effort 探测(9 次)9$0.0166
第 5 步 max_tokens 截断1$0.0003
合计15$0.0176

失败请求(401、404、503)没有返回 completion id,也就没有计费。每一行都是 /v1/tasks/<id>/cost 响应之和,按 2026-10-17 当时的折扣价计。

常见问题

在 SandBase 上 DeepSeek V4.1-Flash-1017 用哪个模型 id?

deepseek/deepseek-v4.1-flash。DeepSeek 原地升级到了 1017 checkpoint,所以 API id 里没有 -1017 后缀。把客户端指向 https://api.sandbase.ai/v1 即可。

为什么我的 DeepSeek 响应是空的?

看 finish_reason。如果是 length,说明隐藏的 reasoning token 在可见回答开始前就用完了 max_tokens。调大 max_tokens;无论如何 reasoning token 都按输出计费。

reasoning_effort 能降成本吗?

在一道难题上,low 的推理 token 中位数比不设和 high 都少,各跑 3 次仍然答对。但区间有重叠,所以拿你自己的工作负载去测,别假设有固定节省。

能用 openai 的 Python SDK 吗?

能。设 base_url="https://api.sandbase.ai/v1",传你的 SandBase key,用 model="deepseek/deepseek-v4.1-flash"。上面的裸 requests 代码可直接映射到 SDK 的 client.chat.completions.create。

本教程的边界与下一步

这里的一切都是每段代码一次运行(reasoning_effort 每档 3 次),来自一台机器、同一天。延迟数字是轶事,不是基准。我没测图片输入、prompt 缓存控制,也没测 Anthropic 格式接口。想看这个 checkpoint 改了什么,见 1017 更新拆解;想看它和 V4 Pro 在价格与视觉上的对比,见 V4.1 Flash 与 V4 Pro;想看 DeepSeek 同天发布的搜索工具,见 Search 与 Fetch API 指南。想自己跑这些代码,从 DeepSeek V4.1 Flash 模型页 拿 key 和模型 id,并从第 5 步的 ask() 开始——那个推理预算守卫是我在任何生产客户端里都会保留的部分。