上下文是什么?Token 怎么计费?从 messages数组到 Prompt Cache

很多人第一次调用大模型 API 时,会把上下文理解成一种模型内部的"记忆"。实际上,API 并不会自动替你保存上一次请求。模型每次收到的,都是当前请求里的输入。

对聊天模型来说,上下文最直接的形式就是一个 messages 数组。多轮对话、token 统计、Prompt Cache,以及 Agent 的上下文管理,都是围绕这个数组展开的。

本文从一个最简单的 HTTP 请求开始,逐步说明:

  • messages 数组如何构成一次对话
  • 为什么模型默认不会记住上一次请求
  • 输入 token、输出 token 分别代表什么
  • 对话变长后,为什么费用会增加
  • Prompt Cache 如何降低重复计算和调用成本
  • 缓存命中、缓存写入分别如何计费

准备工作

本文使用 DeepSeek API 做示例。开始前,需要准备一个 DeepSeek API Key。

curl 是命令行中发送 HTTP 请求的工具。macOS 和 Linux 通常自带,Windows 用户也可以在 Git Bash 或 CMD 中使用。

第一次调用:一个 messages 数组

拿到 API Key 后,在终端执行下面的命令:

bash 复制代码
curl -s https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 你的API_KEY" \
  -d '{
    "model": "deepseek-v4-flash",
    "thinking": {"type": "disabled"},
    "messages": [
      {"role": "user", "content": "你好"}
    ]
  }'

这条命令里的参数并不复杂:

  • -s:不显示进度条。
  • -H:设置 HTTP 请求头。这里分别设置内容类型和 API Key 鉴权信息。
  • -d:指定请求体,内容是 JSON。
  • model:指定使用的模型。
  • messages:存放发送给模型的消息。

我们把发送给 LLM 的输入统称为 Prompt,而 messages 数组就是 Prompt 的载体。

数组中的 roleuser,表示这条消息来自用户;content 是消息正文。单独的一条用户消息,可以称为 User Prompt。后续还会看到 systemassistanttool 等角色。

为什么关闭思考模式

DeepSeek V4 默认开启思考模式。模型会先生成思维链,再给出最终答案。

为了让本文的响应更简洁,所有 curl 示例都带有下面这个字段:

json 复制代码
"thinking": {"type": "disabled"}

在处理复杂任务时,例如后续实现 Coding Agent,可以打开思考模式。

返回结果怎么看

一次调用可能返回类似下面的 JSON。示例省略了部分不重要的字段:

json 复制代码
{
  "id": "b7da8cc7-4487-4ffa-b802-3b250ab53229",
  "object": "chat.completion",
  "created": 1777257146,
  "model": "deepseek-v4-flash",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "你好!很高兴见到你!有什么我可以帮你的吗?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 5,
    "completion_tokens": 32,
    "total_tokens": 37,
    "prompt_tokens_details": {"cached_tokens": 0},
    "prompt_cache_hit_tokens": 0,
    "prompt_cache_miss_tokens": 5
  }
}

模型的回答位于:

text 复制代码
choices[0].message.content

choices 是一个数组,默认通常只返回一条结果,因此直接取第一项即可。

message.role 表示消息角色:

  • 请求中的 user 表示用户消息。
  • 响应中的 assistant 表示模型消息。
  • 后续还会遇到 systemtool 等角色。

finish_reason 表示模型为什么停止生成:

  • stop:正常生成结束。
  • length:达到长度限制,回答被截断。
  • tool_calls:模型需要调用工具。

usage 记录这次调用消耗的 token,后面会用它计算费用。示例中的 token 数来自一次实际调用,自己练习时可能会有小幅波动,重点是理解字段含义和变化趋势。

到这里,一次最小的对话就完成了:客户端发送一个 messages 数组,模型返回一条 message

多轮对话:模型为什么会"忘记"

先发送一条自我介绍:

bash 复制代码
curl -s https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 你的API_KEY" \
  -d '{
    "model": "deepseek-v4-flash",
    "thinking": {"type": "disabled"},
    "messages": [
      {"role": "user", "content": "我叫小明,我是一个程序员"}
    ]
  }'

模型可能返回:

json 复制代码
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "你好,小明!作为同行,很高兴认识你。你现在在做什么项目?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 105,
    "total_tokens": 114,
    "prompt_tokens_details": {"cached_tokens": 0},
    "prompt_cache_hit_tokens": 0,
    "prompt_cache_miss_tokens": 9
  }
}

接着单独发送一句"我叫什么?":

bash 复制代码
curl -s https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 你的API_KEY" \
  -d '{
    "model": "deepseek-v4-flash",
    "thinking": {"type": "disabled"},
    "messages": [
      {"role": "user", "content": "我叫什么?"}
    ]
  }'

