rasa 实现简易的多轮对话

1.3 实现简易的多轮对话

1.3.1 场景描述

用户希望查询特定日期的天气信息。对话过程如下:

  1. 用户:你好
  2. 助手:你好!我可以帮你查询天气信息。你想查询哪一天的天气?
  3. 用户:今天天气怎么样
  4. 助手:今天是晴天,温度25°C。
  5. 用户:明天的天气如何
  6. 助手:你想查询明天的天气。那天是多云,温度22°C。

1.3.2 需求分析

对话系统能够处理用户询问天气信息的多轮对话

既然要尝试多轮对话就会涉及 data\nlu.yml、data\rules.yml、data\stories.yml、domain.yml、config.yml,还需要配置 endpoints.yml 实现对外部服务的调用(本文中用的是 node)

1.3.3 stories 设计

因为 rasa 是通过学习故事的方式来学习对话管理知识

本文进行了简单的设计

version: "3.1"

stories:
  - story: 用户询问天气,但未提供非今天日期
    steps:
      - intent: ask_weather
      - action: action_fetch_weather

  - story: 用户询问天气后提供非今天日期
    steps:
      - intent: ask_weather
      - action: action_fetch_weather
      - intent: inform
      - action: action_handle_inform

  - story: 完整对话
    steps:
      - intent: greet
      - action: utter_greet
      - intent: ask_weather
      - action: action_fetch_weather
      - intent: inform
      - action: action_handle_inform
      - intent: goodbye
      - action: utter_goodbye

本文中我去掉了rules,只是用 stories,规则的优先级是高于故事的,没用规则的时候 rasa 就会依赖 stories 执行了

1.3.4 自然语言处理(nlu)设计

examples 即为输入,intent 为处理完的意图

version: "3.1"

nlu:
- intent: greet
  examples: |
    - 你好
    - 嗨
    - 早上好
    - 晚上好

- intent: goodbye
  examples: |
    - 再见
    - 拜拜
    - 下次见

- intent: ask_weather
  examples: |
    - 今天天气怎么样
    - 你能告诉我天气吗
    - 今天会下雨吗

- intent: inform
  examples: |
    - [明天](date)的天气怎么样?
    - [后天](date)的温度是多少?
    - 那[下周](date)的天气如何?
    - [明天](date)会刮风吗?
    - 我想知道[下个月](date)的天气
    - 请告诉我[下周五](date)的气温
    - [昨天](date)的天气如何?    

1.3.5 domain 设计

domain(领域)定义了对话机器人需要知道的所有信息,包括意图(intent)、实体(entity)、词槽(slot)、动作(action)、表单(from)和回复(response)

当前设计涉及 intents、entities、slots、responses、actions

version: "3.1"

intents:
  - greet
  - goodbye
  - ask_weather
  - inform

entities:
  - date

slots:
  requested_date:
    type: text
    influence_conversation: false
    mappings:
      - type: from_entity
        entity: date

responses:
  utter_greet:
    - text: "你好!我可以帮你查询天气信息。你想查询哪一天的天气?"

  utter_goodbye:
    - text: "再见!祝你有美好的一天。"

  utter_ask_date:
    - text: "你想查询哪一天的天气?"


actions:
  - action_fetch_weather
  - action_handle_inform

session_config:
  session_expiration_time: 60
  carry_over_slots_to_new_session: true

1.3.6 action 设计

action 通过外部动作服务器实现,笔者这里用的是 node

需要在 endpoints.yml 配置

# 启动自定义输入通道
input_channel:
    custom_input_channel.CustomInputChannel
# 外部动作服务器配置
action_endpoint:
  url: "http://localhost:6001/webhook"

action_service.js 代码如下:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');  // 引入 fs 模块

const app = express();
const port = 6001;

// 中间件
app.use(bodyParser.json());
// 日志文件路径
const logFilePath = 'action_logs.txt';

