Rasa:开源的机器学习框架

一、Rasa简介

Rasa是一套用来构建基于上下文的AI小助手和聊天机器人框架。

分为两个主要的模块:

  1. NLU:自然语言理解模块,实现意图识别以及槽值的提取,将用户的输入转化为结构性数据,在训练过程中,为了提高从用户信息的实体识别能力,采用了预先训练的实体提取器Pre-trained Entity Extractors,正则表达式Regexes,同义词Synonyms等
  2. Rasa Core:对话管理模块,也是一个平台,用来预测决定下一步做什么,其中对话数据包括了stories以及rules
    • 其中设计对话数据时分为happy and unhappy paths,前者表示用户按照预期来进行会话,但用户通常会通过提问,闲聊或其他要求来进入后者。
  3. RasaX:一个交互工具,帮助用户构建,改进和部署由Rasa框架搭成的聊天机器人

其中,rasa版本已经发展到了3.0,与之前的1.0,2.0都有较大的差距,在这里会直接介绍最新版本,感兴趣的可以了解其他版本。

二、Rasa的数据构成

在此处,构建小助手是使用rasa官网的交互式网页来构建的,可以直接将训练数据输入并训练,再进行测试,十分方便。也可以将你的模型下载下来,方便之后的使用与改进。以下是Rasa的数据构成

2.1NLUdata

在这里,意向(intent)及其示例(examples)用作聊天小助手的自然语言理解 (NLU) 模型的训练数据

nlu:
- intent: greet
  examples: |
    - Hi
    - Hey!
    - Hallo
    - Good day
    - Good morning

- intent: subscribe
  examples: |
    - I want to get the newsletter
    - Can you send me the newsletter?
    - Can you sign me up for the newsletter?
2.2Responses

现在,聊天小助手已经理解了用户可能会说的几条消息,它需要可以发回给用户的响应Responses

responses:
   utter_greet:
       - text: |
           Hello! How can I help you?
       - text: |
           Hi!
   utter_ask_email:
       - text: |
           What is your email address?
   utter_subscribed:
       - text: |
           Check your inbox at {email} in order to finish subscribing to newsletter!
       - text: |
           You're all set! Check your inbox at {email} to confirm your subscription.
2.3Stories

Stories是示例对话,可根据用户之前在对话中所说的内容正确响应训练助手。格式显示用户消息的意图,后跟助手的操作或响应。

stories:
 - story: greet and subscribe
   steps:
   - intent: greet
   - action: utter_greet
   - intent: subscribe
   - action: newsletter_form
   - active_loop: newsletter_form

Stories的编写注意事项

2.4Forms

在许多情况下,聊天助手需要从用户那里收集信息。例如,当用户想要订阅新闻稿时,助理必须询问其电子邮件地址,您可以使用表单在Rasa中执行此操作

slots:
  email:
    type: text
    mappings:
    - type: from_text
      conditions:
      - active_loop: newsletter_form
        requested_slot: email
forms:
  newsletter_form:
    required_slots:
    - email
2.5Rules

规则描述了对话的某些部分,无论之前在对话中说了什么,这些部分都应始终遵循相同的规则。我们希望我们的助手始终使用特定操作响应某个意图,因此我们使用Rules将该操作映射到意图

rules:
 - rule: activate subscribe form
   steps:
   - intent: subscribe
   - action: newsletter_form
   - active_loop: newsletter_form

 - rule: submit form
   condition:
   - active_loop: newsletter_form
   steps:
   - action: newsletter_form
   - active_loop: null
   - action: utter_subscribed

Rules的好处:

  • 单轮交互:某些消息不需要任何上下文来回答它们。规则Rules是将意向映射到响应的简单方法,可指定这些消息的固定答案。
  • 回退行为:与回退分类器结合使用时,可以编写规则来响应具有特定回退行为的低可信度用户消息。
  • 表单:激活和提交表单通常遵循固定路径。您还可以编写规则来处理表单中的意外输入

举个例子,比如询问使用信用卡A支付还是信用卡B来支付,但是用户通常会询问信用卡的账户余额,而不是直接回答选择哪一个来支付。

以上数据准备完成之后就可以在官网上进行训练并测试结果,也可以下载下来。

三、Rasa的简单安装