这次模型可能回答:

json 复制代码
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "抱歉,我无法知道您的名字,因为我们没有之前的对话记录。"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 7,
    "completion_tokens": 34,
    "total_tokens": 41,
    "prompt_tokens_details": {"cached_tokens": 0},
    "prompt_cache_hit_tokens": 0,
    "prompt_cache_miss_tokens": 7
  }
}

刚才明明介绍过自己,模型为什么还是不知道名字?

原因是:模型本身是无状态的。每次 API 调用都是独立的,上一次请求和下一次请求之间没有自动关联。对模型来说,上一次请求甚至不存在。

messages 数组就是对话记忆

如果希望模型"记住"之前的对话,需要在下一次请求中把完整的聊天历史重新放进 messages 数组。

例如,可以把前面的对话拼成这样:

bash 复制代码
curl -s https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 你的API_KEY" \
  -d '{
    "model": "deepseek-v4-flash",
    "thinking": {"type": "disabled"},
    "messages": [
      {"role": "user", "content": "你好"},
      {"role": "assistant", "content": "你好!很高兴见到你!我是 DeepSeek,由深度求索公司创造的 AI 助手。"},
      {"role": "user", "content": "我叫小明,我是一个程序员"},
      {"role": "assistant", "content": "你好小明!很高兴认识你。"},
      {"role": "user", "content": "我叫什么?"}
    ]
  }'

这次模型就可以根据历史回答:

json 复制代码
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "你叫小明,刚才你告诉我的。"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 63,
    "completion_tokens": 29,
    "total_tokens": 92,
    "prompt_tokens_details": {"cached_tokens": 0},
    "prompt_cache_hit_tokens": 0,
    "prompt_cache_miss_tokens": 63
  }
}

同样的问题"我叫什么?",不带历史时模型答不上来,带上完整历史后就能答出来。两次请求的区别,只在于 messages 数组中是否包含之前的对话记录。

这也解释了为什么前一次请求只有 7 个 prompt_tokens,带上完整历史后变成了 63 个。

每一轮对话通常由一条 user 消息和一条 assistant 消息交替组成。最后再追加一条新的 user 消息,就是当前问题。模型读取整个数组后,会按照这段上下文生成回答。

因此,多轮对话的本质是:每次请求都把聊天记录重新发送一遍。模型没有自动记忆,所谓记忆都存在客户端维护的 messages 数组里。

使用 ChatGPT、Claude 或其他 AI 产品时,客户端会在后台完成这项工作:

  1. 把用户新发送的消息追加到历史数组中。
  2. 把模型之前的回答也追加进去。
  3. 将完整数组再次发送给 API。

不同产品会在此基础上增加上下文压缩、历史裁剪等策略,但底层仍然离不开消息数组。

对 LLM 来说,一切输入都是 token

前面的响应里都有一个 usage 字段,其中的数字会直接影响调用费用。

LLM 的工作方式是根据已有 token 预测下一个 token。token 是模型处理文本时使用的基本单位,可以把它理解为模型切分出来的文本片段。

token 不等于字符,也不等于单词。不同语言和不同模型的切分方式可能不同,大致可以这样理解:

  • 一个常见英文单词通常约为 1 个 token,例如 hello
  • 一个汉字大约占 1 到 2 个 token。
  • 代码和标点符号的切分方式各不相同。

messages 数组发送给模型前,会先经过 tokenizer,被拆成 token 序列。模型从头读取这些 token,然后不断预测后续内容,直到它认为回答已经结束,或者达到长度上限。

从用户角度看,这个过程就是:发送一句话,模型结合整个对话历史,生成一段回复。

输入 token、输出 token 和总 token

先看一个简单的 usage 示例:

json 复制代码
"usage": {
  "prompt_tokens": 5,
  "completion_tokens": 32,
  "total_tokens": 37
}

三个字段分别表示:

  • prompt_tokens:发送给模型的 messages 数组被拆成的 token 数量,也就是输入消耗。
  • completion_tokens:模型生成的回答包含的 token 数量,也就是输出消耗。
  • total_tokens:输入 token 和输出 token 的总和。

模型服务商通常按每百万 token 计价,而且输入和输出使用不同单价。输出一般更贵,因为输入 token 可以并行处理,而输出 token 需要一个接一个地生成,每个 token 都需要模型进行一次预测,GPU 利用效率更低。

对话越长,输入 token 越多

前面几次请求的数据如下:

请求内容 messages 条数 prompt_tokens
只发送"你好" 1 5
只发送"我叫什么?" 1 7
带完整历史询问"我叫什么?" 5 63

问题没有改变,但输入 token 从 7 增加到了 63。

