为什么 Agent 开发中可能更适合用 completion 接口? —基于KV缓存显式管理(复用)的分析

November 27, 2025

一、completionchat/completion接口简单回顾

首先,我们先理清这两个接口的区别:

1) completion接口​(如/v1/completions,这是更原始的接口)

输入: "Once upon a time, there was a princess"
输出: "who lived in a tall tower..."
  • 输入:一个完整的文本字符串(prompt)
  • 输出:模型生成的续写文本
  • 本质:纯粹的文本续写

    你给模型一个完整的、连续的 token 序列,模型返回后续 tokens。

2) chat/completion接口​(如/v1/chat/completions,这是再抽象后的接口)

message: [
  {"role": "system", "content": "You are a helpful assistant"},
  {"role": "user", "content": "Hello"},
  {"role": "assistant", "content": "Hi!"},
  {"role": "user", "content": "How are you?"}
]
  • 输入:一个messages数组,包含多轮对话历史(含系统提示用户历史请求assistant的历史思考、历史答复、历史工具请求历史工具返回用户最新请求等...)
  • 输出:assistant的最新回复
  • 本质:对话续写

    服务商在服务端将 messages 数组转换为实际的 prompt 字符串(通过 chat template),然后再做文本续写


二、从KV缓存管理(复用)的角度分析两者显著差异:

1) completion接口的优势:

  • 用户完全控制prompt的构造

当然,也要承担对不齐模型聊天模版的风险:使用completion接口需要自己对齐 chat template 进行prompt构建,如果格式对不上,模型表现会下降。

  • 可以精确控制哪些部分是"前缀",从而更好地利用prefix caching
  • 服务商可以根据prompt的公共前缀进行KV缓存复用
  • 用户可以设计prompt格式来最大化缓存命中
特性说明
完全透明你构造的 prompt 就是实际送入模型的 token 序列
精确控制你可以精心设计 prompt 结构,让可复用部分放在前缀位置
可预测性相同的 prompt 前缀 → 相同的 KV Cache → 确定性复用

2) chat/completion接口的特点:

  • 服务商在服务端将messages数组转换为实际的prompt(套用各角色(systemuserassistanttool等)标记的聊天模版,然后拼接+可能的裁切
  • 这个转换过程对用户是不透明的

特别地,有些模型的chat template聊天模版,会裁切历史的消息记录(比如reasoning_content),会直接导致后续请求中,不仅历史KV缓存无法完整复用,更麻烦的是导致多轮的复杂智能体开发中的上下文丢失问题!(再请求时,已经丢失了上一步中已经完成的思考,信息有丢失)

# 第一次请求
messages: [system, user1, assistant1(含reasoning、content、tool_call), ..., user2]
→ 实际prompt: "...<think>xxx</think>..."  ← 包含推理过程

# 第二次请求(假设服务商裁切了reasoning_content)
messages: [system, user1, assistant1(不含reasoning), user2, assistant2, user3]
→ 实际prompt: "..."  ← 前缀已变,KV Cache 失效 + 历史思考丢失!
  • 不同厂商的chat template不同
  • 用户无法精确控制最终的prompt结构
问题影响
黑盒转换`messages → actual_prompt` 的 chat template 对你不透明
格式依赖不同厂商模板不同(ChatML、Llama-style、Claude-style...)
缓存更易不可控你无法精确预测最终的 token 序列,难以设计缓存策略
元数据开销角色标记、特殊token、裁切规则会打断你精心设计的前缀连续性

3) KV缓存复用的关键点:

  • Prefix Caching​:LLM推理时,如果多个请求有相同的前缀,可以复用前缀部分的KV缓存,避免重复计算(再推理相同前缀部分的KV),降低算力耗费和api费用。
│  [System Prompt] [User contetent] [Assistant content] [Tool call] [Tool_response]...│ [User new query] │
│  ←──────────────────可缓存复用的部分───────────────────────────────────────────────→ │      <新计算>      │

对于chat/completion接口:服务商内部会将messages数组转换为prompt,再转换为tokenid,最后送去模型推理(list[dict]->str->tokenid),这个不透明的过程(list[dict]->str)会影响KV缓存的复用效率。而且不同厂商可能有不同的转换策略,导致即使messages看起来相似,最终生成的实际prompt也可能存在微妙差异。这种不确定性会降低缓存命中的可能性,增加计算开销。


三、总结:便利性 VS 控制权

Transformer 本质:P(next_token | previous_tokens)

从模型角度,根本不存在"对话"这个概念——它只看到一个 token 序列(previous_tokens),然后预测下一个 token。

无论是completion还是chat/completion,最终都是将输入转换为tokenid序列,预测下一个token。这意味着接口的差异主要在于抽象层的便利性,而非底层能力。

性能选择取决于具体场景。对于需要精确控制和最大化性能的场景,Completion接口提供了更直接的模型交互方式,允许用户精细调整prompt结构,并更有效地利用前缀缓存机制。

相比之下,chat/completion接口提供了更标准化、更简单的API,服务商可以针对聊天场景进行专门优化(开发者不需要关心 prompt 模板)。

Completion:     你的 prompt ──────────→ 模型 ──→ 输出
                    ↑
                你完全控制

Chat Completion: messages ──→ [Chat Template] ──→ actual_prompt ──→ 模型 ──→ 输出
                                    ↑
                             服务商控制(黑盒)

从KV缓存显式管理(复用)的角度看,哪种方式更优呢?

-> 从纯效率/控制角度:确实completion接口更优

结论:chat/completion是对completion的一个抽象层,它以便利性换取了控制权

而Agent 是一个“多轮推理 + 工具协作 + 长上下文”的系统,需要对状态、前缀和 KV Cache 有极强的可控性,而 Completion 才是唯一能把这种可控性完全交还给开发者的接口。

值得注意的是,很多服务商已经在 Chat Completion 接口上额外新增了显式暴露缓存控制能力(如Anthropic里的cache_control 参数),这是在chat/completion的抽象层上补回completion接口的控制能力,说明业界也认识到了纯chat/completion抽象的局限性。