GraphQL + AI——动态查询和 AI 能力的结合
GraphQL + AI——动态查询和 AI 能力的结合
有个同事问我:「老张,我们系统用的是 GraphQL,能和 Spring AI 集成吗?」
我说能,而且结合起来有点意思。
GraphQL 本来就是为了解决「一次查询获取精确数据」而设计的。当你把 AI 加进来,用自然语言描述你想要什么,AI 把这个描述翻译成 GraphQL 查询语句,GraphQL 执行它并返回精确结果——这个链路在某些场景下非常优雅。
比如:用户说「给我看一下上个月销售额前十的商品,以及它们的当前库存」。这是一个跨实体的复杂查询,如果做成 REST 接口,要么定制一个专用接口,要么前端多次请求。但 GraphQL 天生支持这种精确的跨实体查询,只需要一次请求就能拿到所有数据。
这篇文章就来讲怎么用 Spring GraphQL + Spring AI 实现自然语言 GraphQL 查询。
一、为什么 GraphQL 适合和 AI 结合
在讲实现前,先说清楚为什么 GraphQL 和 AI 有天然的契合点。
1.1 Schema 即文档
GraphQL 的 Schema 是强类型的自描述文档,比 OpenAPI 更紧凑,更适合注入进 Prompt:
type Product {
id: ID!
name: String!
price: Float! # 单位:元
stock: Int! # 库存数量
category: Category!
orders(
startDate: String # 格式 YYYY-MM-DD
endDate: String
limit: Int = 10
): [Order!]!
}
type Category {
id: ID!
name: String!
products: [Product!]!
}这个 Schema 直接告诉 AI:Product 有哪些字段、Category 和 Product 的关系是什么、orders 支持哪些过滤参数。AI 根据这个 Schema 生成的查询语句,不需要额外解释。
1.2 查询精度高
GraphQL 允许精确指定需要的字段,AI 可以根据用户问题选择只查询相关字段,不会带回不需要的数据:
# 用户问「销售额前十的商品名和价格」
# AI 生成的查询只取 name 和 price,不取 stock、description 等无关字段
query {
topSellingProducts(limit: 10, period: "LAST_MONTH") {
name
price
}
}1.3 单端点降低工具定义复杂度
REST API 的 Function Calling 需要为每个接口定义一个工具,接口多了工具定义就很冗长。GraphQL 只有一个端点,工具定义极为简单:
@Tool(description = "执行 GraphQL 查询,获取商品、订单、用户等业务数据")
public String executeGraphQLQuery(
@ToolParam(description = "合法的 GraphQL 查询语句") String query
) {
// 执行查询
}一个工具定义,AI 就能查询整个 GraphQL Schema 覆盖的所有数据。
二、整体架构
三、Spring GraphQL 配置
首先设置基础的 Spring GraphQL 项目。
3.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
</dependency>
</dependencies>3.2 GraphQL Schema(src/main/resources/graphql/schema.graphqls)
type Query {
products(
categoryId: ID
minPrice: Float
maxPrice: Float
inStock: Boolean
limit: Int = 20
offset: Int = 0
): ProductConnection!
product(id: ID!): Product
topSellingProducts(
limit: Int = 10
period: SalesPeriod = LAST_MONTH
): [ProductSalesRank!]!
orders(
status: OrderStatus
startDate: String
endDate: String
limit: Int = 20
offset: Int = 0
): OrderConnection!
order(id: ID!): Order
}
type Product {
id: ID!
name: String!
description: String
price: Float!
stock: Int!
category: Category!
salesCount: Int!
createTime: String!
}
type ProductSalesRank {
rank: Int!
product: Product!
totalSales: Int!
totalRevenue: Float!
}
type Category {
id: ID!
name: String!
productCount: Int!
}
type Order {
id: ID!
orderNo: String!
status: OrderStatus!
totalAmount: Float!
items: [OrderItem!]!
createTime: String!
}
type OrderItem {
product: Product!
quantity: Int!
unitPrice: Float!
subtotal: Float!
}
type ProductConnection {
total: Int!
items: [Product!]!
}
type OrderConnection {
total: Int!
items: [Order!]!
}
enum OrderStatus {
PENDING_PAYMENT
PENDING_SHIPMENT
SHIPPED
COMPLETED
CANCELLED
}
enum SalesPeriod {
LAST_WEEK
LAST_MONTH
LAST_QUARTER
LAST_YEAR
}3.3 GraphQL Resolver
@Controller
public class ProductQueryResolver {
@Autowired
private ProductService productService;
@QueryMapping
public ProductConnection products(
@Argument String categoryId,
@Argument Double minPrice,
@Argument Double maxPrice,
@Argument Boolean inStock,
@Argument Integer limit,
@Argument Integer offset) {
ProductQueryParams params = ProductQueryParams.builder()
.categoryId(categoryId)
.minPrice(minPrice)
.maxPrice(maxPrice)
.inStock(inStock)
.limit(limit != null ? limit : 20)
.offset(offset != null ? offset : 0)
.build();
return productService.queryProducts(params);
}
@QueryMapping
public List<ProductSalesRank> topSellingProducts(
@Argument Integer limit,
@Argument String period) {
return productService.getTopSellingProducts(limit, SalesPeriod.valueOf(period));
}
}四、AI 工具层:把 GraphQL 暴露给 AI
核心思路是:把「执行 GraphQL 查询」封装成一个工具,AI 生成查询语句后调用这个工具。
4.1 GraphQL 执行工具
@Component
@Slf4j
public class GraphQLQueryTool {
private final GraphQlClient graphQlClient;
public GraphQLQueryTool(GraphQlClient graphQlClient) {
this.graphQlClient = graphQlClient;
}
@Tool(description = """
执行 GraphQL 查询,查询商品、订单、销售排行等业务数据。
支持以下查询类型:
1. products - 查询商品列表,支持按分类、价格范围、库存状态过滤
2. product(id) - 查询单个商品详情
3. topSellingProducts - 查询销售排行,支持按时间段(LAST_WEEK/LAST_MONTH/LAST_QUARTER/LAST_YEAR)
4. orders - 查询订单列表,支持按状态和日期过滤
5. order(id) - 查询单个订单详情
注意:查询语句必须是合法的 GraphQL 语法,字段名严格按照 Schema 定义
""")
public String executeQuery(
@ToolParam(description = "合法的 GraphQL 查询语句,例如:query { topSellingProducts(limit: 5) { rank product { name price } totalSales } }")
String graphqlQuery) {
log.info("执行 GraphQL 查询: {}", graphqlQuery);
try {
// 先做基本语法验证
validateGraphQLSyntax(graphqlQuery);
// 执行查询
Map<String, Object> result = graphQlClient
.document(graphqlQuery)
.retrieve("*")
.toEntity(new ParameterizedTypeReference<Map<String, Object>>() {})
.block();
if (result == null) {
return "查询返回空结果";
}
// 转换为 JSON 字符串返回给 AI
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
} catch (GraphQLSyntaxException e) {
log.warn("GraphQL 语法错误: {}", e.getMessage());
return "GraphQL 语法错误: " + e.getMessage() + "\n请检查查询语句是否符合 Schema 定义";
} catch (Exception e) {
log.error("GraphQL 查询执行失败: {}", e.getMessage());
return "查询执行失败: " + e.getMessage();
}
}
private void validateGraphQLSyntax(String query) {
// 使用 graphql-java 进行语法验证
try {
Parser.parse(query);
} catch (InvalidSyntaxException e) {
throw new GraphQLSyntaxException(e.getMessage());
}
}
}4.2 Schema 注入到 Prompt(关键步骤)
把 GraphQL Schema 的精简版注入到 System Prompt,让 AI 知道可以查什么:
@Service
public class GraphQLAIService {
private final ChatClient chatClient;
// Schema 的精简版(去掉不必要的注释,减少 Token 消耗)
private static final String SCHEMA_SUMMARY = """
## 可查询的 GraphQL Schema 摘要
### 商品查询
- `products(categoryId, minPrice, maxPrice, inStock, limit, offset)` - 商品列表
- 返回字段:id, name, price(元), stock(库存), salesCount, category{id, name}
- `product(id)` - 单个商品详情
- `topSellingProducts(limit, period: LAST_WEEK|LAST_MONTH|LAST_QUARTER|LAST_YEAR)` - 销售排行
- 返回字段:rank, totalSales, totalRevenue, product{...}
### 订单查询
- `orders(status: PENDING_PAYMENT|PENDING_SHIPMENT|SHIPPED|COMPLETED|CANCELLED, startDate, endDate, limit, offset)` - 订单列表
- 返回字段:id, orderNo, status, totalAmount, createTime, items[{product{...}, quantity, unitPrice, subtotal}]
- `order(id)` - 单个订单详情
### 注意事项
- 所有查询都需要明确指定要返回的字段(GraphQL 规范)
- limit 默认 20,最大 100
- 日期格式:YYYY-MM-DD
- 价格单位:元(浮点数)
""";
public GraphQLAIService(ChatClient.Builder builder, GraphQLQueryTool queryTool) {
this.chatClient = builder
.defaultSystem("""
你是一个数据查询助手,帮助用户通过自然语言查询业务数据。
""" + SCHEMA_SUMMARY + """
当用户询问数据时:
1. 分析用户想要什么数据
2. 根据 Schema 生成合适的 GraphQL 查询语句
3. 调用 executeQuery 工具执行查询
4. 根据查询结果,用自然语言回答用户的问题
5. 对数字要做适当的格式化(价格保留2位小数,大数字用万为单位等)
如果查询结果为空,要友好地告知用户。
如果用户的问题超出了 Schema 的查询能力,要说明局限并建议替代方案。
""")
.defaultTools(queryTool)
.build();
}
public String query(String userQuestion) {
return chatClient.prompt()
.user(userQuestion)
.call()
.content();
}
public Flux<String> queryStream(String userQuestion) {
return chatClient.prompt()
.user(userQuestion)
.stream()
.content();
}
}五、处理复杂的多步查询
有些用户问题需要多次 GraphQL 查询才能回答。比如:「销售额前5的商品,每个的库存还有多少,如果库存不足100,帮我标注出来。」
这需要两步:
- 查
topSellingProducts获取前5商品 - 对每个商品检查库存
Spring AI 的多轮 Tool Calling 可以自动处理这种情况:
@Component
@Slf4j
public class MultiStepGraphQLTool {
private final GraphQlClient graphQlClient;
@Tool(description = "执行 GraphQL 查询,支持复杂的嵌套查询和关联查询")
public String executeQuery(
@ToolParam(description = "GraphQL 查询语句") String graphqlQuery) {
// 同上面的实现
return doExecute(graphqlQuery);
}
/**
* 对于需要多步的复杂问题,AI 会自动多次调用这个工具
* 示例:先查 topSellingProducts,再检查每个的 stock
* 实际上 GraphQL 支持嵌套查询,AI 可以一次查完
*/
private String doExecute(String query) {
try {
// 支持变量的查询(AI 有时会生成带变量的 GraphQL 查询)
if (query.contains("$")) {
log.info("检测到变量查询,尝试无变量版本");
// 简化处理:如果带变量,提示 AI 使用内联参数
return "查询包含变量语法,请改用内联参数形式。例如:" +
"query { product(id: \"123\") { name } } " +
"而不是:query($id: ID!) { product(id: $id) { name } }";
}
Map<String, Object> result = executeGraphQL(query);
return new ObjectMapper().writeValueAsString(result);
} catch (Exception e) {
return "查询失败:" + e.getMessage();
}
}
}实际上,对于上面那个问题,AI 会生成一个聪明的嵌套 GraphQL 查询:
query {
topSellingProducts(limit: 5, period: LAST_MONTH) {
rank
totalRevenue
product {
id
name
stock # 同时查库存,一次搞定
price
}
}
}GraphQL 的嵌套查询能力,让 AI 不需要多轮调用就能获取关联数据。
六、错误处理和查询修正
AI 生成的 GraphQL 查询语句有时候会有问题(字段名拼写错误、参数类型不对),需要有自动修正机制:
@Component
public class GraphQLQueryCorrector {
private final ChatClient correctorClient;
private final String schemaString;
public GraphQLQueryCorrector(ChatClient.Builder builder) {
this.correctorClient = builder
.defaultOptions(OpenAiChatOptions.builder().temperature(0.0).build())
.build();
this.schemaString = loadSchemaFromFile();
}
/**
* 当 GraphQL 执行失败时,尝试用 AI 修正查询语句
*/
public String correctQuery(String failedQuery, String errorMessage) {
String prompt = String.format("""
下面的 GraphQL 查询语句执行失败,请根据错误信息和 Schema 修正它。
## 失败的查询
```graphql
%s
```
## 错误信息
%s
## GraphQL Schema
```graphql
%s
```
请直接返回修正后的 GraphQL 查询语句,不要有任何解释。
""",
failedQuery, errorMessage, schemaString
);
return correctorClient.prompt()
.user(prompt)
.call()
.content();
}
@Tool(description = "执行 GraphQL 查询,查询失败时自动修正并重试")
public String executeWithRetry(
@ToolParam(description = "GraphQL 查询语句") String graphqlQuery) {
String query = graphqlQuery;
for (int attempt = 0; attempt < 2; attempt++) {
try {
validateGraphQLSyntax(query);
Map<String, Object> result = executeGraphQL(query);
// 检查是否有 GraphQL 错误
if (result.containsKey("errors")) {
String errors = result.get("errors").toString();
if (attempt == 0) {
log.warn("GraphQL 查询有错误,尝试修正: {}", errors);
query = correctQuery(query, errors);
continue;
}
return "查询存在错误: " + errors;
}
return new ObjectMapper().writeValueAsString(result.get("data"));
} catch (Exception e) {
if (attempt == 0) {
log.warn("查询执行失败,尝试修正: {}", e.getMessage());
query = correctQuery(query, e.getMessage());
} else {
return "查询执行失败: " + e.getMessage();
}
}
}
return "查询修正后仍然失败,请尝试换一种方式描述您的需求";
}
}七、REST 控制器层
提供一个简单的 HTTP 接口,供前端调用自然语言查询功能:
@RestController
@RequestMapping("/api/v1/ai-query")
public class GraphQLAIController {
private final GraphQLAIService graphQLAIService;
@PostMapping("/query")
public ResponseEntity<QueryResponse> query(@RequestBody QueryRequest request) {
if (request.question() == null || request.question().isBlank()) {
return ResponseEntity.badRequest()
.body(new QueryResponse(null, "问题不能为空"));
}
String answer = graphQLAIService.query(request.question());
return ResponseEntity.ok(new QueryResponse(answer, null));
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> queryStream(@RequestParam String question) {
return graphQLAIService.queryStream(question)
.map(chunk -> ServerSentEvent.builder(chunk).build())
.concatWith(Flux.just(ServerSentEvent.builder("[DONE]").build()));
}
record QueryRequest(String question) {}
record QueryResponse(String answer, String error) {}
}八、真实场景测试
测试几个有代表性的自然语言查询:
问题 1:「上个月卖得最好的商品前5是哪些?」
AI 生成的 GraphQL:
query {
topSellingProducts(limit: 5, period: LAST_MONTH) {
rank
product {
name
price
}
totalSales
totalRevenue
}
}回答:「上个月销售前5的商品是:1. XXX手机壳(销售932件,营收3,214元)2. ...」
问题 2:「库存低于50件的商品有哪些,价格超过100元的优先显示」
AI 生成的 GraphQL(分两次查询):
query {
products(inStock: true, limit: 100) {
items {
name
stock
price
}
}
}然后 AI 在 LLM 层对结果做过滤和排序(因为 GraphQL 不支持 stock < 50 的过滤,AI 取回所有数据后自己处理)。
问题 3:「最近一周已完成的订单总金额是多少?」
AI 生成的 GraphQL:
query {
orders(
status: COMPLETED,
startDate: "2026-04-17",
endDate: "2026-04-24",
limit: 1000
) {
total
items {
totalAmount
}
}
}然后 AI 对 items 里的 totalAmount 求和。
九、性能和安全考量
9.1 查询复杂度限制
AI 生成的 GraphQL 查询可能会触发复杂的嵌套查询,造成数据库压力:
@Configuration
public class GraphQLSecurityConfig {
@Bean
public Instrumentation queryComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(100); // 限制查询复杂度
}
@Bean
public Instrumentation queryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(5); // 限制嵌套深度
}
}9.2 只暴露查询类型给 AI
Mutation 操作(写数据)不应该暴露给 AI 工具,避免 AI 意外修改数据:
@Tool(description = "执行 GraphQL 查询")
public String executeQuery(@ToolParam(description = "GraphQL query 语句(仅支持 query,不支持 mutation)") String graphqlQuery) {
// 安全检查:只允许 query,不允许 mutation
if (graphqlQuery.trim().toLowerCase().startsWith("mutation")) {
return "出于安全考虑,AI 查询工具不支持 mutation 操作";
}
// 执行查询
return doExecute(graphqlQuery);
}9.3 结果截断
AI 的 context window 有限制,GraphQL 返回的数据不能无限制地塞给 AI:
private String truncateIfNeeded(String jsonResult) {
// 超过 8000 字符就截断,并添加说明
if (jsonResult.length() > 8000) {
String truncated = jsonResult.substring(0, 8000);
// 找最后一个完整的 JSON 对象边界
int lastBrace = truncated.lastIndexOf('}');
if (lastBrace > 0) {
truncated = truncated.substring(0, lastBrace + 1);
}
return truncated + "\n\n[数据已截断,实际数据更多,建议缩小查询范围]";
}
return jsonResult;
}十、小结
GraphQL + AI 的组合,最大的价值在于:用户不需要知道 API 的具体结构,只需要用自然语言描述想要什么数据。
这在数据分析、内部工具、管理后台这类场景下特别有用——运营人员不需要找开发写查询,直接自己问就行了。
技术上的核心是:
- 把 GraphQL Schema 的关键信息注入到 System Prompt
- 用一个简单的「执行 GraphQL 查询」工具把 Schema 能力暴露给 AI
- 加上查询失败的自动修正机制,提高成功率
- 做好安全限制:只允许 query 不允许 mutation,限制查询复杂度
还有一点很重要:不是所有问题都能转成 GraphQL 查询。AI 应该能识别出「这个问题超出了数据查询的范畴」,然后友好地给出提示,而不是生成一个错误的查询语句。这需要在 System Prompt 里明确说明工具的能力边界。
