LLM是怎么处理messages数组的:提示词缓存又是什么

时间:2026-07-03 08:29:47 来源:互联网

img_6a47027ba3d7d30.webp

img_6a47027bcf8ff31.webp

背景

大模型本质上执行的是文字接龙任务——给定一段文字后,模型会根据概率预测下一个字。严格来说,模型仅识别token,输入一串token后,它预测下一个token。当我们调用LLM接口时,传递给模型的messages参数如何转化为token ID,这中间涉及哪些流程?理解这一过程,就能明白什么是提示词缓存,以及它解决了什么问题。

什么是上下文

大模型自身不具备记忆能力,每次调用都相互独立。这意味着,为了获取准确回复,每一轮对话都必须附带完整的上下文信息。我们在调用大模型接口时传递的 messages 参数,就承载了完整的上下文。因此,所谓的上下文管理,本质上就是针对messages进行管理。

messages 通常由三部分组成:

  1. system:系统角色设定。
  2. 每一轮的用户提问。
  3. 模型上一轮的回答。

模型正是依据“用户问过什么 + 自己此前答过什么”这一线索,来延续对话。

如果不带历史回复会怎样?

请看一个示例:

第一轮

  1. 你:帮我写一个 Python 排序代码。
  2. 模型:提供了一段完整代码。

第二轮(仅发送新问题,未附带模型之前的代码)

  1. 你:帮我把这段代码改成降序。

在此场景中,由于我们的messages数组没有包含模型刚才的回答(即那段代码),模型实际上只接收到两个问题:

  1. 帮我写 Python 排序代码。
  2. 帮我把这段代码改成降序。

模型并不知道它之前提供过哪段代码,也不清楚“这段代码”指的是什么,因此只能进行猜测或重新编写。

由此可见,不携带历史回复或对话信息,会引发一系列问题:

  1. 上下文断联:无法记住刚刚聊过的内容。
  2. 重复回答:反复从头开始,无法接续话题。
  3. 逻辑混乱:无法衔接上一轮的结论。
  4. 改写、续写或迭代需求彻底失效。

所以,与大模型对话时必须发送完整的上下文信息。

大模型接口收到 messages 后做了什么

LLM 接口接收到 messages 后,大致会经历以下步骤:获取 messages → 按照固定格式拼接成一段长文本 → 切分成 Token → 进入 Prefill 阶段计算 KV。

第一步:理解核心前提

大模型根本不理解 JSON 数组,它只能读取一串连续的token

我们传递给大模型的JSON数据格式如下:

 复制代码[
  { "role": "system", "content": "你是专业程序员" },
  { "role": "user", "content": "解释一下KV缓存" },
  { "role": "assistant", "content": "KV缓存就是..." }
]

但在模型看来,这只是一堆“角色+内容”的组合。接口后台的首要任务,就是将这个数组压缩变形,拼接成一段符合模型规则的连续文本

第二步:如何拼接成文本?

每个大模型都拥有固定的角色分隔模板,拼接过程并非随意进行。

通用的拼接逻辑是:为每个 role 添加专属的标记头和尾,从而串联起整个对话:

  1. 首先写入标记:系统角色开始 + 系统内容。
  2. 接着写入标记:用户开始 + 用户提问。
  3. 然后写入标记:助手开始 + 助手回答。
  4. 最后停留在:助手开始,等待模型继续生成。

举个例子,原始 messages:

  1. system:你是专业程序员。
  2. user:解释一下KV缓存。
  3. assistant:KV缓存就是存注意力中间数据。

后台自动拼接成的完整长文本大致如下(模拟格式):

 复制代码<|system|>
你是专业程序员
<|user|>
解释一下KV缓存
<|assistant|>

最终停在 <|assistant|> 之后,意味着模型应该在此处开始生成回答。

那些 <|system|> <|user|> <|assistant|> 均为特殊占位符 Token,并非普通文字,模型依靠它们来区分说话者。

第三步:如何计算 Token?

分词(Tokenize)

将拼接好的整段长字符串,根据模型自带的词表字典进行切割:

  1. 常见整词:缓存 → 1个 Token。
  2. 生僻长词、英文、字母:拆分成多个 Token。
  3. 角色标记 <|system|> → 单独 1 个特殊 Token。

最终结果

一整段对话加上角色标记,全部被切割成一长串数字 ID 列表。文字 → 被切碎成 Token → 每个 Token 对应一个数字 ID。大模型不识别文字,只认识这串数字 ID

我们常说的“消耗多少输入 token”或“上下文窗口 4k/8k”,指的就是这一整段内容拼接、切割完成后的总 Token 数量。

第四步:Token 如何进入模型计算?

  1. 将这串 Token 数字送入模型的 Prefill 预填充阶段
  2. 一次性并行计算所有 Token 的注意力,生成全套 KV 缓存
  3. 获得 KV 状态后,开始逐字 Decode,逐个输出回答。

