docs: 新增 CALM 设计对照,说明 Copilot 架构来源与取舍

- docs/CALM设计对照.md:CALM 四步架构、Commands DSL 清单、逐层对照表(含代码位置)
- 明确采纳项(取消意图分类/理解与执行分层/槽位确认)、简化项、差异缺口清单
- 记录超出 CALM 的偏好学习增量,以及对外表述口径与生产落地映射
- README:修正 docs/ 目录引用,补充设计来源章节
This commit is contained in:
2026-09-16 15:21:38 +08:00
parent a84d386929
commit a71980e4bf
2 changed files with 309 additions and 2 deletions
+18 -2
View File
@@ -49,10 +49,26 @@ npm run preview # 本地预览构建产物
│ ├── MessageItem.jsx / GenCardView.jsx / TaskCardView.jsx # 消息流三种消息
│ ├── OneClickModal.jsx # 一键发品弹窗(配置→进度→完成 状态机)
│ └── Toast.jsx
├── Prompt提示词设计文档.md # 各工具背后的 Prompt 设计规范(可直接落地真实 LLM)
└── AI领教营/ # 项目方法论笔记(AI 价值/边界/验证框架)
└── docs/ # 设计文档
├── CALM设计对照.md # 架构来源与设计取舍(Rasa CALM 范式逐层对照)
├── Prompt提示词设计文档.md # 各工具背后的 Prompt 设计规范(可直接落地真实 LLM)
└── 追问.md # 方法论笔记(AI 价值 / 边界 / 验证框架)
```
### 设计来源:Rasa CALM 范式
本项目的对话编排架构参考 **Rasa CALM**(Conversational AI with Language Models)的四步架构:
```
① LLM 生成 Commands → ② 校验并更新对话状态 → ③ Conversation Repair → ④ 规则引擎确定性执行
```
关键判断是**只有第 ① 步用模型,「理解」之外全是确定逻辑** —— 由此获得可解释、可中断、可兜底的特性,满足商家侧对内容落库前人工复核的要求。
对应到代码:`lib/agent.js` = 理解层(等价 `SetSlot` / `StartFlow`),`data/genData.js` = 流程定义层(等价 `flows.yml`),`store/useAppStore.js` 的四条编排流 = 执行层(等价规则引擎)。
完整的逐层对照、采纳与简化、以及与 CALM 的差异清单,见 [`docs/CALM设计对照.md`](docs/CALM设计对照.md)。
### 架构要点
- **数据驱动渲染**:v1 中拼接 HTML 字符串的生成卡片,v2 重构为结构化数据(`head / lines / foot / tips`),按钮全部是真实 React 事件
+291
View File
@@ -0,0 +1,291 @@
# 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` 卡片确认 |