# 循环引用处理策略评估

## 📋 当前实现分析

### 现状
当前实现采用**简单占位符**策略：
- ✅ 检测到循环引用时返回占位符
- ✅ 避免无限递归
- ❌ 信息丢失严重（只返回 `{"type": "object", "description": "循环引用: ..."}`）
- ❌ LLM 无法理解循环结构的具体内容

### 问题
```json
// 当前输出（信息丢失）
{
  "type": "object",
  "description": "循环引用: #/components/schemas/Node"
}
```

---

## 🔍 方案对比分析

### 方案1：完全剪枝（简单占位符）

#### 核心思路
检测到循环引用时，立即返回占位符，不展开任何结构。

#### 优点
- ✅ 实现简单
- ✅ 完全避免无限递归
- ✅ 节省 token

#### 缺点
- ❌ 信息丢失严重
- ❌ LLM 无法知道参数类型和结构
- ❌ 无法满足实际使用需求

---

### 方案2：深度限制剪枝（推荐 ⭐⭐⭐⭐⭐）

#### 核心思路
设定最大解析深度（2-3 层），在深度范围内正常展开 properties，超过深度后剪枝但保留类型和原始描述。

#### 实现策略

```go
const (
    MaxSchemaDepth = 2  // 或 3，可配置
)

// 伪代码
resolveSchema(schema, apiSpec, visitedRefs, currentDepth) {
    // 1. 检查是否达到最大深度
    if currentDepth >= MaxSchemaDepth {
        // 超过深度，返回剪枝后的 schema（保留类型和描述，移除 properties）
        return pruneSchema(schema)  // 保留 type, description，移除 properties
    }

    // 2. 检查循环引用
    if visitedRefs[refPath] {
        // 检测到循环引用，但深度未达到上限
        // 继续展开（在深度范围内）
        return resolveWithDepth(schema, currentDepth + 1)
    }

    // 3. 正常解析（在深度范围内）
    // 展开 properties，递归处理子属性
    return resolveProperties(schema, currentDepth)
}

// 剪枝函数：保留类型和描述，移除 properties
pruneSchema(schema) {
    result = {
        "type": schema.type,  // 保留类型
        "description": schema.description  // 保留原始描述（不修改）
    }

    // 如果是 array，保留 items 结构但不展开 properties
    if schema.type == "array" {
        result["items"] = {
            "type": schema.items.type,
            "description": schema.items.description
            // 不包含 properties
        }
    }

    // 不包含 properties（避免继续递归）
    return result
}
```

#### 适用场景
- ✅ 对生成的 Schema 有完全控制权
- ✅ 追求稳定性和确定性
- ✅ 需要节省 token
- ✅ 自动化流程，需要完全兼容的 Schema

#### 优点
- ✅ **节省 token**：不添加冗长的循环引用说明
- ✅ **信息完整**：在深度范围内保留完整结构
- ✅ **避免干扰**：不修改原始 description，不添加额外说明
- ✅ **类型明确**：LLM 能明确知道参数类型（object/array）
- ✅ **完全兼容**：生成的 Schema 完全符合 JSON Schema 规范
- ✅ **可控性强**：通过 maxDepth 参数控制展开深度

#### 缺点
- ⚠️ **信息有损**：超出深度的部分会被剪枝（但保留类型信息）
- ⚠️ **实现复杂度中等**：需要深度控制和剪枝逻辑

#### 示例输出

**输入（循环引用）：**
```json
{
  "Node": {
    "type": "object",
    "description": "节点对象",
    "properties": {
      "id": {"type": "string"},
      "children": {
        "type": "array",
        "description": "子节点列表",
        "items": {
          "$ref": "#/components/schemas/Node"
        }
      }
    }
  }
}
```

**输出（MaxSchemaDepth = 2）：**
```json
{
  "type": "object",
  "description": "节点对象",  // 保留原始描述
  "properties": {
    "id": {"type": "string"},
    "children": {
      "type": "array",
      "description": "子节点列表",  // 保留原始描述
      "items": {
        "type": "object",
        "description": "节点对象"  // 保留原始描述
        // 超过深度，不包含 properties（避免继续递归）
      }
    }
  }
}
```

**关键点：**
- ✅ 在深度范围内（0-2 层），正常展开 properties
- ✅ 超过深度后，保留类型和描述，移除 properties
- ✅ 不添加循环引用说明（节省 token）
- ✅ 保留原始 description（不修改）

---

### 方案3：利用 description 字段说明

#### 核心思路
在 Schema 的 `description` 字段中，用自然语言清晰说明循环关系的存在和结构。

#### 优点
- ✅ 保留完整元数据
- ✅ 实现简单

#### 缺点
- ❌ **浪费 token**：添加冗长的循环引用说明
- ❌ **干扰 LLM**：额外的描述信息可能干扰理解
- ❌ **依赖模型理解能力**：需要 LLM 能够理解自然语言描述
- ❌ **非标准化**：description 格式不统一

#### 结论
**不推荐**：不符合节省 token 和避免干扰的原则。

---

## ✅ 最终推荐方案：深度限制剪枝

### 核心原则

1. **设定最大解析深度**（建议 2-3 层，可配置）
2. **在深度范围内正常展开**（保留完整的 properties）
3. **超过最大深度后剪枝**：
   - ✅ 保留类型信息（`type: object/array`）
   - ✅ 保留原始 `description`（**不修改，不添加循环引用说明**）
   - ✅ 保留 `items` 结构（如果是 array）
   - ❌ 移除 `properties`（避免继续递归）
