从单轮到多轮:Ark / OpenAI 风格大模型 API 的上下文管理与 Token 成本分析
一、单轮请求
1.1 单轮请求是什么
首先,我们用最简单、也是最基础的方式发起一次模型请求,生成初始响应。
- 所谓单轮请求,是指:
- 本次调用不依赖任何历史对话
- 模型只根据当前输入完成一次推理
- 每一次请求之间完全相互独立
- 本质上,这是一场一次性、无记忆的推理过程。
- 请求实现如下:
python
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ["VOLC_ACCESS_KEY"],
base_url="https://ark.cn-beijing.volces.com/api/v3"
)
initial_input = [
{"role": "system", "content": "你是一名技术文档写作助手"},
{"role": "user", "content": "请帮我总结以下内容的核心观点:Python 是一门广泛使用的编程语言,适合数据分析、人工智能开发和自动化脚本。"}
]
first_response = client.responses.create(
model="deepseek-v3-1-terminus",
input=initial_input
)
print("第一次响应:")
print(first_response.output_text)
print("Token 消耗:", first_response.usage)
1.2 system / user 在单轮中的职责
-
在这一请求中,消息只包含两种角色:
system:定义模型的角色与行为边界user:描述当前需要完成的具体任务
-
需要特别强调的是:模型是无状态的(stateless)。
-
所谓"对话能力",并不是模型在内部保存了记忆,而是由调用方在每一次请求中显式携带上下文信息。
换句话说:
- 模型不会记住你之前问过什么
- 如果你不把历史内容传给它,它就一定会"忘记"
1.3 单轮请求中的 Token 消耗
本次请求返回的 response.usage 如下,包含了完整的 Token 使用情况。
ResponseUsage(
input_tokens=35,
input_tokens_details=InputTokensDetails(cached_tokens=0),
output_tokens=149,
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
total_tokens=184
)
这意味着,所有输入 Token 都是首次计算 ,没有任何上下文复用(cached_tokens=0)。
- 各字段含义如下:
| 字段 | 含义 | 说明 |
|---|---|---|
input_tokens |
本次发送给模型的 token 数量 | 包含 system + user 的 token 编码长度,这里是 35 |
input_tokens_details.cached_tokens |
缓存 token 数量 | 0 表示本次没有复用缓存 token |
output_tokens |
模型生成的 token 数量 | 149,表示模型输出长度 |
output_tokens_details.reasoning_tokens |
推理/中间 token | 0,一般用于计划式推理模型 |
total_tokens |
总消耗 token | 184 = input + output |
在单轮场景下,这样的消耗是可预测且稳定的。
二、从单轮到多轮:问题从哪里开始
单轮请求非常适合演示、测试和一次性任务。但在真实应用中,用户往往会不断追问:
- "能再详细一点吗?"
- "你刚才的结论和 XX 有什么关系?"
- "基于前面的讨论,给我一个方案。"
此时,如果模型无法理解"之前发生了什么",对话就无法继续。于是,多轮对话成为几乎所有 LLM 应用的默认形态。
三、多轮对话的挑战:显式拼接历史
3.1 最直观的多轮实现方式
在多轮对话中,最常见、也最直观的实现方式是:每一轮都将 system + 历史消息 + 新 user 输入 一并发送给模型。
python
messages = [
{"role": "system", "content": "你是一名技术助手(长文档)"},
{"role": "assistant", "content": first_response.output_text}, # 显式加入上一轮回答
{"role": "user", "content": "它和 RNN 的 hidden state 有什么区别?"}
]
response = client.responses.create(
model="deepseek-v3-1-terminus",
input=messages
)
print(response.output_text)
print("Token 消耗:", response.usage)
3.2 关于assistant
- 在消息序列中,
assistant并不是模型自动保存的记忆,而只是一个普通的输入字段 :- 它的内容,来自上一轮模型生成的文本结果
- 它是否出现,完全由调用方决定
- 可以从工程角度这样理解:
assistant是一段被重新发送给模型的"对话日志",而不是模型内部的状态。 - 模型并不知道哪些内容是"历史输出",它只是在处理你当前提供的 input。
3.3 为什么要传入 assistant
是否传入 assistant,会直接影响模型的行为:
-
传入
assistant- 模型可以基于上一轮回答继续推理
- 多轮输出具备连贯性
-
不传
assistant- 模型无法感知上一轮内容
- 可能重复回答,或偏离上下文
这再次印证了一个核心事实:多轮对话的连续性,完全是由输入 Token 构造出来的效果。
3.4 显示拼接Token消耗
对多轮调用进行统计,返回的 response.usage 为:
ResponseUsage(input_tokens=105, input_tokens_details=InputTokensDetails(cached_tokens=0),
output_tokens=1054,
output_tokens_details=OutputTokensDetails(reasoning_tokens=0), total_tokens=1159)
- 各字段含义如下:
| 字段 | 含义 | 说明 |
|---|---|---|
input_tokens |
本次请求传给模型的 token 数量 | 包含 system + assistant + user 的编码长度(这里是 105) |
input_tokens_details.cached_tokens |
缓存 token 数量 | 0 表示没有复用历史内容,全部重新计费 |
output_tokens |
模型生成的 token 数量 | 1054,表示模型生成了较长回答 |
output_tokens_details.reasoning_tokens |
推理/中间 token | 0,通常用于计划式或链式推理模型 |
total_tokens |
总 token 消耗 | 1159 = input + output,表示本次调用的全部成本 |
3.5 显式拼接的成本
- 显式拼接历史虽然简单,但存在明显代价:
- 每一轮都会重复计算:system、所有历史 assistant
- Token 成本会随轮数线性增长
- 对长系统提示或长回答尤为明显
- 从本质上看:显式拼接历史,是一种用 Token 换取上下文连续性的方案。
- 它实现简单、行为直观,但并不适合长对话或高频调用的生产环境。
四、小结
-
大模型 API 本身是无状态的
-
多轮对话并不是模型的"记忆能力",而是调用方通过输入 Token 显式模拟出来的效果
-
在最直观的实现方式下,上下文连续性与 Token 成本几乎等价增长
理解这一点,是后续讨论 上下文缓存、摘要压缩、向量检索(RAG) 等优化手段的前提。