上下文是什么?token 怎么计费?

LLM 和 Agent 的核心术语和原理 讲了 LLM 的本质是概率预测,一切 Agent 相关的概念都是围绕上下文(Context)的工程实现。

那么到底什么是「上下文」?其实很简单,就是一个 messages 数组,一切 AI 工具,都在围绕这个东西雕花。

这篇我们直接在终端里用 curl 命令发 HTTP 请求调用大模型的 API。动手和 LLM 交互几次,就能彻底理解什么是上下文,之后再动手开发 Agent 就游刃有余了。

准备工作

你需要一个 DeepSeek API Key,如果还没有就先去创建一个。

curl 是命令行里发 HTTP 请求的工具,macOS 和 Linux 自带,Windows 用户在 Git Bash 或 CMD 里也能直接用。

第一次调用

拿到 API Key 后,在终端执行这条命令:

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": "你好"}
    ]
  }'

curl 的几个参数:-s 让 curl 不显示进度条,-H 设置请求头(设置 API key 鉴权),-d 后面跟 JSON 格式的请求体。

看请求体里的内容:model 指定用哪个模型,messages 是一个数组,里面放着发给模型的消息。

我们把发给 LLM 的所有输入统称为 Prompt(提示词),整个 messages 数组就是 Prompt 的载体。

数组里 roleuser 表示这是用户说的话,content 是具体内容,单独这条消息叫 User Prompt(用户提示词)。后面还会遇到 rolesystem 的消息,叫 System Prompt(系统提示词),到时候再细讲。

关于思考模式

DeepSeek V4 默认开启思考模式,模型会先写出 思维链,再给最终答案。本文所有 curl 示例都会带一个 "thinking": {"type": "disabled"} 字段把思考模式关掉,让响应保持简洁,方便讲解。

在后面 动手实现一个 Coding Agent 中处理复杂任务时,会开启思考模式。

返回的 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
  }
}

就这么简单,你发一个 messages 数组,模型给你返回一条 message,一来一回就是一次对话。

choices 是个数组,但默认只返回一条,取第一个就行,模型的回答就在 choices[0].message.content 里。

注意 message 里的 role 字段:

Request 中我们用 "user" 标记用户说的话,Response 中用 "assistant" 标记这是模型的回答。后面你还会碰到 "system""tool" 等其他角色,具体的作用我们遇到了再说。

finish_reason 告诉你模型为什么停下来,这里是 "stop",说明正常说完了。我们之后还会遇到其他的结束原因,比如因为回答太长被截断,这个字段会变成 "length",或者模型需要调用工具,这个字段会变成 "tool_calls"

usage 记录了这次调用消耗了多少 token,跟费用直接相关,具体的参数含义后面会讲。文档里给出的是某次实测的数字,你跟着练时拿到的数会有小幅波动,看清相对趋势就行。

模拟多轮对话

一次调用不过瘾,我们来模拟一段连续对话。先跟模型做个自我介绍:

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": "你好,小明!👋 作为同行,很高兴认识你。程序员的世界里,我们经常遇到各种挑战和乐趣。你现在在做什么项目?是前端、后端、全栈,还是对某个特定领域(比如AI、游戏开发、云计算)特别感兴趣?"
      },
      "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
  }
}

看起来模型知道你叫小明了。

接下来我们做个实验,直接发一句「我叫什么?」,看看模型怎么回答:

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 数组

比如刚才的对话,我们把所有对话历史都塞进 messages 数组,再问一遍「我叫什么?」:

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 数组里有没有之前的对话记录。

顺便注意一下 usage 里的 prompt_tokens: 63,前面只发一句「我叫什么?」时才 7 个 token,现在带上完整历史变成了 63 个。

看一下这个 messages 数组的结构:每一轮对话就是一条 user 消息加一条 assistant 消息,像三明治一样交替排列。最后追加一条新的 user 消息就是当前的提问。模型读到这个数组,就好像经历了整段对话一样。

所以所谓的「多轮对话」,本质上就是你每次发请求时,把所有聊天记录重新发一遍。模型是无状态的,所有「记忆」都在你手动拼的 messages 数组里

你用 ChatGPT 聊天时感觉它「记得」你说过的话,并不是模型真的有记忆,而是 ChatGPT 的客户端在背后帮你维护了这个数组:

每次你发一句话,客户端把你的消息和模型之前的回答一起追加进去,然后把整个数组发给 API。所有 AI 产品都是这样,没有例外。

动手实现一个 AI chatbot 里我们用代码实现了这个过程,你可以在下面这个交互组件里体验一下:

鼠标移动到 Request 和 Response 按钮上,可以看到每次请求和响应的详细信息,你可以直观地看到 messages 数组不断追加的过程、token 的消耗等情况。

对 LLM 来说一切都是 token

