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

793 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 意图驱动话术流程 - 设计文档
## 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 测试
- 基于历史数据的话术优化建议