很多人第一次调用大模型 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 的载体。
数组中的 role 为 user,表示这条消息来自用户;content 是消息正文。单独的一条用户消息,可以称为 User Prompt。后续还会看到 system、assistant 和 tool 等角色。
为什么关闭思考模式
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表示模型消息。 - 后续还会遇到
system和tool等角色。
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 产品时,客户端会在后台完成这项工作:
- 把用户新发送的消息追加到历史数组中。
- 把模型之前的回答也追加进去。
- 将完整数组再次发送给 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_tokens和cached_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、上下文压缩、工具调用和缓存策略的基础。
参考资料
- 原文:上下文是什么?token 怎么计费?
- DeepSeek API 文档:https://api-docs.deepseek.com/