// 接收 Rasa 的自定义动作请求
app.post('/webhook', (req, res) => {
    console.log('Received request:', req.body);
    const actionName = req.body.next_action;
    const tracker = req.body.tracker; // 包含槽位、会话上下文等
    console.log('Tracker Slots:', tracker.slots);  // 输出所有插槽值
    const requestedDate = tracker.slots.requested_date;
    console.log('Requested Date:', requestedDate);  // 输出 requested_date 的值
    const senderId = tracker.sender_id;

    // 日志记录格式
    const logMessage = `SenderId: ${senderId}, Action: ${actionName}, Time: ${new Date().toISOString()}\n`;

    // 将日志写入 .txt 文件
    fs.appendFileSync(logFilePath, logMessage, 'utf8');

    // 处理不同的自定义动作
    if (actionName === 'action_fetch_weather') {
        // 模拟返回天气信息
        const weatherInfo = "今天是晴天,温度25°C。";
        res.json({
            events: [], // 可以定义槽位更新事件
            responses: [{ text: weatherInfo }]
        });
    } else if (actionName === 'action_handle_inform') {
        const requestedDate = tracker.slots.requested_date || "未知日期";
        const followUpInfo = `你想查询${requestedDate}的天气。那天是多云,温度22°C。`;
        res.json({
            events: [],
            responses: [{ text: followUpInfo }]
        });
    } else {
        res.status(404).json({ error: "Action not found" });
    }
});

// 启动服务器
app.listen(port, () => {
    console.log(`Action service is running on http://localhost:${port}`);
});

然后需要启动 node 服务器

1.3.7 初次尝试

这里笔者配置的对话管理为

# 配置对话管理
policies:
  - name: "MemoizationPolicy"
    max_history: 5
  - name: RulePolicy
    core_fallback_action_name: "action_default_fallback"
    core_fallback_threshold: 0.3
    enable_fallback_prediction: True

其中 config.yml 整体配置为

recipe: default.v1
assistant_id: 20241120-170106-khaki-margarine
language: zh # 改为中文
pipeline: 
  - name: JiebaTokenizer  # 使用支持中文的 Tokenizer
  - name: CountVectorsFeaturizer
    analyzer: "word"  # 基于分词结果生成特征
    min_ngram: 1
    max_ngram: 2
  - name: DIETClassifier
    epochs: 100
  - name: EntitySynonymMapper
policies:
  - name: "MemoizationPolicy"
    max_history: 5
  - name: RulePolicy
    core_fallback_action_name: "action_default_fallback"
    core_fallback_threshold: 0.3
    enable_fallback_prediction: True

在 rules.yml 中简单配置了一个规则

version: "3.1"

rules:
- rule: 问候规则
  steps:
  - intent: greet
  - action: utter_greet

测试前别忘了先训练模型

rasa train

先使用

rasa shell

测试

image-20241218110810659

显然这结果不是我们想得到的,明天天气并没有回答直接退出了

下面命令是验证模型是否正确提取了实体

rasa shell nlu

可以进行实体解读

NLU model loaded. Type a message and press enter to parse it.
Next message:
明天天气怎么样
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\rmc\AppData\Local\Temp\jieba.cache
Loading model cost 0.378 seconds.
Prefix dict has been built successfully.
{
  "text": "明天天气怎么样",
  "intent": {
    "name": "inform",
    "confidence": 0.9999532699584961
  },
  "entities": [
    {
      "entity": "date",
      "start": 0,
      "end": 2,
      "confidence_entity": 0.998909592628479,
      "value": "明天",
      "extractor": "DIETClassifier",
      "processors": [
        "EntitySynonymMapper"
      ]
    }
  ],
  "text_tokens": [
    [
      0,
      2
    ],
    [
      2,
      4
    ],
    [
      4,
      7
    ]
  ],
  "intent_ranking": [
    {
      "name": "inform",
      "confidence": 0.9999532699584961
    },
    {
      "name": "goodbye",
      "confidence": 3.9685361116426066e-05
    },
    {
      "name": "greet",
      "confidence": 5.52419078303501e-06
    },
    {
      "name": "ask_weather",
      "confidence": 1.507416641288728e-06
    }
  ]
}
Next message:

显然 entities 解读出来了

接下来使用调试模式

rasa shell --debug

tips:使用这个命令之后继续对话,state 还会继续记录,即使使用 /exit 停止之后,继续对话,对话 state 还是会保留,最好直接退出 Rasa Shell,也就是使用 Ctrl+c;或者 /stop 也可以

根据调试记录