因为每次请求都会重新发送历史消息,所以:

text 复制代码
对话历史越长
→ messages 数组越长
→ prompt_tokens 越多
→ 输入费用越高

真实的 Agent 场景通常会包含系统提示词、工具描述、文件内容和几十轮对话历史,一次请求达到上万输入 token 并不罕见。

Prompt Cache 如何节省费用

前面的 usage 中除了 prompt_tokens,还出现了缓存相关字段:

json 复制代码
"usage": {
  "prompt_tokens": 63,
  "prompt_tokens_details": {"cached_tokens": 0},
  "prompt_cache_hit_tokens": 0,
  "prompt_cache_miss_tokens": 63
}

字段含义如下:

  • prompt_cache_hit_tokens:命中缓存的 token 数量。
  • prompt_cache_miss_tokens:未命中缓存、需要重新计算的 token 数量。
  • prompt_tokens_details.cached_tokens:与 prompt_cache_hit_tokens 等价的字段,主要用于兼容不同 SDK。

多轮对话有一个特点:每次请求的前半部分通常相同,只有最后一条新消息发生变化。

例如:

text 复制代码
[系统提示词][历史对话][历史对话][历史对话][新的问题]

前面的公共部分就是缓存前缀。第一次请求时,服务端计算这个前缀并保存结果。下一次请求如果前缀仍然一致,就可以复用之前的计算结果,只处理新增加的内容。

这套机制通常称为 Prompt Cache,也就是提示缓存。

缓存命中后,token 价格通常会低很多,响应也会更快。不同服务商的策略并不完全相同,常见差异包括:

  • 触发条件:有些服务商不会缓存很短的 Prompt,只有达到一定长度后才启用缓存。前面的示例内容很短,所以重复发送也可能不会命中。
  • 过期时间:缓存可能只保留几分钟,也可能保留几小时。超过期限且没有再次使用,缓存就会被清理。
  • 字段名称:OpenAI 使用 cached_tokens,DeepSeek 同时提供 prompt_cache_hit_tokenscached_tokens,Anthropic 使用另一套字段。

自动缓存和手动缓存

本文使用的 DeepSeek 模型会自动完成缓存。只要两次请求的 messages 前缀一致,并且达到服务商的触发条件,服务端就会自动检测和复用。

并不是所有服务商都采用自动缓存。例如,Anthropic 的 Claude 模型需要调用方主动设置缓存断点,可以在 messages 中某条消息的 content 里添加 cache_control

json 复制代码
{
  "model": "claude-sonnet-4-6",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "这里是很长的对话历史或系统提示...",
          "cache_control": {"type": "ephemeral"}
        }
      ]
    },
    {
      "role": "user",
      "content": "这是新的提问"
    }
  ]
}

带有 cache_control 标记的消息及其之前的内容会被缓存。后续请求的前缀相同,就可以命中缓存;没有标记的部分不会被缓存。

无论服务商采用自动方式还是手动方式,底层原理都相同:缓存 messages 的公共前缀,命中后减少重复计算。

缓存写入也可能收费

缓存命中可以省钱,但把内容第一次写入缓存并不一定免费。服务器需要保存计算结果,有些服务商会针对这次缓存创建单独收费,而且价格可能比普通输入更高。

严格来说,输入 token 可能分成三类:

类型 含义
普通输入 既没有命中缓存,也没有创建缓存的部分,按普通输入价格计算
缓存写入 第一次把公共前缀写入缓存,通常是三者中最贵的
缓存命中 复用已经保存的前缀,通常是最便宜的

是否对缓存写入收费,取决于服务商:

  • DeepSeek 不单独收取缓存创建费用,因此只需要计算缓存命中和未命中的输入 token。
  • Claude 会将缓存写入单独计费。
  • 最新的 GPT 5.6 也会将缓存写入单独计费。

以下是 Claude 的相对价格示例,以未命中缓存的输入价格为 1 倍:

类型 相对基础输入价
未命中缓存的输入 1 倍
缓存命中 0.1 倍
缓存写入,保留 5 分钟 1.25 倍
缓存写入,保留 1 小时 2 倍

GPT 从 5.6 这一代开始,也会按 1.25 倍收取缓存写入费用。更早的模型写入缓存不额外收费。

缓存写入比普通输入贵,主要是因为它需要占用服务器存储资源。同一份缓存要求保留的时间越长,创建时的费用也可能越高,所以 Claude 保留 1 小时的缓存写入价格高于保留 5 分钟。

这些费用也会记录在 usage 中。DeepSeek 的响应里主要能看到命中和未命中字段;对缓存写入收费的模型,可能会返回下面这类字段:

text 复制代码
cache_creation_input_tokens
cache_write_tokens

