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

17 KiB
Raw Permalink Blame History

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 的四步架构

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 条件:

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 卡片确认