ChatGPT 如何优化其代理循环:Harness、API 与推理

8
分类技术博客
作者ByteByteGo
来源跳转
发表时间

内容

AI 实验室的进展比以往任何时候都更快,正在发布我们见过的最强模型。就在最近,Anthropic 发布了 Fable 5,随后 OpenAI 发布了 GPT-5.6 系列,包括他们迄今最强的模型 GPT-5.6 Sol;Kimi 发布了 Kimi K3;而 Opus 5 也在几天前上线。

但能力只是问题的一半。另一半是这些模型完成任务要花多少钱,也就是每次成功任务成本。更低的成本意味着用户更负担得起,也意味着提供方成本更低。实验室内部投入了大量精力,去优化 AI 应用的每一个组件和层级,让它们更高效,从而降低总体成本。比如,开启最大推理能力的 GPT 5.6 Sol 在 Artificial Analysis Coding Agent Index 上的得分高于 Fable 5,但成本不到后者的一半。

为了了解前沿实验室采用了哪些技术来提升 AI 应用效率,我们采访了将多种效率优化技术开发并落地到 Codex 和 ChatGPT Work 背后系统中的 OpenAI 工程师。感谢 Joe、Ahmed、Steve、Matthew 和 Philippe 与我们分享了许多宝贵细节。

在本文中,你将了解:

  • 当你向 AI agent 发送请求时,实际发生了什么
  • harness 层如何通过持久化 WebSocket、稳定的 prompt 前缀、延迟工具发现和 Code Mode 来减少重复工作
  • API 层如何只对增量部分进行 token 化,并将安全检查与推理并行执行
  • 推理层如何通过缓存感知路由、KV cache 管理、投机解码以及将 prefill 与 decode 分离,充分榨取 GPU 价值
  • OpenAI 从这一切中学到了什么,以及你可以将哪些经验应用到自己的系统中

效率优化技术概览

Agentic AI 应用的结构剖析

当你给 Codex 或 ChatGPT Work 下达一个任务,比如修复一个 bug,查询并不会直接送到 LLM。事情比这复杂得多。像 Codex 这样的 AI 应用并不只是一个 LLM,它是一个系统,由多个组件构成。用户请求会先经过多个层级,之后 LLM 才会看到哪怕一个 token。

像 Codex 这样的 AI 应用由多个组件构成。LLM 只是其中之一。

但为什么不能直接把用户请求发给 LLM 呢?要理解这一点,先看看 LLM 到底是什么。

LLM 是一个经过训练、用于预测下一个 token 的神经网络。它以一串 token 作为输入,并输出一串 token。它不能运行 shell 命令,不能编辑文件,也不能在多次调用之间记住任何东西。但像“修复这个 bug 并运行测试”这样的 agent 任务,本质上大多是动作。必须有某种机制把模型预测出的 token 变成真实命令,把结果再喂回去,并持续执行,直到任务完成。

这就是 harness 层 的职责:它是构建在 LLM 之上的系统,负责处理这些工作。它将用户任务作为输入,决定在上下文中包含哪些指令、哪些工具定义以及多少历史记录,并维护对话历史。当模型返回一个工具调用时,harness 会在沙箱环境中按照批准策略执行该调用,追加结果,然后把对话重新发送给 LLM。

harness 负责构建上下文并执行工具

但即便 harness 发出的请求也不会直接送到 LLM。在面向生产的应用中,harness 和推理端点之间还隔着一个 API 层(应用层)。之所以需要这一层,是因为真实产品要处理一些 harness 层或 LLM 本身无法覆盖的事情。例如,对调用方进行身份认证、执行限流,以及——尤其重要的是——把文本转换成 LLM 所期望的 token ID,并将生成的 token 再转换回文本。

一旦 API 层把上下文准备好并将其 token 化为一串 token ID,就轮到 LLM 处理输入了。这就是推理层。它是由 GPU 集群支撑的远程端点,承载着模型。它的任务是在准备好的 token 上尽可能高效地运行模型计算,并生成响应,无论响应是新的工具调用还是最终答案。随后再把生成的 token 返回给 API 层。

agentic AI 应用中,请求会穿过的三个层级。

当你向 AI agent 发送请求时,会发生什么?

现在我们已经理解了 agentic AI 应用的三个主要层级,接下来用一个具体例子看看请求是如何在这些层级之间流转的。假设你让 Codex “追踪 checkout 回归问题,打补丁,然后运行测试”。流程如下:

  1. harness 将指令、工具定义和你的任务组装成一个请求,并发送给 API。
  2. API 将请求体缓冲到内存中,解析 JSON 并进行校验:请求格式正确,且所选模型支持它要求的所有功能。接着检查调用者身份、施加速率限制,并运行预检。
  3. API 将对话渲染为模型输入格式,并进行 token 化。
  4. API 将 token 发送到推理层,同时启动安全检查:分类器会检查诸如网络攻击和生物武器内容等风险。安全检查会尽量在第一个生成 token 返回之前完成。
  5. 推理层处理 prompt 并开始生成。这里的输出不是最终答案,而是一个工具调用:搜索代码中是否有“checkout timeout”。
  6. 生成的 token 流回 API
  7. API 将 token 转换成文本,并包装成 API 事件
  8. API 将这些事件流式发送给 harness。
  9. harness 识别出这是一个工具调用,在沙箱中执行搜索,将输出追加到对话中,再把更新后的对话发送回 API。

从用户请求到最终摘要返回

这只是一次迭代。实际运行中,一个任务会重复这个循环很多次,直到模型生成最终摘要,harness 再把控制权交还给用户。每一轮迭代都会重新做上面提到的大量重复工作。例如,历史记录会再次发送,文本会再次进行 token 化,或者 prompt 会重新处理。消除这些重复劳动,正是优化的主要机会。接下来的三部分将介绍 OpenAI 为让每一层更高效而采用的优化技术。

Harness 优化

harness 是最靠近用户的编排层,它把原始用户请求转换成上下文,并以循环方式与 LLM 交互,直到任务完成。它有两个主要职责。

第一,它是对话的唯一事实来源。它保存每一条指令、消息、工具调用和工具结果的权威记录。第二,它运行 agentic 循环。它决定哪些内容进入发送给 LLM 的上下文(哪些指令、哪些工具定义、以及多少历史记录)。它发送请求、接收流式响应、解析响应并监听工具调用。一旦出现工具调用,它就会在沙箱环境中按照批准策略执行该调用,追加结果到对话中,然后再把它发送回 LLM。如此循环,直到任务完成。重任务理论上可能重复超过 100 次。每次迭代都会带来额外开销。比如,每次模型调用多 1 秒,长任务就可能多花大约半分钟。

agentic 循环周期

如何让 harness 层更高效?

从 harness 层的角度看,延迟主要来自四个方面:网络、prompt 处理、上下文内容,以及循环往返次数。OpenAI 在 harness 层采用了以下四种技术来提升效率。

1. 持久化 WebSocket 与增量请求

harness 运行在用户机器上,而模型运行在 OpenAI 的数据中心,因此每次模型调用都意味着一次网络交换。完成这种交换的标准方式是 HTTPS。harness 会先建立连接,发送包含服务器(API 层)所需全部内容的请求,然后接收响应。由于语言模型的响应是逐 token 到达的,聊天应用通常会在 HTTPS 之上使用 Server-Sent Events(SSE),即客户端发送一次请求,服务器通过保持开放的响应通道,分块流式返回答案。SSE 是单向通道。它非常适合聊天时代:一个用户消息对应一次模型调用和一次流式答案。但 agent 不是单次往返。一次 Codex 交互就可能包含很多次模型调用。使用 HTTPS 时,每一次调用都是新的请求,且有两项独立成本。

第一项成本是连接建立。每次打开新的 HTTPS 连接,都要先进行 TCP 握手,再进行 TLS 握手,在传输任何有效负载之前就会产生多次网络往返。对单个用户消息付这笔费用尚可接受,但在一次对话轮次内反复支付就很昂贵。

