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

793 lines
23 KiB
Markdown
Raw Normal View 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`
**新增方法**:
```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. **性能**: 步骤数量通常不多(<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 日志记录
**关键日志**:
```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 测试
- 基于历史数据的话术优化建议