Files
ai-assistant-travel/docs/CALM设计对照.md
mp_1787300862_s8r25x a71980e4bf docs: 新增 CALM 设计对照,说明 Copilot 架构来源与取舍
- docs/CALM设计对照.md:CALM 四步架构、Commands DSL 清单、逐层对照表(含代码位置)
- 明确采纳项(取消意图分类/理解与执行分层/槽位确认)、简化项、差异缺口清单
- 记录超出 CALM 的偏好学习增量,以及对外表述口径与生产落地映射
- README:修正 docs/ 目录引用,补充设计来源章节
2026-09-16 15:21:38 +08:00

292 lines
17 KiB
Markdown
Raw Permalink 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.
# CALM 设计对照:从 Rasa CALM 到本 Demo
> 本文说明本项目「商品发布 AI 助手(Copilot / Agent)」的**架构来源与设计取舍**。
> 核心参考范式为 **Rasa CALM**(Conversational AI with Language Models),
> 项目在意图理解层与流程执行层的分层方式,直接借鉴了 CALM 的四步架构。
---
## 1. 参考源
| 编号 | 来源 | 类型 | 链接 |
|---|---|---|---|
| R1 | Rasa CALM 原始论文(arXiv:2402.12234) | 一手 · 论文 | https://arxiv.org/pdf/2402.12234 |
| R2 | Rasa Pro 文档 · Dialogue Understanding | 一手 · 官方文档 | https://rasa.com/docs/rasa-pro/concepts/dialogue-understanding |
| R3 | Rasa Pro 文档 · Conversation Repair | 一手 · 官方文档 | https://rasa.com/docs/rasa-pro/concepts/conversation-repair |
| R4 | Rasa Studio 可视化流程编排 | 一手 · 官方文档 | https://rasa.com/docs/studio/user-guide/create-flow |
| R5 | 《大模型与任务型对话的结合:Rasa CALM》 | 二手 · 社区解读 | https://mp.weixin.qq.com/s/L_0IeGVeTml_N-Ypb6AwEQ |
| R6 | tod-in-context-learning 参考实现 | 一手 · 代码 | https://github.com/RasaHQ/tod-in-context-learning |
> 证据分级说明:R1–R4、R6 为论文/官方文档/官方代码仓库(一手);R5 为第三方技术解读(二手,用于快速建立术语认知,关键结论均回溯至 R1/R2 核对)。
---
## 2. CALM 要解决什么问题
### 2.1 传统任务型对话的 pipeline
业界标准做法是三段式:
```
语义理解(意图识别 + 词槽抽取)→ 对话管理(状态更新 + 策略选择)→ 话术生成
```
其内核是一条强假设:
```
f(对话状态) = 对话 action
```
而**对话状态的编码强依赖意图类别的识别**。一旦遇到预置意图之外、或本身就模糊的表达,整条链路就会退化成僵硬的分支判断——鲁棒性不足。
典型反例(来自 R5 的转述):
- 用户说「订餐厅」——可能是订中餐、订西餐,也可能只是查询,本身就难以归入单一类别;
- 用户说了一段很长的澄清文本——无法映射到预设类别体系里。
### 2.2 CALM 的主张
> **意图分类不是一件合理的事情**,因为人类表达意图可不仅仅是一个类别。
既然分类不合理,那就**取消意图分类**,让大模型直接把用户意图表达为**一组动作(Commands)**——即论文所述 *Commands as a Domain-Specific Language*。
---
## 3. CALM 的四步架构
```mermaid
flowchart TD
A["用户最新消息 + 对话历史"] --> B["① LLM 生成 Commands<br/>f(任务说明, 全部 flow 描述,<br/>当前 flow 与 slots, 对话历史) = commands"]
B --> C["② 校验 Commands 合理性<br/>对话管理引擎更新对话状态<br/>(当前 flow + slots)"]
C --> D["③ Conversation Repair<br/>判断是否需修复并更新状态"]
D --> E["④ 规则引擎按确定逻辑执行<br/>输出 Action 及对应话术"]
E --> F["表单/系统状态变更 + 回复用户"]
style B fill:#EEEDFE,stroke:#534AB7
style E fill:#E1F5EE,stroke:#0F6E56
```
**架构的关键判断:只有第 ① 步用大模型,其余三步都是确定逻辑。**
这一点决定了系统的工程性质:大模型负责它擅长的「理解」,规则引擎负责它擅长的「确定执行」。可解释性、可测试性、可兜底性都建立在这条边界上。
### 3.1 Commands DSL 清单
| Command | 语义 | 示例 |
|---|---|---|
| `StartFlow(flow_name)` | 启动一个业务流程 | `StartFlow(transfer_money)` |
| `SetSlot(slot, value)` | 填充槽位 | `SetSlot(recipient, John)` |
| `CancelFlow()` | 取消当前流程 | 用户说「算了」「stop that」 |
| `Clarify(flow_1, flow_2, ...)` | 歧义澄清 | 用户只说「contacts」→ `Clarify(list, add, remove)` |
| `SkipQuestion()` | 跳过当前追问 | 用户说「下一个问题」 |
| `SearchAndReply()` | 走知识库作答 | 非任务型知识问询 |
| `ChitChat()` | 闲聊 | 无其他可执行动作时兜底 |
| `HumanHandoff()` | 转人工 | 用户表达不满或明确要求 |
**关键设计**:`SetSlot` 中的值不再是通用实体(如「John」被识别为 person、45 被识别为 number),而是**直接用 flow 定义中的槽位语义来承接**——理解与流程定义在同一套语汇下对齐,消除了 NLU 中间表示的语义损耗。
### 3.2 Flow 的定义形态
flow = `description` + `steps`,其中 `steps` 只允许若干枚举类型(`action` / `collect` / `set_slots` / `noop`),并支持 `next` 分支与 `if` 条件:
```yaml
flows:
transfer_money:
description: This flow lets users send money to friends and family.
steps:
- collect: transfer_money_recipient
description: the name of a person
- id: "ask_amount"
collect: transfer_money_amount_of_money
- action: check_transfer_funds
next:
- if: not slots.transfer_money_has_sufficient_funds
then:
- action: utter_transfer_money_insufficient_funds
next: "ask_amount"
- else: transfer_money_final_confirmation
```
配套三个配置文件:`domain.yml`(话术/action/slot 声明)、`config.yaml`(LLM 版本与超时)、`endpoints.yml`(服务地址)。
---
## 4. 本 Demo 的逐层对照
本 Demo 是**纯前端可交互原型**(无后端、不接真实 LLM),因此第 ① 步的「理解」以规则/关键词实现,但**分层结构与数据契约完全按 CALM 的形态设计**,替换为 LLM function calling 即可落地(见 §7.1)。
| # | CALM 概念 | 本 Demo 的实现 | 代码位置 |
|---|---|---|---|
| 1 | 意图不再分类,直接生成 Commands | 不走 intent 分类,直接产出「工具序列 + 槽位值」 | `app/src/lib/agent.js` |
| 2 | `SetSlot(slot, value)` | `parseBrief(text)` → `{ dest, days, type, theme }` | `agent.js:26` |
| 3 | `StartFlow(flow_name)` 序列 | `parseMultiTools(text)` → 命中的工具类型数组(保序去重) | `agent.js:52` |
| 4 | Commands DSL 的取值域 | `toolKeywords` 关键词表(8 工具)+ `DEST_MAP` / `THEME_MAP` / `TYPES` 值域映射 | `agent.js:11,19,23,40` |
| 5 | 全量流程触发 | `agentIntentRe` 命中 → 启动 8 步全量流程 | `agent.js:8` |
| 6 | `flows.yml`:flow 定义(description + steps) | `agentSteps`(8 步自主发品流程定义)/ `ocStepsDef`(6 步一键发品流程定义) | `data/genData.js:10,22` |
| 7 | `actions/*.py`:action 实现 | `buildGen(type, ctx)` → `{ pre, card, data }` 结构化生成 | `data/genData.js:54` |
| 8 | domain 中的 action / slot 声明 | `cardMap`(工具 → 表单卡片锚点)/ `actionLabel`(工具 → 动作名) | `data/genData.js:48,32` |
| 9 | 对话状态:当前 flow + slots | `form`(槽位)+ `generatedRef`(各工具最新产物)+ `serialRef`(当前等待点) | `store/useAppStore.js:42,63,64` |
| 10 | 规则引擎确定性执行 | `agentRun` / `multiToolRun` 的 `for` 循环 + 任务清单状态机 | `useAppStore.js:325,351` |
| 11 | slot 的 `ask_before_filling` | `serialConfirm`:生成卡片后 Promise 挂起,等 5s 自动倒计时或手动确认 | `useAppStore.js:204,236` |
| 12 | 重新收集槽位 / 分支重走 | `serialRegen`「换一版」:重新生成当前步骤卡片并重置倒计时 | `useAppStore.js:214` |
| 13 | Conversation Repair(偏题/打断) | `sendMessage` 开头:若存在等待中的步骤,先自动确认收口再处理新消息 | `useAppStore.js:383` |
| 14 | `ChitChat()` / `KnowledgeAnswer()` 兜底 | `CANNED` 关键词兜底 + 3 条上下文感知的闲聊回复 | `useAppStore.js:29,410` |
| 15 | 执行过程的可见性(flow steps 逐条执行) | 任务清单卡片:`todo → doing → done` 三态 + 每步完成备注 | `useAppStore.js:253,265`;`components/TaskCardView.jsx` |
| 16 | 最终确认(final confirmation) | 生成卡片的「⚡ 一键填入」与「🔄 换一版」双动作 | `components/GenCardView.jsx` |
### 4.1 一次完整交互的等价展开
以 CALM 的样例句式对照本 Demo 的实际行为:
```
用户:帮我发个海南亲子 6 天自由行
CALM 侧等价输出:
StartFlow(publish_product)
SetSlot(dest, 海南(三亚))
SetSlot(theme, 亲子家庭)
SetSlot(days, 6天5晚)
本 Demo 实际执行:
parseBrief() → { dest: '海南(三亚)', theme: '亲子家庭', days: '6天5晚' }
applyBriefToForm()→ 槽位回写左侧表单
agentIntentRe → 命中「发」→ 启动 8 步全量流程
agentRun() → 逐工具 buildGen → 生成卡片 → 等确认 → applyGen 回填
```
---
## 5. 采纳了什么
### 5.1 取消意图分类,直接生成动作(采纳)
本 Demo **不存在 intent 分类器**。用户的自然语言直接映射为「要启动哪些流程 + 各槽位取值」,与 CALM 的主张一致。
这条选择在本场景的价值尤其明显:商家说「帮我优化一下」,实际上可能是要标题、要详情、要主图——强行归入一个 intent 会立刻失真;而输出「工具序列」天然可容纳这种多目标表达(`parseMultiTools` 返回的就是一个数组)。
### 5.2 理解层与执行层分离,且只有理解层是「智能」的(采纳)
这是 CALM 最值得复用的工程判断,也是本 Demo 的核心架构约束:
- **理解层**(`lib/agent.js`):唯一需要「聪明」的地方,负责把自然语言转成结构化动作;
- **执行层**(`store/useAppStore.js` 的四条编排流):纯确定性循环,每一步都可解释、可中断、可测试;
- **定义层**(`data/genData.js`):流程与产物以声明式数据描述,与执行引擎解耦。
由此得到三个直接收益:
1. **可解释**:每一步「为什么调用这个工具」都能回溯到一次解析结果;
2. **可中断**:执行中任何时候都能插入确认、换版、切换自动/手动模式;
3. **可测试**:解析层与生成层可分别单测,无需端到端跑 UI。
### 5.3 human-in-the-loop 的槽位确认(采纳并强化)
CALM 在 flow 中通过 `ask_before_filling: true` 让槽位在填充前先向用户确认。本 Demo 把这个机制从「槽位级」提升到「模块级」:
- `serialConfirm` 让每个工具生成后先出卡片,用户确认才回填表单;
- 支持 `autoConfirmMode` 运行中随时切换(⚡自动 5s / ✋手动无限等);
- `serialRegen` 提供「换一版」——等价于 CALM 重新收集槽位后重走分支。
对 B 端商家场景而言,这个机制的必要性高于 C 端对话:**AI 生成的内容会直接进入商品详情页,落库前必须由人过一道眼。**
---
## 6. 简化了什么
| CALM 的做法 | 本 Demo 的简化 | 简化理由与代价 |
|---|---|---|
| 第 ① 步由 LLM 生成 Commands | 关键词表 + 正则解析(`toolKeywords` / `parseBrief`) | 纯前端 demo 不引入模型依赖;代价是泛化能力受限,仅覆盖演示话术。**替换点已预留**(见 §7.1) |
| flow 定义用 YAML 配置 + 支持 `next` 分支与 `if` 条件 | `agentSteps` / `ocStepsDef` 为硬编码声明式数组,线性执行 | 演示流程本身线性(8 步固定顺序);代价是新增流程或分支需改代码 |
| 对话状态收敛为一个 `dialogue_state` 对象 | 状态分散在 `form` / `generatedRef` / `serialRef` | 复用 React 单向数据流;代价是缺少统一的「当前 flow / 当前槽位」视图 |
| Commands 显式落成可审计的中间表示 | 解析结果直接驱动执行,无 `commands` 中间层 | 少一次数据转换;代价是失去命令级日志与回放能力(见 §7.2) |
---
## 7. 差异与演进方向
### 7.1 已被预留的替换点:意图理解接 LLM
`lib/agent.js` 的三个导出(`agentIntentRe` / `parseBrief` / `parseMultiTools`)构成一个**窄接口**:入参为自然语言,出参为「工具序列 + 槽位值」。
落地真实 LLM 时,只需把这三个函数替换为一次 function calling 调用,**执行层(`agentRun` / `multiToolRun` / `startOC`)零改动**:
```
自然语言
├─ 现状:关键词/正则解析(前端模拟)
└─ 目标:LLM function calling,按 CALM 的 Prompt 模板组织
f(全部 flow 描述, 当前 flow 与 slots, 对话历史, 用户消息) = commands
执行层:不变
```
CALM 的 Prompt 模板(R1/R2)可直接作为契约蓝本:把「可用流程及其描述与槽位」「当前 flow 与槽位取值」「可用动作枚举」「槽位填充约束」四段填进 system prompt,要求模型**先输出分析、再逐行输出动作**。
### 7.2 尚未覆盖的 CALM 能力(下一版清单)
| 缺口 | 现状行为 | 补齐后的目标行为 | 对应 Command |
|---|---|---|---|
| 歧义澄清 | 用户只说「帮我优化一下」→ 落到闲聊兜底 | 反问「您是要优化标题、详情还是配图?」 | `Clarify(title, intro, detail, ...)` |
| 显式取消 | 说「算了」→ 落到闲聊兜底 | 中止当前流程、回滚已生成步骤 | `CancelFlow()` |
| 跳过当前步骤 | 说「跳过这步」→ 落到闲聊兜底 | 跳过当前槽位收集,继续后续步骤 | `SkipQuestion()` |
| 转人工 | 无 | 输出转人工指令并携带上下文 | `HumanHandoff()` |
| 命令中间表示 | 解析结果直接执行 | 生成 `commands[]` → 校验 → 执行,可日志化、可回放、可单测 | — |
| 对话状态视图 | 状态散在三个 ref | 统一 `dialogueState = { activeFlow, currentStep, slots }` | — |
| 流程声明式配置 | 硬编码数组 | 外置 `flows.json`(description + steps + next 分支) | — |
其中**「命令中间表示」优先级最高**。它是 §5.2 三项收益(可解释/可中断/可测试)的落点:一旦 commands 成为显式数据结构,就可以做命令级单测、命令级日志、以及「同一段对话回放不同模型输出」的评估——这正是 CALM 把 Commands 称为 DSL 的用意。
---
## 8. 超出 CALM 的部分:偏好学习
CALM 的边界止于「理解 + 执行」,**不处理跨会话的用户习惯沉淀**。本 Demo 在此之上加了一层产品化增量:
| 机制 | 实现 | 代码位置 |
|---|---|---|
| 偏好沉淀 | 每次发品写入 5 条偏好条目(路线天数、人群文案风格、话术偏好、定价策略、促销习惯) | `lib/oc.js:28` |
| 跨会话持久化 | `localStorage`(键 `fliggy_oc`) | `lib/oc.js:8,16` |
| 成长等级 | Lv.1 通用模板(0–2 次)→ Lv.2 风格匹配(3–9 次)→ Lv.3 爆款复刻(10+ 次) | `lib/oc.js:21` |
| 下次预填 | 一键发品弹窗自动回填上次配置并提示风格沿用语 | `store/useAppStore.js:419` |
| 双通道沉淀 | 对话式自主发品与弹窗式一键发品共用同一套记忆 | `store/useAppStore.js:305` |
**定位说明**:CALM 解决「怎么把话说准、把事做对」;偏好学习解决「怎么越用越贴合这个商家」。二者正交——偏好作为上下文注入理解层的 Prompt,不改变执行层的确定性。
---
## 9. 对外表述口径
需要向他人解释本项目的设计来源时,建议采用如下表述(避免把 CALM 的作用夸大或缩小):
> 任务型对话的经典做法是把用户表达归入一个预设意图类别,再走状态机选动作。这个假设在商家场景下很容易失效——「帮我优化一下」可能 одновременно 指向标题、详情和主图。
>
> 我参考的是 Rasa CALM 的思路:**不做意图分类,让模型直接生成一组动作**(启动哪个流程、填哪些槽位),再交给确定性的流程引擎执行。这套架构里只有「理解」这一步是模型在做事,剩下的执行、确认、修复全是确定逻辑——这正好满足商家侧对**可解释、可中断、可兜底**的要求。
>
> 在这之上我做了一层 CALM 没有的东西:**商家使用习惯库**。每次发品沉淀偏好,下次自动预填并按商家风格生成,让产品「越用越好用」。
---
## 10. 落地对应关系(接真实 LLM / 后端 Agent Loop)
| 本 Demo(前端原型) | 生产环境对应物 |
|---|---|
| `lib/agent.js` 关键词解析 | LLM function calling,按 CALM Prompt 模板构造上下文 |
| `data/genData.js` `buildGen()` | 各工具的生成服务(Prompt 见 `Prompt提示词设计文档.md`) |
| `store/useAppStore.js` 编排流 | 后端 Agent Loop:命令生成 → 校验 → 状态更新 → 执行 → 回填 |
| `agentSteps` / `ocStepsDef` | 流程配置中心(flow 声明式定义,支持分支与版本) |
| `lib/oc.js` `localStorage` | 商家画像 / 习惯库服务,作为个性化上下文注入 Prompt |
| `serialConfirm` 前端确认 | 同构的落库前人工复核节点(保持前端一致交互) |
| 任务清单卡片 | 执行过程可观测性:每步工具调用、耗时、结果状态 |
---
## 附:术语对照
| CALM 术语 | 本项目术语 | 说明 |
|---|---|---|
| Command | 动作 / 指令 | 理解层的输出单元 |
| Flow | 工具流程 / 任务清单 | 如「8 步自主发品」「6 步一键发品」 |
| Flow steps | 流程步骤 | `agentSteps` 的每一项 |
| Slot | 表单字段 / 上下文槽位 | 目的地、天数、类型、人群 |
| Action | 工具(Tool) | 8 个生成工具 |
| Dialogue Understanding | 意图解析 | `parseBrief` / `parseMultiTools` |
| Conversation Repair | 打断收口 / 兜底回复 | `sendMessage` 的等待点收口与闲聊兜底 |
| `ask_before_filling` | 逐工具确认 | `serialConfirm` 卡片确认 |