# 并发策略与超时配置(简化版)

## 目标

当一次请求包含多个 `properties` 时,并发调用 LLM 生成 `dynamic_params`,提升整体响应速度。

## 1. 并发策略(简化)

### 1.1 核心原则

- **按 property 并发生成**:每个 property 的 `dynamic_params[property]` 相互独立,可并行调用 LLM
- **固定并发池**:使用固定大小的并发池(不需要动态调整)
- **全成功或全失败**:任一 property 失败则整体失败,不支持部分成功

### 1.2 并发参数(推荐默认值)

| 参数 | 默认值 | 说明 | 调优建议 |
|------|--------|------|----------|
| `max_concurrency` | 4 | 最大并发 LLM 调用数 | 根据 LLM 网关限流调整(3~8) |
| `llm_timeout` | 10s | 单次 LLM 调用超时 | P95 < 3s,预留容错空间 |
| `total_timeout` | 30s | 整体请求超时 | 避免 Agent 长时间等待 |
| `max_retry` | 2 | LLM 调用失败重试次数 | 429/超时重试,其他错误不重试 |

### 1.3 并发上限选择依据

**为什么选 4?**

1. **LLM 网关限流**:多数 LLM 服务对单账户有 QPS 限制(如 10 QPS),并发 4 可避免频繁触发 429
2. **延迟收益递减**:实测 3~5 并发时延迟最优,超过 8 后排队时间抵消并发收益
3. **内存开销**:每个 LLM 调用需要完整上下文(含 logic_property.parameters),4 个并发内存开销可控

**可调整性**:
- 如果 LLM 网关限流宽松(如 50 QPS):可调至 6~8
- 如果请求 properties 通常 ≤ 3:可降至 3

---

## 2. 超时处理

### 2.1 超时层级

```
total_timeout (30s)
├── ontology-manager 调用 (5s)
└── LLM 并发生成 (20s)
    ├── property_1: llm_timeout (10s) + retry
    ├── property_2: llm_timeout (10s) + retry
    └── ...
└── ontology-query 调用 (5s)
```

### 2.2 超时行为

- **llm_timeout 超时**:重试 1 次(总计 2 次),仍失败则返回缺参错误
- **total_timeout 超时**:直接返回 `TIMEOUT` 错误,不重试

---

## 3. 重试策略(仅 LLM)

### 3.1 可重试错误

- HTTP 429(限流)
- HTTP 5xx(服务端错误)
- 网络超时

### 3.2 不重试错误

- HTTP 4xx(除 429):参数错误/鉴权失败
- JSON 解析失败(已执行 max_repair_rounds 修复)
- 缺参错误(业务逻辑,不是 LLM 故障)

### 3.3 重试间隔(指数退避)

- 第 1 次重试:延迟 100ms
- 第 2 次重试:延迟 200ms

---

## 4. 实现伪代码(简化)

```go
func GenerateDynamicParams(properties map[string]*LogicPropertyDef, context Context) (map[string]interface{}, error) {
    // 1. 准备阶段 - 构建任务列表
    tasks := []PropertyTask{}
    for name, prop := range properties {
        tasks = append(tasks, PropertyTask{Name: name, Property: prop})
    }

    // 2. 限制并发数
    semaphore := make(chan struct{}, maxConcurrency) // 默认 4

    // 3. 并发调用 LLM（按 property 并发，不区分类型）
    results := make(chan PropertyResult, len(tasks))
    for _, task := range tasks {
        go func(t PropertyTask) {
            semaphore <- struct{}{} // 获取信号量
            defer func() { <-semaphore }() // 释放信号量

            // Step 1: 根据类型构建提示词
            var prompt string
            if t.Property.Type == "metric" {
                prompt = BuildMetricPrompt(t.Property, context)
            } else {
                prompt = BuildOperatorPrompt(t.Property, context)
            }

            // Step 2: 调用 LLM(含重试)
            llmResponse := CallLLMWithRetry(prompt, context, maxRetry, llmTimeout)

            // Step 3: 解析和修复 JSON
            params := ParseAndRepairJSON(llmResponse, maxRepairRounds)

            // Step 4: 根据类型校验参数
            var err error
            if t.Property.Type == "metric" {
                err = ValidateMetricParams(params)
            } else {
                err = ValidateOperatorParams(params)
            }

            results <- PropertyResult{
                Property: t.Name,
                Params:   params,
                Error:    err,
            }
        }(task)
    }

    // 4. 收集结果
    dynamicParams := make(map[string]interface{})
    missingParams := []MissingParam{}

    for i := 0; i < len(tasks); i++ {
        res := <-results
        if res.Error != nil {
            // 收集缺参信息，而不是直接失败
            missingParams = append(missingParams, MissingParam{
                Property: res.Property,
                Error:    res.Error,
            })
        } else {
            dynamicParams[res.Property] = res.Params
        }
    }

    // 如果有缺参，返回缺参错误
    if len(missingParams) > 0 {
        return nil, MissingParamsError{Missing: missingParams}
    }

    return dynamicParams, nil
}

func CallLLMWithRetry(property string, context Context, maxRetry int, timeout time.Duration) (interface{}, error) {
    for attempt := 1; attempt <= maxRetry; attempt++ {
        ctx, cancel := context.WithTimeout(context.Background(), timeout)
        defer cancel()

        result, err := CallLLM(ctx, property, context)
        if err == nil {
            return result, nil
        }

        // 判断是否可重试
        if !isRetriable(err) || attempt == maxRetry {
            return nil, err
        }

        // 指数退避
        time.Sleep(time.Duration(attempt * 100) * time.Millisecond)
    }
    return nil, errors.New("max retry exceeded")
}

func isRetriable(err error) bool {
    // HTTP 429, 5xx, 超时可重试
    return err.StatusCode == 429 ||
           (err.StatusCode >= 500 && err.StatusCode < 600) ||
           errors.Is(err, context.DeadlineExceeded)
}
```

---

## 5. 可观测性(必须)

每次 LLM 调用必须记录:

```json
{
  "trace_id": "3f5d6c1c-xxxx",
  "span_id": "property_approved_drug_count",
  "property": "approved_drug_count",
  "llm_model": "gpt-4",
  "attempt": 1,
  "latency_ms": 2341,
  "status": "success",
  "error_code": null,
  "prompt_tokens": 850,
  "completion_tokens": 120
}
```

聚合指标:
- `llm.call.duration.p95`:P95 延迟
- `llm.call.retry.rate`:重试率
- `llm.call.failure.rate`:失败率(按 error_code 分组)
- `dynamic_params.generation.concurrency`:实际并发数

---

## 6. 配置建议(按场景)

| 场景 | max_concurrency | llm_timeout | total_timeout |
|------|-----------------|-------------|---------------|
| **生产环境(默认)** | 4 | 10s | 30s |
| **高并发场景**(LLM 网关限流宽松) | 6~8 | 8s | 30s |
| **低延迟场景**(properties 通常 ≤ 3) | 3 | 5s | 20s |
| **测试环境** | 2 | 15s | 60s |

---

## 7. 未来优化方向(暂不实现)

- 自适应并发:根据 LLM 网关限流动态调整
- 智能重试:根据历史成功率决定是否重试
- 缓存 dynamic_params:相同 query + property 缓存结果(需要处理时间依赖)