完整流程串联

  1. 我们在前端构建 messages 对话数组。
  2. 接口后台按照角色模板将数组拼接成带有特殊标记的长文本。
  3. 对长文本进行 Token 分词,转换为数字 ID 序列。
  4. 统计 Token 数量(用于计费和判断是否超长)。
  5. 送入模型 Prefill,计算并生成 KV 缓存。
  6. 模型基于完整上下文,逐字生成回复。

大模型如何生成文本

如前所述,LLM 的核心功能就是根据当前输入预测下一个字或词。那么,LLM 接口为何能按照 JSON 格式返回回复、工具调用等指令呢?

实际上,从生成、续写到多轮对话的完整过程,模型始终严格遵循一套固定的文本拼接规则,这与 messages 数组转文本的规则相同。

无论我们发送的 messages,还是模型正在逐字生成的回复,底层都遵循相同的角色模板拼接规则:使用 <|系统|>、<|用户|>、<|助手|> 这类特殊标记与内容进行拼接。

模型并非随意组合文字,而是按照固定格式拼成一条结构化长文本,然后再预测下一个字。这些特殊标记和结构化文本都经过了训练

模型如何回复

我们传递的 messages 会被拼接成:

 复制代码<|system|>
你是智能助手
<|user|>
世界最高峰是
<|assistant|>

模型停留在 <|assistant|> 之后,准备从此处开始续写。

模型逐字生成时,每生成一个字,就自动拼接到整个结构化文本的末尾:

第一步生成「珠」→ 全局文本变为:

 复制代码<|system|>
你是智能助手
<|user|>
世界最高峰是
<|assistant|>

第二步再生成「穆」→ 自动继续向后拼接:

 复制代码<|system|>
你是智能助手
<|user|>
世界最高峰是
<|assistant|>
珠穆

每生成一个 token,都按照这套格式,追加到整段结构化文本的最后。

模型如何生成调用工具的指令

普通对话使用<|system|> <|user|> <|assistant|>等特殊标记进行拼接。

而工具调用则会多出两个角色标签:

  1. <|function_call|>:表示助手要调用工具。
  2. <|function_result|>:表示工具返回结果给模型。

工具调用完整拼接过程

  1. System 固定:需要告知模型可调用的工具、参数格式、技能列表以及入参规则等。
  2. 用户提问<|user|> 今天北京天气怎么样?
  3. 模型判断要调工具:模型不会直接回答,而是在 <|assistant|] 后面,按格式生成工具调用结构:
 复制代码<|assistant|>
<|function_call|>
{"name":"查天气","parameters":{"城市":"北京"}}

底层原理仍是普通文本续写,只是续写的内容是 JSON 格式的工具调用指令。

  1. 后端拦截这个 function_call:接口后端识别到模型输出了工具调用指令后,不再继续生成回答,而是去执行相应函数,并获取结果:北京:晴,25℃
  2. 把工具结果按规则拼回去:后端构建一条工具结果消息,并将其塞入 messages,按照模板进行拼接:
 复制代码<|function_result|>
北京:晴,25℃

6. 模型再接着续写正常回答:模型在接收到 <|function_result|> 结果后,继续在其后面生成自然语言回复:

 复制代码<|assistant|>
北京今天天气晴朗,气温25度,适合出门。

整体拼接后的完整长文本

 复制代码<|system|>
你有查天气、计算器等工具,严格按JSON格式调用
<|user|>
今天北京天气怎么样?
<|assistant|>
<|function_call|>
{"name":"查天气","parameters":{"城市":"北京"}}
<|function_result|>
北京:晴,25℃
<|assistant|>
北京今天天气晴朗,气温25度,适合出门。

整个流程只用了一套拼接逻辑,只是额外增加了两个标签。

和普通对话的区别

  1. 普通对话:仅循环 user ↔ assistant。
  2. 工具调用:多了一轮闭环 user → assistant(发工具调用) → function_result → assistant(给答案)

所有工具调用、Skill 调用、插件调用,底层并无特殊魔法,依然是模板拼接 + Token 预测 + KV 缓存。模型只是“按格式续写一段工具调用文本”,后端识别后执行,再将结果拼回上下文。

为什么必须按规则拼接?

  1. 模型通过训练学会了使用格式标记来区分说话者:<|user|> 后面是用户说的,<|assistant|] 后面是模型要说的。
  2. 模型续写时只在 <|assistant|] 标签后面接字,不会跑到用户位置或系统位置去生成。
  3. 如果整套格式混乱,模型就会插话、逻辑偏差、答非所问。

什么是提示词缓存

