当PHP通过cURL调用大模型接口时,返回的JSON往往不是一张扁平键值表,而是混合了数组、对象、嵌套列表甚至分类元数据的多层结构。例如一次多模态分析可能返回文本片段、图片描述、置信度分数和关键词列表,这些内容分布在不同的层级中。如果只依赖json_decode之后的一两层foreach,很容易漏掉深层数据,或者写出大量重复的判断逻辑。递归函数正好适合处理这种未知深度和不确定结构的数据,它让遍历逻辑保持简洁,同时保留原始数据之间的层级关系。

一、AI返回复杂结构的常见形态
AI接口返回的复杂结构通常表现为以下三种形态。第一种是深度嵌套数组,例如聊天记录中每条消息包含多个内容块,每个内容块又包含文本、图片、引用等子节点。第二种是对象与数组交错,JSON中一部分使用对象表示键值对,另一部分使用数组表示有序列表,解析时需要区分这两种容器。第三种是带元数据的包装结构,例如返回体包含code、message、data,而data内部才是真正的业务数据,业务数据又嵌套多层。
这些结构给传统解析带来的问题在于无法预先知道每一层的键名和深度。例如一个生成商品描述的接口,可能返回一个商品数组,每个商品包含属性列表,属性列表中又包含不同规格的参数。如果写死循环读取,未来接口增加一层分类信息,代码就要改动。递归解析把这种变化交给函数自身处理,每遇到一个数组或对象就继续调用自身,直到遇到标量值为止。
在PHP中,json_decode的第二个参数设置为true可以让返回的JSON对象变成关联数组,这样对象和数组在PHP中都统一为数组,简化递归判断。但有些开发者希望保留对象,使用is_object判断也可以。为了通用性,本文采用转为数组的方式,这样只需要判断is_array即可。
二、基础递归解析函数的实现
先从一个最简单的递归函数开始,它的作用是完整遍历AI返回的数据,保持结构不变,但把对象统一转为数组。这样后续无论处理什么格式,都能把注意力放在数组层级上。函数定义如下:
function parseAiStructure($data)
{
if (is_array($data)) {
$result = [];
foreach ($data as $key => $value) {
$result[$key] = parseAiStructure($value);
}
return $result;
}
if (is_object($data)) {
$data = get_object_vars($data);
return parseAiStructure($data);
}
// 标量值直接返回
return $data;
}函数逻辑很清楚:如果输入是数组,就遍历每个键值对,对值再次调用自身;如果输入是对象,就先用get_object_vars转成数组再递归;遇到字符串、数字、布尔值或null时直接返回。这样任意深度的数据都能被完整访问,而且原结构得以保留。使用时只需把json_decode的结果传入:
$response = file_get_contents('php://input');
$decoded = json_decode($response, true);
$parsed = parseAiStructure($decoded);不过仅做原样递归还不能满足业务需求。比如AI返回中某些节点只是容器,实际需要提取子节点,或者需要过滤空值和临时字段。另外,如果不同层级出现相同键名,例如多个评论节点都包含text,直接保留键名会导致后面的覆盖前面的。因此需要加入路径追踪和过滤逻辑。
三、加入路径追踪与键名冲突处理
当嵌套结构中出现同名键时,可以使用路径作为键来避免冲突。递归时携带一个路径字符串,把每一层的键名用点号连接起来,叶子节点用完整路径作为最终键。这样即使有多个text,也能区分为0.text、1.text等。实现方式如下:
function flattenAiStructure($data, $path = '', &$result = [])
{
if (is_array($data)) {
foreach ($data as $key => $value) {
$currentPath = $path === '' ? $key : $path . '.' . $key;
if (is_array($value) || is_object($value)) {
flattenAiStructure($value, $currentPath, $result);
} else {
$result[$currentPath] = $value;
}
}
} elseif (is_object($data)) {
flattenAiStructure(get_object_vars($data), $path, $result);
} else {
$result[$path] = $data;
}
return $result;
}这个函数会把所有叶子节点的路径作为键,适合平面化存储,比如把AI返回的深层结果直接写入数据库或日志。路径追踪对调试也很有帮助,如果某个节点解析失败,可以根据键名快速定位,而不是在嵌套数组中逐层寻找。
如果希望保留原有层级关系,但又需要过滤空值和特定字段,可以在基础递归函数上增加条件判断。例如只保留非空字符串和数组长度大于零的节点,丢弃null和空数组。这样递归过程中就可以同时完成清洗工作,避免后续再做一次遍历。
四、实际调用AI接口并兼容多种返回格式
不同AI服务商返回的结构差异很大。以OpenAI的聊天接口为例,返回体中choices是一个数组,每个元素包含message对象,message里又有content字段。而Claude的返回中content可能是数组,包含多个文本块和图片块。递归解析函数能够屏蔽这些差异,只要把对应节点传入函数即可。下面是一个完整的调用示例:
$url = 'https://api.ipipp.com/v1/chat/completions';
$data = [
'model' => 'gpt-4o',
'messages' => [
['role' => 'user', 'content' => '分析这张图片']
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
curl_close($ch);
$decoded = json_decode($response, true);
$message = $decoded['choices'][0]['message'] ?? [];
$parsed = parseAiStructure($message);拿到$parsed后,还可以进一步提取需要的字段。例如AI返回的内容中可能包含多个content块,有的块是纯文本,有的块是图片链接。可以写一个专用函数,递归查找所有text键,把文本片段拼接起来;或者查找所有image_url键,收集图片地址。这样就不用关心这些键在结构中的具体层级。
递归解析的另一个优势是易于扩展。当AI接口升级、返回结构增加新层级时,只要递归函数本身不依赖具体键名,就不需要修改解析代码。如果需要在解析过程中做类型转换,比如把数字字符串转成浮点数,或者在遇到特定键时执行回调,也可以在递归函数中加入参数,让调用方传入自定义处理器。
总体而言,面对AI返回的复杂结构,递归方法比多层循环更加灵活和安全。它把遍历逻辑抽象成统一入口,配合路径追踪、键名冲突处理和过滤规则,可以让PHP代码在多变的数据形态下保持稳定,减少因结构变化带来的维护成本。