# 意图驱动话术流程 - 设计文档

## 1. 架构概览

### 1.1 系统架构图

```
┌─────────────────────────────────────────────────────────────┐
│                    前端配置界面                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 模式选择器   │  │ 意图配置表单 │  │ 约束管理器   │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                            │ HTTP API
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    后端 API 层                               │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  ScriptFlowService (CRUD)                            │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                  FlowEngine (执行引擎)                       │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  start() / advance()                                 │   │
│  │    ↓                                                 │   │
│  │  _generate_step_content()  ← 核心扩展点              │   │
│  │    ├─ fixed: 返回 content                           │   │
│  │    ├─ flexible: 调用 ScriptGenerator                │   │
│  │    └─ template: 调用 TemplateEngine                 │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            │
            ┌───────────────┼───────────────┐
            ▼               ▼               ▼
    ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
    │ScriptGenerator│ │TemplateEngine│ │ Orchestrator │
    │  (新增)       │ │  (新增)      │ │  (LLM调用)   │
    └──────────────┘ └──────────────┘ └──────────────┘
```

### 1.2 数据流图

```
用户配置流程
    │
    ├─ 选择 script_mode
    │   ├─ fixed: 配置 content
    │   ├─ flexible: 配置 intent + constraints
    │   └─ template: 配置 content (模板)
    │
    ▼
保存到数据库 (ScriptFlow.steps JSON)
    │
    ▼
执行时加载流程
    │
    ├─ FlowEngine.start() / advance()
    │   │
    │   ├─ 获取当前步骤配置
    │   │
    │   ├─ 调用 _generate_step_content()
    │   │   │
    │   │   ├─ fixed: 直接返回 content
    │   │   │
    │   │   ├─ flexible:
    │   │   │   ├─ 构建 Prompt (intent + constraints + history)
    │   │   │   ├─ 调用 LLM 生成话术
    │   │   │   └─ 失败时返回 fallback (content)
    │   │   │
    │   │   └─ template:
    │   │       ├─ 解析模板变量
    │   │       ├─ 调用 LLM 生成变量值
    │   │       └─ 替换模板占位符
    │   │
    │   └─ 返回生成的话术
    │
    ▼
返回给用户
```

---

## 2. 核心模块设计

### 2.1 后端：话术生成引擎

#### 2.1.1 FlowEngine 扩展

**文件位置**: `ai-service/app/services/flow/engine.py`

**新增方法**:

```python
async def _generate_step_content(
    self,
    step: dict,
    context: dict,
    history: list[dict]
) -> str:
    """
    [AC-IDS-03] 根据步骤配置生成话术内容

    Args:
        step: 步骤配置 (包含 script_mode, intent, constraints 等)
        context: 会话上下文 (从 FlowInstance.context 获取)
        history: 对话历史 (最近 N 轮)

    Returns:
        生成的话术文本
    """
    script_mode = step.get("script_mode", "fixed")

    if script_mode == "fixed":
        return step.get("content", "")

    elif script_mode == "flexible":
        return await self._generate_flexible_script(step, context, history)

    elif script_mode == "template":
        return await self._generate_template_script(step, context, history)

    else:
        logger.warning(f"Unknown script_mode: {script_mode}, fallback to fixed")
        return step.get("content", "")
```

**修改方法**:

```python
async def start(
    self,
    tenant_id: str,
    session_id: str,
    flow_id: uuid.UUID,
) -> tuple[FlowInstance | None, str | None]:
    """
    [AC-IDS-05] 修改：启动流程时生成首步话术
    """
    # ... 现有逻辑 ...

    first_step = flow.steps[0]

    # 修改：调用话术生成引擎
    history = await self._get_conversation_history(tenant_id, session_id, limit=3)
    first_content = await self._generate_step_content(
        first_step,
        instance.context,
        history
    )

    return instance, first_content
```

#### 2.1.2 ScriptGenerator (新增模块)

**文件位置**: `ai-service/app/services/flow/script_generator.py`

**职责**: 灵活模式的话术生成逻辑