python版本为3.7或者3.8,推荐使用Ubuntu系统,并且使用虚拟环境。

Linux环境下python虚拟环境virtualenv安装和使用——包含虚拟环境集中管理库

python3 -m venv ./venv #创建虚拟环境,
source venv/bin/activate #激活虚拟环境
pip3 install -U pip && pip3 install rasa # 安装rasa,pip的版本在21.3
rasa init  #创建默认的初始项目,项目文件

此项目还需要其他的依赖包,需要什么就下载什么就可以了。

命令行界面——主要是rasa train,训练并将模型保存起来

3.1初始项目文件的组成——数据格式
├── actions
│   ├── __init__.py
│   └── actions.py
├── config.yml
├── credentials.yml
├── data
│   ├── nlu.yml
│   └── stories.yml
├── domain.yml
├── endpoints.yml
├── models
│   └── <timestamp>.tar.gz
└── tests
   └── test_stories.yml

主要文件的作用

__init__.py空文件
actions.py可以自定义 actions 的代码文件
config.yml ‘*’Rasa NLU 和 Rasa Core 的配置文件
credentials.yml定义和其他服务连接的一些细节,例如rasa api接口
data/nlu.md ‘*’Rasa NLU 的训练数据
data/stories.md ‘*’Rasa stories 数据
domain.yml ‘*’Rasa domain 文件
endpoints.yml和外部消息服务对接的 endpoins 细则
models/<timestamp>.tar.gz初始训练的模型数据
  • 这些文件的后缀名是yml——一个统一和可扩展的方式,可以用来管理训练数据
  • 将训练数据分割到任意数量的yml文件中,每个文件可以包含NLU数据、stories和规则的任意组合
2.2.1NLU训练数据——NLU.yml

Rasa NLU是重要模块之一,负责对用户的消息内容进行语义理解,所以需要提供一份训练数据,然后Rasa会根据这些数据进行模型训练

NLU数据样例

# 默认是3.0
version: "3.0"
nlu:
- intent: greet
  examples: |
    - Hi
    - Hey!
    - Hallo
    - Good day
    - Good morning
    
- intent: chitchat/ask_weather
  examples: |
    - What's the weather like today?
    - Does it look sunny outside today?
    - Oh, do you mind checking the weather for me please?
    - I like sunny days in Berlin.

- intent: check_balance
  examples: |
    - What's my [credit](account) balance?
    - What's the balance on my [credit card account]

- synonym: credit
  examples: |
    - credit card account
    - credit account
    
- regex: account_number
  examples: |
    - \d{10,12}
  • 其中以 -开头的行就是用户定义的 intents(意图),examples: |下面是一组有相同意图的消息内容,大致格式就是这样的
  • 在这其中可以包括带注释的实体,同义词,正则表达式,查阅表格等,这些都是用来改进意图分类和实体识别的方法。
  • 以上提到的方法需要用到组件,组件的使用一般是在domain.yml文件中
2.2.2对话管理文件——stories.yml
stories:
- story: collect restaurant booking info  # name of the story  
  steps:
  - intent: greet                         # user message with no entities
  - action: utter_ask_howcanhelp
  - intent: inform                        # user message with entities
    entities:
    - location: "rome"
    - price: "cheap"
  - action: utter_on_it                  # action that the bot should execute
  - action: utter_ask_cuisine
  - intent: inform
    entities:
    - cuisine: "spanish"
  - action: utter_ask_num_people
  
- story: story with entities
  steps:
  - intent: account_balance #必需的
    entities: #可有可无
    - account_type: credit
  - action: action_credit_account_balance# 必须的
  
- story: story with a slot
  steps:
  - intent: celebrate_bot
  # 设置槽
  - slot_was_set:
    - feedback_value: positive
  - action: utter_yay

# 检查点的使用,在一个故事的结束,和下一个故事的开始
- story: story_with_a_checkpoint_1
  steps:
  - intent: greet
  - action: utter_greet
  - checkpoint: greet_checkpoint

- story: story_with_a_checkpoint_2
  steps:
  - checkpoint: greet_checkpoint
  - intent: book_flight
  - action: action_book_flight
  
