764 lines
32 KiB
Markdown
764 lines
32 KiB
Markdown
|
|
# 复杂场景重构第二阶段 — 架构设计
|
|||
|
|
|
|||
|
|
## 1. 文档信息
|
|||
|
|
|
|||
|
|
| 属性 | 值 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 文档名称 | 复杂场景重构第二阶段 — 架构设计文档 |
|
|||
|
|
| 版本 | v1.0 |
|
|||
|
|
| 创建日期 | 2025-07-11 |
|
|||
|
|
| 作者 | 高见远(架构师) |
|
|||
|
|
| 关联文档 | Phase 2 PRD、Phase 1 架构设计 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 实现方案 + 框架选型
|
|||
|
|
|
|||
|
|
### 2.1 P2 上下文压缩
|
|||
|
|
|
|||
|
|
#### 2.1.1 Tokenizer 选择
|
|||
|
|
|
|||
|
|
| 方案 | 说明 | 优先级 |
|
|||
|
|
|------|------|--------|
|
|||
|
|
| `tiktoken`(cl100k_base) | OpenAI 官方编码器,与 Dify/GPT 系列模型一致,计数精确 | 首选 |
|
|||
|
|
| 字符估算兜底 | tiktoken 不可用时,按 1 token ≈ 1.5 个中文字符估算(误差 ≤10%) | 兜底 |
|
|||
|
|
|
|||
|
|
- 封装为 `TokenCounter` 工具类,内部根据 `tiktoken` 可用性自动热切换
|
|||
|
|
- 接口统一:`count_tokens(text: str) -> int`,上层无感知底层差异
|
|||
|
|
|
|||
|
|
#### 2.1.2 压缩引擎设计(ContextCompressor 类)
|
|||
|
|
|
|||
|
|
**文件位置**:`backend/app/services/automation/context_compressor.py`
|
|||
|
|
|
|||
|
|
**核心方法**:
|
|||
|
|
|
|||
|
|
| 方法 | 签名 | 职责 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `count_tokens` | `(messages: list) -> int` | 计算消息列表的 token 总数 |
|
|||
|
|
| `should_compress` | `(session_id) -> bool` | 判断是否需要压缩(token > threshold) |
|
|||
|
|
| `compress` | `(session_id, messages, info_items, actions, task_node) -> CompressedContext` | 执行压缩,返回压缩后上下文 |
|
|||
|
|
| `_extract_key_info` | `(info_items, actions, task_node) -> str` | 提取关键信息(信息项当前值、已执行动作、任务节点) |
|
|||
|
|
| `_summarize_history` | `(messages_to_compress) -> str` | 调用 LLM 对历史消息生成摘要 |
|
|||
|
|
| `_build_compressed_context` | `(key_info, summary, recent_messages) -> str` | 组装压缩后上下文(结构化 Markdown) |
|
|||
|
|
| `_progressive_compress` | `(context, level=1) -> str` | 渐进式压缩(最多3级,超出降级截断) |
|
|||
|
|
|
|||
|
|
**渐进式压缩策略**:
|
|||
|
|
|
|||
|
|
| 级别 | 策略 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| Level 1 | LLM 摘要 + 保留最近4轮对话 | 标准压缩,token 降低约 60% |
|
|||
|
|
| Level 2 | LLM 摘要 + 保留最近2轮对话 | 二次压缩,token 降低约 80% |
|
|||
|
|
| Level 3 | 纯关键信息 + 保留最近1轮对话 | 极限压缩,token 降低约 90% |
|
|||
|
|
| 超出 | 截断最旧消息 | 降级保护,确保不超限 |
|
|||
|
|
|
|||
|
|
#### 2.1.3 LLM 调用策略
|
|||
|
|
|
|||
|
|
- 复用现有 `DifyClient`,调用 `/v1/chat-messages` 端点
|
|||
|
|
- **摘要 prompt** 设计要求:将对话历史压缩为结构化摘要,保留关键决策点和信息项
|
|||
|
|
- 摘要调用超时 **30s**,失败则降级为截断最旧消息(`_progressive_compress` 的 Level 3 策略)
|
|||
|
|
|
|||
|
|
#### 2.1.4 压缩触发时机
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
AutoSessionService._run_background()
|
|||
|
|
├── 1. SessionManager.appendMessage(新消息)
|
|||
|
|
├── 2. TokenCounter.count_tokens(当前消息列表)
|
|||
|
|
├── 3. if tokens > threshold:
|
|||
|
|
│ ├── ContextCompressor.compress() ← 压缩
|
|||
|
|
│ ├── 用压缩后上下文替换原始消息列表
|
|||
|
|
│ └── 写入 auto_context_compressions 表(日志)
|
|||
|
|
├── 4. DifyClient.chat(压缩后/原始消息) ← 调LLM
|
|||
|
|
└── 5. 返回响应
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.1.5 压缩后上下文格式
|
|||
|
|
|
|||
|
|
压缩后生成一条结构化 Markdown 系统消息,替换原始历史:
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
## 会话上下文摘要(系统压缩)
|
|||
|
|
### 已收集信息项
|
|||
|
|
- {name}: {value}(v{version})
|
|||
|
|
### 已执行动作
|
|||
|
|
- ✅/⏳ {action_title}({time})
|
|||
|
|
### 当前任务节点
|
|||
|
|
{task_node}
|
|||
|
|
### 历史摘要
|
|||
|
|
{LLM生成的摘要}
|
|||
|
|
### 最近对话
|
|||
|
|
[最近4轮原始对话保留]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2.2 P3 多轮纠错
|
|||
|
|
|
|||
|
|
#### 2.2.1 版本管理
|
|||
|
|
|
|||
|
|
- 复用 Phase 1 已有的 `InformationItem.version` + `update_history` 机制
|
|||
|
|
- **移除单次更正限制**:Phase 1 实际未硬编码限制,需在 `IntentRouter` 和 `SessionManager` 中确认无限制逻辑
|
|||
|
|
- 每次 CORRECT / SUPPLEMENT 意图都正常 `version += 1`,无次数上限
|
|||
|
|
|
|||
|
|
#### 2.2.2 快照机制(SnapshotService 类)
|
|||
|
|
|
|||
|
|
**文件位置**:`backend/app/services/automation/snapshot_service.py`
|
|||
|
|
|
|||
|
|
**核心方法**:
|
|||
|
|
|
|||
|
|
| 方法 | 签名 | 职责 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `create_snapshot` | `(session_id, trigger_item_key, correction_ids) -> InformationSnapshot` | 在每次更正前创建快照 |
|
|||
|
|
| `get_latest_snapshot` | `(session_id) -> Optional[InformationSnapshot]` | 获取最近未撤销的快照 |
|
|||
|
|
| `undo_correction` | `(session_id) -> dict` | 撤销最近一次更正(限制最近5次) |
|
|||
|
|
| `get_snapshot_history` | `(session_id) -> list` | 获取快照历史列表 |
|
|||
|
|
| `get_version_diff` | `(session_id, v1, v2) -> dict` | 对比两个版本的差异 |
|
|||
|
|
|
|||
|
|
**撤销逻辑流程**:
|
|||
|
|
|
|||
|
|
1. 查询最近一条 `is_undone=False` 的快照
|
|||
|
|
2. 将快照中记录的信息项值回滚到快照前的版本
|
|||
|
|
3. 标记该快照 `is_undone=True`
|
|||
|
|
4. 限制:最多撤销最近 5 次(通过计数 `is_undone=True` 的快照数判断,超过5次拒绝撤销)
|
|||
|
|
5. 撤销后向 AI 上下文注入"更正撤销"系统消息,让 AI 感知用户撤销了更正
|
|||
|
|
|
|||
|
|
#### 2.2.3 依赖检查(混合方案)
|
|||
|
|
|
|||
|
|
采用**模板预定义 + AI 自动标注 + 运行时检查**三层混合方案:
|
|||
|
|
|
|||
|
|
| 层次 | 机制 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 模板预定义 | `ScenarioConfig.actions` 中预定义 `derived_from` 关系 | 如"设备分配人" derived_from "工号" |
|
|||
|
|
| AI 自动标注 | IntentRouter 识别 CORRECT 意图时,通过 Dify prompt 让 LLM 标注依赖关系 | 补充模板未覆盖的动态依赖 |
|
|||
|
|
| 运行时检查 | `CorrectionService.check_dependencies(session_id, item_key) -> list` | 查询所有 `derived_from` 包含该 item_key 的其他 item,返回联动提示列表 |
|
|||
|
|
|
|||
|
|
**运行时检查逻辑**:
|
|||
|
|
- 查询当前 session 中所有 `derived_from` 数组包含该 `item_key` 的其他信息项
|
|||
|
|
- 如果存在依赖项且已有值,生成联动提示(如"修改工号后,设备分配人可能需要同步更新")
|
|||
|
|
- 返回提示列表,由前端展示给用户/坐席确认
|
|||
|
|
|
|||
|
|
#### 2.2.4 批量更正
|
|||
|
|
|
|||
|
|
- **方法**:`CorrectionService.batch_correct(session_id, corrections: list) -> dict`
|
|||
|
|
- **原子性**:单事务内执行多个更正,任何一项失败则整体回滚(SQLAlchemy transaction rollback)
|
|||
|
|
- **快照策略**:只生成**一条快照**(记录本次批量更正涉及的所有信息项)
|
|||
|
|
- **结果返回**:包含所有已更正项 + 依赖联动警告列表
|
|||
|
|
|
|||
|
|
#### 2.2.5 CorrectionService 类
|
|||
|
|
|
|||
|
|
**文件位置**:`backend/app/services/automation/correction_service.py`
|
|||
|
|
|
|||
|
|
**核心方法**:
|
|||
|
|
|
|||
|
|
| 方法 | 签名 | 职责 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `batch_correct` | `(session_id, corrections: list[dict]) -> BatchCorrectResult` | 批量更正(单事务原子操作) |
|
|||
|
|
| `check_dependencies` | `(session_id, item_key) -> list[dict]` | 依赖检查,返回联动提示列表 |
|
|||
|
|
| `get_correction_history` | `(session_id) -> list` | 更正历史 |
|
|||
|
|
| `get_version_chain` | `(session_id, item_key) -> list` | 版本链(某信息项的所有版本) |
|
|||
|
|
| `get_version_diff` | `(session_id, v1, v2) -> dict` | 版本对比 |
|
|||
|
|
| `undo_correction` | `(session_id) -> dict` | 撤销更正(委托 SnapshotService) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 数据结构和接口
|
|||
|
|
|
|||
|
|
### 3.1 新增/变更 SQLAlchemy 模型
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# ============================================================
|
|||
|
|
# 新增:auto_context_compressions 表(上下文压缩日志)
|
|||
|
|
# ============================================================
|
|||
|
|
class ContextCompression(Base):
|
|||
|
|
"""P2 上下文压缩记录表"""
|
|||
|
|
__tablename__ = "auto_context_compressions"
|
|||
|
|
|
|||
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|||
|
|
session_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|||
|
|
tokens_before: Mapped[int] = mapped_column(Integer, nullable=False)
|
|||
|
|
tokens_after: Mapped[int] = mapped_column(Integer, nullable=False)
|
|||
|
|
compression_ratio: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
|
|||
|
|
task_node: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
|
|||
|
|
duration_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
|||
|
|
compression_level: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1)
|
|||
|
|
summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|||
|
|
created_at: Mapped[datetime] = mapped_column(
|
|||
|
|
DateTime(timezone=True), nullable=False, default=datetime.now
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 新增:auto_information_snapshots 表(信息项快照)
|
|||
|
|
# ============================================================
|
|||
|
|
class InformationSnapshot(Base):
|
|||
|
|
"""P3 信息项快照表(用于撤销更正)"""
|
|||
|
|
__tablename__ = "auto_information_snapshots"
|
|||
|
|
|
|||
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|||
|
|
session_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|||
|
|
trigger_item_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
|||
|
|
snapshot_data: Mapped[dict] = mapped_column(
|
|||
|
|
JSON, nullable=False
|
|||
|
|
) # {item_key: {value, version}}
|
|||
|
|
correction_ids: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|||
|
|
is_undone: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|||
|
|
created_at: Mapped[datetime] = mapped_column(
|
|||
|
|
DateTime(timezone=True), nullable=False, default=datetime.now
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 变更:InformationItem 新增2列
|
|||
|
|
# ============================================================
|
|||
|
|
class InformationItem(Base):
|
|||
|
|
"""信息项模型(Phase 2 变更)"""
|
|||
|
|
# ... Phase 1 已有字段省略 ...
|
|||
|
|
|
|||
|
|
# Phase 2 新增字段
|
|||
|
|
derived_from: Mapped[Optional[list]] = mapped_column(
|
|||
|
|
JSON, nullable=True
|
|||
|
|
) # 推导来源 item_key 数组,如 ["工号"]
|
|||
|
|
correction_reason: Mapped[Optional[str]] = mapped_column(
|
|||
|
|
String(200), nullable=True
|
|||
|
|
) # 更正备注
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 Mermaid 类图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
classDiagram
|
|||
|
|
class AutoSession {
|
|||
|
|
+str id
|
|||
|
|
+str scenario_id
|
|||
|
|
+str status
|
|||
|
|
+str current_task_node
|
|||
|
|
+datetime created_at
|
|||
|
|
+datetime updated_at
|
|||
|
|
+appendMessage(message)
|
|||
|
|
+getContext()
|
|||
|
|
+updateTaskNode(node)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class InformationItem {
|
|||
|
|
+str id
|
|||
|
|
+str session_id
|
|||
|
|
+str item_key
|
|||
|
|
+str item_name
|
|||
|
|
+str value
|
|||
|
|
+int version
|
|||
|
|
+dict update_history
|
|||
|
|
+Optional~list~ derived_from
|
|||
|
|
+Optional~str~ correction_reason
|
|||
|
|
+datetime created_at
|
|||
|
|
+updateValue(new_value, reason)
|
|||
|
|
+getHistory()
|
|||
|
|
+checkDependency()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class ContextCompression {
|
|||
|
|
+int id
|
|||
|
|
+str session_id
|
|||
|
|
+int tokens_before
|
|||
|
|
+int tokens_after
|
|||
|
|
+float compression_ratio
|
|||
|
|
+Optional~str~ task_node
|
|||
|
|
+int duration_ms
|
|||
|
|
+int compression_level
|
|||
|
|
+Optional~str~ summary
|
|||
|
|
+datetime created_at
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class InformationSnapshot {
|
|||
|
|
+int id
|
|||
|
|
+str session_id
|
|||
|
|
+str trigger_item_key
|
|||
|
|
+dict snapshot_data
|
|||
|
|
+list correction_ids
|
|||
|
|
+bool is_undone
|
|||
|
|
+datetime created_at
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class ContextCompressor {
|
|||
|
|
+TokenCounter token_counter
|
|||
|
|
+DifyClient dify_client
|
|||
|
|
+count_tokens(messages) int
|
|||
|
|
+should_compress(session_id) bool
|
|||
|
|
+compress(session_id, messages, info_items, actions, task_node) CompressedContext
|
|||
|
|
-_extract_key_info(info_items, actions, task_node) str
|
|||
|
|
-_summarize_history(messages) str
|
|||
|
|
-_build_compressed_context(key_info, summary, recent) str
|
|||
|
|
-_progressive_compress(context, level) str
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class SnapshotService {
|
|||
|
|
+create_snapshot(session_id, trigger_item_key, correction_ids) InformationSnapshot
|
|||
|
|
+get_latest_snapshot(session_id) Optional~InformationSnapshot~
|
|||
|
|
+undo_correction(session_id) dict
|
|||
|
|
+get_snapshot_history(session_id) list
|
|||
|
|
+get_version_diff(session_id, v1, v2) dict
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class CorrectionService {
|
|||
|
|
+batch_correct(session_id, corrections) BatchCorrectResult
|
|||
|
|
+check_dependencies(session_id, item_key) list
|
|||
|
|
+get_correction_history(session_id) list
|
|||
|
|
+get_version_chain(session_id, item_key) list
|
|||
|
|
+get_version_diff(session_id, v1, v2) dict
|
|||
|
|
+undo_correction(session_id) dict
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AutoSession "1" --> "*" InformationItem : session_id 关联
|
|||
|
|
AutoSession "1" --> "*" ContextCompression : session_id 关联
|
|||
|
|
AutoSession "1" --> "*" InformationSnapshot : session_id 关联
|
|||
|
|
ContextCompressor --> AutoSession : 压缩会话上下文
|
|||
|
|
ContextCompressor --> ContextCompression : 写入压缩日志
|
|||
|
|
SnapshotService --> InformationSnapshot : 管理快照
|
|||
|
|
SnapshotService --> InformationItem : 回滚信息项值
|
|||
|
|
CorrectionService --> SnapshotService : 委托撤销
|
|||
|
|
CorrectionService --> InformationItem : 批量更正
|
|||
|
|
CorrectionService --> InformationSnapshot : 创建快照
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 新增 Pydantic Schema
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# ============================================================
|
|||
|
|
# 请求模型
|
|||
|
|
# ============================================================
|
|||
|
|
|
|||
|
|
class BatchCorrectRequest(BaseModel):
|
|||
|
|
"""批量更正请求"""
|
|||
|
|
corrections: List[CorrectRequest] # 批量更正数组
|
|||
|
|
reason: Optional[str] = None # 更正备注
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UndoCorrectRequest(BaseModel):
|
|||
|
|
"""撤销更正请求"""
|
|||
|
|
pass # 无参数,撤销最近一次
|
|||
|
|
|
|||
|
|
|
|||
|
|
class VersionDiffRequest(BaseModel):
|
|||
|
|
"""版本对比请求"""
|
|||
|
|
v1: int
|
|||
|
|
v2: int
|
|||
|
|
item_key: Optional[str] = None # 指定信息项,None则对比全局快照
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 响应模型
|
|||
|
|
# ============================================================
|
|||
|
|
|
|||
|
|
class ContextCompressionResponse(BaseModel):
|
|||
|
|
"""上下文压缩响应"""
|
|||
|
|
id: int
|
|||
|
|
session_id: str
|
|||
|
|
tokens_before: int
|
|||
|
|
tokens_after: int
|
|||
|
|
compression_ratio: float
|
|||
|
|
task_node: Optional[str]
|
|||
|
|
duration_ms: int
|
|||
|
|
compression_level: int
|
|||
|
|
summary: Optional[str]
|
|||
|
|
created_at: Optional[str]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SnapshotResponse(BaseModel):
|
|||
|
|
"""快照响应"""
|
|||
|
|
id: int
|
|||
|
|
session_id: str
|
|||
|
|
trigger_item_key: str
|
|||
|
|
snapshot_data: dict
|
|||
|
|
correction_ids: list
|
|||
|
|
is_undone: bool
|
|||
|
|
created_at: Optional[str]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class VersionDiffResponse(BaseModel):
|
|||
|
|
"""版本差异响应"""
|
|||
|
|
item_key: str
|
|||
|
|
v1: int
|
|||
|
|
v1_value: str
|
|||
|
|
v2: int
|
|||
|
|
v2_value: str
|
|||
|
|
changed: bool
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BatchCorrectResponse(BaseModel):
|
|||
|
|
"""批量更正响应"""
|
|||
|
|
corrected_items: List[InformationItemResponse]
|
|||
|
|
snapshot_id: int
|
|||
|
|
dependency_warnings: List[dict] = Field(default_factory=list)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 程序调用流程
|
|||
|
|
|
|||
|
|
### 4.1 P2 上下文压缩时序图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Employee as 员工
|
|||
|
|
participant SM as SessionManager
|
|||
|
|
participant TC as TokenCounter
|
|||
|
|
participant CC as ContextCompressor
|
|||
|
|
participant DB as Database
|
|||
|
|
participant LLM as DifyClient / LLM
|
|||
|
|
|
|||
|
|
Employee->>SM: 发送消息
|
|||
|
|
SM->>SM: appendMessage(新消息)
|
|||
|
|
|
|||
|
|
SM->>TC: count_tokens(当前消息列表)
|
|||
|
|
TC-->>SM: 返回 token 总数
|
|||
|
|
|
|||
|
|
alt token > 压缩阈值
|
|||
|
|
SM->>CC: should_compress(session_id)
|
|||
|
|
CC-->>SM: true
|
|||
|
|
|
|||
|
|
SM->>CC: compress(session_id, messages, info_items, actions, task_node)
|
|||
|
|
|
|||
|
|
CC->>CC: _extract_key_info(info_items, actions, task_node)
|
|||
|
|
Note over CC: 提取已收集信息项当前值<br/>已执行动作、当前任务节点
|
|||
|
|
|
|||
|
|
CC->>LLM: _summarize_history(messages_to_compress)
|
|||
|
|
Note over LLM: 摘要 prompt:<br/>将历史压缩为结构化摘要<br/>保留关键决策点和信息项
|
|||
|
|
LLM-->>CC: 返回历史摘要文本
|
|||
|
|
|
|||
|
|
alt LLM 摘要超时/失败(30s)
|
|||
|
|
CC->>CC: 降级 _progressive_compress(level=3)
|
|||
|
|
Note over CC: 截断最旧消息,保留关键信息
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
CC->>CC: _build_compressed_context(key_info, summary, recent_messages)
|
|||
|
|
Note over CC: 组装结构化 Markdown 上下文
|
|||
|
|
|
|||
|
|
CC->>DB: 写入 auto_context_compressions 表
|
|||
|
|
Note over DB: 记录 tokens_before/after<br/>compression_ratio, duration_ms
|
|||
|
|
|
|||
|
|
CC-->>SM: 返回 CompressedContext
|
|||
|
|
SM->>SM: 用压缩后上下文替换原始消息列表
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
SM->>LLM: chat(压缩后/原始消息)
|
|||
|
|
LLM-->>SM: 返回 AI 响应
|
|||
|
|
SM-->>Employee: 返回响应
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 P3 多轮纠错时序图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Employee as 员工
|
|||
|
|
participant IR as IntentRouter
|
|||
|
|
participant CS as CorrectionService
|
|||
|
|
participant SS as SnapshotService
|
|||
|
|
participant DB as Database
|
|||
|
|
participant AI as AI 上下文
|
|||
|
|
|
|||
|
|
Employee->>IR: 发送更正请求(如"工号改成12345")
|
|||
|
|
|
|||
|
|
IR->>IR: 识别 CORRECT 意图
|
|||
|
|
Note over IR: AI 自动标注 derived_from 依赖关系<br/>(通过 Dify prompt)
|
|||
|
|
|
|||
|
|
IR->>CS: batch_correct(session_id, corrections)
|
|||
|
|
|
|||
|
|
CS->>SS: create_snapshot(session_id, trigger_item_key, correction_ids)
|
|||
|
|
Note over SS: 在更正前创建快照<br/>记录所有涉及信息项的当前值和版本
|
|||
|
|
|
|||
|
|
SS->>DB: INSERT auto_information_snapshots
|
|||
|
|
SS-->>CS: 返回 snapshot_id
|
|||
|
|
|
|||
|
|
CS->>DB: BEGIN TRANSACTION
|
|||
|
|
Note over CS: 单事务内批量更新信息项
|
|||
|
|
|
|||
|
|
loop 遍历 corrections 列表
|
|||
|
|
CS->>DB: UPDATE InformationItem<br/>SET value=new_value, version=version+1
|
|||
|
|
alt 某项更新失败
|
|||
|
|
CS->>DB: ROLLBACK
|
|||
|
|
CS-->>IR: 抛出异常,整体回滚
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
CS->>DB: COMMIT
|
|||
|
|
|
|||
|
|
CS->>CS: check_dependencies(session_id, item_key)
|
|||
|
|
Note over CS: 查询所有 derived_from 包含该 item_key 的信息项<br/>生成联动提示列表
|
|||
|
|
|
|||
|
|
CS-->>IR: 返回 BatchCorrectResult<br/>{corrected_items, snapshot_id, dependency_warnings}
|
|||
|
|
|
|||
|
|
IR->>AI: 注入更正结果系统消息
|
|||
|
|
Note over AI: AI 感知信息项已更正<br/>后续对话基于新值继续
|
|||
|
|
|
|||
|
|
IR-->>Employee: 返回更正结果 + 联动提示
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 P3 更正撤销时序图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant User as 用户/坐席
|
|||
|
|
participant API as CorrectionAPI
|
|||
|
|
participant SS as SnapshotService
|
|||
|
|
participant DB as Database
|
|||
|
|
participant AI as AI 上下文
|
|||
|
|
|
|||
|
|
User->>API: POST /undo-correction (session_id)
|
|||
|
|
|
|||
|
|
API->>SS: undo_correction(session_id)
|
|||
|
|
|
|||
|
|
SS->>DB: 查询最近一条 is_undone=False 的快照
|
|||
|
|
DB-->>SS: 返回 latest_snapshot
|
|||
|
|
|
|||
|
|
alt 无可撤销快照
|
|||
|
|
SS-->>API: 返回错误"无可撤销的更正"
|
|||
|
|
API-->>User: 400 无可撤销
|
|||
|
|
else 已撤销次数 >= 5
|
|||
|
|
SS-->>API: 返回错误"撤销次数超限"
|
|||
|
|
API-->>User: 400 撤销次数超限
|
|||
|
|
else 正常撤销
|
|||
|
|
SS->>DB: 读取 snapshot_data<br/>{item_key: {value, version}}
|
|||
|
|
Note over SS: 快照记录了更正前的信息项值和版本
|
|||
|
|
|
|||
|
|
loop 遍历 snapshot_data 中每个信息项
|
|||
|
|
SS->>DB: UPDATE InformationItem<br/>SET value=快照前值, version=快照前版本
|
|||
|
|
Note over DB: 回滚信息项到更正前状态
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
SS->>DB: UPDATE InformationSnapshot<br/>SET is_undone=True
|
|||
|
|
Note over DB: 标记该快照为已撤销
|
|||
|
|
|
|||
|
|
SS->>AI: 注入"更正撤销"系统消息
|
|||
|
|
Note over AI: 系统消息内容:<br/>"用户撤销了最近一次更正,<br/>信息项 {item_key} 已回滚到 v{version}"<br/>AI 后续对话基于回滚后的值
|
|||
|
|
|
|||
|
|
SS-->>API: 返回撤销结果<br/>{undone_items, restored_values}
|
|||
|
|
API-->>User: 200 撤销成功
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 文件列表及相对路径
|
|||
|
|
|
|||
|
|
### 5.1 后端新增文件
|
|||
|
|
|
|||
|
|
| 文件路径 | 职责 |
|
|||
|
|
|----------|------|
|
|||
|
|
| `backend/app/services/automation/context_compressor.py` | P2 上下文压缩引擎(ContextCompressor 类),包含 token 计数、压缩判断、LLM 摘要、渐进式压缩、压缩日志记录 |
|
|||
|
|
| `backend/app/services/automation/snapshot_service.py` | P3 快照管理服务(SnapshotService 类),包含创建快照、撤销更正、快照历史查询、版本对比 |
|
|||
|
|
| `backend/app/services/automation/correction_service.py` | P3 纠错服务(CorrectionService 类),包含批量更正、依赖检查、更正历史、版本链、撤销委托 |
|
|||
|
|
| `backend/app/utils/token_counter.py` | Token 计数工具类(TokenCounter),封装 tiktoken + 字符估算兜底,支持热切换 |
|
|||
|
|
| `backend/app/schemas/automation_p2.py` | P2/P3 Pydantic Schema 定义(BatchCorrectRequest/Response、UndoCorrectRequest、VersionDiffRequest/Response、ContextCompressionResponse、SnapshotResponse 等) |
|
|||
|
|
| `backend/app/models/automation_p2.py` | P2/P3 SQLAlchemy 模型定义(ContextCompression、InformationSnapshot) |
|
|||
|
|
| `backend/migrations/versions/049_add_p2_p3_tables.py` | 数据库迁移脚本:新增 `auto_context_compressions`、`auto_information_snapshots` 表;`information_items` 表新增 `derived_from`、`correction_reason` 列 |
|
|||
|
|
|
|||
|
|
### 5.2 后端修改文件
|
|||
|
|
|
|||
|
|
| 文件路径 | 修改内容 |
|
|||
|
|
|----------|----------|
|
|||
|
|
| `backend/app/services/automation/auto_session_service.py` | `_run_background()` 方法中增加压缩触发逻辑:每次调用 LLM 前检查 token 数,超阈值执行压缩;注入压缩后上下文 |
|
|||
|
|
| `backend/app/services/automation/session_manager.py` | 确认无单次更正限制逻辑;增加更正结果和撤销结果的系统消息注入方法 |
|
|||
|
|
| `backend/app/services/automation/intent_router.py` | CORRECT 意图识别后调用 CorrectionService.batch_correct;增加 AI 自动标注 derived_from 依赖关系的 prompt |
|
|||
|
|
| `backend/app/models/automation.py` | InformationItem 模型新增 `derived_from`(JSON)和 `correction_reason`(String)两个映射字段 |
|
|||
|
|
| `backend/app/api/v1/automation.py` | 新增 API 端点:`POST /batch-correct`、`POST /undo-correction`、`GET /correction-history`、`GET /version-diff`、`GET /compression-logs` |
|
|||
|
|
| `backend/app/config.py` | 新增 P2/P3 配置项:`CONTEXT_COMPRESS_THRESHOLD`(压缩阈值)、`CONTEXT_COMPRESS_TIMEOUT`(摘要超时)、`MAX_UNDO_COUNT`(最大撤销次数)等 |
|
|||
|
|
| `backend/app/scenarios/it_helpdesk.py` | 场景配置中预定义 `derived_from` 依赖关系(如设备分配人 derived_from 工号) |
|
|||
|
|
|
|||
|
|
### 5.3 前端新增/修改文件
|
|||
|
|
|
|||
|
|
#### H5 端(员工侧)
|
|||
|
|
|
|||
|
|
| 文件路径 | 职责 |
|
|||
|
|
|----------|------|
|
|||
|
|
| `frontend-h5/src/components/automation/CorrectionNotice.vue` | 更正结果通知组件:展示更正成功提示 + 依赖联动警告 |
|
|||
|
|
| `frontend-h5/src/components/automation/UndoButton.vue` | 撤销更正按钮组件:展示可撤销状态 + 撤销次数剩余提示 |
|
|||
|
|
|
|||
|
|
#### 坐席端(Agent 侧)
|
|||
|
|
|
|||
|
|
| 文件路径 | 职责 |
|
|||
|
|
|----------|------|
|
|||
|
|
| `frontend-agent/src/views/automation/CorrectionHistory.vue` | 更正历史页面:展示更正时间线、版本链、快照列表 |
|
|||
|
|
| `frontend-agent/src/views/automation/VersionDiff.vue` | 版本对比页面:选择两个版本对比信息项值差异,高亮变更项 |
|
|||
|
|
| `frontend-agent/src/views/automation/CompressionLogs.vue` | 上下文压缩日志页面:展示压缩记录、压缩率、耗时、摘要内容 |
|
|||
|
|
| `frontend-agent/src/components/automation/DependencyWarning.vue` | 依赖联动提示组件:更正某项时展示受影响的关联信息项列表 |
|
|||
|
|
| `frontend-agent/src/api/automation_p2.ts` | P2/P3 API 请求封装:batchCorrect、undoCorrection、getCorrectionHistory、getVersionDiff、getCompressionLogs |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 任务列表
|
|||
|
|
|
|||
|
|
### 6.1 后端任务(按实现顺序)
|
|||
|
|
|
|||
|
|
| 任务 ID | 描述 | 依赖 | 涉及文件 | 复杂度 |
|
|||
|
|
|---------|------|------|----------|--------|
|
|||
|
|
| T01 | 数据库迁移 + 模型层:新增 ContextCompression、InformationSnapshot 模型;InformationItem 新增 derived_from、correction_reason 列;编写迁移脚本 049 | 无 | `models/automation_p2.py`、`models/automation.py`、`migrations/versions/049_*.py` | 中 |
|
|||
|
|
| T02 | Token 计数工具类 + 上下文压缩引擎:实现 TokenCounter(tiktoken + 兜底)、ContextCompressor(压缩判断、关键信息提取、LLM 摘要、渐进式压缩、日志记录) | T01 | `utils/token_counter.py`、`services/automation/context_compressor.py` | 高 |
|
|||
|
|
| T03 | 快照服务 + 纠错服务:实现 SnapshotService(创建快照、撤销更正、历史查询、版本对比)、CorrectionService(批量更正、依赖检查、版本链、撤销委托) | T01 | `services/automation/snapshot_service.py`、`services/automation/correction_service.py` | 高 |
|
|||
|
|
| T04 | API 层 + Schema + 集成:定义 Pydantic Schema;新增 API 端点;修改 AutoSessionService 注入压缩逻辑;修改 IntentRouter 接入纠错服务;修改 SessionManager 注入系统消息;场景配置预定义依赖关系 | T02, T03 | `schemas/automation_p2.py`、`api/v1/automation.py`、`services/automation/auto_session_service.py`、`services/automation/intent_router.py`、`services/automation/session_manager.py`、`scenarios/it_helpdesk.py`、`config.py` | 中 |
|
|||
|
|
|
|||
|
|
### 6.2 前端任务
|
|||
|
|
|
|||
|
|
| 任务 ID | 描述 | 依赖 | 涉及文件 | 复杂度 |
|
|||
|
|
|---------|------|------|----------|--------|
|
|||
|
|
| T05 | H5 端组件 + 坐席端页面 + API 封装:实现更正通知、撤销按钮(H5 端);更正历史、版本对比、压缩日志、依赖提示页面(坐席端);API 请求封装 | T04 | `frontend-h5/src/components/automation/*.vue`、`frontend-agent/src/views/automation/*.vue`、`frontend-agent/src/components/automation/DependencyWarning.vue`、`frontend-agent/src/api/automation_p2.ts` | 中 |
|
|||
|
|
|
|||
|
|
### 任务依赖图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
T01[T01: 数据库迁移 + 模型层]
|
|||
|
|
T02[T02: Token计数 + 压缩引擎]
|
|||
|
|
T03[T03: 快照服务 + 纠错服务]
|
|||
|
|
T04[T04: API层 + Schema + 集成]
|
|||
|
|
T05[T05: 前端组件 + 页面 + API封装]
|
|||
|
|
|
|||
|
|
T01 --> T02
|
|||
|
|
T01 --> T03
|
|||
|
|
T02 --> T04
|
|||
|
|
T03 --> T04
|
|||
|
|
T04 --> T05
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 依赖包列表
|
|||
|
|
|
|||
|
|
| 包名 | 版本要求 | 用途 |
|
|||
|
|
|------|----------|------|
|
|||
|
|
| `tiktoken` | `>= 0.5.0` | Token 计数(OpenAI cl100k_base 编码,与 Dify/GPT 模型一致) |
|
|||
|
|
|
|||
|
|
> **说明**:除 `tiktoken` 外,P2/P3 其余功能均复用 Phase 1 已有技术栈(SQLAlchemy、FastAPI、Pydantic、DifyClient 等),无新增依赖。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 共享知识
|
|||
|
|
|
|||
|
|
### 8.1 命名约定
|
|||
|
|
|
|||
|
|
| 约定 | 规则 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 压缩日志表 | `auto_context_compressions` | — |
|
|||
|
|
| 快照表 | `auto_information_snapshots` | — |
|
|||
|
|
| 服务类命名 | `XxxService` / `XxxCompressor` | `CorrectionService`、`ContextCompressor`、`SnapshotService` |
|
|||
|
|
| Schema 命名 | `XxxRequest` / `XxxResponse` | `BatchCorrectRequest`、`SnapshotResponse` |
|
|||
|
|
| API 路径前缀 | `/api/v1/automation/` | `/api/v1/automation/batch-correct` |
|
|||
|
|
| 系统消息注入 | 以 `[SYSTEM]` 前缀标识 | `[SYSTEM] 更正撤销:信息项 工号 已回滚到 v2` |
|
|||
|
|
|
|||
|
|
### 8.2 错误码扩展
|
|||
|
|
|
|||
|
|
| 错误码 | 含义 | HTTP 状态码 | 说明 |
|
|||
|
|
|--------|------|-------------|------|
|
|||
|
|
| 4013 | 上下文压缩失败 | 500 | LLM 摘要调用超时或异常,已降级为截断处理 |
|
|||
|
|
| 4014 | 撤销次数超限 | 400 | 已撤销次数达到上限(5次),无法继续撤销 |
|
|||
|
|
| 4015 | 批量更正部分失败 | 500 | 事务回滚,所有更正均未生效 |
|
|||
|
|
|
|||
|
|
**错误响应格式**(与 Phase 1 统一):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 4014,
|
|||
|
|
"data": null,
|
|||
|
|
"message": "撤销次数超限,最多可撤销最近5次更正"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.3 配置项
|
|||
|
|
|
|||
|
|
| 配置项 | 默认值 | 说明 |
|
|||
|
|
|--------|--------|------|
|
|||
|
|
| `CONTEXT_COMPRESS_THRESHOLD` | `6000` | 上下文压缩触发阈值(token 数),超过则执行压缩 |
|
|||
|
|
| `CONTEXT_COMPRESS_TIMEOUT` | `30` | LLM 摘要调用超时时间(秒),超时降级截断 |
|
|||
|
|
| `CONTEXT_MAX_COMPRESS_LEVEL` | `3` | 渐进式压缩最大级别,超出则降级截断 |
|
|||
|
|
| `CONTEXT_KEEP_RECENT_TURNS` | `4` | 压缩后保留的最近对话轮数(Level 1) |
|
|||
|
|
| `MAX_UNDO_COUNT` | `5` | 最大撤销更正次数 |
|
|||
|
|
| `CONTEXT_COMPRESS_RATIO_WARN` | `0.3` | 压缩率警告阈值(压缩后/压缩前 < 0.3 时告警) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 数据库迁移方案
|
|||
|
|
|
|||
|
|
### 迁移脚本:`049_add_p2_p3_tables.py`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
"""Add P2 context compression and P3 snapshot tables
|
|||
|
|
|
|||
|
|
Revision ID: 049
|
|||
|
|
Revises: 048
|
|||
|
|
Create Date: 2025-07-11
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from alembic import op
|
|||
|
|
import sqlalchemy as sa
|
|||
|
|
from sqlalchemy.dialects import postgresql
|
|||
|
|
|
|||
|
|
revision = "049"
|
|||
|
|
down_revision = "048"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def upgrade():
|
|||
|
|
# 1. 新增 auto_context_compressions 表
|
|||
|
|
op.create_table(
|
|||
|
|
"auto_context_compressions",
|
|||
|
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|||
|
|
sa.Column("session_id", sa.String(36), nullable=False),
|
|||
|
|
sa.Column("tokens_before", sa.Integer(), nullable=False),
|
|||
|
|
sa.Column("tokens_after", sa.Integer(), nullable=False),
|
|||
|
|
sa.Column("compression_ratio", sa.Numeric(5, 2), nullable=False),
|
|||
|
|
sa.Column("task_node", sa.String(128), nullable=True),
|
|||
|
|
sa.Column("duration_ms", sa.Integer(), nullable=False),
|
|||
|
|
sa.Column("compression_level", sa.SmallInteger(), nullable=False, server_default="1"),
|
|||
|
|
sa.Column("summary", sa.Text(), nullable=True),
|
|||
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
|||
|
|
sa.PrimaryKeyConstraint("id"),
|
|||
|
|
)
|
|||
|
|
op.create_index("ix_auto_context_compressions_session_id", "auto_context_compressions", ["session_id"])
|
|||
|
|
|
|||
|
|
# 2. 新增 auto_information_snapshots 表
|
|||
|
|
op.create_table(
|
|||
|
|
"auto_information_snapshots",
|
|||
|
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|||
|
|
sa.Column("session_id", sa.String(36), nullable=False),
|
|||
|
|
sa.Column("trigger_item_key", sa.String(64), nullable=False),
|
|||
|
|
sa.Column("snapshot_data", postgresql.JSON(), nullable=False),
|
|||
|
|
sa.Column("correction_ids", postgresql.JSON(), nullable=False, server_default="[]"),
|
|||
|
|
sa.Column("is_undone", sa.Boolean(), nullable=False, server_default="false"),
|
|||
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
|||
|
|
sa.PrimaryKeyConstraint("id"),
|
|||
|
|
)
|
|||
|
|
op.create_index("ix_auto_information_snapshots_session_id", "auto_information_snapshots", ["session_id"])
|
|||
|
|
|
|||
|
|
# 3. information_items 表新增列
|
|||
|
|
op.add_column("information_items", sa.Column("derived_from", postgresql.JSON(), nullable=True))
|
|||
|
|
op.add_column("information_items", sa.Column("correction_reason", sa.String(200), nullable=True))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def downgrade():
|
|||
|
|
op.drop_column("information_items", "correction_reason")
|
|||
|
|
op.drop_column("information_items", "derived_from")
|
|||
|
|
op.drop_index("ix_auto_information_snapshots_session_id", table_name="auto_information_snapshots")
|
|||
|
|
op.drop_table("auto_information_snapshots")
|
|||
|
|
op.drop_index("ix_auto_context_compressions_session_id", table_name="auto_context_compressions")
|
|||
|
|
op.drop_table("auto_context_compressions")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**迁移影响评估**:
|
|||
|
|
|
|||
|
|
| 影响项 | 说明 |
|
|||
|
|
|--------|------|
|
|||
|
|
| 新增表 | 2 张(`auto_context_compressions`、`auto_information_snapshots`),均为追加表,不影响现有数据 |
|
|||
|
|
| 变更表 | 1 张(`information_items` 新增 2 列),均为 nullable 列,向后兼容 |
|
|||
|
|
| 数据迁移 | 无需迁移历史数据,新列默认 NULL |
|
|||
|
|
| 回滚风险 | 低,`downgrade()` 完整可逆 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 待明确事项
|
|||
|
|
|
|||
|
|
| 序号 | 待明确事项 | 当前假设 | 影响范围 |
|
|||
|
|
|------|-----------|----------|----------|
|
|||
|
|
| 1 | 压缩阈值的精确值需根据实际模型上下文窗口确定 | 暂设 6000 tokens(GPT-3.5 4K 上下文留 2K 余量) | `ContextCompressor` 配置项 |
|
|||
|
|
| 2 | LLM 摘要使用的 Dify 应用是否独立配置专用摘要 prompt | 假设复用现有 Dify 应用,通过 system message 注入摘要 prompt | `ContextCompressor._summarize_history` |
|
|||
|
|
| 3 | `derived_from` 依赖关系的场景模板具体内容需与业务方确认 | 已在 `it_helpdesk.py` 中预定义常见依赖(如设备分配人 derived_from 工号),待业务方补充 | `scenarios/it_helpdesk.py` |
|
|||
|
|
| 4 | 撤销操作是否需要二次确认(防误操作) | 假设前端弹出确认框,后端直接执行 | 前端 `UndoButton.vue` |
|
|||
|
|
| 5 | 压缩摘要是否需要人工审核后才生效 | 假设自动生效,无需人工审核 | `ContextCompressor.compress` |
|
|||
|
|
| 6 | tiktoken 在企业内网环境(无外网)是否可正常安装和运行 | 假设可通过内网 PyPI 镜像安装;运行时编码数据文件(`~/.cache/tiktoken`)需预置 | `utils/token_counter.py` |
|
|||
|
|
| 7 | 批量更正的最大数量限制 | 假设无硬限制,由前端 UI 交互控制(单次通常 ≤10 项) | `CorrectionService.batch_correct` |
|