```python
class ScriptGenerator:
    """
    [AC-IDS-04] 灵活模式话术生成器
    """

    def __init__(self, orchestrator):
        self._orchestrator = orchestrator

    async def generate(
        self,
        intent: str,
        intent_description: str | None,
        constraints: list[str],
        context: dict,
        history: list[dict],
        fallback: str
    ) -> str:
        """
        生成灵活话术

        Args:
            intent: 步骤意图
            intent_description: 意图详细说明
            constraints: 话术约束条件
            context: 会话上下文
            history: 对话历史
            fallback: 失败时的 fallback 话术

        Returns:
            生成的话术文本
        """
        try:
            prompt = self._build_prompt(
                intent, intent_description, constraints, context, history
            )

            # 调用 LLM，设置 2 秒超时
            response = await asyncio.wait_for(
                self._orchestrator.generate(prompt),
                timeout=2.0
            )

            return response.strip()

        except asyncio.TimeoutError:
            logger.warning(f"[AC-IDS-05] Script generation timeout, use fallback")
            return fallback

        except Exception as e:
            logger.error(f"[AC-IDS-05] Script generation failed: {e}, use fallback")
            return fallback

    def _build_prompt(
        self,
        intent: str,
        intent_description: str | None,
        constraints: list[str],
        context: dict,
        history: list[dict]
    ) -> str:
        """
        [AC-IDS-04] 构建 LLM Prompt
        """
        prompt_parts = [
            "你是一个客服对话系统，当前需要执行以下步骤：",
            "",
            f"【步骤目标】{intent}"
        ]

        if intent_description:
            prompt_parts.append(f"【详细说明】{intent_description}")

        if constraints:
            prompt_parts.append("【约束条件】")
            for c in constraints:
                prompt_parts.append(f"- {c}")

        if history:
            prompt_parts.append("")
            prompt_parts.append("【对话历史】")
            for msg in history[-3:]:  # 最近 3 轮
                role = "用户" if msg["role"] == "user" else "客服"
                prompt_parts.append(f"{role}: {msg['content']}")

        if context.get("inputs"):
            prompt_parts.append("")
            prompt_parts.append("【已收集信息】")
            for inp in context["inputs"]:
                prompt_parts.append(f"- {inp}")

        prompt_parts.extend([
            "",
            "请生成一句符合目标和约束的话术（不超过50字）。",
            "只返回话术内容，不要解释。"
        ])

        return "\n".join(prompt_parts)
```

#### 2.1.3 TemplateEngine (新增模块)

**文件位置**: `ai-service/app/services/flow/template_engine.py`

**职责**: 模板模式的变量填充逻辑

```python
import re

class TemplateEngine:
    """
    [AC-IDS-06] 模板话术引擎
    """

    VARIABLE_PATTERN = re.compile(r'\{(\w+)\}')

    def __init__(self, orchestrator):
        self._orchestrator = orchestrator

    async def fill_template(
        self,
        template: str,
        context: dict,
        history: list[dict]
    ) -> str:
        """
        填充模板变量

        Args:
            template: 话术模板（包含 {变量名} 占位符）
            context: 会话上下文
            history: 对话历史

        Returns:
            填充后的话术
        """
        # 提取模板中的变量
        variables = self.VARIABLE_PATTERN.findall(template)

        if not variables:
            return template

        # 为每个变量生成值
        variable_values = {}
        for var in variables:
            value = await self._generate_variable_value(var, context, history)
            variable_values[var] = value

        # 替换模板中的占位符
        result = template
        for var, value in variable_values.items():
            result = result.replace(f"{{{var}}}", value)

        return result

    async def _generate_variable_value(
        self,
        variable_name: str,
        context: dict,
        history: list[dict]
    ) -> str:
        """
        为单个变量生成值
        """
        # 先尝试从上下文中获取
        if variable_name in context:
            return str(context[variable_name])

        # 否则调用 LLM 生成
        prompt = f"""
        根据对话历史，为变量 "{variable_name}" 生成合适的值。

        对话历史：
        {self._format_history(history[-3:])}

        只返回变量值，不要解释。
        """

        try:
            response = await asyncio.wait_for(
                self._orchestrator.generate(prompt),
                timeout=1.0
            )
            return response.strip()
        except:
            return f"[{variable_name}]"  # fallback
```

