大语言模型(LLM)与 Agent 的崛起,让「对话即接口」成为新的交互范式。然而模型本身并不知道你的数据库在哪里、有哪些表、字段叫什么。把 GraphQL 的强类型 Schema 变成 LLM 可以调用的工具契约,让模型像调用函数一样查询真实业务数据——这正是本文要解决的核心问题。我们将从 Function Calling 的工具定义出发,讨论结构化输出、Schema 裁剪、GraphQL MCP Server 与 MCP 协议对比,帮助你构建一个既安全又高效的 AI 数据访问层。
一、为什么 LLM 需要 GraphQL
1.1 模型与数据的鸿沟
LLM 的训练数据是互联网文本,而你的业务数据存在私有数据库里。让 Agent 直接写 SQL 访问数据库极其危险——它会构造任意查询、可能拖垮库、更可能越权读取。GraphQL 恰好提供了一层「类型安全 + 权限可控 + 字段可选」的中间契约:
- 字段级白名单:模型只能请求 Schema 里暴露的字段。
- 参数校验:
ID!、Int等类型在进入 resolver 前就被校验。 - 授权挂钩:每个 resolver 都可以注入权限判断,基于调用者身份裁剪数据。
1.2 Schema 就是工具的天然描述
工具调用(Function/Tool Calling)要求把每个可用操作描述成「名称 + 描述 + 参数 JSON Schema」。GraphQL 的 introspection 结果本质上就是一份完整的 JSON Schema 描述:类型、字段、参数、非空约束一应俱全。把 introspection 转成 LLM 工具定义,几乎是零成本的映射。
二、Function Calling 工具定义
2.1 把 GraphQL 查询转成工具
主流 LLM 的工具协议(OpenAI Function Calling、Anthropic Tool Use)都接受 JSON Schema 格式的参数定义。以下展示如何把一条 GraphQL 查询描述为工具:
{
"name": "search_products",
"description": "根据关键词搜索商品,返回标题、价格与库存。适合回答商品查询类问题。",
"input_schema": {
"type": "object",
"properties": {
"keyword": { "type": "string", "description": "搜索关键词" },
"first": { "type": "integer", "description": "返回条数,默认 10" }
},
"required": ["keyword"]
}
}
服务端收到模型发出的工具调用后,将参数映射到 GraphQL 查询并执行:
const toolResult = await client.request(`
query SearchProducts($keyword: String!, $first: Int) {
searchProducts(keyword: $keyword, first: $first) {
id title price stock
}
}
`, { keyword: args.keyword, first: args.first ?? 10 });
2.2 从 introspection 自动生成工具
手写每个工具既繁琐又易漂移。借助 @graphql-tools 的打印工具,可以从 Schema 自动生成工具清单:
import { printSchema, lexicographicSortSchema } from 'graphql';
const schemaSDL = printSchema(lexicographicSortSchema(schema));
// 将 SDL 发给模型,或进一步用 JSON Schema 生成器把类型转成工具定义
实践中推荐「显式工具白名单」而非「全量暴露」:为 Agent 精选 10~20 个高频、低风险的工具(如 getUser、searchArticles、listOrders),而不是把整个 Schema 的每个查询都注册成工具。工具越少,模型越不容易选错。
2.3 工具描述的质量决定成功率
LLM 在「选哪个工具」上的准确率,与工具描述的质量强相关:
- 描述里写明用途与触发条件:「当用户询问天气时使用此工具」比「获取天气」更易命中。
- 参数描述标明格式:日期参数写明
YYYY-MM-DD,ID 参数标明是整数还是 UUID。 - 字段说明标注成本:对昂贵字段(如需要调用推荐算法)标注「高成本,仅在必要时请求」。
三、结构化输出:让模型查询更可靠
3.1 约束输出的两种方式
Agent 调用 GraphQL 后,模型的后续推理依赖返回数据。为了让模型「看懂」结果,结构化输出至关重要,主要有两条路径。
路径一:工具结果的 JSON 直接回填。工具调用返回的 data 本身就是结构化 JSON,模型无需额外解析。
{
"search_products": {
"data": [
{ "id": "p1", "title": "手冲咖啡壶", "price": 299, "stock": 12 }
],
"errors": null
}
}
路径二:GraphQL 响应 Schema 化。使用 graphql-json-schema 之类的库把类型转成 JSON Schema,作为模型输出的校验器:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"price": { "type": "number" }
},
"required": ["id", "title"]
}
}
}
}
3.2 错误语义化
LLM 面对 GraphQL 错误数组时往往不知所措。服务端应把错误转成模型友好的格式:
{
"data": null,
"errors": [
{
"message": "商品 p_not_exist 不存在",
"extensions": { "code": "NOT_FOUND", "path": ["searchProducts"] }
}
]
}
在把结果回填给模型之前,将 extensions.code 归一化为 NOT_FOUND/UNAUTHORIZED/RATE_LIMITED/BAD_INPUT 枚举,并附一句可执行的修复建议,能显著提升 Agent 多轮重试的成功率。
3.3 截断与分页
模型上下文有限,工具返回超大结果会被截断甚至污染推理。对列表型工具强制 first 参数,并把响应封装为「前 N 条摘要 + totalCount」:
query ListOrders($first: Int! = 5, $after: String) {
orders(first: $first, after: $after) {
edges { node { id status total } }
pageInfo { hasNextPage endCursor }
}
}
服务端在工具执行层统一注入 first <= 20 的硬上限,防止模型构造出一次性拉取全表的查询。
四、Schema 裁剪与安全边界
4.1 面向 Agent 的 Schema 裁剪
直接把生产 Schema 暴露给模型,意味着模型能看到内部字段(如 internalScore、rawSql),也可能触发昂贵的计算字段。应构建一个独立的 Agent Schema:
# agent-schema.graphql —— 仅供 LLM 工具调用
type Query {
searchProducts(keyword: String!, first: Int = 10): [Product!]!
product(id: ID!): Product
article(id: ID!): Article
articlesByTag(tag: String!, first: Int = 10): [Article!]!
}
type Product {
id: ID!
title: String!
price: Float!
stock: Int!
}
type Article {
id: ID!
title: String!
summary: String!
publishedAt: String!
}
裁剪原则:只保留 Agent 高频需要的读操作,去除写操作(除非专门设计工具)、去除内部字段、限制分页上限。Schema 裁剪层可以使用 @graphql-tools 的 filterSchema,也可以直接新建一个子图作为 Agent BFF。
4.2 权限与租户隔离
Agent 调用本质上是「无登录态的程序化调用」,权限必须显式注入:
- 通过请求头注入
x-agent-id与x-tenant-id,resolver 在 context 中解析。 - 所有查询强制带上租户过滤,防止模型跨租户读取数据。
- 对敏感字段(手机号、邮箱、账单)在 Agent Schema 中直接移除,而不是依赖权限过滤。
const context = ({ req }) => ({
agentId: req.headers['x-agent-id'],
tenantId: req.headers['x-tenant-id'],
isAgentCall: true,
});
const resolvers = {
Query: {
orders: (_, args, ctx) => {
// Agent 调用永远只查本租户
return db.orders.where({ tenantId: ctx.tenantId }).first(args.first);
},
},
};
4.3 查询成本控制
LLM 生成的查询可能很深、别名很多、列表很大。对 Agent 流量应启用比人类客户端更严格的限制:
- 深度限制:
max_depth: 8。 - 别名限制:
max_aliases: 10,防止模型构造重复字段放大响应。 - 复杂度上限:对列表字段乘以权重,超过阈值拒绝执行并返回可读错误。
五、GraphQL MCP Server:标准化的工具桥
5.1 MCP 是什么
MCP(Model Context Protocol)是 Anthropic 于 2024 年底开源的开放协议,目标是统一「LLM 访问外部工具/数据」的标准。MCP Server 暴露三类原语:Tools(工具)、Resources(资源)、Prompts(提示模板)。客户端(Claude Desktop、Claude Code、各类 SDK)通过标准化的 JSON-RPC 传输层发现并调用这些能力。
5.2 官方 GraphQL MCP Server
MCP 生态中已有 @modelcontextprotocol/server-graphql 等官方参考实现,其核心思路是:把 introspection 结果转成 MCP Tools,每个查询字段对应一个工具,参数映射为 JSON Schema。
{
"tools": [
{
"name": "query_users",
"description": "执行 GraphQL 查询 users",
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"first": { "type": "integer", "default": 10 }
}
}
}
]
}
MCP Server 通常运行在独立进程中,通过 stdio 或 SSE/HTTP 传输与 LLM 客户端通信。GraphQL 服务只需提供 introspection 即可被桥接,无需改造原有 API。
5.3 自定义 GraphQL MCP Server
以 TypeScript 实现一个把 GraphQL 包装为 MCP Tools 的最小服务:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { fetch } from 'undici';
const server = new McpServer({ name: 'graphql-gateway', version: '1.0.0' });
server.tool(
'searchProducts',
'按关键词搜索商品',
{ keyword: 'string', first: 'number?' },
async ({ keyword, first }) => {
const res = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-agent-id': 'demo' },
body: JSON.stringify({
query: `query($k: String!, $f: Int) {
searchProducts(keyword: $k, first: $f) { id title price }
}`,
variables: { k: keyword, f: first ?? 10 },
}),
});
const json = await res.json();
return { content: [{ type: 'text', text: JSON.stringify(json.data) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
MCP 的标准化收益在于:同一个 GraphQL 服务可以同时服务于 Claude、其他 Agent 框架与自研编排器,工具描述、参数校验、传输协议都是统一的。
六、GraphQL 直接调用 vs MCP 对比
| 维度 | GraphQL 直接调用 | MCP(GraphQL MCP Server) |
|---|---|---|
| 契约来源 | GraphQL Schema(字段/类型/参数) | MCP 工具描述(基于 introspection 生成) |
| 协议 | HTTP POST/GET + JSON | JSON-RPC over stdio/SSE/HTTP |
| 发现机制 | 手动注册工具或 introspection | MCP Server 自动暴露工具清单 |
| 权限模型 | 复用 GraphQL resolver 授权 | 在 MCP 层注入代理身份,转发给 GraphQL |
| 适用场景 | 自研 Agent、受控工具集 | 多客户端接入、标准化工具生态 |
| 灵活性 | 查询语言完全自由 | 工具粒度较粗,往往按查询字段包装 |
| 治理 | 需要自己做 Schema 裁剪与限流 | MCP Server 层可集中做裁剪与审计 |
选型建议:如果 Agent 是你自己写的、工具集固定,直接调 GraphQL 更灵活高效;如果希望开放给多种 MCP 客户端、降低接入成本,用 MCP Server 包装 GraphQL 更合适。两者本质上是「协议层」与「数据层」的分工:MCP 管工具编排,GraphQL 管数据契约。
七、生产实践:Agent 数据访问层架构
7.1 推荐架构
LLM 客户端 / Agent
│ Function Calling / Tool Use
▼
工具编排层(选工具、填参数、解析结果、错误重试)
│ 标准 JSON
▼
Agent BFF(Schema 裁剪、权限注入、分页上限、成本限制)
│ GraphQL over HTTP
▼
GraphQL 网关(认证、限流、日志、观测)
│
▼
子图 / 数据源(users、orders、products ...)
7.2 关键落地清单
- 裁剪:为 Agent 建独立 Schema,删除内部与敏感字段。
- 白名单:只注册 10~20 个精选工具,不要全量暴露。
- 注入身份:所有 Agent 请求带
x-agent-id,权限与审计都挂在它上面。 - 硬上限:
first上限、深度上限、复杂度上限三层叠加。 - 错误归一化:把 GraphQL errors 转成模型可读的枚举与建议。
- 观测:记录每次工具调用的输入输出、token 消耗与延迟,评估工具成功率。
7.3 成本与 token 控制
Agent 场景的 token 成本集中在「工具描述 + 返回数据」重复进上下文。优化手段:
- 工具描述精简:字段级描述只写最必要的。
- 返回裁剪:只返回模型推理需要的字段,去掉大文本正文。
- 缓存:对高频查询(如商品价格)用短期缓存,减少真实 DB 调用与重复返回。
八、一句话总结
GraphQL 的强类型 Schema 天然适合作为 LLM 的工具契约——通过 Function Calling 转成工具定义、用结构化输出保证可靠性、以 MCP 协议标准化接入,配合 Schema 裁剪与权限注入,就能构建一个安全高效的 AI 数据访问层。
FAQ
Q1: 让 LLM 直接访问 GraphQL Schema,安全性如何保障?
A: 不建议直接暴露生产 Schema。应构建独立的 Agent Schema,只保留白名单读操作;在 Agent BFF 层注入代理身份(agent-id + tenant-id),强制租户过滤;对敏感字段直接移除而非依赖权限。GraphQL 的类型系统与 resolver 授权是第二道防线,但工具层裁剪才是第一道。
Q2: Function Calling 与 MCP 是什么关系?
A: Function Calling 是 LLM 供应商(OpenAI、Anthropic 等)定义的单模型工具协议,描述「工具长什么样」;MCP 是跨供应商、跨客户端的开放协议,管理「工具如何被发现与调用」。GraphQL MCP Server 可以把 introspection 转成 MCP 工具,从而让同一个 GraphQL 服务服务多种 Agent 客户端。
Q3: 如何防止 LLM 构造超深查询拖垮服务?
A: 三管齐下:深度限制(如 8 层)、别名限制(如 10 个)、复杂度分析(列表字段乘以权重)。对 Agent 流量的限制应严于人类客户端,超限时返回模型可读的错误码(如 QUERY_TOO_COMPLEX)而非静默截断。
Q4: 工具返回的数据太大,污染模型上下文怎么办?
A: 强制 first 上限(如 20),返回「前 N 条 + totalCount」摘要;对大文本字段(正文、日志)在 Agent Schema 中替换为摘要字段;对高频结果启用短期缓存。必要时把长数据写进附件/文件而非对话上下文。
Q5: 自研 Agent 场景下,直接调 GraphQL 和走 MCP 哪个更好?
A: 自研且工具集固定的场景直接调 GraphQL 更灵活——查询语言自由、工具粒度精确、权限复用 resolver 授权。MCP 的价值在于多客户端标准化接入。成熟团队常用「GraphQL 数据层 + 工具编排层」的组合,MCP 只是可选的协议适配壳。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。