第二项成本是请求载荷本身的重复。HTTP 是无状态的,所以每个请求都必须携带服务器所需的一切。例如,指令、工具定义以及当前全部对话历史都要放进请求里。到了第 20 次工具调用时,harness 发送的其实已经是原始 prompt、19 次工具调用和 19 次工具结果,只为了在末尾再加一个新结果。于是载荷会随着每次迭代不断变大,harness 也不得不花越来越多时间上传服务器早就看过的数据。

HTTPS 与 WebSocket 对比

那该如何解决这两类成本呢?

连接成本的解决办法是只建立一次连接并保持存活,而不是每次调用都新建连接。WebSocket 正是为此而设计的。WebSocket 只需要一次初始握手,之后双方就可以随时发送消息,而无需为每条消息单独建立连接。Codex 会与 API 建立一个 WebSocket,并在整个轮次中的所有模型调用之间保持它打开。这样就消除了反复进行 TCP 和 TLS 建立连接的开销。

减少载荷重复的办法是停止重传服务器已经知道的内容。harness 会保留上一次请求和已完成的响应。如果变化的只有对话输入,它只发送新增项,并引用前一次响应。一次工具调用之后,socket 上的下一条消息可以小到如下程度:

{
  “type”: “response.create”,
  “previous_response_id”: “resp_abc123”,
  “input”: [
    {
      “type”: “function_call_output”,
      “call_id”: “call_xyz”,
      “output”: “...the new tool result...”
    }
  ]
}

注意,这条消息里没有指令、工具 schema 或历史记录。服务器会基于它保存的状态重建完整上下文,因而模型看到的内容不会变。变化的只是跨网络传输了多少数据。

harness 只发送新的工具结果

2. 稳定的 prompt 前缀

LLM 提供方会使用一种叫做 prompt caching 的技术来避免重复计算。当 prompt 到来时,模型会复用与之前请求匹配的 prompt 开头部分(前缀)所对应的缓存内部状态,只计算剩余部分。这个匹配必须逐 token 精确一致。如果你在 prompt 前部改动一个 token,那么其后的所有内容都必须重新计算。

在 prompt 末尾追加内容可保持缓存有效。

对 harness 来说,这意味着它每次构建的 prompt 开头都必须与上一次一模一样,字节级一致。这听起来很简单,但 harness 每次都会根据内存中的实时状态重新组装请求,因此拼接方式上的任何细微差异都可能悄然破坏匹配。OpenAI 分享了一个例子:Codex 将 MCP 工具定义保存在哈希映射中,而哈希映射并不保证顺序,因此同一组工具在每次请求中序列化出来的顺序可能不同。上下文里的工具是同一批工具,只是顺序变了。Codex 仍然能完成任务,只是代价更高。

这里的效率优化做法,是把历史视为只追加不修改,并将诸如审批设置这类易变的运行时状态排除在 prompt 之外。例如,当用户在会话中途修改审批设置时,harness 不会去改 prompt 中的工具定义,而是在下一次工具运行时由自己应用新策略。这样一来,prompt 保持不变,缓存也就继续有效。

3. 延迟工具发现

稳定的 prompt 前缀能让重复上下文更便宜,但无法让 prompt 本身更小、更简洁。对于能访问大量工具的 agent 来说,这会成为问题。例如,仅工具 schema 就可能占用大量空间,因为算上已连接的集成和 MCP 服务器后,一个 Codex 会话可能暴露出数百个工具。每个工具 schema 都是一个 JSON 对象,包含名称、描述和参数定义。一旦这些内容全部进入 prompt,LLM 在计算时就必须全部处理,即使其中很多对当前任务根本不会用到。

为了把未使用的工具排除在 prompt 之外,OpenAI 采用了一种叫做延迟发现(deferred discovery)的技术。prompt 里只保留核心工具,比如 shell、文件编辑,以及一个 tool_search 工具。其他数百个集成定义完全不进入 prompt。当模型需要某项能力时,它会带着诸如“list deployments”这样的关键词调用搜索工具。harness 会搜索工具目录,匹配到的定义就会加载到上下文中。搜索本身使用的是 BM25,一种词法排序算法,用于查找描述与生成关键词有重叠的工具。

