
我越是使用通过提供商公开的 API 来处理大型语言模型,就越觉得我们把自己置于一个相当糟糕的 API 接口区域。它可能并非底层运行机制的正确抽象方式。我现在倾向于将这个问题视为一个分布式状态同步问题。
大型语言模型的核心在于:它接收文本,将其分词成数字,然后将这些数字输入到GPU上的一系列矩阵乘法和注意力层中。利用一组固定的权重,它生成激活值并预测下一个分词。如果没有温度(随机化)机制,你可以认为它至少在理论上是一个更具确定性的系统。
就核心模型而言,“用户文本”和“助手文本”之间并没有什么本质区别——一切都只是标记。唯一的区别在于一些特殊的标记和格式,它们编码了角色(系统、用户、助手、工具),并通过提示模板注入到数据流中。您可以查看 Ollama 上不同模型的系统提示模板来了解这一点。
基本代理状态
我们暂且忽略现有的 API,只考虑智能体系统中通常发生的情况。如果我的 LLM 在同一台机器上本地运行,仍然需要维护状态,但这种状态仅限于本地。对话历史以 token 的形式保存在 RAM 中,模型则在 GPU 上维护一个派生的“工作状态”——主要是基于这些 token 构建的注意力键值缓存。权重本身保持不变;每一步变化的是激活值和键值缓存。
从心智模型的角度来看,缓存意味着“记住你对给定前缀已经完成的计算,这样你就不必重新计算”。在内部,这通常意味着将这些前缀标记的注意力键值缓存存储在服务器上,并允许你重复使用它,而不是直接将原始的 GPU 状态传递给你。
这其中可能还有一些我忽略的细微之处,但我认为这是一个相当不错的思考模型。
完成 API
一旦你开始使用像 OpenAI 或 Anthropic 这样的自动补全型 API,就会引入一些抽象层,使情况与这个非常简单的系统有所不同。首先,你实际上并没有发送原始的 token。GPU 处理对话历史的方式和你查看对话历史的方式处于截然不同的抽象层次。虽然你可以在等式的一侧对 token 进行计数和操作,但还有一些额外的 token 被注入到你无法看到的流中。其中一些 token 来自将 JSON 消息表示转换为输入到机器的底层 token 的过程。此外,还有一些工具定义之类的东西,它们以专有的方式注入到对话中。还有一些带外信息,例如缓存点。
除此之外,还有一些你永远看不到的令牌。例如,对于推理模型,你通常看不到任何真正的推理令牌,因为一些逻辑层级模型(LLM)提供商会尽可能地隐藏这些信息,以防止你使用他们的推理状态重新训练自己的模型。另一方面,他们可能会提供一些其他信息文本,以便你可以向用户展示一些内容。模型提供商也喜欢隐藏搜索结果以及这些结果是如何注入到令牌流中的。相反,你只会收到一个加密的数据块,你需要将其发送回去才能继续对话。突然之间,你需要将一些信息从你的端传递回服务器,以便双方的状态能够协调一致。
在自动补全式 API 中,每一轮对话都需要重新发送整个提示历史记录。虽然每次请求的大小会随着对话轮数线性增长,但长时间对话中累计发送的数据量会呈二次方增长,因为每一步都需要重新传输线性大小的历史记录。这也是长时间聊天会话成本越来越高的原因之一。在服务器端,模型对该序列的注意力成本也会随着序列长度呈二次方增长,因此缓存机制开始变得至关重要。
响应 API
OpenAI 尝试解决这个问题的方法之一是引入 Responses API,它会在服务器端维护对话历史记录(至少在启用了保存状态标志的版本中是这样)。但现在的情况很奇怪,你需要完全处理状态同步:服务器端有隐藏状态,而你的设备端也有状态,但 API 提供的同步功能非常有限。到目前为止,我仍然不清楚对话究竟能持续多久。如果出现状态分歧或损坏会发生什么也不清楚。我遇到过 Responses API 卡住无法恢复的情况。如果出现网络分区,或者一方收到了状态更新而另一方没有收到,又会发生什么也不清楚。至少就目前而言,启用了保存状态的 Responses API 使用起来相当困难。
显然,这对 OpenAI 来说非常棒,因为它允许他们隐藏更多幕后状态,否则这些状态必须通过每条对话消息传递出去。
状态同步 API
无论你使用的是自动补全式 API 还是响应式 API,提供商始终需要在后台注入额外的上下文信息——提示模板、角色标记、系统/工具定义,有时甚至是提供商端的工具输出——这些信息永远不会出现在你可见的消息列表中。不同的提供商处理这些隐藏上下文的方式各不相同,而且对于如何表示或同步这些上下文信息也没有通用的标准。其底层实际情况远比基于消息的抽象概念所呈现的要简单得多:如果你自己运行一个开放权重模型,你可以直接使用令牌序列来驱动它,并设计出比我们标准化的 JSON 消息接口简洁得多的 API。当你使用像 OpenRouter 这样的中间件或像 Vercel AI SDK 这样的 SDK 时,复杂性会进一步增加,这些中间件试图掩盖提供商之间的差异,但无法完全统一每个提供商维护的隐藏状态。实际上,统一 LLM API 最困难的部分并非用户可见的消息,而是每个提供商都以不兼容的方式管理着各自的部分隐藏状态。
关键在于如何以某种形式传递这些隐藏状态。我理解从模型提供者的角度来看,能够对用户隐藏某些信息是件好事。但是同步隐藏状态很棘手,而且据我所知,这些 API 的设计初衷并非如此。或许我们应该开始思考状态同步 API 应该是什么样子,而不是基于消息的 API。
我越是使用这些代理,就越觉得我其实并不需要统一的消息API。它目前基于消息的核心理念本身就是一种抽象概念,这种概念可能经不起时间的考验。
先向本地学习?
有一个完整的生态系统曾经处理过类似的问题:本地优先运动。他们花了十年时间研究如何在互不信任、会离线、分叉、合并和修复的客户端和服务器之间同步分布式状态。点对点同步和无冲突复制存储引擎之所以存在,是因为“共享状态但存在间隙和分歧”是一个难题,仅靠简单的消息传递是无法解决的。他们的架构明确地分离了规范状态、派生状态和传输机制——而这正是目前大多数本地生命周期管理 (LLM) API 所缺乏的分离。
其中一些想法与模型出奇地吻合:KV 缓存类似于可以进行检查点和恢复的派生状态;提示历史记录实际上是一个仅追加的日志,可以增量同步而不是整体重新发送;提供者端不可见的上下文的行为就像一个带有隐藏字段的复制文档。
但与此同时,如果远程状态因为远程站点不想保留那么长时间而被清除,我们希望能够从头开始完全重放它——例如,目前的响应 API 就不允许这样做。
未来统一API
关于统一基于消息的 API 的讨论很多,尤其是在 MCP(模型上下文协议)出现之后。但如果我们真的要制定任何标准,都应该从这些模型的实际行为出发,而不是从我们沿用的表面约定出发。一个好的标准应该考虑到隐藏状态、同步边界、重放语义和故障模式——因为这些都是实际存在的问题。我们总是存在这样的风险:急于将当前的抽象形式化,反而锁定了它们的弱点和缺陷。我不知道合适的抽象形式应该是什么样的,但我越来越怀疑现有的解决方案是否真的合适。