rules:
- rule: Only say `hey` when the user provided a name
  condition:
  - slot_was_set:
    - user_provided_name: true
  steps:
  - intent: greet
  - action: utter_greet
  • 故事由以下部分组成:
    • story:故事的名称。该名称是任意的,不在训练中使用
    • metadata:任意和可选,不用于训练,
    • steps:构成故事的用户消息和操作steps,组成——用户的消息,机器人的action,一个表单,设置槽点
  • 在用户的消息中,实体虽然不是必需的,但是学习过程会根据意图和实体的组合来预测下一个操作。可以使用use_entities属性更改此行为
  • action:有两种形式
    • 一:Responses:是以utter_开头,发送特定的消息
    • 二:Custom actions:是以action_开头,自定义发送任意数量的消息
  • slot_was_set——设置槽值键值对的形式,设置槽值不是必须的,如果槽位的值对会话无影响或不重要则可以只插入一个槽名。
  • 检查点checkpoint的使用,可以作为故事的开头,可以作为故事的结尾。
2.2.2.1Rules:一般在Stories文件中

Rules非常适合处理小的特定对话模式,但与Stories不同,Rules无法推广到看不见的对话路径,描述了应始终遵循相同路径的简短对话片段。

在开始编写规则之前,必须确保将Rules策略添加到模型配置config.yml中:

policies:
- ... # Other policies
- name: RulePolicy

对话开始的Rules只需要conversation_start: true,仅在开始时生效

rules:

- rule: Say `hello` when the user starts a conversation with intent `greet`
  conversation_start: true
  steps:
  - intent: greet
  - action: utter_greet

条件满足时才有的适用的规则

rules:

- rule: Only say `hello` if the user provided a name
  condition:
  - slot_was_set:
    - user_provided_name: true
  steps:
  - intent: greet
  - action: utter_greet
2.2.2domain.yml文件
version: "3.0"

intents:
  - affirm
  - deny
  - greet
  - thankyou
  - goodbye
  - search_concerts
  - search_venues
  - compare_reviews
  - bot_challenge
  - nlu_fallback
  - how_to_get_started

entities:
  - name

slots:
  concerts:
    type: list
    influence_conversation: false
    mappings:
    - type: custom
  venues:
    type: list
    influence_conversation: false
    mappings:
    - type: custom
  likes_music:
    type: bool
    influence_conversation: true
    mappings:
    - type: custom

responses:
   utter_greet:
       - text: |
           Hello! How can I help you?
       - text: |
           Hi!
   utter_ask_email:
       - text: |
           What is your email address?
   utter_subscribed:
       - text: |
           Check your inbox at {email} in order to finish subscribing to the newsletter!
       - text: |
           You're all set! Check your inbox at {email} to confirm your subscription.
actions:
  - action_search_concerts
  - action_search_venues
  - action_show_concert_reviews
  - action_show_venue_reviews
  - action_set_music_preference

session_config:
  session_expiration_time: 60  # value in minutes
  carry_over_slots_to_new_session: true

domain域定义了您的助手在其中操作,指定机器人应了解的意向、实体、槽位、响应、表单和操作。它还定义会话的配置。