模型在需要工具时才进行搜索

除了延迟发现,还有两种技术也能保持上下文更小:schema 压缩和对话压缩。schema 压缩会在保留参数名的前提下,通过删减描述、压缩嵌套结构,把过大的工具 schema 裁剪到 token 预算以内。对话压缩则把很长的历史浓缩成简短摘要。

4. Code Mode

在常规设置中,模型会先发出一个工具调用,等待结果,再进行推理,然后发出下一个工具调用。很多场景下,这些工具调用之间其实并不需要推理。模型只是一个接一个地发出调用来收集所需结果。这很昂贵,因为每次工具调用都需要一次完整的模型往返。而且每次工具调用之后,其结果都会继续被加入上下文,浪费上下文空间。

使用 Code Mode 时,模型不再逐个发出工具调用,而是编写一段执行这些调用的小程序。harness 会在嵌入式 JavaScript 运行时中运行这段程序,所有工具都以函数形式可用。脚本可以并行发起彼此独立的调用,用普通代码对结果进行筛选和合并,并只返回紧凑的答案。这样,中间数据会留在运行时里,只有最终结果进入上下文。

直接工具调用 vs Code Mode

API 优化

API 层是 harness 所交互的服务。它位于 harness 层与推理层之间,运行在普通 CPU 上。harness 发送的是描述结构化对话项的 JSON,而模型处理和生成的是 token ID。当请求到达时,会经历以下步骤:

  1. 请求体先缓冲到内存中。
  2. 对 JSON 进行解析,并按照 schema 校验(字段是否完整、数组是否在预期范围内等)。
  3. 进行第二轮语义校验。例如,所选模型是否支持请求的功能?
  4. 运行预检:授权和权限、速率限制,以及对请求中任何图片的检查。
  5. API 将对话渲染并 token 化,然后同时启动两个任务:向 GPU 发起推理请求,以及对 prompt 进行安全检查。
  6. 当生成 token 从推理层返回时,API 会把每个 token 转换成文本,包装成 API 事件,并流式发送给客户端。

请求在 API 层内经历的步骤

这些步骤会在循环的每次迭代中重复。推理并不受 API 层控制。API 无法让 GPU 变得更快,它能做的只是尽量减少围绕推理产生的额外延迟。API 层在解析、校验或 token 化上花费的时间,都会直接变成用户感受到的额外延迟。

如何让 API 层更高效?

OpenAI 采用了以下技术,来降低 API 层不同环节带来的开销。

1. 只对增量部分进行 token 化

模型不会直接读取文本。推理开始之前,prompt 必须先转换为 token ID。tokenizer 会以 O(n) 的方式把文本转换成 token ID,线性遍历 prompt 中的每个 token 并替换成对应 ID。在无状态 HTTP 下,每次调用都要重新对整段对话进行 token 化。到了第 20 次工具调用,API 为了提取一个新的结果,已经要重新读取成千上万的 token。GPU 只需要新的工具结果,但 CPU 却像从第一页开始重新读整本书一样,且成本会随着输入长度不断增长。

有了 WebSocket 后,API 会把 token 化后的对话保存在服务器内存中。第一次请求会对完整 prompt 进行 token 化,但后续每次请求只发送新增项。API 只对这部分做 token 化,然后把它追加到已保存的序列中。这样,单次调用的 token 化成本就不再依赖对话长度,而更接近 O(1),取决于增量输入的长度。

无状态 HTTP 与 WebSocket 下的 token 化

2. 并行运行安全检查与推理

agent 在向用户展示输出之前,必须先对每个请求做安全检查。OpenAI 在 API 层实现了安全检查。图片会经过各自的分类器,而 prompt 文本则会送入用于识别危险内容的分类模型,例如网络攻击或生物武器指令。这些分类器都需要时间运行。最简单的设计是先跑安全检查,全部通过后再启动推理。问题在于,这会把延迟加到所有请求的首 token 时间(TTFT)上,而绝大多数请求其实完全无害。