前面每次调用的返回值里都有一个 usage 字段,里面的数字跟你的钱包直接相关。

LLM 和 Agent 的核心术语和原理 讲过,LLM 的工作方式是根据已有的 token 预测下一个 token。token 是模型处理文本的最小单位,你可以理解为模型把文本切成的「碎片」。

但 token 不等于字符,也不等于单词,不同语言、不同模型的切法不一样。大致的规律是一个英文常见词通常是 1 个 token(如 hello),一个汉字大约 1-2 个 token,代码和标点的切法各有不同。

你发给 API 的 messages 数组,在送进模型之前,会先被一个叫 tokenizer 的工具拆成 token 序列。模型从头读取所有 token,然后开始预测后续内容,直到它认为该停了(或者达到了长度上限)。

这个过程从用户的视角就是:发给模型一句话,模型根据整个对话历史的信息,给出了一句回复。

输入 token 和输出 token

看一个具体的例子:

"usage": {
    "prompt_tokens": 5,
    "completion_tokens": 32,
    "total_tokens": 37
}

prompt_tokens 是你发送的 messages 数组被拆成了多少个 token,也就是输入消耗。

completion_tokens 是模型生成的回答包含多少个 token,也就是输出消耗。total_tokens 是两者之和。

模型服务商按 token 数量收费,通常以「每百万 token」为单位标价,而且输入和输出的单价不同,输出一般更贵。

因为输入 token 可以并行处理,而输出 token 只能一个一个串行生成,每个都需要模型单独做一次预测计算,GPU 利用效率更低,所以更贵一些。

提示

关于 LLM 推理的具体流程,会在 LLM 怎么预测下一个 token 中具体展开。

对话越长,token 越多

回看前面几次调用的 prompt_tokens

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

同样问「我叫什么?」,不带历史只花 7 个输入 token,带上完整对话历史就要 63 个。

因为每次请求都要把所有历史消息重新发一遍,所以对话历史越长,messages 数组越长,prompt_tokens 就越大,费用也越高。

Prompt Cache 节省费用

回头看前面几次响应,usage 里除了 prompt_tokens,还有几个跟缓存有关的字段:

"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_tokensprompt_cache_hit_tokens 是等价的,只是为了兼容不同的 SDK。

为什么需要这个机制?想一下多轮对话的场景:每一轮请求的 messages 前半部分(之前的对话历史)都是一样的,只有最后一条新消息不同。前缀都一样,那把前缀的计算结果缓存起来,下一次请求只计算新增的部分就行了。

业界把这个机制叫 Prompt Cache(提示缓存),模型服务商会自动做这件事:第一次请求时把输入的前缀算一遍并缓存,后续请求如果前缀和缓存里某段一致,就直接复用,只计算新增的尾巴。

缓存命中的 token 价格通常便宜得多(依模型提供商而定,大部分是正常输入价格的 10%),响应也更快。

所以多轮对话虽然每次都会发送全量 messages 对话历史,但前缀命中缓存后实际成本低得多,费用没有想象中那么夸张。

不同服务商的缓存策略不太一样,常见的几个差异点:

  • 触发条件:有的服务商对短 prompt 不缓存,需要 prompt 达到一定长度才会启动缓存。我们上面的示例中,对话内容非常少,没有达到缓存的触发条件,所以你会发现即便把相同的请求重发多次,都不会命中缓存。
  • 过期时间:从几分钟到几小时不等,超过过期时间未被使用的缓存会被清理。
  • 字段名:OpenAI 用 cached_tokens,DeepSeek 同时提供 prompt_cache_hit_tokenscached_tokens,Anthropic 又是另一套字段。
自动缓存 vs 手动缓存

本文用的 DeepSeek 模型,缓存是服务商自动完成的,你不需要做任何额外操作,只要两次请求的 messages 前缀相同(且达到触发条件),服务端就会自动检测并复用。

但并非所有服务商都是自动缓存。比如 Anthropic 的 Claude 模型需要调用方主动标记缓存断点。具体做法是在 messages 中某条消息的 content 里添加 cache_control 字段:

{
  "model": "claude-sonnet-4-6",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "这里是很长的对话历史或系统提示...",
          // 调用方添加这个字段,手动管理缓存点
          "cache_control": {"type": "ephemeral"}
        }
      ]
    },
    {
      "role": "user",
      "content": "这是新的提问"
    }
  ]
}

带了 cache_control 标记的消息及其之前的内容会被缓存,后续请求如果前缀相同就能命中,没有标记的部分不会被缓存。我们会在 给 Claude Code 发一句 Hi,会发生什么 里分析一个实际请求。

不过不管哪种方式,底层原理都一样:缓存 messages 的公共前缀,命中缓存的话 token 价格更便宜。

缓存写入也要花钱