文件内容组件的介绍

  • intents:包含了文件的所有意图,你的对话训练数据,以及nlu中的data

  • entities:实体,可以有忽略以及不用忽略。

    • #这是忽略了所有的实体
      intents:
        - greet:
            use_entities: []
      
    • #这是忽略了一部分的实体,被排除的实体不会影响下一轮的预测
      intents:
      - greet:
          use_entities:
            - name
            - first_name
          ignore_entities:
            - location
            - age
      
    • 如果想要的实体不想要影响预测,则可以在slots下加入influence_conversation: false,指定其是否会影响下一轮的预测

  • slots:可以说是机器人的记忆,充当键值存储,存储用户提供的信息

    • 插槽影响会话的方式将取决于它的插槽类型。槽值类型有text,bool,categorical,float,list,any.

    • 此外还可以自定义槽值类型。自定义槽值类型示例

    • # from_entity根据实体来填充槽值entity_name
      entities:
      - entity_name
      slots:
        slot_name:
          type: any
          mappings:
          - type: from_entity
            entity: entity_name
            role: role_name
            group: group name
            intent: intent_name
            not_intent: exc
            luded_intent
      
  • Forms:表单——表单的工作原理是提示用户输入信息,直到用户收集了所有必需的信息。信息存储在插槽slots中。填满所有必需的槽位后,机器人将满足用户的原始请求。

    • stories:
      - story: interrupted food
        steps:
          - intent: request_restaurant
          - action: restaurant_form
          - intent: chitchat
          - action: utter_chitchat
          - active_loop: restaurant_form
          - active_loop: null
          - action: utter_slots_values
      
  • Actions动作——是机器人执行的操作,可以是响应用户,调用外部的API,查询数据库等,且所有的自定义操作全在domain文件中

    • responses:
        utter_greet:
        # 用大括号来表示变量,插入槽值后,将值填入
        - text: "Hey, {name}. How are you?"
      
  • session_config默认会话配置——下面是默认的配置,表示如果用户在60分钟不活动后发送第一条消息,就会触发一个新的会话,而任何现有的插槽都将被转移到新的会话中。将session_expiration_time的值设置为0意味着会话不会结束。

    • session_config:
        session_expiration_time: 60  # value in minutes, 0 means infinitely long
        carry_over_slots_to_new_session: true  # set to false to forget slots between sessions
      
    • slots:
        logged_in:
          type: bool
          influence_conversation: False
          mappings:
          - type: custom
        name:
          type: text
          influence_conversation: False
          mappings:
          - type: custom
      # 还可以有约束相应的变体
      responses:
        utter_greet:
          - condition:
              - type: slot
                name: logged_in
                value: true
            text: "Hey, {name}. Nice to see you again! How are you?"
      
          - text: "Welcome. How is your day going?"
      
    • 响应可以直接返回给用户,也可以直接在action.py中定义,以Rasa SDK为操作的服务器

      from rasa_sdk.interfaces import Action
      
      class ActionGreet(Action):
          def name(self):
              return 'action_greet'
      
          def run(self, dispatcher, tracker, domain):
              dispatcher.utter_message(template="utter_greet")
              return []
      

包含这种的文件可以有多个,但应在同一目录下,同时训练的时候的命令

rasa train --domain path_to_domain_directory
2.2.3配置文件——config.yml
language: "zh"

pipeline:
# 指定语言模型
- name: "MitieNLP"
# 训练模型
  model: "data/total_word_feature_extractor_zh.dat"
# 分词器,此处的是jieba中文分词器
- name: "JiebaTokenizer"
# 实体的提取
- name: "MitieEntityExtractor"
- name: "EntitySynonymMapper"
# 文本特征化
- name: "RegexFeaturizer"
- name: "MitieFeaturizer"
# 意图分类
- name: "SklearnIntentClassifier"

policies:
  - name: KerasPolicy
    epochs: 500
    max_history: 5
  - name: FallbackPolicy
    fallback_action_name: 'action_default_fallback'
  - name: MemoizationPolicy
    max_history: 5
  - name: FormPolicy

以上的组件都是可以自己选择的,每个组件都有自己独特的功能,具体用到哪一个组件就看需要哪种功能,都是在写在pipeline下面的

有关配置文件的所有详细的介绍以及使用

2.2.5测试对话管理文件test_stories.yml
stories:
- story: A basic end-to-end test
  steps:
  - user: |
     hey
    intent: greet
  - action: utter_ask_howcanhelp
  - user: |
     show me [chinese]{"entity": "cuisine"} restaurants
    intent: inform
  - action: utter_ask_location
  - user: |
     in [Paris]{"entity": "location"}
    intent: inform
  - action: utter_ask_price

测试文件预训练文件最大的不同就是添加了用户信息,来验证是否是用户想要的

四、对话模式

3.1闲聊以及常见问题
3.1.1更新配置

