一、CALM
三个关键要素:
- 业务逻辑:Flow,描述了AI助手可以处理的业务流程
- 对话理解:旨在解释最终用户与助手沟通的内容。此过程涉及生成反映用户意图的命令,与业务逻辑和正在进行的对话的上下文保持一致。
- 自动对话修复:修复处理会话“脱离脚本”的所有方式
CALM使用一种称为对话理解(DU)的新方法,将用户所说的内容转化为对业务逻辑的意义,关键点:
- DU输出一系列命令,表示用户希望如何推进对话,而不是像NLU系统那样产生意图和实体
- NLU系统被限制在一个固定的意图列表中,而DU是生成性的,根据内部语法产生一系列命令。这为我们提供了一种更丰富的语言来表达用户的需求。
- 使用CALM比构建基于NLU的助手要快得多。CALM不依赖于意图,使用一个名为Flows的原语来定义对话逻辑
CALM使用LLM来确定用户希望如何推进对话。它不使用LLM来猜测完成该过程的正确步骤集。相比于LLM Agent,CALM可以确保业务逻辑被明确地遵循
二、工作流
在CALM中,AI助手的业务逻辑被实现为一组流。每个流程都描述了您的AI助手完成任务所使用的逻辑步骤。它描述了用户需要的信息、需要从API或数据库检索的数据,以及基于收集的信息的分支逻辑。
1. 流的定义
flows:
a_flow: # required id
name: "A flow" # optional name
description: "required description of what the flow does"
always_include_in_prompt: false # optional boolean, defaults to false
if: "condition" # optional flow guard
nlu_trigger: # optional list of intents that can start a flow
- intent: "starting_flow_intent"
persisted_slots: [] # optional list of slots that should be persisted at the conversation level after the flow ends
steps: [] # required list of steps
- description:必填,描述流程为用户做了什么。编写清晰的描述很重要,因为对话理解组件使用它来决定何时开始此流程
- always_include_in_prompt:如果always_include_in_prompt字段设置为true,并且If字段中定义的流保护计算结果为true,则该流将始终包含在提示中
- nlu_trigger:可以启动该流的意图。
- persisted_slots:默认情况下,当流程结束时,在collect和set_slot步骤中设置的插槽将被重置。要更改此行为,可以将这些插槽添加到persisted_slots字段中,其值应在流结束后持久化。且这些slot必须在collect 或 set_slots步骤中被填充
- steps:列出了流程步骤,有以下几种类型
- action step: 由流运行的自定义动作或话语动作
- collect step:向用户提出一个问题以填充一个插槽
- call step:调用另一个流的步骤
- link step:在该流完成后链接另一个流的步骤
- set slots step:设置插槽的步骤
- noop步骤:创建条件的步骤
step有id和next两个属性,用来标识当前步骤和下一个步骤,如果不标识next,则使用列表中的下一步
- collect: name
next: the_next_step
- id: the_next_step
collect: age
匿名next
- collect: name
next:
- collect: age
- collect: age
next:
- if: slots.age < 18
then:
- action: utter_not_old_enough
next: END
- if: slots.age >= 18 and slots.age < 65
then: 18_to_65_step
- else: over_65_step
2. Step
- action:action_do_something执行命令,不需要等用户输入
- action: action_check_sufficient_funds
- collect:用户需要输入的slot,可以添加描述字段以指导语言模型提取槽值
- collect: account_type
description: "Account type is one of checking, savings, money market, or IRA."
ask_before_filling:true
reset_after_flow_ends:false
utter: utter_ask_secondary_account_type
- collect: age
rejections:
- if: slots.age < 1
utter: utter_invalid_age
- if: slots.age < 18
utter: utter_age_at_least_18
next: ask_email
默认情况下,如果slot已被提取则跳过收集步骤,ask_before_filling设置为true则依旧需要询问。可以设置为false,在域文件中添加默认值
slots:
dress_type:
type: categorical
values:
- "shirt"
- "pants"
- "jacket"
mappings:
- type: custom
dress_size:
type: categorical
values:
- "small"
- "medium"
- "large"
mappings:
- type: custom
默认情况下,当流程完成时,通过收集步骤填充的所有插槽都会重置。一旦流结束,插槽的值要么重置为null,要么重置为插槽的初始值。如果要在流完成后保留插槽的值,将reset_after_flow_ends属性设置为false。该设置和persisted_slots字段冲突
utter_ask_{slot_name}可以为收集的slot定义不同的key
actions:
- action_ask_{slot_name}
使用action_ask_{slot_name}可以自定义收集步骤,如想将从数据库中获取的插槽的可用值显示为按钮
rejections属性用于slot的验证,如果if条件为True,则返回utter定义的内容。如果没有通过验证,助手会自动尝试再次收集信息,直到值没有被拒绝,并默认返回Sorry,可以通过utter_internal_error_rasa修改
对于更复杂的验证逻辑,您还可以在自定义操作中定义插槽验证。请注意,此自定义操作必须遵循以下命名约定:validate_{slot_name}。
- link:
链接只能用作流程的最后一步。当达到链接步骤时,当前流结束,目标流开始。
- link: "id_of_another_flow"
- call:可以在流(父流)中使用调用步骤来嵌入另一个流(子流)。当执行到达调用步骤时,CALM会启动子流。子流完成后,执行将继续父流。
flows:
parent_flow:
steps:
- call: child_flow
child_flow:
if: False
steps:
- action: utter_child_flow
子流遇到next:END,则控件将传递回父流
激活子流时,父流的插槽不会重置。子流可以访问和修改父流的插槽。当控件返回到父流时,子流的插槽将不会重置。
如果在子流中触发CancelFlow()命令,父流也将被取消。
为了防止子流直接被用户消息触发,您可以在子流中添加流保护if: False,该流只有在父流调用它时才会触发。
另外,子流不能使用link
如果用例要求启动一个流作为另一个流的后续,那么link更适合完成两个流之间的连接。但是,如果一个流需要表现得像是另一个更大流的一部分,并且在子流完成后需要在更大流内执行更多步骤,那么应该使用call。
- set_slots:设置slot的值,主要用于取消设置插槽值。例如,如果用户要求转账的金额超过了他们账户中的可用金额,您可能需要通知他们。只需重置金额槽,而不是结束流程。
flows:
transfer_money:
description: This flow lets users send money to friends and family.
steps:
- collect: recipient
- id: ask_amount
collect: amount
description: the number of US dollars to send
- action: action_check_sufficient_funds
next:
- if: not has_sufficient_funds
then:
- action: utter_insufficient_funds
- set_slots:
- amount: null
next: ask_amount
- else: final_confirmation
- noop:不需要actions、collect就能创建分支条件
flows:
change_address:
description: Allow a user to change their address.
steps:
- noop: true
next:
- if: not slots.authenticated
then:
- call: authenticate_user
next: ask_new_address
- else: ask_new_address
- id: ask_new_address
collect: address
3. 条件表达式
支持 not、and、or、is、is not 、contains、matches等
命名空间:
- slots:用于访问插槽值
- context:用于访问当前对话帧属性
pattern_completed:
description: a flow has been completed and there is nothing else to be done
steps:
- noop: true
next:
- if: context.previous_flow_name != "greeting"
then:
- action: utter_what_can_help_with
next: END
- else: stop
- id: stop
action: action_stop
对话管理器在对话框架堆栈中组织流(用户定义和内置)的推进。对话帧堆栈表示LIFO(后进先出)对话帧堆栈。不同类型的对话框架被映射到内置的对话模式,以实现对话修复。
每个对话帧都有一个flow_id和step_id属性。flow_id是当前流的id,step_id是流中当前步骤的id。
4. 流的启动
nlu_trigger:包含一个可以启动流的意图列表,可以通过confidence_threshold添加置信区间阈值
flows:
my_flow:
description: "A flow triggered with <intent-name>"
nlu_trigger:
- intent:
name: <intent-name>
confidence_threshold: 0 # threshold value, optional
steps:
- action: my_action
在流定义中添加额外的if字段来指定流保护,但是如果是通过link,call,intent触发的则可以绕过限制
flows:
show_latest_bill:
description: A flow with a flow guard.
if: slots.authenticated AND slots.email_verified
steps:
- action: my_action
三、对话理解
CALM的对话理解模块将带有对话上下文的最新用户消息转换为一组命令,助手使用这些命令来执行定义的业务逻辑。它是在命令生成器的帮助下完成的
目前支持以下几种命令生成器:
- SingleStepLLMCommandGenerator
- MultiStepLLMCommandGenerator
- NLUCommandAdapter
如果想同时利用LLM和经典的NLU管道来预测命令,那么可以通过在基于LLM的命令生成器之前添加NLUCommandAdapter来实现
一般来说,如果第一个命令生成器预测了一个命令,则管道中接下来的所有其他命令生成器都会被跳过。
config.yml
pipeline:
# - ...
- name: NLUCommandAdapter
- name: SingleStepLLMCommandGenerator
# - ...
生成的命令包含以下:
- Start Flow、Cancel Flow、Skip Question、Set Slot、Correct Slots、Clarify、Chit-Chat Answer、Knowledge Answer、Human Handoff、Error、Cannot Handle、Change Flow
四、域
域定义了助手的活动空间,包括:
- Responses:发送给用户的响应模板
- Actions:操作
- Slots:对话的记忆
- Session配置,如超时时间
1. Slots
插槽在域的插槽部分中定义,包含其名称、类型和默认值。存在不同的插槽类型来限制插槽可以取的可能值。
插槽类型:text、bool、categorical、float、any、list
slots:
risk_level:
type: categorical
values:
- low
- medium
- high
- Slot Mappings
NLUCommandAdapter将NLU管道的输出(意图和实体)与域文件中定义的槽映射进行匹配。如果满足插槽映射,NLUCommandAdapter将发出设置插槽命令来填充插槽。
- from _ lm
可以使用from_llm槽映射类型用基于llm的命令生成器生成的值填充槽。如果域文件中没有明确定义映射,则这是默认的插槽映射类型。
slots:
user_name:
type: text
mappings:
- type: from_llm
在这个例子中,user_name插槽将填充基于LLM的命令生成器生成的值。允许基于LLM的命令生成器在会话流中的任何点填充此槽,而不仅仅是在该槽的相应收集步骤。NLUCommandAdapter将跳过任何具有from_llm映射的插槽,并且不会发出set插槽命令来填充这些插槽。
- Mapping Conditions:可以定义在填充插槽之前满足插槽映射的条件。如果定义了映射到同一实体的多个插槽,但不希望在提取实体时填充所有插槽,则这特别有用
entities:
- person
slots:
first_name:
type: text
mappings:
- type: from_entity
entity: person
conditions:
- active_flow: greet_user
last_name:
type: text
mappings:
- type: from_entity
entity: person
conditions:
- active_flow: issue_invoice
initial_value: "xx"
shared_for_coexistence: True
- 自定义Slot Mappings
actions:
- action_fill_user_name
slots:
user_name:
type: text
mappings:
- type: custom
action: action_fill_user_name
- 基于LLM的生成器在对话的任何时候都无法用基于NLU的预定义映射填充插槽。
- 在插槽名称的collect步骤中,基于LLM的生成器将不会处于活动状态。如果您希望用户话语包含离题或其他意图,而不仅仅是设置插槽的信息,那么您应该使用NLU触发器来处理流中的这些特定意图。
- 基于LLM的生成器可以在不收集插槽名称且具有from_LLM映射类型的步骤中填充其他插槽。
initial_value可以指定插槽的默认值
shared_for_coexistence属性指定共享一些CALM和基于NLU的系统都应该能够使用的插槽,如基于NLU的系统和CALM都应该能够知道用户是否登录。如果选项shared_for_coexistence未设置为true,则流定义中的reset_after_flow_ends:False属性将无效
2. Session配置
- session_expiration_time:定义了新会话开始后的不活动时间(分钟)。
- carry_over_slots_to_new_session:决定是否应将现有的插槽转移到新会话
默认配置:
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
这意味着,如果用户在60分钟不活动后发送了第一条消息,则会触发一个新的对话会话,并且任何现有的插槽都会被转移到新会话中。
五、事件
Rasa Pro中的每个对话都代表一系列事件。事件用于跟踪用户消息和机器人响应,以及Rasa Pro在对话中采取的行动,如推进流程或设置插槽。Rasa Pro在对话的不同阶段发出事件,可以通过查询Rasa Pro分析数据管道或运行端到端测试来分析和改进您的助手
事件存储在Rasa Pro tracker store中,可以通过Rasa Pro HTTP API进行额外访问。
六、Responses
响应是助手发送给用户的消息。响应通常只是文本,但也可以包括图像和按钮等内容。
响应位于域文件中的Responses键下或单独的“Responses.yml”文件中。每个响应名称都应该以utter_开头。
responses:
utter_greet:
- text: "Hi there!"
utter_bye:
- text: "See you!"
可以使用变量将信息插入到响应中。如下Rasa会自动用名为name的插槽中的值填充变量。如果这样的槽不存在或为空,则变量将填充None。
可以设置多个响应内容,会随机返回其中一个
channel属性会根据用户连接到的通道指定不同的响应变化
responses:
utter_greet:
- text: "Hey, {name}. How are you?"
channel: "slack"
- text: "Hey, {name}. How is your day going?"
根据条件决定返回值
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?"
Rephraser
通过使用LLM来改写静态响应模板,机器人生成的响应听起来会更自然、更具对话性,从而增强用户交互。
通过配置 rephrase: True 来开启重写。也可以开启全局重写。rephrase_prompt用来添加提示词
responses:
utter_greet:
- text: "Hey! How can I help you?"
metadata:
rephrase: True
rephrase_prompt: |
The following is a conversation with
an AI assistant. The assistant is helpful, creative, clever, and very friendly.
Rephrase the suggested AI response staying close to the original message and retaining
its meaning. Use simple english.
Context / previous conversation with the user:
{{history}}
{{current_input}}
Suggested AI Response: {{suggested_response}}
Rephrased AI Response:
历史对话配置
nlg:
- type: rephrase
summarize_history: False // 摘要模式
max_historical_turns: 5 // 转录模式,保留最后n次对话的直接转录。
此外,可以配置使用的大模型、温度等参数
七、组件
1. 配置文件
通过配置文件config.yml定义需要的组件和策略
recipe: default.v1
language: en
assistant_id: 20230405-114328-tranquil-mustard
pipeline:
- name: SingleStepLLMCommandGenerator
policies:
- name: rasa.core.policies.flow_policy.FlowPolicy
- pipeline:用于处理和理解最终用户发送助手的消息的组件
pipeline:
- name: SingleStepLLMCommandGenerator
llm:
model_group: openai_llm
flow_retrieval:
embeddings:
model_group: text_embedding_model
user_input:
max_characters: 420
- policies:助手将用于推进对话的对话策略
2. 模型组配置
LLM通常被部署在一个或多个组中,模型组允许在一个ID下定义多个模型,任何组件都可以使用该ID
模型组在endpoints.yml文件的model_groups键下定义
model_groups:
- id: openai_llm
models:
- provider: openai
model: gpt-4-0613
api_key: ${MY_OPENAI_API_KEY}
- id: openai-gpt-35-turbo
models:
- provider: openai
model: gpt-3.5-turbo
timeout: 7
需要使用环境变量使用${}
embedding模型也在model下配置
model_groups:
- id: text_embedding_model
models:
- provider: openai
model: text-embedding-ada-002
3. 路由组件
根据路由组件决定将消息路由到NLU系统还是CALM系统,有两个路由组件可供选择:
- IntentBasedRouter:NLU管道预测意图用于决定消息应该去哪里
- LLMBasedRouter:此组件利用LLM来决定消息是应该路由到基于NLU的系统还是CALM
在config.yml中配置即可
- name: IntentBasedRouter
nlu_entry:
sticky:
- transfer_money
- check_balance
- search_transactions
non_sticky:
- chitchat
calm_entry:
sticky:
- book_hotel
- cancel_hotel
- list_hotel_bookings
- nlu_entry:
- sticky:应该以sticky方式路由到基于NLU的系统的意图列表。
- non_stricky:应以非粘性方式路由到基于NLU的系统的意图列表。
- calm_entry:
- sticky:应该以粘性方式路由到CALM的意图列表。
4. LLM 命令生成器
LLM 命令生成器利用上下文学习来输出一系列命令,这些命令表示用户希望如何推进对话
当前支持两种LLM 命令生成器:
- SingleStepLLMCommandGenerator
- MultiStepLLMCommandGenerator
生成器使用的默认prompt模板由静态部分和动态部分组成。动态部分由当前的上下文内容渲染得到。也可以自定义模板,自定义模板可以使用{}来获取流信息插槽信息
pipeline:
- name: SingleStepLLMCommandGenerator
prompt_template: prompts/command-generator.jinja2
{% for flow in available_flows %}
{{ flow.name }}: {{ flow.description }}
{% for slot in flow.slots -%}
slot: {{ slot.name }}{% if slot.description %} ({{ slot.description }}){% endif %}{% if slot.allowed_values %}, allowed values: {{ slot.allowed_values }}{% endif %}
{% endfor %}
{%- endfor %}
MultiStepLLMCommandGenerator使用上下文学习在上下文中解释用户的消息,将任务分解为几个步骤,使LLM的工作更容易,步骤包括:
- 处理流程(开始、结束等)
- 填充插槽
因此也包含两套prompt:handle_flows和 fill_slots
- 如果当前没有活动的流,则使用handle_flows提示符来启动或澄清流。
- 如果启动了一个流,接下来将执行fill_slots提示符来填充新启动的流的任何槽。
- 如果流当前处于活动状态,则调用fill_slots提示符来填充当前活动流的任何新插槽。如果用户消息(也)指示,例如,应启动新流或取消活动流,则会触发ChangeFlow命令。这导致调用handle_flows提示符来启动、取消或澄清流。如果该提示导致启动流,则再次执行fill_slots提示以填充该新流的任何槽。
同样的,MultiStepLLMCommandGenerator的prompt模板也可以自定义
pipeline:
- name: MultiStepLLMCommandGenerator
prompt_templates:
handle_flows:
file_path: prompts/handle_flows_template.jinja2
fill_slots:
file_path: prompts/fill_slots_template.jinja2
5. 相关流召回
随着助理技能的发展,功能流程的数量可能会扩展到数百个。然而,由于LLM上下文窗口的限制,将所有这些流同时呈现给LLM是不切实际的。为了确保效率,只考虑与给定对话相关的流的子集。我们实现了一个流检索机制,以识别和过滤命令生成器最相关的流。这种有针对性的选择有助于在LLM的上下文窗口范围内制定有效的提示。
在训练过程中,所有定义的流,都会被转换为包含流描述、插槽描述的文档。然后使用嵌入模型将这些文档转换为向量,并存储在向量存储中。当与助手交谈时,即在推理过程中,当前对话上下文被转换为向量,并与向量存储中的流进行比较。此比较确定了与当前对话上下文最相似的流,并将其包含在SingleStepLLMCommandGenerator和MultiStepLLMComandGenerator的提示中
always_include_in_prompt设置为true,则该流总会被包含在prompt内
相关流召回功能默认是开启的,流召回的表现取决于流描述的质量
自定义prompt,提升准确率的技巧:
- 提供详细的信息描述:确保流程描述准确且信息丰富,直接概述流程的目的和范围。力求简洁和信息密度之间的平衡,使用命令式语言,避免不必要的单词,以防止歧义。目标是尽可能清楚地传达基本信息。
- 使用清晰标准的语言:避免不寻常的措辞或措辞。坚持使用清晰、普遍理解的语言
- 明确定义上下文:明确定义流上下文,以提高模型的情境感知能力
- 澄清隐性知识:澄清描述中的任何专业知识
- 添加用户输入示例
pipeline:
- name: SingleStepLLMCommandGenerator
...
flow_retrieval:
embeddings:
model_group: openai_text_embedding
turns_to_embed: 1
should_embed_slots: true
num_flows: 20
- embeddings:模型选择,默认使用text-embedding-ada-002
- turns_to_embed:要转换为向量的对话次数。将该值设置为1意味着只使用最新的对话回合
- should_embed_slots:是否在训练期间将槽描述与流描述一起嵌入
- num_flows:从向量存储中检索的最大流数
6. 多模型路由器
model_groups:
- id: azure_llm_deployments
models:
- provider: azure
deployment: gpt-4-instance-france
api_base: https://azure-deployment-france/
api_version: "2024-02-15-preview"
api_key: ${MY_AZURE_API_KEY_FRANCE}
- provider: azure
deployment: gpt-4-instance-canada
api_base: https://azure-deployment-canada/
api_version: "2024-02-15-preview"
api_key: ${MY_AZURE_API_KEY_CANADA}
router:
routing_strategy: simple-shuffle
在endpoints.yml中配置router实现多个模型的负载均衡。建议模型组配置中的模型部署使用相同的底层模型
可选的路由策略:
- simple-shuffle:基于RPM(每分钟请求数)或权重进行分配
- least-busy:把请求分发到正在进行的请求数量最少的模型
- latency-based-routing:将请求分发到响应时间最短的模型
需要配置Redis的策略:
- cost-based-routing:以最低成本将请求分发到部署。
- usage-based-routing:向TPM(每分钟令牌数)使用率最低的部署分发请求
7. Policy
policies:
- name: FlowPolicy
- name: EnterpriseSearchPolicy
可以配置多个策略,在每一个转折点,配置中定义的每个策略都有机会以一定的置信度预测下一个操作。策略也可以决定不预测任何行动。最高置信度的策略决定了助理的下一步行动。
- FlowPolicy
流策略是一个状态机,它确定性地执行流中定义的业务逻辑。流策略监督助手的状态,处理状态转换,并在会话修复需要时启动新流。
FlowPolicy采用对话堆栈结构(后进先出)和内部插槽来管理对话的状态。
- Intentless Policy
无意图策略用于发送在域中定义的Response,但这些Response不是任何流的一部分。这有助于有效地处理闲聊、上下文问题和高风险话题
policies:
- name: IntentlessPolicy
prompt: prompts/intentless-policy-template.jinja2
可以通过在config.yml中设置prompt属性来更改用于生成Response的prompt
无意图策略通常可以以零样本方式选择正确的响应。可以通过添加示例对话来提高策略的性能,将端到端的故事添加到data/e2e_stories.yml到您的训练数据中。这些对话将被用作示例,以帮助学习无障碍政策。可以定义您的助手可以发送的经过审查的、人工撰写的回复
- story: currencies
steps:
- user: How many different currencies can I hold money in?
- action: utter_faq_4
- story: automatic transfers travel
steps:
- user: Can I add money automatically to my account while traveling?
- action: utter_faq_5
- story: user gives a reason why they can't visit the branch
steps:
- user: I'd like to add my wife to my credit card
- action: utter_faq_10
- user: I've got a broken leg
- action: utter_faq_11
此外,对于高风险的话题,最安全的方法是发送一个自包含的、经过审查的答案,而不是依赖于生成模型。无障碍政策在CALM助理中提供了这种能力。