是否值得写入缓存,取决于后续能命中多少次:

  • 如果前缀写入后只使用一次就过期,写入费用可能抵消不了节省的费用。
  • 如果同一个前缀能被反复命中几十次,写入成本就会被摊薄。

Agent 通常会不断在消息末尾追加新内容,而系统提示词和历史前缀变化较少,因此缓存命中率通常较高。即使缓存写入需要额外收费,稳定的前缀仍然可能带来明显收益。

Token 计费示例

下面用 deepseek-v4-flash 做一次完整计算。

价格会调整,以下是原文采用的 2026 年 7 月价格示例,实际使用时应以 DeepSeek 官方文档 的最新价格为准。

项目 单价,元 / 百万 token
输入,缓存未命中 1
输入,缓存命中 0.02
输出 2

大多数模型的缓存命中价格约为未命中价格的 1/10,DeepSeek 示例中的命中价格为未命中价格的 1/50。

假设某次请求返回以下 usage

json 复制代码
"usage": {
  "prompt_tokens": 12000,
  "completion_tokens": 800,
  "total_tokens": 12800,
  "prompt_tokens_details": {"cached_tokens": 11500},
  "prompt_cache_hit_tokens": 11500,
  "prompt_cache_miss_tokens": 500
}

有缓存时

缓存命中的输入 token:

text 复制代码
11500 × 0.02 ÷ 1,000,000 = 0.00023 元

缓存未命中的输入 token:

text 复制代码
500 × 1 ÷ 1,000,000 = 0.0005 元

输出 token:

text 复制代码
800 × 2 ÷ 1,000,000 = 0.0016 元

总费用:

text 复制代码
0.00023 + 0.0005 + 0.0016 = 0.00233 元

没有缓存时

如果同样的 12,000 个输入 token 全部按未命中价格计算:

text 复制代码
输入:12000 × 1 ÷ 1,000,000 = 0.012 元
输出:800 × 2 ÷ 1,000,000 = 0.0016 元
总费用:0.0136 元

两种情况的费用相差接近 6 倍。

这也是 Agent 设计中经常强调"保持 messages 前缀稳定"的原因。只要缓存命中率较高,即使输入 token 增加到几万,费用也不一定会失控。

相反,如果 Agent 每一步都修改系统提示词,或者在 messages 中间插入消息,就会破坏前缀一致性。缓存可能全部失效,费用也可能增加数倍。

总结

和 LLM 对话的基本结构其实很直接:

text 复制代码
HTTP 接口 + messages JSON 数组 + 一次请求和一次响应

ChatGPT、Claude 和各种 AI 编程工具,底层都可以抽象成这个结构。它们主要替用户维护 messages 数组,再加入上下文压缩、历史保留和信息裁剪等策略,让模型在有限的上下文窗口内继续工作。

从这个角度看:

  • LLM 是负责预测和生成内容的基础模型。
  • Agent 是围绕 LLM 组织上下文、调用工具和完成任务的一套工程系统。
  • messages 数组是连接两者的核心载体。
  • token 既影响模型的上下文容量,也直接影响调用成本。
  • 稳定的消息前缀有利于 Prompt Cache 命中,从而降低重复计算费用。

理解 messages 和 token,是继续学习 Agent、上下文压缩、工具调用和缓存策略的基础。

参考资料

相关推荐
牛油果子哥q1 小时前
Prompt工程核心落地:结构化约束、Few-Shot示例、输出稳定性控
大数据·人工智能·prompt
JaguarJack9 小时前
OpenAI 研发人员称使用 GPT-6 Astra 模型 请立刻更新 Skills 与提示词 否则会是负优化
ai·openai·codex
lifallen11 小时前
Skill 的生命周期:问题消失以后
人工智能·学习·ai·ai编程
金智维科技官方12 小时前
财务核算如何提效?这三条核心链路正在加速自动化
运维·人工智能·ai·自动化·财务·智能体
熊猫钓鱼>_>13 小时前
MiniMax 深度观察:不是又一个大模型,是AI生产效率的范式革新
人工智能·ai·自然语言处理·媒体·benchmark·minimax·行业
大模型码小白14 小时前
AI大模型接入SDK:人工智能核心概念与发展史
大数据·运维·人工智能·安全·prompt
腾视科技-AI14 小时前
腾视科技TS-NV-P200车载系列AI边缘算力盒子:引领车路协同新时代,赋能多元场景应用
人工智能·科技·ai·ai算力·ai边缘算力盒子·腾视科技·ai算力盒
2601_9622946115 小时前
AI 基础设施的“去 Python 化“:Rust 与 C# 的两条替代路径
ai·rust·c·基础设施·去python化
风流 少年15 小时前
gradio
ai