Chapter 02

原理:它怎么工作,以及为什么是新品类

01 章命名了零件:data plane、三类流量、四件套、backend 多态、policy。这一章让零件动起来——先走一遍请求生命周期,再回答全书的核心问题:为什么这套机制没法用旧网关 retrofit,而要从零重写。

本章你将建立的 schema

  • 一条 agent 请求穿过网关的完整生命周期,以及 token usage 为什么只能在最后提取
  • 「body-aware 而非 header-aware」——经典网关 retrofit 不了的根本机制,及三条备选路线为何被放弃
  • MCP multiplexing 怎么把多个 server 聚合成一个端点,代价是什么
  • LLM 感知的路由与 failover:为什么按 token 限流、为什么只在 429 才驱逐节点
  • 语义缓存为什么既省钱又危险(locality 与 avalanche 的结构性矛盾)
  • guardrails 为什么要在 4 个闸口各跑一次

2.1一条请求的生命周期

把 01 章的零件按请求实际经过的顺序串起来。一条带工具的流式 LLM 请求,穿过网关时大致经过五步,再原路返回。

客户端 解析 body ① 第一步 入口策略 authn · 限流 按 body 路由 读 model/tool 出口策略 凭据 · guardrails 上游 ← 流式返回:流结束才拿到 token usage,再跑输出 guardrails
图 2.1请求去程读 body、回程读 usage。 注意:朱红的两处——解析 body 在最前,提取 usage 在最后。计费所需的 token 数,要等整个流式响应结束才拿得到。这个时序错位是后面好几个失败模式的根源。

五步里,第一步「解析 body」就是这个品类的分水岭。经典网关在这一步只看 header;agent 网关必须拆开 body,因为路由依据(model、tool 名)、限流依据(token 数)、策略依据(prompt 内容)全在里面。下一节把这件事讲透。

2.2核心机制:body-aware,而非 header-aware

Agent 网关把请求体当作一等路由与策略依据;经典网关把请求体当不透明信封,只读 header。

为什么这是分水岭

经典 API 网关的高性能,建立在一个假设上:决策只需要 header(host、path、method、token),body 是要原样转发的二进制负载,碰都不碰。这个假设让它能零拷贝地转发巨大的 body。但 agent 流量把这个假设打穿了——决定「转给哪个模型」的 model 字段、决定「扣多少额度」的 token 数、决定「要不要拦」的 prompt 文本,统统在 body 里。网关不拆 body,就什么决策都做不了。

底层机制(比文档深一层):body-aware 不是「多解析一下 JSON」这么轻。它连带改变了三个底层假设——

  • 计量单位:经典网关按「请求数」计量(QPS、RPM)。但一条 LLM 请求小到 100 个 token,大到 10 万个;按请求限流形同虚设。必须按 token 计量(TPM),而 token 数要等响应流结束才知道(从响应尾部的 usage 块提取)。
  • 连接形态:经典网关假设「请求-响应」彼此独立、短暂。但 MCP 是有状态、双向、长连接的;LLM 流式响应是长时间持续的。把这些塞进无状态模型,要么破坏流式,要么累积状态把网关拖垮。
  • 解析成本在热路径上:拆 body、数 token、跑 guardrails,这些都发生在每条请求的延迟关键路径上。语言运行时的一次 GC 暂停,就会卡住正在吐字的流式响应。
经典 API 网关 header host · path · auth ↑ 只读这里 body { ... } 不透明信封 网关不解析 Agent 网关 header 照常转发 body(解析) model — 路由依据 prompt — guardrails token — 限流依据 tool — 工具路由
图 2.2同一个请求,两种看法。 注意:朱红高亮的区域从左卡片的 header 整个挪到了右卡片的 body——这一处迁移,就是「为什么需要一个新品类」的全部答案。

这三个假设全被打穿,意味着无法靠「在 Envoy 上加个插件」补齐。Solo.io 的工程团队对此的选择,是用 Rust 从零写一个数据平面(就是 agentgateway),而不是扩展 Envoy。三条路线的取舍如下。