解决办法是让安全检查和推理同时进行。模型在输出第一个 token 之前,需要一段时间来处理 prompt,因此安全检查可以利用这段窗口完成。如果某项检查失败,API 会根据模型类型作出不同反应。对于某些模型,它会先向用户流式输出 token,一旦检查失败就中断流;对于更敏感的模型,它会先扣住输出,等检查全部通过后再释放。无论哪种方式,安全检查的时间都被隐藏在原本就要等待的那段时间里。

并行运行安全检查

3. 将流量路由到更新的 CPU

当一家公司大规模运行服务器时,它的机群里通常会混有不同年份采购的机器,因此 CPU 代际也不同。调度软件通常把同一型号的机器视为相同,但现实并非如此。OpenAI 通过 Kubernetes 查询每个部署背后的实际处理器型号,发现同样的机器标签下混着较老的 Broadwell 芯片和较新的 Ice Lake 芯片。老芯片在承载相同流量时,首 token 时间大约差 20%,CPU 使用量却大约高出一倍。

为了解决这个问题,OpenAI 将更多流量导向配备更新处理器的机器,并开始把 CPU 代际作为容量规划中的显式因素。有时候,最好的软件优化其实是更好的硬件。

推理优化

推理层是模型真正运行的地方。这里有 GPU 和其他加速器组成的机群,以及负责调度它们的服务软件(engine)。模型的核心计算是巨量矩阵运算,它能在加速器提供的成千上万核心上并行执行。该层负责在整个机群中调度和批处理传入请求,保存生成所依赖的每个对话状态,执行模型的前向传播,并把每个生成的 token 返回给 API 层。

每台机器都保存着模型权重以及它所服务对话的 KV 状态。

如何让推理层更高效?

对 token 的需求增长速度超过了硬件扩容速度,因此在这一层,慢和浪费其实是一回事。浪费主要隐藏在四个地方:工作被路由到错误的机器、内存中保存了错误的状态、并行硬件在顺序生成中空转,以及两个不匹配的阶段共享同一批机器。

OpenAI 采用了以下技术来提高推理层效率。

1. 用缓存感知路由平衡负载

OpenAI 的模型部署在很多 GPU 机器上。每个请求都必须被发送到其中一台。如果路由不均衡,有些机器会闲置,而另一些则排着队处理任务。闲置 GPU 是整套系统里最昂贵的浪费。还有一个不那么显眼的问题:每台机器都会保留自己最近服务过的对话的计算状态(缓存)。如果同一个对话的下一次请求落到另一台机器上,这份缓存就没用了。新机器必须从头重新计算,即使这项工作其实已经在别处完成了。

因此,路由器有两个目标:

  1. 均匀分配负载:把请求发到最不忙的机器上。
  2. 利用缓存:把请求发回已经认识这个对话的那台机器。

缓存感知路由

OpenAI 的路由会为每个请求同时权衡这两点,以及地理位置和可用容量等基础因素。仅仅改进负载均衡,就已经大幅降低了模型服务成本。

2. 管理 KV cache

当 transformer 生成一个 token 时,它会关注之前的所有 token。为了避免每生成一个新 token 都重新计算注意力状态,服务系统会把这些状态保存在加速器内存中,这就是所谓的 KV cache。缓存会随着对话长度线性增长,并乘以机群正在服务的并发对话数。对于长上下文,它的体积甚至可能接近模型权重本身。如果软件错误地驱逐了某些状态,等这个对话回来时,推理层就得再次付出完整处理成本。

这里的效率做法是基于真实使用情况来管理缓存。OpenAI 会分析生产环境中的 trace,了解哪些缓存状态很可能还会再次用到;然后不是凭直觉,而是根据这些数据测试驱逐策略,并优化缓存状态在不同内存层级之间的存储与迁移方式。这样既能让最有价值的工作保留更久,又不会把内存耗尽或在搬运数据上浪费时间。