4. **不添加额外的循环引用说明**（节省 token，避免干扰 LLM）

### 统一输出格式

**深度范围内（正常展开）：**
```json
{
  "type": "object",
  "description": "节点对象",
  "properties": {
    "id": {"type": "string"},
    "children": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {"type": "string"}
          // 在深度范围内，继续展开
        }
      }
    }
  }
}
```

**超过最大深度（剪枝处理）：**
```json
{
  "type": "object",
  "description": "节点对象"  // 保留原始 description，不修改
  // 超过最大深度，不包含 properties
}
```

**数组类型（超过深度）：**
```json
{
  "type": "array",
  "description": "子节点列表",  // 保留原始 description
  "items": {
    "type": "object",
    "description": "节点对象"  // 保留原始 description
    // 超过最大深度，不包含 properties
  }
}
```

### 实现要点

#### 1. 深度控制
```go
const (
    MaxSchemaDepth = 2  // 可配置，建议 2-3 层
)

// 每次递归 $ref 时，深度 +1
// 达到最大深度时，停止展开 properties
```

#### 2. 剪枝逻辑
```go
// 超过深度后，构建剪枝后的 schema
func pruneSchema(schema map[string]interface{}) map[string]interface{} {
    result := map[string]interface{}{
        "type": schema["type"],  // 保留类型
    }

    // 保留原始 description（如果存在）
    if desc, ok := schema["description"].(string); ok {
        result["description"] = desc  // 不修改，不添加说明
    }

    // 如果是 array，保留 items 结构但不展开 properties
    if schema["type"] == "array" {
        if items, ok := schema["items"].(map[string]interface{}); ok {
            result["items"] = pruneSchema(items)  // 递归剪枝 items
        }
    }

    // 不包含 properties（避免继续递归）
    return result
}
```

#### 3. 保留信息
- ✅ 类型信息（`type`, `items.type`）
- ✅ 原始 `description`（不修改）
- ✅ 其他非 properties 字段（`enum`, `required` 等，如果存在）

#### 4. 移除信息
- ❌ `properties`（超过深度后）
- ❌ 循环引用说明（不添加额外 description）

### 优势总结

| 特性 | 完全剪枝 | 深度限制剪枝 | description 说明 |
|------|---------|------------|----------------|
| **信息完整性** | ❌ 低 | ✅ 高（深度范围内） | ✅ 高 |
| **Token 消耗** | ✅ 低 | ✅ 低 | ❌ 高 |
| **LLM 理解** | ❌ 差 | ✅ 好 | ⚠️ 中 |
| **避免干扰** | ✅ 是 | ✅ 是 | ❌ 否 |
| **实现复杂度** | ✅ 低 | ⚠️ 中 | ✅ 低 |
| **可控性** | ✅ 高 | ✅ 高 | ⚠️ 中 |

### 配置参数

```go
const (
    // MaxSchemaDepth 最大 Schema 展开深度
    // 建议值：2-3 层
    // - 2 层：适合简单循环引用（如树形结构）
    // - 3 层：适合复杂循环引用（多层嵌套）
    MaxSchemaDepth = 2
)
```

---

## 🔧 实现建议

### 代码结构

```go
type schemaResolver struct {
    maxDepth    int
    visitedRefs map[string]bool
}

func (r *schemaResolver) resolveSchema(
    schema interface{},
    apiSpec map[string]interface{},
    currentDepth int,
) (map[string]interface{}, error) {
    // 1. 检查深度限制
    if currentDepth >= r.maxDepth {
        return r.pruneSchema(schema), nil
    }

    // 2. 检查循环引用
    if refPath, hasRef := schema["$ref"].(string); hasRef {
        if r.visitedRefs[refPath] {
            // 检测到循环，但深度未达到上限，继续展开
            return r.resolveWithDepth(refPath, currentDepth + 1)
        }
        // 标记为已访问
        r.visitedRefs[refPath] = true
        defer delete(r.visitedRefs, refPath)
    }

    // 3. 正常解析（在深度范围内）
    return r.resolveProperties(schema, currentDepth)
}

func (r *schemaResolver) pruneSchema(schema map[string]interface{}) map[string]interface{} {
    // 保留类型和描述，移除 properties
    // ...
}
```

### 关键实现细节

1. **深度计算**：
   - 每次解析 `$ref` 时，深度 +1
   - 解析 `properties` 中的属性时，深度不变（同一层级）

2. **循环引用检测**：
   - 使用 `visitedRefs` map 记录已访问的 `$ref`
   - 检测到循环时，检查当前深度
   - 如果深度未达到上限，继续展开

3. **剪枝时机**：
   - 达到最大深度时
   - 检测到循环引用且深度已达到上限时

---

## 📊 决策总结

### 最终选择：深度限制剪枝

**理由：**
1. ✅ **节省 token**：不添加冗长的循环引用说明
2. ✅ **信息完整**：在深度范围内保留完整结构
3. ✅ **避免干扰**：不修改原始 description
4. ✅ **类型明确**：LLM 能明确知道参数类型
5. ✅ **可控性强**：通过深度参数控制展开程度

**适用场景：**
- ✅ 所有循环引用场景（简单/中等/复杂）
- ✅ 需要节省 token 的场景
- ✅ 需要 LLM 明确理解参数类型的场景

**不适用场景：**
- ❌ 需要完整展开所有层级的场景（不现实，会导致无限递归）

---

**文档版本：** v2.0
**最后更新：** 2025-12-23
**策略状态：** ✅ 已确定，待实现