前面说命中缓存能省钱,但缓存不是白存的。第一次把前缀存进缓存,服务器要腾一块地方把计算结果存下来,这个「写入」动作,有的服务商会单独收一笔钱,而且比普通输入还贵。

所以严格来说,输入 token 的价格有三档:

  • 普通输入:既没命中缓存、也没打算缓存的部分,按原价算。
  • 缓存写入(缓存创建):第一次把前缀存进缓存,最贵。
  • 缓存命中:复用已经存好的前缀,最便宜。

要不要对这一步单独收费,各家策略不一样:有的免费,有的要单收一笔创建费

本文一直用的 DeepSeek 就属于免费那类,并未单独对创建缓存收费,所以你只需要计算命中缓存和未命中缓存的输入 token 费用;而 Claude 和最新的 GPT 5.6 则把缓存写入单拎出来收费。

以 Claude 为例,以未命中缓存的输入价格为基准:

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

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

写入缓存为什么比普通输入还贵?因为缓存要实打实占用服务器的存储资源。而且同一份缓存,你要求保留的时间越长,创建时收的费用越高,所以 Claude 里保留 1 小时(2 倍)就比保留 5 分钟(1.25 倍)贵。

这笔写入费也会记在 usage 里。DeepSeek 的返回里只有命中和未命中,其他对缓存写入收费的模型,你可能会看到类似 cache_creation_input_tokens 或者 cache_write_tokens 的字段,记录缓存写入的 token 数量。

那缓存到底还划不划算?关键看这段前缀之后会被命中多少次。写入时多花的那点钱,要靠后续每次命中省下的钱慢慢摊平。一段前缀如果存进去只用一次就过期了,写入费就白花,反而不如不缓存;如果能被反复命中几十次,写入成本早就摊得可以忽略了。

对于 Agent 场景来说,由于消息前缀基本不变,只是不断往最后追加消息,所以缓存命中率一般都很高,即便对缓存写入额外收费,也是非常划算的。

token 如何计费

到底调用一次 LLM 花多少钱?我们造个例子算一下。

先看价格表,这里以我们一直在用的 deepseek-v4-flash 为例(价格常有调整,这里是 2026 年 7 月的数据,具体以 DeepSeek 官网 为准):

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

注意缓存命中的输入单价,大部分其他模型的缓存命中价格是未命中缓存价格的 1/10,而 DeepSeek 只有未命中的 1/50,所以说 DeepSeek 模型性价比非常高。

前面 curl 示例里的 messages 都只有几十 token,都没有触发缓存命中。真实场景中一次请求上万 token 很常见(一段系统提示加上几十轮对话历史就能到这个量级),假设某次请求返回的 usage 长这样:

"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
}

来手动算一下这个请求的计费:

  • 缓存命中的输入(prompt_cache_hit_tokens):11500 × 0.02 / 1,000,000 = 0.00023 元
  • 缓存未命中的输入(prompt_cache_miss_tokens):500 × 1 / 1,000,000 = 0.0005 元
  • 输出(completion_tokens):800 × 2 / 1,000,000 = 0.0016 元

累加起来,这次调用总共花了 0.00233 元。

再看一下没有 Prompt Cache 时的对比,同样的 12000 输入 token 全部按 miss 计价:

  • 输入:12000 × 1 / 1,000,000 = 0.012 元
  • 输出:800 × 2 / 1,000,000 = 0.0016 元

累加起来,这次调用总共花了 0.0136 元。

有没有缓存差了近 6 倍。所以后面讲 Agent 设计时你会反复看到「保持 messages 前缀稳定」的思路,只要缓存命中率高,输入 token 就算堆到几万也不会让费用失控。

反过来,如果一个 Agent 每一步都去改系统提示或者往 messages 中间插消息,破坏了前缀一致性,缓存全部失效,费用会翻好几倍。

总结

本文展示了和 LLM 对话最原始的样子:一个 HTTP 接口,一个 messages JSON 数组,一来一回。

ChatGPT、Claude,以及各种 AI 编程工具,底层都是这个结构。它们做的事情无非是帮你维护 messages 数组,再加上一些上下文管理策略(什么时候该压缩历史、什么信息要保留、什么可以丢掉),让 LLM 在有限的上下文窗口里尽可能聪明地工作。

现在你应该更直观的理解 LLM 和 Agent 的核心术语和原理 中,为什么说 LLM 是基座,Agent 是围绕 LLM 的一套工程体系了。

和 LLM 交互就是这么朴实无华,全靠一个 messages 数组,而 Agent 的核心任务就是组织好这个 messages 数组,让它在有限的上下文窗口里尽可能聪明地工作。

是不是觉得很神奇,基于一个普通的 messages 数组,就能构造出能帮我们解决复杂任务的 Agent 工具?

让我们逐步深入学习,下一篇 使用 AI 工具的实用技巧 会基于本文的原理,聊聊日常使用 AI 工具时的几个实用技巧。