热点对话状态留在 GPU 上,较冷的状态则会迁移或被驱逐。

3. 投机解码

生成是顺序进行的。每个 token 都依赖前一个 token,因此一个能以单次并行计算处理巨大 prompt 的模型,仍然必须一个 token 一个 token 地生成答案。对于大模型来说,即使下一个 token 很容易猜,生成它也昂贵且缓慢。比如输入 “the capital of France is” 时,下一个 token 几乎肯定是 “Paris”,但模型仍然必须处理整个对话并执行大规模矩阵运算才能预测它。

投机解码(speculative decoding)会先用一个小的草稿模型提出接下来的多个 token,然后由大模型在一次并行计算中统一验证它们。在很多情况下,草稿模型给出的候选会被大模型接受,从而节省了大模型逐个生成这些 token 的时间。如果某个候选错了,大模型会丢弃它并生成自己的 token,因此输出质量不会改变。评价草稿模型的主要指标是平均接受长度,也就是主模型每次会接受多少个候选 token。

投机解码

4. 将 prefill 与 decode 分离

推理有两个阶段:prefill 和 decode。prefill 会在一次高度并行的计算中处理完整输入 prompt,并构建模型所需的内部状态(KV cache)。decode 则是一个 token 一个 token 地生成响应。这两者是非常不同的工作负载。prefill 偏计算密集型,而 decode 偏内存密集型,大部分时间都在通过内存流式读取权重和缓存状态。如果把它们放在同一批机器上运行,就等于两者都没能运行在为自己瓶颈量身定制的硬件上。

效率优化的办法是将二者分离。一部分机群负责 prefill,另一部分负责 decode,各自按照自己的瓶颈进行配置。当请求完成 prefill 后,其计算出的状态(KV cache)会被传送到 decode 机器上,由后者逐 token 生成响应。

三层效率优化技术总结

以上就是 agentic AI 应用各层的效率优化技术:harness 避免重复发送服务器已经拥有的数据,API 避免重复处理已经处理过的数据,而推理则避免重新计算已经计算过的数据。最后,我们来看看这些工作中可推广到 OpenAI 之外的经验。

从 OpenAI 推动效率与降低每次成功任务成本中能学到什么

从更宏观的角度看,这些分布在不同层级的优化技术,本质上都是在避免为同一份工作付两次钱。harness 只发送新增内容,并保持前缀稳定,从而让缓存得以保留。API 只对增量部分进行 token 化,并把不可避免的工作隐藏在本来就要等待的窗口中。推理则把对话路由回已有缓存,而不是重新计算。这些层级还会彼此放大收益:当 harness 保持缓存完整、API 提高缓存命中率时,节省最终会落到 GPU 上,也就意味着用户成本更低。除了这些技术本身,OpenAI 工程师还与我们分享了几条经验。

保持设计简单。 优先考虑简单性,而不是复杂性。例如,工具发现用词法搜索而不是 embedding,压缩流程用单一路径而不是菜单式方案。LLM 本身已经相当聪明,因此在它前面再加更多机制,往往只会增加复杂度而不增加价值。先做出最简单可行的方案,再去扩展。

用 agent 自己优化它的栈。 Codex 参与了大量为 Codex 服务的 API 迁移工作,把原本需要几年重写的工作压缩成了两位工程师几个月就完成的事情。推理团队也用它来分析 trace 和原型化 kernel。当 agent 来写代码时,就不再需要那种“方便人类编写”的语言了,你只需要选择运行效率最高的那一种。效率优化正在变成一个能够自我加速的闭环。

端到端优化。 推理团队告诉我们,每当他们挑选一个最喜欢的技术作为重点时,后来都会后悔。只盯住栈中的某一部分,会导致对其他部分投入不足。没有任何单一优化能独自成为决定性突破。真正的大幅收益来自许多小优化的串联。他们还学会了要用与生产环境实际服务的流量形态一致的测试方式,因为在离线环境里看起来是胜利的改动,到了真实流量中可能反而会变差。

评论

(0)
未配置登录方式
暂无评论