2024-12-18 15:52:43 DEBUG    rasa.core.policies.memoization  - [debug    ] memoization.predict.actions    tracker_states=[{'user': {'intent': 'greet'}, 'prev_action': {'action_name': 'action_listen'}}, {'user': {'intent': 'greet'}, 'prev_action': {'action_name': 'utter_greet'}}, {'user': {'intent': 'ask_weather'}, 'prev_action': {'action_name': 'action_listen'}}, {'user': {'intent': 'ask_weather'}, 'prev_action': {'action_name': 'action_fetch_weather'}}, {'user': {'intent': 'inform', 'entities': ('date',)}, 'prev_action': {'action_name': 'action_listen'}}]
2024-12-18 15:52:43 DEBUG    rasa.core.policies.memoization  - There is no memorised next action

经过测试 Memoization Policy 无法完成后面的记忆,实力有点弱,按理说我的 stories 都给他设计好了,不应该无法获取下一个 action

1.3.8 启用 TEDPolicy

TED(Transformer Embedding Dialogue) Policy, Transformer 嵌入对话策略是一种用于下一步动作预测和实体识别的多任务架构

该架构由两个任务共享的多个 Transformer 编码器组成

在输入词条序列的对应用户序列 Transformer 编码输出上通过一个条件随机场(CRF)预测实体标签序列

对于下一个动作预测,对话 Transformer 编码器输出和系统动作标签被嵌入到同一个语义向量空间中

我们使用点积损失来最大化与目标标签的相似性,同时最小化与负样本的相似性

官网的介绍就比较专业

总之这个策略比较牛,主要用于下一步动作预测实体识别,核心用的是 transformer,还用了多个 transformer 编码器

# 配置对话管理
policies:
  - name: "MemoizationPolicy"
    max_history: 5
  - name: TEDPolicy
    max_history: 5
    epochs: 100

修改一下对话策略

image-20241218163800498

加上 TEDpolicy 之后直接秒了

1.3.9 Apipost交互

input_service.js 的代码

const express = require('express');
const axios = require('axios');
const bodyParser = require('body-parser');

const app = express();
const port = 6000;

// 中间件
app.use(bodyParser.json());

const sessions = {}; // 用于存储 sender_id 和上下文

// 定义与 Rasa 交互的端点
app.post('/input', async (req, res) => {
    const userMessage = req.body.message;
    const userId = req.body.sender || 'default';

    if (!userMessage) {
        return res.status(400).json({ error: "Message is required" });
    }

    try {
        // 获取用户上下文
        if (!sessions[userId]) {
            sessions[userId] = { id: userId, context: {} };
        }
        // 发送用户消息到 Rasa
        const response = await axios.post('http://127.0.0.1:5005/webhooks/rest/webhook', {
            sender: sessions[userId].id,
            message: userMessage
        });

        // 收集 Rasa 的响应
        const rasaResponses = response.data.map(r => r.text).filter(text => text);

        // 返回响应到客户端
        res.json({ replies: rasaResponses });
    } catch (error) {
        console.error('Error communicating with Rasa:', error.message);

        // 检查 Axios 错误并处理
        if (error.response) {
            // Rasa 服务返回了非 2xx 状态
            res.status(error.response.status).json({ error: error.response.data });
        } else if (error.request) {
            // 没有收到 Rasa 的响应
            res.status(500).json({ error: 'No response from Rasa server' });
        } else {
            // 请求配置错误
            res.status(500).json({ error: 'Request error: ' + error.message });
        }
    }
});

// 启动服务器
app.listen(port, () => {
    console.log(`Node.js server is running on http://localhost:${port}`);
});

我们还可以在 package.json 中配置

  "scripts": {
    "start:input": "node input_service.js",
    "start:action": "node action_service.js",
    "start": "concurrently \"npm run start:input\" \"npm run start:action\""
  },

方便一下子打开两个服务

rasa 启动!!

rasa run -m models --enable-api --cors "*" --debug
image-20241218164821045 image-20241218164927016

不得不说 TEDPolicy 的实力强大

END

本文主要制作了简易的多轮对话,包括场景描述,需求分析以及实现的设计,会涉及 nlu、rules、stories、domain、config、endpoints,其中 rules 最后没用,主要用到 menmories Policy 和 TED Policy,menmories Policy在使用过程中遇到了问题,笔者多次尝试未果,动作服务器和交互都使用的 Node.js 实现,最终实现简单的多轮对话

posted @ 2024-12-18 16:58  goicandoit  阅读(10)  评论(0编辑  收藏  举报