读懂 DeepSeek 对话接口的返回

一张”DeepSeek 对话补全接口到底返回了什么”的字段字典,搞不懂这张表,就没法写出健壮的大模型调用代码,也算不清成本和缓存收益。

ds 接口返回信息原图

图注:原图是三列对照表,字段 / 示例 / 说明,逐行列出了 DeepSeek chat.completion 返回体里的 14 个字段。下面所有内容都是对这张表逐行展开。注意示例里 model 是 deepseek-v4-flash、finish_reason 是 stop、total_tokens 是 19。


一、这份材料解决什么问题

调用大模型 = 发一次 HTTP 请求,拿回一个 JSON。绝大多数人只从 JSON 里抠 choices[0].message.content 就完事,剩下的字段全丢掉。但真实工程里这些”剩下的字段”决定三件事:

  1. 能不能稳定地写代码,不知道 finish_reason 就分不清”模型回答完了”和”被长度截断了”;
  2. 能不能算钱,不知道 usage 的分项就估不准成本,更没法做缓存优化;
  3. 能不能排查线上问题,不知道 id / system_fingerprint 就没法把某次报错定位到具体请求和后端配置。

这份材料就是这套字段的完整字典,是新闻分类项目里用 DeepSeek API 做新闻分类那一步的地基。


二、核心结论

  1. 返回体是三层嵌套:顶层元信息(id/object/created/model)→ 生成结果(choices,是数组)→ 用量统计(usage)。写代码时三者要分别处理。
  2. choices 是列表不是因为会返回多个答案,而是接口沿用了”n 参数可要多个候选”的设计;默认只取 choices[0]。
  3. finish_reason 是最该被检查却最常被忽略的字段:stop 表示模型自然结束,length 表示被 max_tokens 截断,后者直接导致下游 JSON 解析失败。
  4. usage 里 prompt_tokens 计费的是输入,completion_tokens 计费输出,二者单价不同(输出通常更贵)。做成本估算必须分开乘。
  5. prompt_cache_hit_tokens / prompt_cache_miss_tokens 是省钱的关键:命中缓存的那部分输入 token 单价大幅降低,所以”把稳定不变的内容(system 提示、few-shot 示例)放在提示词前缀”是有真金白银收益的工程习惯。

三、知识地图

字段 示例 说明
id 07ae05cb-3852-45fb-a615 请求唯一标识符
object chat.completion 对象类型
created 1779756041 生成时的 Unix 时间戳
model deepseek-v4-flash 使用的模型名称
system_fingerprint fp_8b30_prod0820_fp8_kvcache 后端配置指纹
service_tier None 服务层级(未指定)
choice.finish_reason stop 生成结束原因(自然结束)
choice.index 0 选择项索引
choice.message.role assistant 消息角色(助手)
choice.message.content Hello! How can I assist you today? 模型回复内容
usage.prompt_tokens 10 输入 token 数量
usage.completion_tokens 9 输出 token 数量
usage.total_tokens 19 总 token 数量
usage.prompt_cache_hit_tokens 0 命中缓存的输入 token 数
usage.prompt_cache_miss_tokens 10 未命中缓存的输入 token 数

读表要点:这张表有 15 行但实际是 3 个命名空间,无前缀的 6 个是顶层字段,choice.* 的 4 个在 choices 数组元素里,usage.* 的 5 个在用量对象里。示例值 19 = 10 + 9、10 = 0 + 10 这两组加法能自校验,说明缓存字段是 prompt_tokens 的拆分而非额外增量。


四、原理详解

4.1 顶层元信息:这次的请求”是谁、什么时候、用什么后端产生的”

  • id:一次请求一个 UUID,日志里必须记这个字段。线上出现”用户说答案不对”时,只有 id 能让你从网关日志反查那次请求的入参。
  • object:固定为 chat.completion(对话补全)或 chat.completion.chunk(流式返回的每一片)。写通用客户端时拿它做类型判断。
  • created:Unix 秒级时间戳。1779756041 换算过来是 2026 年,和示例里 model=deepseek-v4-flash 的时代对得上。注意它是服务端生成时刻,不是你发请求的时刻,两者差值就是排队+推理耗时。
  • model:实际服务的模型名。这一点很关键,你请求里写的是别名(例如 deepseek-chat),返回里是真实部署的版本。做 A/B 或线上归因时以返回值为准。
  • system_fingerprint:fp_8b30_prod0820_fp8_kvcache 这种字符串是后端配置指纹。拆开读:8b30 像构建批号,prod0820 是生产环境 8 月 20 日的配置,fp8_kvcache 说明 KV Cache 用了 FP8 存储。它的用途只有一个:同一个提示词昨天输出稳定、今天结果漂移了,先比对 system_fingerprint 是否变了,变了就是服务端换了配置,不是你的代码问题。
  • service_tier:服务层级,示例是 None(未指定)。有些平台提供”标准/弹性/批量”等不同延迟与价格的档位,这一栏告诉你这次走的是哪档。