表 2.1 · 承接 agent 流量的三条路线
方案优势为什么没选 / 选它
扩展 Envoy(ext_proc / WASM 外挂) 复用 Envoy 成熟生态与运维经验 body 解析、有状态双向都是外挂在无状态内核上,延迟与脆弱性叠加;托管语言的 GC 暂停会卡住流式响应
复用 service mesh(Istio 等) 已有 mTLS、重试、东西向流量治理 没有 model / tool / agent 语义,body-blind,仍按请求计量——等于没解决核心问题
用 Rust 从零写数据平面 body 一等解析、无 GC 暂停、有状态原生支持 选中——代价是放弃 Envoy 的插件生态,一切重新长
洞察 · 全书的核心就在这

「为什么不给 Kong 装个插件就行」的答案,到这里完整了:因为 agent 流量打穿的不是某个功能点,而是经典网关三个最底层的假设(计量单位、连接形态、解析位置)。插件能加功能,改不了内核假设。记住这一点,本教程的核心就拿到了。

想一想

有人提议:「LLM 调用方在 header 里塞一个 X-Model: gpt-4o,网关读 header 路由不就行了,何必解析 body?」这个方案能成立吗?它在什么情况下崩掉?

展开答案(先停 10 秒)

路由这一项勉强能用,但只解决了三件事里的一件。token 限流还是要数 body 里(以及响应里)的 token;guardrails 还是要看 body 里的 prompt;语义缓存还是要算 prompt 的向量。把这些都搬到 header,等于让客户端自己算 token、自己判断注入——网关存在的意义就没了。

更现实的崩点:客户端塞的 header 不可信。X-Model 可以伪造、可以和 body 里真实的 model 不一致。权威信息源是 body,header 只是它的影子。

2.3MCP multiplexing:多个 server 聚合成一个端点

网关把多个后端 MCP server 聚合到一个端点,合并它们的工具清单,并给工具名加前缀防冲突。

为什么需要它

一个 agent 往往要用十几个 MCP server(文件、数据库、搜索、各种 SaaS)。让 agent 直连每一个,意味着 agent 侧要管十几条连接、十几套鉴权。网关把它们聚合成一个端点,agent 只连这一个;增删后端 server 对 agent 透明。

底层机制(比文档深一层):聚合的难点不在「转发」,在「命名」。两个 server 都有一个叫 search 的工具时,agent 看到两个 search 就会调错。网关的做法是给工具名加 server 前缀(如 github__issue_read、fs__read_file):

  • 发现:网关向所有后端 server 扇出 tools/list,把返回的工具清单合并。
  • 命名:给每个工具名加上来源 server 的前缀,消除冲突。
  • 路由:agent 调 github__issue_read 时,网关按前缀剥离、定位到 github server、转发。
  • 传输桥接:agent 侧走 Streamable HTTP,后端 server 常是本地 stdio——网关在两种传输间桥接,并用 MCP-Session-Id 头维持会话。
Agent 连一个端点 MCP backend 聚合 + 加前缀 github server tool: issue_read fs server tool: read_file search server tool: query github__ fs__ search__
图 2.3一个端点扇出到三个 server,工具名被加上 server 前缀。 注意:前缀(github__ / fs__ / search__)既是防冲突的命名,也是回程路由的依据——网关靠它把 tools/call 送回正确的 server。
带来的代价

前缀和聚合不是免费的。① 前缀拉长了工具名,而所有工具的定义会在对话开始时一次性塞进模型上下文——server 一多,光工具描述就能吃掉几万 token。② 某个后端 server 的工具清单变了(notifications/tools/list_changed),客户端若不处理这个通知,就会拿着过期的工具名反复调用。这两点在 03 章会作为具体失败模式展开。

2.4LLM 感知的路由、failover 与缓存

网关按 token 限流、在多个供应商间路由与故障转移,并可对相似 prompt 命中语义缓存。

底层机制(比文档深一层):三件事,每件都和经典网关的直觉不同。

① 按 token 限流,而非按请求

网关从响应里抽取 token 计数做限流。这里有个反直觉的细节:token 不止「输入/输出」两种,而是五种独立计量——输入、输出、缓存创建、缓存命中的输入、推理 token 等,各家供应商口径还不一样。更麻烦的是,流式响应的 usage 块在整个流结束时才到,所以限流是「先放行、后结算」,中间有一个计量窗口。

② Failover:只在 429 才驱逐节点

