本节摘要:对话系统像一家"餐厅流水线"——NLU 是"听单员",DM 是"厨师长",NLG 是"上菜员",知识库是"食材仓库"。本节逐个拆解四个模块的职责与协作,让你看到"一次回答背后发生了什么"。
阅读完本节,你应当能够:
"机器怎么回答我的问题?"——不是一步到位的魔法,是流水线的协作。用"餐厅"类比:NLU 听单(听懂你要什么)→ DM 决定(想想怎么处理)→ NLG 上菜(组织语言回答),知识库是后厨仓库(提供食材/知识)。四者缺一,对话就断。
三大模块串起一次对话:

用户:"帮我订明天北京到上海的机票" NLU:意图=订机票,槽位={日期:明天, 出发:北京, 到达:上海} DM:查航班(调外部系统),确认信息 NLG:"明天北京到上海有 3 个航班,最早的是 8:00……"
💡 关键直觉:模块间传"结构化意图",不传原始文本——NLU 把自然语言变成结构化数据,DM 才好处理。这是三模块协作的关键。
| 模块 | 输入 | 输出 |
|---|---|---|
| NLU | 用户文本 | 意图 + 槽位 |
| DM | 意图 + 状态 | 决策 + 回复意图 |
| NLG | 回复意图 | 自然语言 |
知识库(FAQ、文档、图谱)供三模块使用:NLU 用它辅助理解、DM 用它找答案、NLG 用它组织内容。
⚠️ 常见坑:模块之间耦合过深。NLU 直接输出最终回复、跳过了 DM——架构图上有三层就该走三层,跳层短期快、长期乱。
构成清楚了,下一节看历史——发展历程与技术演进。
"模块间传结构化意图,不传原始文本"是全书最重要的一句话,这里把它落实成接口的样子。假设要设计一个最小任务型系统,四个模块之间的数据契约大致如下。
NLU 的输出是一个意图加一组槽位。意图是枚举值(如 BOOK_FLIGHT、QUERY_WEATHER),槽位是键值对:
# NLU 输出示例(结构化意图) nlu_result = { "intent": "BOOK_FLIGHT", "slots": {"date": "明天", "origin": "北京", "dest": "上海"}, "confidence": 0.93 # 置信度,供 DM 判断要不要澄清 }
DM 拿到这个结构化结果后,把它与自己的对话状态合并——状态里记录"这个任务还缺哪些槽位、进行到哪一步"。DM 的决策输出是一个"动作",同样结构化:
# DM 决策示例 dm_action = {"type": "ask_slot", "slot": "time", "text_hint": "请问几点出发?"} # 也可能是 confirm / execute / handoff_human 等
NLG 只消费 DM 的决策,不接触用户原始文本:决策说要问 time,NLG 就根据模板或生成模型产出"请问几点出发?"。把这条链路画出来,会发现每个模块的边界就是"它消费什么、产出什么"。
用户文本 → NLU → {intent, slots} → DM → {action} → NLG → 用户文本 ↓ ↑ 知识库/外部系统 ←——————— 调用与返回
工程上两个地方最容易破坏边界:一是 NLU 直接拼回复绕过 DM(跳层),二是 DM 输出里夹带未加工的原始文本(让 NLG 无活可干)。守住"结构化契约"这条线,系统就清晰、可测试、可替换——比如将来把规则版 NLU 换成大模型版,只需保证输出仍是 {intent, slots} 即可。
动手验证一下对模块边界的理解:设想一个"天气查询"对话系统,用户说"明天北京会下雨吗"。请按三模块各自的输入输出,把这句话到最终回复之间每一层传递的数据写出来——NLU 该输出什么意图与槽位?DM 该决策什么动作?NLG 该生成什么话术?再想一个边界情况:如果用户说"那后天呢",这句话在哪个模块被理解成"改日期"?答案是 NLU 负责把它解析成"日期=后天"的槽位更新,DM 负责据此覆盖原槽位,NLG 无需知道"改口"这回事——这正好印证了"各模块只消费结构化契约"的设计价值。能完整写出这条数据流,说明你已经真正理解了三段式架构。建议把这张数据流图保存下来,后续学习第 3 到 5 章时随时对照——它就是整个文集的骨架索引。