4.2 choices:为什么是数组

{
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": { "role": "assistant", "content": "Hello! How can I assist you today?" }
    }
  ]
}

请求参数里的 n 表示”要几个候选答案”,默认 1,所以返回数组长度为 1。index 就是候选的序号。

message.role 固定是 assistant。注意角色语义:system/developer 是规则层,user 是提问层,assistant 是输出层,它记录的是模型说过的话,不是给模型的指令。把 assistant 历史当成指令来信任是常见的提示注入入口。

message.content 才是你要的结果。但在实际工程里,深挖顺序应该是:

先看 finish_reason  →  是 length 就直接判失败重试/加长
再看 content 是否为空  →  空内容多半是触发了安全策略或 reasoning 阶段的特殊返回
最后才解析 content 里的结构化数据(如 JSON)

4.3 finish_reason:一个字段决定重试逻辑

取值 含义 工程动作
stop 模型自然输出结束(遇到停止符或说完了) 正常处理
length 达到 max_tokens 被截断 判定为不完整,加大 max_tokens 或让模型分段输出
content_filter(部分平台) 内容被安全策略拦截 提示用户改问法,不要盲目重试
tool_calls(支持工具调用时) 模型要求调用工具,等待执行结果 执行工具并把结果喂回

示例值是 stop,说明这次是自然结束,可以放心用。

在新闻分类项目里的实际影响:LLM 做 10 分类时,我们要求模型只输出类别名(比如 __label__体育 或纯 JSON)。一旦被 length 截断,返回内容就带上半个 JSON,json.loads 直接抛异常。所以”先判 finish_reason == 'stop' 再解析”必须在封装函数的第一行。

4.4 usage:成本与缓存的账本

示例的账:输入 10 token,输出 9 token,合计 19 token。缓存命中 0、未命中 10,首次请求没有前缀可复用,全部按原价。

公式:

\text{cost} = \text{prompt\_tokens} \times p_{in} + \text{completion\_tokens} \times p_{out}

缓存生效时,prompt_tokens 拆成两段计价:

\text{cost} = (\text{cache\_hit} \times p_{hit} + \text{cache\_miss} \times p_{in}) + \text{completion\_tokens} \times p_{out},\quad p_{hit} \ll p_{in}

算一个小例子(示意单价,仅说明口径):假设输入 ¥1/百万 token、输出 ¥2/百万 token、缓存命中价是输入价的 1/10。

  • 场景 A:system 提示 + 8 个 few-shot 示例共 3000 token,前面 3000 token 完全一样,每次只换最后的标题。首次:3000×1 + 20×2 = 3040 单位;后续命中部 3000:3000×0.1 + (3000−3000 未命中 0)×1 + 20×2 = 340 单位。单次成本降到约 1/9。
  • 场景 B:把”示例”和”动态标题”交替拼接,前缀每次都在变 → 命中 0,成本始终按原价。

任务说明、示例、约束这类不变内容必须放在提示词最前面且一字不改;把变量拼在中间会让缓存彻底失效。这是 RAG/分类这类大批量调用场景最容易省下来的一笔钱。

4.5 一次完整的调用示例(把字段用起来)

import os, json, time, logging
from openai import OpenAI

client = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"],
                base_url="https://api.deepseek.com")

SYSTEM = (
    "你是新闻标题分类器。类别只能是:"
    "体育、财经、科技、娱乐、社会、军事、国际、汽车、房产、教育。"
    "只输出一个类别名,不要任何解释。"
)
FEWSHOT = [
    ("梅西帽子戏法助球队逆转夺冠", "体育"),
    ("央行宣布下调存款准备金率0.5个百分点", "财经"),
]