多供应商负载均衡常用 Power-of-Two-Choices(随机挑两个、选更优的那个)。但故障转移的触发条件有个尖锐的坑:agentgateway 只在收到带 Retry-After 的 429(限流)时才驱逐一个供应商节点;5xx、超时、DNS 失败只会降低它的健康评分,不会把它移出轮转——于是流量还在往一个坏掉的节点上送。把「failover」想当然地等同于「5xx 自动切换」,在这里会出事。

表 2.2 · 限流与 failover 放在哪一层
方案优势为什么没选 / 选它
按 RPM(请求数)限流 实现简单,经典网关现成 1 请求 = 100~100k token,按请求数限流约束不住真实成本
在每个 app 的 SDK 里做 failover 无网关跳数,延迟低 每个 app 重复实现,策略分散,无法统一管控成本与配额
网关按 TPM + 429 感知做 failover 一处管控、token 级配额、跨供应商统一 选中——代价是 usage 要等流结束才结算,且要清楚 429 之外的故障不自动驱逐

③ 语义缓存:既省钱又危险

语义缓存把 prompt 嵌入成向量,与缓存里的向量比相似度,超过阈值就直接返回缓存的回答,省掉一次模型调用。它的危险来自一个结构性矛盾:语义缓存依赖「相似文本 → 相邻向量」的局部性(locality),而安全的缓存键需要的是「输入微小变化 → 输出剧烈变化」的雪崩效应(avalanche)。两者直接冲突。后果:

  • 阈值定高(如 0.98):误命中少,但命中率低,省不了多少。
  • 阈值定低(如 0.85):省得多,但「重置密码」会误命中「修改邮箱」的缓存答案,而且返回时是 200、看不出错。
  • 更糟的是攻击面:有研究表明,攻击者能构造语义不同、但向量落在阈值内的输入,强行命中、读到另一个用户的缓存回答——多租户下成了越权泄露。
带来的代价

语义缓存不是「免费加速」。它把一个正确性 / 安全性问题引入了系统:缓存键必须混入租户 ID、角色、模型、温度等,否则就是跨用户串答。能不能开语义缓存,取决于业务能不能容忍「偶尔返回一个相似但不对的答案」。

想一想

一个供应商连续返回 500,但你的网关没把它切走,流量还在往它送。结合本节,问题出在哪个机制的哪个假设上?

展开答案(先停 10 秒)

出在 failover 的驱逐条件上:只有带 Retry-After 的 429 会触发驱逐,500 只降健康分、不移出轮转。健康分降低让它少拿流量,但「少拿」不等于「不拿」。要让 5xx 也能切走,得显式配置基于错误率的熔断,而不是依赖默认行为。

这正是「把 failover 想当然等于自动切换」的代价——默认策略只覆盖了限流这一种故障。

2.5Guardrails:为什么是 4 个闸口

Guardrails 不在单点运行,而在请求生命周期的 4 个闸口各检查一次:输入、构造 prompt、工具调用、输出。

为什么不能只在入口检查一次

直觉是「在入口把脏输入挡掉就行」。但 prompt 注入最危险的形态,不是用户直接输入的——而是藏在一份被检索回来的文档、或一个工具返回的结果里,在请求中途才进入 prompt。只在入口设一道闸,挡不住中途注入。

底层机制(比文档深一层):更前沿的做法不是「关键词黑名单」,而是把请求按信任来源分段(系统 > 开发者 > 用户 > 检索文档),在管线中途运行,做跨段污染监控——本质上是把「控制流完整性」这个系统安全概念搬到了 prompt 上。关键词黑名单是内容分类,容易误杀;按信任来源分段是结构判断,更准。

同一条请求,guardrails 在 4 个点各跑一次 ① 输入 用户输入注入 ② 构造 prompt 拼接时污染 ③ 工具调用 检索结果注入 ④ 输出 PII 泄露 ③ 最反直觉:注入这里才从检索文档进来,入口闸挡不住
图 2.4四道闸沿请求生命周期排开。 注意:朱红的是第 ③ 道——工具/检索结果是注入最常钻进来的地方,而它在入口闸(①)之后,所以「入口检查一次」必然漏。

2.6综合:五个机制在一条请求里怎么协同

