ai-robot-core/spec/intent-driven-script/design.md

23 KiB
Raw Blame History

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

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

新增方法:

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", "")

修改方法:

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

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

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

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

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

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 结构:

<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

<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 类型。

现有结构:

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 示例:

[
  {
    "step_no": 1,
    "script_mode": "flexible",
    "intent": "获取用户姓名",
    "intent_description": "礼貌询问用户姓名",
    "script_constraints": ["必须礼貌", "语气自然"],
    "content": "请问怎么称呼您?",
    "wait_input": true,
    "timeout_seconds": 60
  }
]

3.2 向后兼容策略

读取时:

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. 性能: 步骤数量通常不多(<20JSON 查询性能足够

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

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 日志记录

关键日志:

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 测试
  • 基于历史数据的话术优化建议