---

### 2.2 前端：配置界面设计

#### 2.2.1 类型定义扩展

**文件位置**: `ai-service-admin/src/types/script-flow.ts`

```typescript
export type ScriptMode = 'fixed' | 'flexible' | 'template'

export interface FlowStep {
  step_id: string
  step_no: number

  // 原有字段
  content: string
  wait_input: boolean
  timeout_seconds?: number
  timeout_action?: 'repeat' | 'skip' | 'transfer'
  next_conditions?: NextCondition[]

  // 新增字段
  script_mode?: ScriptMode
  intent?: string
  intent_description?: string
  script_constraints?: string[]
  expected_variables?: string[]
}

export const SCRIPT_MODE_OPTIONS = [
  { value: 'fixed', label: '固定话术', description: '话术内容固定不变' },
  { value: 'flexible', label: '灵活话术', description: 'AI根据意图和上下文生成' },
  { value: 'template', label: '模板话术', description: 'AI填充模板中的变量' }
]
```

#### 2.2.2 配置表单组件

**文件位置**: `ai-service-admin/src/views/admin/script-flow/index.vue`

**UI 结构**:

```vue
<template>
  <el-form-item label="话术模式">
    <el-radio-group v-model="currentStep.script_mode">
      <el-radio-button
        v-for="option in SCRIPT_MODE_OPTIONS"
        :key="option.value"
        :label="option.value"
      >
        {{ option.label }}
        <el-tooltip :content="option.description">
          <el-icon><QuestionFilled /></el-icon>
        </el-tooltip>
      </el-radio-button>
    </el-radio-group>
  </el-form-item>

  <!-- 固定模式 -->
  <template v-if="currentStep.script_mode === 'fixed'">
    <el-form-item label="话术内容" required>
      <el-input
        v-model="currentStep.content"
        type="textarea"
        :rows="3"
        placeholder="输入固定话术内容"
      />
    </el-form-item>
  </template>

  <!-- 灵活模式 -->
  <template v-if="currentStep.script_mode === 'flexible'">
    <el-form-item label="步骤意图" required>
      <el-input
        v-model="currentStep.intent"
        placeholder="例如：获取用户姓名"
      />
    </el-form-item>

    <el-form-item label="意图说明">
      <el-input
        v-model="currentStep.intent_description"
        type="textarea"
        :rows="2"
        placeholder="详细描述这一步的目的和期望效果"
      />
    </el-form-item>

    <el-form-item label="话术约束">
      <ConstraintManager v-model="currentStep.script_constraints" />
    </el-form-item>

    <el-form-item label="Fallback话术" required>
      <el-input
        v-model="currentStep.content"
        type="textarea"
        :rows="2"
        placeholder="AI生成失败时使用的备用话术"
      />
    </el-form-item>
  </template>

  <!-- 模板模式 -->
  <template v-if="currentStep.script_mode === 'template'">
    <el-form-item label="话术模板" required>
      <el-input
        v-model="currentStep.content"
        type="textarea"
        :rows="3"
        placeholder="使用 {变量名} 标记可变部分，例如：您好{user_name}，请问您{inquiry_style}？"
      />
      <div class="template-hint">
        提示：使用 {变量名} 标记需要AI填充的部分
      </div>
    </el-form-item>
  </template>
</template>
```

#### 2.2.3 约束管理组件

**文件位置**: `ai-service-admin/src/views/admin/script-flow/components/ConstraintManager.vue`