对于每次提出的相同类型的问题,都希望能以相同的方式回答,这就需要用到Rules,具体用法是在配置文件`config.yml中将下面的内容写入

policies:
# other policies
- name: RulePolicy

之后在配置文件的 NLU 管道中包含 ResponseSelector。ResponseSelector 需要特征化程序和意向分类器才能工作,因此它应该位于管道中的以下组件之后,例如

pipeline:
  - name: WhitespaceTokenizer
  - name: RegexFeaturizer
  - name: LexicalSyntacticFeaturizer
  - name: CountVectorsFeaturizer
  - name: CountVectorsFeaturizer
    analyzer: char_wb
    min_ngram: 1
    max_ngram: 4
  - name: DIETClassifier
    epochs: 100
  - name: EntitySynonymMapper
  - name: ResponseSelector
    epochs: 100
3.1.2 定义检索意图和响应选择器

单个操作使用 ResponseSelector 的输出为用户请求的特定常见问题解答返回正确的响应。

3.1.3创建Rules

只需要为每个检索意图编写一个规则Rules。然后,将以相同的方式处理在该检索意图下分组的所有意向。操作名称以检索意图的名称开头和结尾。编写响应常见问题解答和闲聊的规则:utter_

rules:
  - rule: respond to FAQs
    steps:
    - intent: faq
    - action: utter_faq
  - rule: respond to chitchat
    steps:
    - intent: chitchat
    - action: utter_chitchat

之后将使用响应选择器的预测来返回实际的响应消息。

3.1.4更新NLU训练数据

ResponseSelector 的 NLU 训练示例看起来与常规训练示例相同,只是它们的名称必须引用它们所分组的检索意图

nlu:
  - intent: chitchat/ask_name
    examples: |
      - What is your name?
      - May I know your name?
      - What do people call you?
      - Do you have a name for yourself?
  - intent: chitchat/ask_weather
    examples: |
      - What's the weather like today?
      - Does it look sunny outside today?
      - Oh, do you mind checking the weather for me please?
      - I like sunny days in Berlin.
3.1.5定义响应

响应选择器的响应遵循与检索意图相同的命名约定。

responses:
  utter_chitchat/ask_name:
  - image: "https://i.imgur.com/zTvA58i.jpeg"
    text: Hello, my name is Retrieval Bot.
  - text: I am called Retrieval Bot!
  utter_chitchat/ask_weather:
  - text: Oh, it does look sunny right now in Berlin.
    image: "https://i.imgur.com/vwv7aHN.png"
  - text: I am not sure of the whole week but I can see the sun is out today.
3.1.6总结

完成以下操作后,可以训练机器人并试用闲聊对话。

  • 将规则策略RulePolicy添加到您的策略,并将响应选择器 ResponseSelector 添加到管道pipeline中config.yml
  • 添加至少一条规则rule以响应常见问题解答/闲聊
  • 为您的常见问题解答/闲聊意图添加示例examples
  • 为您的常见问题解答/闲聊意图添加回复 responses
  • 更新域domain中的意向intents
3.2 处理业务逻辑

会话助手通常支持用户目标,这些目标涉及在为用户执行某些操作之前从用户那里收集所需的信息。例如,餐厅搜索机器人需要收集有关用户偏好的一些信息,以便为他们找到合适的餐厅

3.2.1定义表单
  • Slot mappings: 插槽映射
  • Responses: 机器人如何请求每条信息

可以通过在表单名称下指定所需槽位的列表来定义domain.yml中的表单,由表单填充的插槽通常不应影响对话

比如订购餐厅的菜人数以及是否想在外面

forms:
  restaurant_form:
    required_slots:
        - cuisine
        - num_people
        - outdoor_seating

这些插槽需要添加到domain.yml的插槽部分,以及定义如何填充插槽的插槽映射。对于填充了from_entity的任何槽位,也需要将该实体添加到域中

entities:
  - cuisine
  - number
slots:
  cuisine:
    type: text
    mappings:
    - type: from_entity
      entity: cuisine
  num_people:
    type: float
    mappings:
    - type: from_entity
      entity: number
  outdoor_seating:
    type: bool
    mappings:
    - type: from_intent
      intent: affirm
      value: true
      conditions:
       - active_loop: restaurant_form
         requested_slot: outdoor_seating
    - type: from_intent
      intent: deny
      value: false
      conditions:
      - active_loop: restaurant_form
        requested_slot: outdoor_seating

其中只有当用户回答“你想坐在外面吗?”这个问题时,插槽才应该设置为true或false。

请求插槽

要指定机器人应该如何请求所需的信息,你可以在你的domain.yml文件中中定义名为utter_ask_{slotname}的响应

responses:
  utter_ask_cuisine:
    - text: "What cuisine?"
  utter_ask_num_people:
    - text: "How many people?"
  utter_ask_outdoor_seating:
    - text: "Do you want to sit outside?"
3.2.2更新配置

表单的Happypath应定义为规则Rules,这意味着您需要将规则策略添加到策略中,与闲聊对话模式的更新配置相同

3.2.3创建规则

Forms本身负责处理要求用户提供所有必需信息的逻辑,因此只需要两个规则Rules来定义表单的Happypath一个定义Forms何时启动,另一个定义填充后会发生什么。对于餐厅搜索示例,在现实生活中,助手会根据用户的偏好查找餐厅。在这种情况下,机器人将发出响应,其中包含将用于搜索的详细信息。

rules:
  - rule: activate restaurant form
    steps:
      - intent: request_restaurant   # 触发激活表单的意图
      - action: restaurant_form      # 激活表单
      - active_loop: restaurant_form # 表单处于活动状态

  - rule: submit form
    condition:
    - active_loop: restaurant_form   # 条件是表单必须是活动的
    steps:
      - action: restaurant_form      # 执行表单
      - active_loop: null            # 表单不再活动
      - action: utter_submit         # 表单完成后要执行的操作
      - action: utter_slots_values   # 表单完成后要执行的操作

分离表单的激活和提交好处:如果用户提供了意外的输入或通过聊天中断了表单,Rules仍然适用

3.2.4更新NLU训练数据

就是为意图添加示例,同闲聊的对话模式,nlu.yml文件

nlu:
- intent: request_restaurant
  examples: |
    - im looking for a restaurant
    - can i get [swedish](cuisine) food in any area
    - a restaurant that serves [caribbean](cuisine) food
    - id like a restaurant
    - im looking for a restaurant that serves [mediterranean](cuisine) food
    - can i find a restaurant that serves [chinese](cuisine)

如果用户在第一条消息中提供实体,则该槽将在表单开头填充,并且机器人就不会通过再次向他们询问美食来填充槽。根据真实的场景来添加实例数据

nlu:
- intent: affirm
  examples: |
    - Yes
    - yes, please
    - yup
- intent: deny
  examples: |
    - no don't
    - no
    - no I don't want that

- intent: inform
  examples: |
    - [afghan](cuisine) food
    - how bout [asian oriental](cuisine)
    - what about [indian](cuisine) food
    - uh how about [turkish](cuisine) type of food
    - um [english](cuisine)
    - im looking for [tuscan](cuisine) food
    - id like [moroccan](cuisine) food
    - for ten people
    - 2 people
    - for three people
    - just one person
    - book for seven people
    - 2 please
    - nine people

一定要记得在nlu.yml文件中出现的意图都要写在domain.yml文件中

以下是更新之后的domain.yml文件

intents:
  - request_restaurant
  - affirm
  - deny
  - inform
3.2.5定义响应

添加提交表单后发送的响应,在domain.yml文件中

responses:
  utter_submit:
  - text: "All done!"
  utter_slots_values:
  - text: "I am going to run a restaurant search using the following parameters:\n
            - cuisine: {cuisine}\n
            - num_people: {num_people}\n
            - outdoor_seating: {outdoor_seating}"
3.2.6总结

表单可以简化收集用户信息的逻辑。要定义一个最小的表单(如上面的餐厅搜索示例),以下是您需要执行的操作的摘要:

  • 将规则rules策略比如RulesPolicy添加到config.yml
  • 在域domain.yml中定义具有所需槽slots的表单forms
  • 为域domain.yml中所有必需的槽slots添加槽映射
  • 添加用于激活和提交表单forms的规则rules
  • 为激活表单的意图添加示例,examples
  • 为意图添加示例以填充所需槽slots
  • 定义机器人在表单填写时要执行的操作或响应action or response
  • 使用您定义的新意图和操作更新您的域domain.yml
3.3其他对话模式

除以上两种对话模式外,还有以下四种对话模式,具体模式的文件配置以及要注意的地方都在下面的网址上面,四种对话模式的配置,操作都是大同小异,感兴趣的可以去了解一下。

  • 处理意外输入
  • 情景对话
  • 联系用户
  • 回退和人工交接

五、总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Indra_ran

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值