💬 LLM 上下文与 Token 计费
一切 AI 工具都在围绕一个 messages 数组雕花——从 API 调用到 Agent 工程,底层逻辑一文讲透
✦ 本文目录
本文基于 labuladong 的文章 《LLM 的上下文是什么?token 怎么计费?》 整理。原文用 curl 命令直接调用大模型 API,在终端里一步步演示了 LLM 交互的最原始形态。
上下文就是一个 messages 数组
所有 AI 工具——ChatGPT、Claude、Cursor——底层都在围绕一个东西雕花:messages 数组。
核心概念所谓「上下文」(Context),说穿了就是一个 JSON 数组。你发给大模型 API 的请求体长这样:
发给 LLM 的所有输入统称为 Prompt(提示词),整个 messages 数组就是 Prompt 的载体。数组中 role 为 user 的消息叫 User Prompt(用户提示词),role 为 system 的叫 System Prompt(系统提示词)。
模型返回的回答在 choices[0].message.content 里,其中 role 为 assistant,表示这是模型的回复。一来一回,就是一次对话。
messages 数组
预测下一个 Token
choices[0].message
模型是无状态的
你以为它「记住」了你说的话?并没有。每次 API 调用都是独立的。
关键原理一个实验就能验证:先告诉模型「我叫小明」,下一次请求直接问「我叫什么」——模型答不上来,因为它根本不知道「上一次请求」的存在。
❌ 误解:模型有记忆
- 模型记住了之前的对话内容
- 像人类一样有短期记忆
- 所以能接住上下文聊天
✅ 实际:完全无状态
- 每次 API 调用都是全新的
- 上一次请求与这一次毫无关系
- 「记忆」全靠你把历史塞进 messages
多轮对话的本质:重发全部历史
要让模型「记住」之前的对话,唯一的办法就是把完整的对话历史全部塞进 messages 数组。
上下文工程同样的「我叫什么?」,不带历史时模型答不上来,带上完整历史就能答出来。区别只在于 messages 数组里有没有之前的对话记录。
messages 数组像三明治一样交替排列:每一轮对话就是一条 user 消息加一条 assistant 消息,最后追加一条新的 user 消息就是当前的提问。模型读到这个数组,就好像经历了整段对话一样。
Token:模型处理文本的最小单位
Token 不等于字符,也不等于单词。它是模型把文本切成的「碎片」。
基础概念LLM 的工作方式是根据已有的 Token 预测下一个 Token。你发给 API 的 messages 数组,在送进模型之前,会先被一个叫 tokenizer 的工具拆成 Token 序列。
英文 Token
一个英文常见词通常是 1 个 Token(如 hello)。罕见的词可能被拆成多个 Token。
中文 Token
一个汉字大约 1-2 个 Token。中文的 Token 化效率通常比英文低,同样内容用中文写会消耗更多 Token。
代码 Token
代码和标点的切法各有不同,空格、括号、关键字都有各自的 Token 规则。
模型差异
不同模型用不同的 tokenizer,同样的文本在不同模型上 Token 数会有差异。
JSON 文本
拆成 Token 序列
从头开始预测
直到结束符
输入 Token 与输出 Token
每次 API 调用的返回值里都有一个 usage 字段,里面的数字跟你的钱包直接相关。
计费基础| 字段 | 含义 | 价格特点 |
|---|---|---|
| prompt_tokens | 你发送的 messages 数组被拆成的 Token 数(输入) | 可以并行处理,较便宜 |
| completion_tokens | 模型生成的回答包含的 Token 数(输出) | 串行逐个生成,更贵 |
| total_tokens | 输入 + 输出的总和 | — |
对话越长,Token 越多
| 请求内容 | messages 条数 | prompt_tokens |
|---|---|---|
| 只发「你好」 | 1 条 | 5 |
| 只发「我叫什么?」 | 1 条 | 7 |
| 带完整历史问「我叫什么?」 | 5 条 | 63 |
同样问「我叫什么?」,不带历史只花 7 个输入 Token,带上完整对话历史就要 63 个。因为每次请求都要把所有历史消息重新发一遍。
Prompt Cache:省钱的缓存机制
多轮对话每次都发全量历史,但前缀都是一样的——把前缀缓存起来,只算新增的部分就行。
成本优化每一轮请求的 messages 前半部分(之前的对话历史)都是一样的,只有最后一条新消息不同。前缀都一样,把前缀的计算结果缓存起来,下一次请求只计算新增的尾巴——这就是 Prompt Cache(提示缓存)。
完整计算 + 缓存前缀
前缀命中缓存
只算新增消息
缓存命中率更高
成本接近线性增长
缓存命中的 Token 价格通常便宜得多(大部分是正常输入价格的 10%),响应也更快。所以多轮对话虽然每次发送全量历史,但前缀命中缓存后实际成本低得多。
自动缓存 vs 手动缓存
自动缓存(如 DeepSeek)
- 服务商自动完成,不需要额外操作
- 两次请求的 messages 前缀相同(且达到触发条件)即自动复用
- 开发者无感知,开箱即用
- 但短 prompt 可能不触发缓存
手动标记(如 Claude)
- 需要调用方在 messages 中添加
cache_control字段 - 手动指定缓存断点位置
- 更灵活但更繁琐
- 带标记的消息及其之前的内容会被缓存
• OpenAI:
cached_tokens• DeepSeek:
prompt_cache_hit_tokens + cached_tokens(兼容)• Anthropic:另一套字段,且需手动
cache_control 标记
缓存写入也要花钱
命中缓存能省钱,但第一次把前缀存进缓存,有的服务商会单独收一笔钱——而且比普通输入还贵。
进阶知识严格来说,输入 Token 的价格有三档:
| 类型 | 说明 | 相对价格 |
|---|---|---|
| 普通输入 | 既没命中缓存、也没打算缓存的部分 | 1 倍(基准) |
| 缓存写入 | 第一次把前缀存进缓存 | 1.25 ~ 2 倍(最贵) |
| 缓存命中 | 复用已存好的前缀 | 0.1 倍(最便宜) |
各服务商策略对比
| 服务商 | 缓存方式 | 缓存写入收费 |
|---|---|---|
| DeepSeek | 自动缓存 | 免费(不单独收费) |
| Claude | 手动标记 cache_control | 5 分钟 1.25 倍 / 1 小时 2 倍 |
| GPT 5.6+ | 自动缓存 | 1.25 倍 |
| GPT 5.6 以前 | 自动缓存 | 免费 |
反过来,如果一个 Agent 每一步都改系统提示或往 messages 中间插消息,破坏前缀一致性,缓存全部失效,费用会翻好几倍。
Token 计费实例
用一个具体例子算一次账,看看调用一次 LLM 到底花多少钱。
实战计算以 deepseek-v4-flash 为例(2026 年 7 月价格,具体以官网为准):
| 项目 | 单价(元 / 百万 Token) |
|---|---|
| 输入(缓存未命中) | 1 |
| 输入(缓存命中) | 0.02(仅 1/50,性价比极高) |
| 输出 | 2 |
假设某次请求返回的 usage 如下(一段系统提示加几十轮对话,上万 Token 很常见):
有 Prompt Cache 时的费用
| 项目 | 计算 | 费用(元) |
|---|---|---|
| 缓存命中的输入 | 11,500 × 0.02 / 1,000,000 | 0.00023 |
| 缓存未命中的输入 | 500 × 1 / 1,000,000 | 0.0005 |
| 输出 | 800 × 2 / 1,000,000 | 0.0016 |
| 合计 | — | 0.00233 元 |
无 Prompt Cache 时的费用(对比)
| 项目 | 计算 | 费用(元) |
|---|---|---|
| 输入(全部按未命中) | 12,000 × 1 / 1,000,000 | 0.012 |
| 输出 | 800 × 2 / 1,000,000 | 0.0016 |
| 合计 | — | 0.0136 元 |
有没有缓存差了近 6 倍。这就是为什么 Agent 设计中反复强调「保持 messages 前缀稳定」——只要缓存命中率高,输入 Token 就算堆到几万也不会让费用失控。
全文总结
ChatGPT、Claude、各种 AI 编程工具,底层都是这个结构。它们做的事情无非是帮你维护 messages 数组,再加上一些上下文管理策略(什么时候压缩历史、什么信息要保留、什么可以丢掉),让 LLM 在有限窗口里尽可能聪明地工作。