```vue
<template>
  <div class="constraint-manager">
    <div class="constraint-tags">
      <el-tag
        v-for="(constraint, index) in modelValue"
        :key="index"
        closable
        @close="removeConstraint(index)"
      >
        {{ constraint }}
      </el-tag>
    </div>

    <el-input
      v-model="newConstraint"
      placeholder="输入约束条件后按回车添加"
      @keyup.enter="addConstraint"
      class="constraint-input"
    >
      <template #append>
        <el-button @click="addConstraint">添加</el-button>
      </template>
    </el-input>

    <div class="constraint-presets">
      <span class="preset-label">常用约束：</span>
      <el-button
        v-for="preset in PRESET_CONSTRAINTS"
        :key="preset"
        size="small"
        @click="addPreset(preset)"
      >
        {{ preset }}
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
const PRESET_CONSTRAINTS = [
  '必须礼貌',
  '语气自然',
  '简洁明了',
  '不要生硬',
  '不要重复'
]

const addConstraint = () => {
  if (newConstraint.value.trim()) {
    emit('update:modelValue', [...modelValue.value, newConstraint.value.trim()])
    newConstraint.value = ''
  }
}
</script>
```

---

## 3. 数据模型设计

### 3.1 数据库 Schema

**无需修改表结构**，因为 `script_flows.steps` 已经是 JSON 类型。

**现有结构**:
```sql
CREATE TABLE script_flows (
    id UUID PRIMARY KEY,
    tenant_id VARCHAR NOT NULL,
    name VARCHAR NOT NULL,
    description TEXT,
    steps JSONB NOT NULL,  -- 直接扩展此字段
    is_enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);
```

**扩展后的 steps JSON 示例**:
```json
[
  {
    "step_no": 1,
    "script_mode": "flexible",
    "intent": "获取用户姓名",
    "intent_description": "礼貌询问用户姓名",
    "script_constraints": ["必须礼貌", "语气自然"],
    "content": "请问怎么称呼您？",
    "wait_input": true,
    "timeout_seconds": 60
  }
]
```

### 3.2 向后兼容策略

**读取时**:
```python
def _normalize_step(step: dict) -> dict:
    """确保步骤配置包含所有必需字段"""
    return {
        "script_mode": step.get("script_mode", "fixed"),
        "intent": step.get("intent"),
        "intent_description": step.get("intent_description"),
        "script_constraints": step.get("script_constraints", []),
        "expected_variables": step.get("expected_variables", []),
        **step  # 保留其他字段
    }
```

**写入时**:
- 前端默认 `script_mode = 'fixed'`
- 后端不做强制校验，允许字段缺失

---

## 4. 技术决策

### 4.1 为什么选择 JSON 扩展而不是新表？

**决策**: 在现有的 `steps` JSON 字段中扩展，而不是创建新表

**理由**:
1. **简化数据模型**: 步骤配置是流程的一部分，不需要独立管理
2. **避免数据迁移**: 无需修改表结构，现有数据自动兼容
3. **灵活性**: JSON 字段易于扩展，未来可以继续添加新字段
4. **性能**: 步骤数量通常不多（<20），JSON 查询性能足够

**权衡**: 无法对意图字段建立索引，但实际场景中不需要按意图查询流程

### 4.2 为什么设置 2 秒超时？

**决策**: LLM 调用超时设置为 2 秒

**理由**:
1. **用户体验**: 对话系统需要快速响应，2 秒是可接受的上限
2. **Fallback 保障**: 超时后立即返回 fallback 话术，不影响流程执行
3. **成本控制**: 避免长时间等待消耗资源

**权衡**: 可能导致部分复杂话术生成失败，但有 fallback 保障

### 4.3 为什么对话历史只取最近 3 轮？

**决策**: 传递给 LLM 的对话历史限制为最近 3 轮

**理由**:
1. **Token 成本**: 减少 Prompt 长度，降低成本
2. **相关性**: 最近 3 轮对话最相关，更早的对话影响较小
3. **性能**: 减少数据库查询和网络传输

**权衡**: 可能丢失更早的上下文信息，但实际影响有限

### 4.4 为什么不缓存生成的话术？

**决策**: 不对生成的话术进行缓存

**理由**:
1. **灵活性优先**: 每次生成都考虑最新的上下文，更符合"灵活话术"的定位
2. **缓存复杂度**: 需要考虑缓存失效策略（上下文变化、配置变化）
3. **实际收益有限**: 同一步骤在同一会话中通常只执行一次

**未来优化**: 如果性能成为瓶颈，可以考虑基于上下文哈希的缓存

---

## 5. 错误处理与降级策略

### 5.1 话术生成失败

**场景**: LLM 调用超时或返回错误