def build_messages(title: str):
    msgs = [{"role": "system", "content": SYSTEM}]
    for q, a in FEWSHOT:                      # 稳定前缀:利于缓存命中
        msgs.append({"role": "user", "content": q})
        msgs.append({"role": "assistant", "content": a})
    msgs.append({"role": "user", "content": title})
    return msgs

def classify(title: str, retries: int = 3) -> str | None:
    for i in range(retries):
        t0 = time.time()
        resp = client.chat.completions.create(
            model="deepseek-chat",
            messages=build_messages(title),
            temperature=0.0,          # 分类任务要确定性
            max_tokens=16,            # 只要一个类别名,防跑飞
        )
        dt = time.time() - t0
        ch = resp.choices[0]
        log = {
            "id": resp.id,                          # 出错时可归因
            "model": resp.model,                    # 实际服务的模型
            "fingerprint": getattr(resp, "system_fingerprint", None),
            "finish_reason": ch.finish_reason,
            "prompt_tokens": resp.usage.prompt_tokens,
            "completion_tokens": resp.usage.completion_tokens,
            "total_tokens": resp.usage.total_tokens,
            "latency_s": round(dt, 2),
        }
        logging.info("ds_call %s", json.dumps(log, ensure_ascii=False))

        if ch.finish_reason != "stop":              # 截断/被拦 → 重试
            continue
        label = (ch.message.content or "").strip()
        if label in LABELS:
            return label
    return None

这段代码体现了这张表的所有字段怎么用:finish_reason 控重试、usage 做成本日志、id+model+system_fingerprint 做可观测性、稳定的 few-shot 前缀吃缓存。


五、易错点与常见误解

常见说法 正确说法 为什么
choices 是数组,所以一次能拿多个答案 默认 n=1,返回长度就是 1;要多个候选必须显式传 n 数组只是接口的历史设计,别把它当特性
content 拿到了就算成功 必须先判 finish_reason,再判 content 是否为空 length 截断会返回”半个 JSON”,直接 json.loads 必炸
total_tokens = prompt + completion 就够了,成本按总数算 输入输出单价不同,必须分开乘 用总数乘一个平均价,大批量估算会系统性偏低或偏高
缓存是平台自动优化的,跟我写法无关 前缀一字不变才命中;变量拼在前面会让命中率归零 提示词结构直接决定成本
model 字段就是我请求里写的模型 它反映实际服务的模型(可能是别名对应的真实版本) 归因、对比实验要以返回值为准
created 是客户端发起请求的时间 是服务端生成响应的时刻 两者差值就是排队 + 推理时延
system_fingerprint 是给用户看的版本号,可以忽略 它变了意味着后端配置变了,输出差异可能来源于此 排查”昨天还好今天不一样”的第一现场

六、横向对比与研究脉络

不同厂商返回体的字段命名对照(同一层语义,换平台就要换字段名):

语义 DeepSeek / OpenAI 兼容 常见差异点
请求唯一标识 id 各平台一致
实际模型 model 国内平台常返回带版本号的真实模型名
结束原因 choices[0].finish_reason 部分平台叫 stop_reason(Anthropic 系)
回复内容 choices[0].message.content Anthropic 是顶层 content 数组;Gemini 是 candidates[0].content.parts
用量 usage.{prompt,completion,total}_tokens Anthropic 拆成 input_tokens/output_tokens
缓存分项 prompt_cache_hit_tokens / prompt_cache_miss_tokens 这是 DeepSeek 明确暴露缓存收益的设计,OpenAI 系叫 cached_tokens
后端指纹 system_fingerprint 并非所有平台都暴露

为什么”缓存分项”这类字段值得单列:它把”提示词工程”从玄学变成了可测量指标。你优化提示词顺序之后,prompt_cache_hit_tokens/prompt_tokens 这个比值会直接上升,这是一个可以写进项目汇报的量化收益。


七、在新闻分类项目中的落地