Prompt Caching(提示词缓存/前缀缓存) 是大模型推理的核心优化技术:它将请求中重复的前缀(静态内容)缓存下来,当下次请求的前缀完全相同时,直接复用缓存,无需再次计算,从而降低延迟、节省成本、减少算力消耗。

核心原理

大模型推理分为两个阶段:

  1. Prefill(预填充):将提示词转换为 Token,并计算注意力 KV 状态。此阶段计算量最大、成本最高。
  2. Decode(解码):在已有上下文基础上逐字生成回复,计算成本较低。

Prompt Caching 的做法是:将 Prefill 阶段计算出的 KV Cache 跨请求暂存;当新请求的前缀完全一致(Token 级精确匹配)时,直接复用缓存,跳过 Prefill 阶段,仅计算新增部分。

能带来什么好处

  1. 延迟大幅降低:在长提示词场景下,响应时间可从秒级降至百毫秒级。
  2. 成本显著下降:缓存命中的输入 Token 计费通常是原价的 1/10~1/4(如 OpenAI/Anthropic 新模型)。
  3. 节省算力:避免了重复的 Transformer 注意力计算,大幅降低 GPU 占用。

生效规则

  1. 前缀必须精确匹配:任何 Token 差异(如空格、标点、大小写)都会导致缓存失效。
  2. 长度门槛:例如 OpenAI 要求前缀 ≥ 1024 Token,并按 128 Token 的增量进行对齐。
  3. 缓存时效:通常 5~10 分钟无访问后自动过期,在低峰期最长可维持约 1 小时。
  4. 最佳写法:将静态内容(系统指令、角色设定、示例)放在前面,动态内容(用户输入、变量)放在后面。

典型适用场景

  1. 多轮对话(上下文复用)。
  2. 固定系统指令 + 动态用户问题。
  3. 反复处理同一份长文档或知识库。
  4. 高频模板化查询(如客服、FAQ、RAG)。

一句话总结:Prompt Caching 的本质就是“相同的前缀只计算一次”,使长提示词请求既快速又经济。

Prompt Caching 的缓存前缀是什么

Prompt Caching 的缓存前缀,实际上就是我们传入的整个 messages 数组,按照模型的固定模板拼接并转换为 Token 之后的那整串 Token 序列。

缓存的 key 是什么?

缓存的key可以简单理解为,将 messages 数组转换后生成的完整 Token 序列:

  1. 按模型模板拼接 messages → 变成一整段带有 <|system|>``<|user|>``<|assistant|> 标记的长文本。
  2. 对这段文本进行 Tokenize → 得到一长串数字 Token ID。
  3. 从开头开始的前缀 Token,就是 Prompt Caching 用于匹配缓存的唯一标准。

多轮对话如何命中缓存?

Prompt Cache 并非要求整个前缀完全一致,而是指开头的一截前缀完全相同。前缀越长,能够复用的部分就越多。

我们用符号来表示:S=system,U1=用户1,A1=助手1,U2=用户2,A2=助手2,U3=用户3

第一轮

  1. 请求:S+U1
  2. 运行完 Prefill,缓存:Token 序列 [S, U1]

第二轮

  1. 请求:S+U1+A1+U2
  2. 其完整 Token 前缀为:S+U1+A1+U2

模型用这个前缀从头开始匹配缓存:

  1. 开头先比对:S+U1 → 与第一轮缓存完全一致。
  2. 命中最长公共前缀:S+U1
  3. 直接复用这一段的 KV,无需重新计算 S+U1。
  4. 仅需对新增的 A1+U2 进行 Prefill 补算。
  5. 然后将更长的 S+U1+A1+U2 重新存入缓存,覆盖旧的。

第三轮

  1. 请求:S+U1+A1+U2+A2+U3
  2. 拿它从头与第二轮缓存进行前缀匹配。
  3. 第二轮缓存为:S+U1+A1+U2
  4. 第三轮的开头正好是:S+U1+A1+U2,Token 完全对齐。
  5. 直接完整命中第二段缓存,无需重算前面大部分内容,仅补算后面的 A2+U3

只要新请求的开头与缓存中的开头存在一长截完全一致的 Token 前缀,就能命中这一部分,无需重新计算。

缓存的前缀是可以复用,而非要求整段等长才能复用。例如,缓存存储了:1234,新请求前缀为:1234567

虽然长度不同,但开头 1234 完全一致,因此直接复用 1234,仅需计算后面的 567

这就是多轮对话能够每轮都复用前一轮缓存的根本原因。

总结

掌握大模型处理messages的完整流程,从数组拼接到Token序列化再到KV缓存计算,让我们能更深刻地理解提示词缓存的工作原理。在实际的Agent应用中,合理组织提示词、优化前缀结构,可以充分利用缓存机制,降低响应延迟和计算成本。