把本章机制和 01 章概念合到一条真实请求上(这一步刻意把分散的知识拧成一股)。场景:一个 agent 发起带工具调用的流式 LLM 请求,经过 agent 网关。

  1. 解析 body(§2.2)→ 拿到 model、prompt、声明的 tools。这是后续一切的前提。
  2. 入口 policy(§1.5)→ authn 通过、查 TPM 配额(§2.4 ①);此刻还不知道最终 token 数,先按预估放行。
  3. guardrails 闸 ①②(§2.5)→ 检查用户输入、检查拼好的 prompt。
  4. 按 body 路由(§1.3 + §2.4 ②)→ 按 model 选供应商,P2C 负载均衡;若命中 429 则驱逐换节点。
  5. 工具调用触发 → 流量转成 MCP(§1.2),经 multiplexing(§2.3)路由到对应 server;guardrails 闸 ③ 检查工具返回。
  6. 流式返回 → 边转发边过 guardrails 闸 ④(§2.5);流结束,从 usage 块拿到真实 token 数,回填 TPM 配额(§2.4 ①)。

注意第 2 步和第 6 步的时序矛盾:配额要在第 2 步先放行,真实用量第 6 步才回来。这个「先放行、后结算」的窗口,是 03 章「token 计数被绕过」失败模式的根。

§本章 self-check

先合上教程,把答案写在纸上或编辑器里。写完再点开对照。

  1. 「body-aware」连带改变了经典网关的哪三个底层假设?(各一句)
  2. 为什么扩展 Envoy 这条路被放弃,而选择用 Rust 从零重写?(说出至少两个技术原因)
  3. MCP multiplexing 里,「工具名前缀」同时承担了哪两个作用?
  4. 语义缓存的「locality 与 avalanche 矛盾」具体指什么?为什么它让语义缓存变成一个安全问题而不只是性能问题?
  5. (跨机制综合)一条带工具的流式请求里,TPM 配额为什么会出现「先放行、后结算」的窗口?哪两个机制的时序凑在一起造成了它?
答案(先做完再展开)
  1. ① 计量单位:从「请求数」变成「token 数」,且 token 要等流结束才知道。② 连接形态:从「短暂、独立的请求-响应」变成「有状态、双向、长连接」。③ 解析位置:body 解析 / 数 token / 跑 guardrails 都落在每条请求的延迟热路径上。
  2. 至少两个:① body 解析与有状态双向是外挂在 Envoy 无状态内核上,延迟与脆弱性叠加;② 托管语言的 GC 暂停会卡住流式响应,Rust 无 GC 暂停;③ 复用 Envoy 等于把核心问题(body-blind、按请求计量)留着没解决。
  3. ① 防冲突命名:两个 server 都有 search 时,加前缀避免 agent 调错。② 回程路由依据:网关靠前缀把 tools/call 剥离、定位回正确的源 server。
  4. 语义缓存要靠「相似输入 → 相邻向量」的局部性才能命中;而抗碰撞的缓存键要的是「输入微变 → 输出剧变」的雪崩效应——两者结构上对立。后果不只是误命中(性能),而是攻击者能构造落在阈值内的不同输入,命中并读到别人的缓存回答(安全)。所以缓存键必须混入租户 ID 等。
  5. 因为 token 真实用量在响应流结束时才从 usage 块拿到(§2.4 ①),而限流闸必须在请求放行前就决定放不放(§1.5 入口 policy)。一个要「先」、一个只能「后」,中间就是先放行、后结算的窗口。这个窗口可被「提前断开连接、不让 usage 回来」利用,变成限流绕过。
进阶挑战 · 刚好够不着

给「先放行、后结算」的窗口设计一道补丁

已知:TPM 配额先按预估放行,真实用量流结束才回填;攻击者可提前断开连接让 usage 永不回来,从而不被计费、绕过限流。不看 03 章,设计一个能堵住这个窗口的机制。你的方案会牺牲什么?

提示(卡住再展开)

方向之一:不依赖 usage 块,改用「按已转发字节估算 token」做兜底计量,客户端断开也照扣。或者:强制请求带 include_usage、对未回 usage 的请求按预估上限扣。两者都用「估算」换「即时性」——牺牲的是计量精度。03 章会对照真实做法。