这套新闻分类流水线的最后一环是 LLM 零样本/少样本分类(前三环是 TF-IDF+随机森林、FastText、BERT)。这张表在三处直接决定项目质量:

  1. 封装调用函数(项目里 LLM 部分的第一步就是”把官网 API 案例封装成函数,再加提示词”):
  2. 入参只有标题,出参必须是受控的类别名;
  3. 函数内先判 finish_reason,再校验类别是否在 10 个合法类别里,不合法就重试,这样才有一个能和前三个模型同口径比较的准确率数字。
  4. 成本与延迟统计:1w 条测试集 × 每条约 20 输出 token。把 usage 逐条落库,最后能算出”LLM 方案单次推理成本是 BERT 的 N 倍、准确率高 M 个点”,这就是模型对比章节要的结论。同时记录 latency_s,说明 LLM 方案在吞吐上不可用于线上实时。
  5. 提示词前缀稳定 → 吃缓存:10 个类别的定义 + few-shot 示例是固定不变的,放在 system 和最前面的 user/assistant 对里;变化的标题只放最后一句。批量跑 1w 条时命中率会非常高,直接砍掉大部分输入成本。
  6. 可观测性:日志记 id + model + system_fingerprint。跑批过程中如果某天准确率突然掉,先看指纹和模型名有没有变,这是把”模型漂移”和”我的代码 bug”区分开的唯一手段。

八、重点问答

Q1:调大模型接口,返回里你最关注哪几个字段?为什么?
A:我关注四个。一是 finish_reason,它告诉我模型是自然结束还是被 max_tokens 截断了,length 意味着返回内容不完整,我必须在解析前就判掉、走重试。二是 usage 的 prompt_tokens 和 completion_tokens,因为输入输出单价不一样,成本必须分开算。三是 model 和 system_fingerprint,前者是实际服务的模型,后者是后端配置指纹,出问题时用它们区分”模型漂移”和”我的 bug”。四是 prompt_cache_hit_tokens,它直接告诉我提示词前缀有没有命中缓存。

Q2:choices 是数组,是不是能一次拿多个答案?
A:默认不能。请求参数 n 默认是 1,所以数组长度就是 1,choices[0] 就是唯一答案。数组是接口的历史设计,只有显式传 n>1 才会返回多个候选,而且多候选会让输出 token 成倍增长,分类任务里完全用不上。

Q3:怎么用接口返回值做成本优化?
A:核心是吃缓存。返回体里的 prompt_cache_hit_tokens 和 prompt_cache_miss_tokens 把输入 token 拆成了两段,命中部分单价远低于未命中部分。所以工程上要保证提示词的稳定前缀一字不变:把任务说明、类别定义、few-shot 示例全放前面,把每条变化的标题只放最后一句话。反过来,如果把变量拼在提示词中间,前缀每次都变,命中率归零,成本直接差一个数量级。

Q4:拿到 content 就直接 json.loads 有什么问题?
A:两个坑。第一,finish_reason 是 length 时会返回被截断的半个 JSON,json.loads 必抛异常;第二,模型可能加上”好的,以下是结果:”这类解释性前缀。所以正确顺序是先判 finish_reason == 'stop',再剥掉 markdown 代码块标记,再解析,最后还要校验解析出的类别是否在合法枚举里,四步都过了才算一次成功调用。

Q5:system_fingerprint 有什么用?
A:它是后端配置的指纹,像示例里的 fp_8b30_prod0820_fp8_kvcache,能看出构建批号、生产配置日期和 KV Cache 用了 FP8 存储。用处是排查输出漂移:同一个提示词昨天稳定今天变了,先比对指纹,变了就是服务端换了配置,不是你的代码问题。

Q6:created 字段是客户端发起请求的时间吗?
A:不是,是服务端生成响应的时刻。所以它和客户端发请求时间的差值,正好等于排队加推理的耗时。要做端到端延迟统计,需要在客户端自己打时间戳,两者相减才是真实时延。


九、延伸阅读

  • DeepSeek 官方 API 文档(对话补全接口的请求/响应字段与计费口径)
  • 上下文缓存(Context Caching)机制说明,理解 prompt_cache_hit_tokens 的前缀匹配规则
  • OpenAI Chat Completions 兼容接口规范,对照理解跨平台字段差异
  • 本目录《(提示词主课件)大模型 Prompt 工程指南》研究报告,finish_reason 与提示词约束的配合
  • 本目录《【了解】中间特征》等模型压缩笔记,KV Cache 与 FP8 量化为什么会影响后端配置指纹
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