**处理**:
1. 记录错误日志（包含 tenant_id, session_id, flow_id, step_no）
2. 返回 `step.content` 作为 fallback
3. 在 ChatMessage 中标记 `is_error=False`（因为有 fallback，不算错误）

### 5.2 配置错误

**场景**: flexible 模式但 intent 为空

**处理**:
1. 前端校验：提交时检查必填字段
2. 后端容错：如果 intent 为空，降级为 fixed 模式

### 5.3 模板解析错误

**场景**: 模板语法错误（如 `{unclosed`）

**处理**:
1. 捕获正则匹配异常
2. 返回原始模板（不做替换）
3. 记录警告日志

---

## 6. 性能考虑

### 6.1 预期性能指标

| 指标 | 目标值 | 说明 |
|------|--------|------|
| 话术生成延迟 (P95) | < 2s | LLM 调用时间 |
| API 响应时间增加 | < 10% | 相比固定模式 |
| 数据库查询增加 | +1 次 | 获取对话历史 |

### 6.2 优化策略

1. **并行查询**: 获取对话历史和流程配置可以并行
2. **限制历史长度**: 只查询最近 3 轮对话
3. **超时控制**: 严格的 2 秒超时，避免长时间等待

---

## 7. 测试策略

### 7.1 单元测试

**测试文件**: `ai-service/tests/services/flow/test_script_generator.py`

**测试用例**:
- 固定模式：直接返回 content
- 灵活模式：正常生成、超时 fallback、异常 fallback
- 模板模式：变量替换、变量缺失、模板语法错误

### 7.2 集成测试

**测试文件**: `ai-service/tests/api/test_script_flow_intent_driven.py`

**测试场景**:
1. 创建灵活模式流程
2. 启动流程，验证首步话术生成
3. 推进流程，验证后续步骤话术生成
4. 验证对话历史正确传递

### 7.3 端到端测试

**测试场景**:
1. 前端配置灵活模式流程
2. 保存并启用流程
3. 通过 Provider API 触发流程
4. 验证生成的话术符合意图和约束

---

## 8. 部署与发布

### 8.1 发布顺序

1. **Phase 1**: 后端数据模型和 API 扩展
   - 部署后端代码
   - 验证 API 向后兼容性

2. **Phase 2**: 后端话术生成引擎
   - 部署话术生成逻辑
   - 验证 fallback 机制

3. **Phase 3**: 前端配置界面
   - 部署前端代码
   - 验证配置保存和加载

4. **Phase 4**: 灰度发布
   - 选择部分租户启用灵活模式
   - 监控性能和错误率
   - 全量发布

### 8.2 回滚策略

**如果出现问题**:
1. 前端回滚：恢复旧版本，用户无法配置灵活模式
2. 后端回滚：恢复旧版本，灵活模式降级为固定模式
3. 数据无需回滚：JSON 字段扩展，旧版本可以忽略新字段

---

## 9. 监控与告警

### 9.1 关键指标

| 指标 | 说明 | 告警阈值 |
|------|------|---------|
| script_generation_latency | 话术生成延迟 | P95 > 2.5s |
| script_generation_timeout_rate | 超时率 | > 5% |
| script_generation_error_rate | 错误率 | > 1% |
| fallback_usage_rate | Fallback 使用率 | > 10% |

### 9.2 日志记录

**关键日志**:
```python
logger.info(
    f"[AC-IDS-03] Generated script: tenant={tenant_id}, "
    f"session={session_id}, flow={flow_id}, step={step_no}, "
    f"mode={script_mode}, latency={latency_ms}ms"
)

logger.warning(
    f"[AC-IDS-05] Script generation timeout, use fallback: "
    f"tenant={tenant_id}, session={session_id}, step={step_no}"
)
```

---

## 10. 未来扩展

### 10.1 短期优化（v1.1）

- 话术生成缓存（基于上下文哈希）
- 更丰富的约束条件预设
- 话术效果评估（用户满意度）

### 10.2 长期规划（v2.0）

- 多轮对话规划（提前生成后续步骤话术）
- 话术 A/B 测试
- 基于历史数据的话术优化建议
