跳到主要内容

GraphQL 终于找到了它的客户端,而且它不是人类

· 阅读需 12 分钟
Tian Pan
Software Engineer

GraphQL 的核心赌注是客户端应该能够自主组合其数据需求。在过去的十年里,这个赌注大体上是输了——因为客户端是由人类团队组成的,而他们根本不想组合任何东西。前端工程师想要的是一个可以调用后就不用管的稳定端点。GraphQL 推销的灵活性是以解析器复杂度(resolver complexity)、缓存变通方案和安全审查为代价的,而换取的收益——按请求进行字段选择——对于已知的、变化缓慢的 Web 应用来说几乎毫无必要。到 2024 年,从业者的共识已明显降温:大多数内部 API 只服务于两三个已知客户端,一个设计良好的 REST 端点或 BFF 层就能很好地覆盖这些需求。

接着,一种新型的客户端出现了。AI 智能体没有固定的一套屏幕。它根据每个任务决定自己需要什么数据,并且它为响应的每一个字节付费——字面意义上是在为 Token 付费,而在认知意义上,随着上下文窗口被没人要求的字段填满,推理能力也会随之下降。真正能够自主组合数据需求的客户端终于出现了。只是它不是人类。

一个令人不安的转折是:那些使 GraphQL 自然契合智能体的特性,也使其历史遗留的弱点变得更加糟糕。无约束的解析器成本、单一端点的授权、无法设置白名单的查询——GraphQL 在人类客户端战争中失败的每一个原因,当客户端是一个可能以十足信心发出病态查询的 Token 采样过程时,都会成倍放大。答案并不是“把智能体指向你的图谱”。而是一个更窄的模式:让智能体组合查询,然后像审查代码一样审查并固定(pin)它们。

REST 工具迫使智能体陷入两种糟糕的形式

观察一个智能体针对典型的 REST 工具集工作,你会看到两种失败模式中的一种,通常两者兼有。

第一种是频繁交互的 N+1 循环。智能体调用 get_orders,得到一个订单 ID 列表,然后调用十次 get_order_details,接着为每个订单的客户调用 get_customer。每一次往返都是一个完整的推理周期:模型读取工具结果,对其进行推理,然后发出下一次调用。GraphQL 客户端在一个请求中就能完成的操作,智能体却做了二十次——而且每一次跳跃都增加了延迟、成本,以及一个丢失上下文线索的新机会。

第二种是大杂烩式的响应。为了避免循环,有人构建了一个 get_order_with_everything 端点,返回订单、其行项目、客户、运输历史以及其他四十个字段。现在,无论任务需要什么,每次调用都会向上下文窗口倾倒数 KB 的 JSON。智能体只想要一个交货日期;它却得到了一本长篇小说。那些将 REST 工具集整合到 GraphQL 层之后的从业者报告称,Token 减少了 70–80%,这符合直觉:固定形状的响应中,大部分内容对于任何特定问题都是无关的。

这两种失败模式都是 GraphQL 在 2015 年设计旨在解决的过度获取(over-fetching)和获取不足(under-fetching)问题。不同之处在于代价。当 Web 应用过度获取时,你浪费的是带宽。当智能体过度获取时,你污染的是它进行推理的运行内存。无关字段不仅仅是冗余负担——它们还是干扰项,会显著降低模型挑选关键信息的能力。Token 成本是可见的税收;推理能力的退化则是隐形的代价。

还有第三种更微妙的成本:工具定义蔓延。你暴露的每个 REST 端点都会变成模型在执行任何操作之前必须保留在上下文中的工具模式。团队遇到了一个“金发姑娘问题”——工具太少,智能体无法完成工作;工具太多,仅在定义上就消耗了数千个 Token。类型化的图谱解决了这个问题:一个模式描述了整个表面,模型在一个往返中请求它确切需要的切片。

那些在人类身上杀死 GraphQL 的因素在智能体身上变得更糟

如果故事到此为止,建议会很简单:暴露一个 execute_graphql 工具,将你的模式交给模型,搞定。几个 MCP 服务器正是这样做的。这在演示中可行,但在生产中却是负债,因为 GraphQL 第一个十年中每一个未解决的问题都会卷土重来,而且声音更响。

无约束的解析器成本。 一个跨解析器展开的嵌套查询仅凭几行文本就能产生数千次数据库命中。安全审计反复发现,部署的大多数 GraphQL API 都容易受到某种形式的基于复杂性的拒绝服务攻击。人类客户端偶尔会因为意外或恶意触发这种情况。而在运行时组合查询的智能体,会像模糊测试工具(fuzzer)一样探索模式——不是因为它具有对抗性,而是因为它不知道你的数据库拓扑结构,也没有什么是“昂贵操作”的直觉。它会非常愉快地嵌套 orders { customer { orders { customer } } },因为模式显示它可以这样做。

每个节点的授权。 REST 允许你在端点级别进行授权:此路由需要此角色,搞定。GraphQL 的单一端点将授权下推到字段级别,而字段级授权是实际部署中最容易出错的部分——只要漏掉一个解析器检查,一条遍历路径就会暴露客户端永远不该看到的数据。对于人类客户端,外部存在的查询集实际上是有限的;通过图谱的路径会得到练习和审计。一个智能体会按需生成新颖的遍历。你图谱中每条未经审计的路径,现在在第一天就是可触达的。

无法设置白名单的查询。 针对这两个成熟的对策一直是持久化查询:固定客户端可以运行的确切操作,拒绝其他所有操作。但这种约束与 GraphQL 的初衷存在冲突——如果客户端只能运行预先批准的查询,为什么还要费心使用查询语言呢?对于人类团队来说,诚实的回答通常是“别费心了”,这也是为什么很多 GraphQL 安装悄然变成了带有额外步骤的 REST。对于智能体来说,这种冲突更加尖锐:核心重点就在于运行时组合,而白名单恰恰禁止了这一点。

因此,这种天真的架构——输入模式,输出任意查询——等于是把通往你最昂贵且最缺乏审计的 API 表面的钥匙交给了一个模糊测试工具。那些已经这样交付产品的团队,现在正是写事故回顾报告的人。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates