与 Dialogflow ES 智能体相比,Dialogflow CX 智能体为您提供更强大的对话控制和工具。如果您的 Dialogflow ES 代理处理复杂的对话,则应考虑迁移到 Dialogflow CX。
本指南介绍了如何将代理从 Dialogflow ES 迁移到 Dialogflow CX。这两种代理类型有许多根本差异,因此没有直接的方法来执行此迁移。
如果您使用本指南进行迁移,请点击上方的发送反馈按钮,提供正面或负面反馈。我们会根据这些反馈不断改进本指南。
概括来讲,建议的流程是自动化/手动混合流程。您将使用一个工具来读取部分 Dialogflow ES 代理数据,将这些数据写入 Dialogflow CX 智能体,并捕获待办事项列表。然后,您可以使用最佳实践、待办事项列表和该工具迁移的数据重新创建完整的 Dialogflow CX 智能体。
了解 Dialogflow CX
在尝试此迁移之前,您应该对 Dialogflow CX 流程的工作原理有深入的了解。您可以从这里开始:
您还应通读其他概念文档,其中包含您可能需要在新代理中使用的功能。重点关注以下几点:
了解 Dialogflow ES/Dialogflow CX 的区别
本部分列出了 Dialogflow ES 与 Dialogflow CX 之间最重要的区别。 稍后执行手动迁移步骤时,您应参阅本部分获取指导。
结构和对话路径控制
Dialogflow ES 提供以下功能,用于控制结构和对话路径:
- 意图用作代理的构建块。在对话中的任何时刻, 都会匹配一个意图, 从某种意义上说, 每个意图都是对话的一个节点。
- 上下文用于控制对话。上下文用于控制在任何给定时间可以匹配哪些 intent。上下文会在一定数量的对话轮次后过期,因此这种类型的控制对于长对话可能不准确。
Dialogflow CX 提供结构资源层次结构,并对对话路径进行更精确的控制:
- 页面是对话的图节点。Dialogflow CX 对话类似于状态机。 在对话中的任何给定时间点,只有一个页面处于活跃状态。根据最终用户输入或事件,对话可能会转换到另一个页面。一个页面在多个对话轮次中保持活跃状态是很常见的。
- 流是一组相关页面。每个流都应处理一个简要对话主题。
- 状态处理程序用于控制转换和回答。状态处理程序有三种类型:
- 意图路由:包含必须匹配的意图、可选回答和可选页面转换。
- 条件路由:包含必须满足的条件、可选回答和可选页面转换。
- 事件处理脚本:包含必须调用的事件名称、可选响应和可选页面转换。
- 范围用于控制是否可以调用状态处理程序。大多数处理程序都与页面或整个流程相关联。 如果关联的页面或流处于活跃状态,则处理程序在范围内,可以调用。范围内的 Dialogflow CX 意图路由类似于具有有效输入上下文的 Dialogflow ES 意图。
在设计代理的流和页面时,请务必了解代理设计指南的流部分中的建议。
表单填充
Dialogflow ES 使用槽填充从最终用户那里收集必需的参数:
- 这些参数是标记为必需的 intent 参数。
- 系统会继续匹配该 intent,直到收集到所有必需参数。
- 您可以定义一个提示,要求最终用户提供值。
Dialogflow CX 使用表单填充从最终用户那里收集必需参数:
- 这些参数与网页相关联,并在网页处于活动状态时收集。
- 您可以使用页面的条件路由来确定表单填写已完成。这些条件路由通常会转换到另一个页面。
- 您可以定义提示以及重新提示处理程序,以妥善处理多次尝试收集值的情况。
转换
当最终用户输入与某个意图匹配时,Dialogflow ES 会自动从一个意图转换到下一个意图。只有没有输入上下文的 intent 或具有活跃输入上下文的 intent 才能发生这种匹配。
当范围内的状态处理程序满足其要求并提供转换目标时,Dialogflow CX 会从一个页面转换到下一个页面。使用这些转换,您可以可靠地引导最终用户完成对话。您可以通过多种方式控制这些过渡:
- 意图匹配可以触发意图路由。
- 满足条件可以触发条件路由。
- 调用事件可以触发事件处理脚本。
- 当最终用户在多次尝试后仍未能提供值时,重新提示处理程序可能会导致转换。
- 您可以将符号转换目标用作转换目标。
代理回答
当意图匹配时,Dialogflow ES 代理回答会发送给最终用户:
- 代理可以从可能的回答列表中选择一条消息作为回答。
- 回答可以针对特定平台,这些平台可以使用富回答格式。
- 回答可以由 webhook 驱动。
调用fulfillment时,Dialogflow CX 智能体回答会发送给最终用户。与始终涉及 webhook 的 Dialogflow ES 执行方式不同,Dialogflow CX 执行方式可能涉及也可能不涉及调用 webhook,具体取决于执行方式资源是否配置了 webhook。基于网络钩子响应的静态和动态响应均由执行方式控制。您可以通过多种方式创建代理回答:
- 可以为任何类型的状态处理程序提供执行方式。
- 在对话回合期间,可以通过响应队列串联多个响应。在某些情况下,此功能可以简化代理设计。
- Dialogflow CX 不支持内置的平台专用响应。不过,它提供了多种回答类型,包括可用于平台特定回答的自定义载荷。
参数
Dialogflow ES 参数具有以下特征:
- 仅在 intent 中定义。
- 由最终用户输入、事件、Webhook 和 API 调用设置。
- 在回答、参数提示、Webhook 代码和参数值中引用:
- 基本引用格式为
$parameter-name。 - 引用支持
.original、.partial和.recent后缀语法。 - 引用可以指定活跃上下文:
#context-name.parameter-name。 - 引用可以指定事件参数:
#event-name.parameter-name。
- 基本引用格式为
Dialogflow CX 参数具有以下特征:
- 在意图和页面表单中定义。
- 意图和表单参数会传播到会话参数,在会话期间可供引用。
- 由最终用户输入、网络钩子、执行参数预设和 API 调用设置。
- 在回答、参数提示、重新提示处理程序、参数预设和 webhook 代码中引用:
- 会话参数的引用格式为
$session.params.parameter-id,意图参数的引用格式为$intent.params.parameter-id。 - 意图参数引用支持
.original和.resolved后缀语法。会话参数不支持此语法。
- 会话参数的引用格式为
系统实体
Dialogflow ES 支持许多系统实体。
Dialogflow CX 支持许多相同的系统实体,但存在一些差异。迁移时,请验证您在 Dialogflow ES 中使用的系统实体是否也受 Dialogflow CX 支持(针对同一语言)。 如果没有,您应该为这些内容创建自定义实体。
事件
Dialogflow ES 事件具有以下特征:
- 可通过 API 调用或网络钩子调用,以匹配 intent。
- 可以设置参数。
- 集成平台会调用少量事件。
Dialogflow CX 事件具有以下特征:
- 可通过 API 调用或网络钩子调用,以调用事件处理脚本。
- 无法设置参数。
- 许多内置事件可用于处理缺少最终用户输入、无法识别的最终用户输入、被网络钩子失效的参数以及网络钩子错误。
- 调用可以由与其他状态处理程序相同的范围规则控制。
内置意图
Dialogflow ES 支持以下内置 intent:
下面介绍了 Dialogflow CX 对内置 intent 的支持:
- 支持欢迎 intent。
- 不提供回退 intent。 请改为在事件处理脚本中使用 no-match 事件。
- 对于否定示例,请使用默认否定意图。
- 不提供预定义的后续意图。您必须根据代理的要求创建这些 intent。 例如,您可能需要创建一个 intent 来处理对智能体问题的否定回答(“不”、“不用了”、“不,我不想”等)。Dialogflow CX 意图可在整个代理中重复使用,因此您只需定义一次。在不同范围内为这些常见 intent 使用不同的 intent 路由,可以更好地控制对话。
网络钩子
Dialogflow ES webhook 具有以下特征:
- 您可以为代理配置一个 webhook 服务。
- 每个 intent 都可以标记为使用 webhook。
- 没有用于处理 webhook 错误的内置支持。
- Webhook 使用 intent 操作或 intent 名称来确定它是从代理中的哪个位置调用的。
- 控制台提供了直接修改。
Dialogflow CX 网络钩子具有以下特征:
- 您可以为代理配置多个网络钩子服务。
- 每个 fulfillment 都可以选择指定一个 webhook 调用。
- 内置了对 Webhook 错误处理的支持。
- Dialogflow CX 执行网络钩子包含一个标记。此标记类似于 Dialogflow ES 操作,但仅在调用网络钩子时使用。Webhook 服务可以使用这些标记来确定它是从代理中的哪个位置调用的。
- 控制台没有内置的 webhook 代码编辑器。 通常会使用 Cloud Run functions,但也有许多其他选项。
迁移到 Dialogflow CX 时, 您需要更改网络钩子代码, 因为请求和响应属性不同。
集成
Dialogflow ES 集成和 Dialogflow CX 集成支持不同的平台。对于两种代理类型都支持的平台,配置可能存在差异。
如果 Dialogflow CX 不支持您使用的 Dialogflow ES 集成,您可能需要切换平台或自行实现集成。
更多 Dialogflow CX 专属功能
还有许多其他功能仅由 Dialogflow CX 提供。 您应考虑在迁移期间使用这些功能。 例如:
- 高级 NLU
- 高级语音设置(结束语音识别的灵敏度、无语音超时等)
- 更改历史记录
- 条件逻辑
- 用于电话集成的 DTMF 输入
- 特定于环境的 webhook
- 实验
- 路由组
- 搜索代理数据
- 安全设置 (隐去和数据保留)
- 用于高级回答和条件的系统函数
- 测试用例
- 验证代理数据
最佳做法
在迁移之前,请先熟悉 Dialogflow CX 智能体设计最佳实践。其中许多 Dialogflow CX 最佳实践与 Dialogflow ES 最佳实践类似,但有些是 Dialogflow CX 独有的。
关于迁移工具
迁移工具会将大部分 Dialogflow ES 数据复制到 Dialogflow CX 智能体,并写入一个 TODO 文件,其中包含必须手动迁移的项的列表。该工具仅复制自定义实体类型和意图训练短语。您应考虑根据自己的具体需求自定义此工具。
迁移工具代码
以下是该工具的代码。 您应该查看此工具的代码, 以便了解其功能。 您可能需要更改此代码,以处理代理中的特定情况。 在以下步骤中,您将执行此工具。
// Package main implements the ES to CX migration tool. package main import ( "context" "encoding/csv" "flag" "fmt" "os" "strings" "time" v2 "cloud.google.com/go/dialogflow/apiv2" proto2 "cloud.google.com/go/dialogflow/apiv2/dialogflowpb" v3 "cloud.google.com/go/dialogflow/cx/apiv3" proto3 "cloud.google.com/go/dialogflow/cx/apiv3/cxpb" "google.golang.org/api/iterator" "google.golang.org/api/option" ) // Commandline flags var v2Project *string = flag.String("es-project-id", "", "ES project") var v3Project *string = flag.String("cx-project-id", "", "CX project") var v2Region *string = flag.String("es-region-id", "", "ES region") var v3Region *string = flag.String("cx-region-id", "", "CX region") var v3Agent *string = flag.String("cx-agent-id", "", "CX region") var outFile *string = flag.String("out-file", "", "Output file for CSV TODO items") var dryRun *bool = flag.Bool("dry-run", false, "Set true to skip CX agent writes") // Map from entity type display name to fully qualified name. var entityTypeShortToLong = map[string]string{} // Map from ES system entity to CX system entity var convertSystemEntity = map[string]string{ "sys.address": "sys.address", "sys.any": "sys.any", "sys.cardinal": "sys.cardinal", "sys.color": "sys.color", "sys.currency-name": "sys.currency-name", "sys.date": "sys.date", "sys.date-period": "sys.date-period", "sys.date-time": "sys.date-time", "sys.duration": "sys.duration", "sys.email": "sys.email", "sys.flight-number": "sys.flight-number", "sys.geo-city-gb": "sys.geo-city", "sys.geo-city-us": "sys.geo-city", "sys.geo-city": "sys.geo-city", "sys.geo-country": "sys.geo-country", "sys.geo-state": "sys.geo-state", "sys.geo-state-us": "sys.geo-state", "sys.geo-state-gb": "sys.geo-state", "sys.given-name": "sys.given-name", "sys.language": "sys.language", "sys.last-name": "sys.last-name", "sys.street-address": "sys.location", "sys.location": "sys.location", "sys.number": "sys.number", "sys.number-integer": "sys.number-integer", "sys.number-sequence": "sys.number-sequence", "sys.ordinal": "sys.ordinal", "sys.percentage": "sys.percentage", "sys.person": "sys.person", "sys.phone-number": "sys.phone-number", "sys.temperature": "sys.temperature", "sys.time": "sys.time", "sys.time-period": "sys.time-period", "sys.unit-currency": "sys.unit-currency", "sys.url": "sys.url", "sys.zip-code": "sys.zip-code", } // Issues found for the CSV output var issues = [][]string{ {"Field", "Issue"}, } // logIssue logs an issue for the CSV output func logIssue(field string, issue string) { issues = append(issues, []string{field, issue}) } // convertEntityType converts an ES entity type to CX func convertEntityType(et2 *proto2.EntityType) *proto3.EntityType { var kind3 proto3.EntityType_Kind switch kind2 := et2.Kind; kind2 { case proto2.EntityType_KIND_MAP: kind3 = proto3.EntityType_KIND_MAP case proto2.EntityType_KIND_LIST: kind3 = proto3.EntityType_KIND_LIST case proto2.EntityType_KIND_REGEXP: kind3 = proto3.EntityType_KIND_REGEXP default: kind3 = proto3.EntityType_KIND_UNSPECIFIED } var expansion3 proto3.EntityType_AutoExpansionMode switch expansion2 := et2.AutoExpansionMode; expansion2 { case proto2.EntityType_AUTO_EXPANSION_MODE_DEFAULT: expansion3 = proto3.EntityType_AUTO_EXPANSION_MODE_DEFAULT default: expansion3 = proto3.EntityType_AUTO_EXPANSION_MODE_UNSPECIFIED } et3 := &proto3.EntityType{ DisplayName: et2.DisplayName, Kind: kind3, AutoExpansionMode: expansion3, EnableFuzzyExtraction: et2.EnableFuzzyExtraction, } for _, e2 := range et2.Entities { et3.Entities = append(et3.Entities, &proto3.EntityType_Entity{ Value: e2.Value, Synonyms: e2.Synonyms, }) } return et3 } // convertParameterEntityType converts a entity type found in parameters func convertParameterEntityType(intent string, parameter string, t2 string) string { if len(t2) == 0 { return "" } t2 = t2[1:] // remove @ if strings.HasPrefix(t2, "sys.") { if val, ok := convertSystemEntity[t2]; ok { t2 = val } else { t2 = "sys.any" logIssue("Intent<"+intent+">.Parameter<"+parameter+">", "This intent parameter uses a system entity not supported by CX English agents. See the migration guide for advice. System entity: "+t2) } return fmt.Sprintf("projects/-/locations/-/agents/-/entityTypes/%s", t2) } return entityTypeShortToLong[t2] } // convertIntent converts an ES intent to CX func convertIntent(intent2 *proto2.Intent) *proto3.Intent { if intent2.DisplayName == "Default Fallback Intent" || intent2.DisplayName == "Default Welcome Intent" { return nil } intent3 := &proto3.Intent{ DisplayName: intent2.DisplayName, } // WebhookState if intent2.WebhookState != proto2.Intent_WEBHOOK_STATE_UNSPECIFIED { logIssue("Intent<"+intent2.DisplayName+">.WebhookState", "This intent has webhook enabled. You must configure this in your CX agent.") } // IsFallback if intent2.IsFallback { logIssue("Intent<"+intent2.DisplayName+">.IsFallback", "This intent is a fallback intent. CX does not support this. Use no-match events instead.") } // MlDisabled if intent2.MlDisabled { logIssue("Intent<"+intent2.DisplayName+">.MlDisabled", "This intent has ML disabled. CX does not support this.") } // LiveAgentHandoff if intent2.LiveAgentHandoff { logIssue("Intent<"+intent2.DisplayName+">.LiveAgentHandoff", "This intent uses live agent handoff. You must configure this in a fulfillment.") } // EndInteraction if intent2.EndInteraction { logIssue("Intent<"+intent2.DisplayName+">.EndInteraction", "This intent uses end interaction. CX does not support this.") } // InputContextNames if len(intent2.InputContextNames) > 0 { logIssue("Intent<"+intent2.DisplayName+">.InputContextNames", "This intent uses context. See the migration guide for alternatives.") } // Events if len(intent2.Events) > 0 { logIssue("Intent<"+intent2.DisplayName+">.Events", "This intent uses events. Use event handlers instead.") } // TrainingPhrases var trainingPhrases3 []*proto3.Intent_TrainingPhrase for _, tp2 := range intent2.TrainingPhrases { if tp2.Type == proto2.Intent_TrainingPhrase_TEMPLATE { logIssue("Intent<"+intent2.DisplayName+">.TrainingPhrases", "This intent has a training phrase that uses a template (@...) training phrase type. CX does not support this.") } var parts3 []*proto3.Intent_TrainingPhrase_Part for _, part2 := range tp2.Parts { parts3 = append(parts3, &proto3.Intent_TrainingPhrase_Part{ Text: part2.Text, ParameterId: part2.Alias, }) } trainingPhrases3 = append(trainingPhrases3, &proto3.Intent_TrainingPhrase{ Parts: parts3, RepeatCount: 1, }) } intent3.TrainingPhrases = trainingPhrases3 // Action if len(intent2.Action) > 0 { logIssue("Intent<"+intent2.DisplayName+">.Action", "This intent sets the action field. Use a fulfillment webhook tag instead.") } // OutputContexts if len(intent2.OutputContexts) > 0 { logIssue("Intent<"+intent2.DisplayName+">.OutputContexts", "This intent uses context. See the migration guide for alternatives.") } // ResetContexts if intent2.ResetContexts { logIssue("Intent<"+intent2.DisplayName+">.ResetContexts", "This intent uses context. See the migration guide for alternatives.") } // Parameters var parameters3 []*proto3.Intent_Parameter for _, p2 := range intent2.Parameters { if len(p2.Value) > 0 && p2.Value != "$"+p2.DisplayName { logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Value", "This field is not set to $parameter-name. This feature is not supported by CX. See: https://cloud.google.com/dialogflow/es/docs/intents-actions-parameters#valfield.") } if len(p2.DefaultValue) > 0 { logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.DefaultValue", "This intent parameter is using a default value. CX intent parameters do not support default values, but CX page form parameters do. This parameter should probably become a form parameter.") } if p2.Mandatory { logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Mandatory", "This intent parameter is marked as mandatory. CX intent parameters do not support mandatory parameters, but CX page form parameters do. This parameter should probably become a form parameter.") } for _, prompt := range p2.Prompts { logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Prompts", "This intent parameter has a prompt. Use page form parameter prompts instead. Prompt: "+prompt) } if len(p2.EntityTypeDisplayName) == 0 { p2.EntityTypeDisplayName = "@sys.any" logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.EntityTypeDisplayName", "This intent parameter does not have an entity type. CX requires an entity type for all parameters..") } parameters3 = append(parameters3, &proto3.Intent_Parameter{ Id: p2.DisplayName, EntityType: convertParameterEntityType(intent2.DisplayName, p2.DisplayName, p2.EntityTypeDisplayName), IsList: p2.IsList, }) //fmt.Printf("Converted parameter: %+v\n", parameters3[len(parameters3)-1]) } intent3.Parameters = parameters3 // Messages for _, message := range intent2.Messages { m, ok := message.Message.(*proto2.Intent_Message_Text_) if ok { for _, t := range m.Text.Text { warnings := "" if strings.Contains(t, "#") { warnings += " This message may contain a context parameter reference, but CX does not support this." } if strings.Contains(t, ".original") { warnings += " This message may contain a parameter reference suffix of '.original', But CX only supports this for intent parameters (not session parameters)." } if strings.Contains(t, ".recent") { warnings += " This message may contain a parameter reference suffix of '.recent', but CX does not support this." } if strings.Contains(t, ".partial") { warnings += " This message may contain a parameter reference suffix of '.partial', but CX does not support this." } logIssue("Intent<"+intent2.DisplayName+">.Messages", "This intent has a response message. Use fulfillment instead."+warnings+" Message: "+t) } } else { logIssue("Intent<"+intent2.DisplayName+">.Messages", "This intent has a non-text response message. See the rich response message information in the migration guide.") } if message.Platform != proto2.Intent_Message_PLATFORM_UNSPECIFIED { logIssue("Intent<"+intent2.DisplayName+">.Platform", "This intent has a message with a non-default platform. See the migration guide for advice.") } } return intent3 } // migrateEntities migrates ES entities to your CX agent func migrateEntities(ctx context.Context) error { var err error // Create ES client var client2 *v2.EntityTypesClient options2 := []option.ClientOption{} if len(*v2Region) > 0 { options2 = append(options2, option.WithEndpoint(*v2Region+"-dialogflow.googleapis.com:443")) } client2, err = v2.NewEntityTypesClient(ctx, options2...) if err != nil { return err } defer client2.Close() var parent2 string if len(*v2Region) == 0 { parent2 = fmt.Sprintf("projects/%s/agent", *v2Project) } else { parent2 = fmt.Sprintf("projects/%s/locations/%s/agent", *v2Project, *v2Region) } // Create CX client var client3 *v3.EntityTypesClient options3 := []option.ClientOption{} if len(*v3Region) > 0 { options3 = append(options3, option.WithEndpoint(*v3Region+"-dialogflow.googleapis.com:443")) } client3, err = v3.NewEntityTypesClient(ctx, options3...) if err != nil { return err } defer client3.Close() parent3 := fmt.Sprintf("projects/%s/locations/%s/agents/%s", *v3Project, *v3Region, *v3Agent) // Read each V2 entity type, convert, and write to V3 request2 := &proto2.ListEntityTypesRequest{ Parent: parent2, } it2 := client2.ListEntityTypes(ctx, request2) for { var et2 *proto2.EntityType et2, err = it2.Next() if err == iterator.Done { break } if err != nil { return err } fmt.Printf("Entity Type: %s\n", et2.DisplayName) if *dryRun { convertEntityType(et2) continue } request3 := &proto3.CreateEntityTypeRequest{ Parent: parent3, EntityType: convertEntityType(et2), } et3, err := client3.CreateEntityType(ctx, request3) entityTypeShortToLong[et3.DisplayName] = et3.Name if err != nil { return err } // ES and CX each have a quota limit of 60 design-time requests per minute time.Sleep(2 * time.Second) } return nil } // migrateIntents migrates intents to your CX agent func migrateIntents(ctx context.Context) error { var err error // Create ES client var client2 *v2.IntentsClient options2 := []option.ClientOption{} if len(*v2Region) > 0 { options2 = append(options2, option.WithEndpoint(*v2Region+"-dialogflow.googleapis.com:443")) } client2, err = v2.NewIntentsClient(ctx, options2...) if err != nil { return err } defer client2.Close() var parent2 string if len(*v2Region) == 0 { parent2 = fmt.Sprintf("projects/%s/agent", *v2Project) } else { parent2 = fmt.Sprintf("projects/%s/locations/%s/agent", *v2Project, *v2Region) } // Create CX client var client3 *v3.IntentsClient options3 := []option.ClientOption{} if len(*v3Region) > 0 { options3 = append(options3, option.WithEndpoint(*v3Region+"-dialogflow.googleapis.com:443")) } client3, err = v3.NewIntentsClient(ctx, options3...) if err != nil { return err } defer client3.Close() parent3 := fmt.Sprintf("projects/%s/locations/%s/agents/%s", *v3Project, *v3Region, *v3Agent) // Read each V2 entity type, convert, and write to V3 request2 := &proto2.ListIntentsRequest{ Parent: parent2, IntentView: proto2.IntentView_INTENT_VIEW_FULL, } it2 := client2.ListIntents(ctx, request2) for { var intent2 *proto2.Intent intent2, err = it2.Next() if err == iterator.Done { break } if err != nil { return err } fmt.Printf("Intent: %s\n", intent2.DisplayName) intent3 := convertIntent(intent2) if intent3 == nil { continue } if *dryRun { continue } request3 := &proto3.CreateIntentRequest{ Parent: parent3, Intent: intent3, } _, err := client3.CreateIntent(ctx, request3) if err != nil { return err } // ES and CX each have a quota limit of 60 design-time requests per minute time.Sleep(2 * time.Second) } return nil } // checkFlags checks commandline flags func checkFlags() error { flag.Parse() if len(*v2Project) == 0 { return fmt.Errorf("Need to supply es-project-id flag") } if len(*v3Project) == 0 { return fmt.Errorf("Need to supply cx-project-id flag") } if len(*v2Region) == 0 { fmt.Printf("No region supplied for ES, using default\n") } if len(*v3Region) == 0 { return fmt.Errorf("Need to supply cx-region-id flag") } if len(*v3Agent) == 0 { return fmt.Errorf("Need to supply cx-agent-id flag") } if len(*outFile) == 0 { return fmt.Errorf("Need to supply out-file flag") } return nil } // closeFile is used as a convenience for defer func closeFile(f *os.File) { err := f.Close() if err != nil { fmt.Fprintf(os.Stderr, "ERROR closing CSV file: %v\n", err) os.Exit(1) } } func main() { if err := checkFlags(); err != nil { fmt.Fprintf(os.Stderr, "ERROR checking flags: %v\n", err) os.Exit(1) } ctx := context.Background() if err := migrateEntities(ctx); err != nil { fmt.Fprintf(os.Stderr, "ERROR migrating entities: %v\n", err) os.Exit(1) } if err := migrateIntents(ctx); err != nil { fmt.Fprintf(os.Stderr, "ERROR migrating intents: %v\n", err) os.Exit(1) } csvFile, err := os.Create(*outFile) if err != nil { fmt.Fprintf(os.Stderr, "ERROR opening output file: %v", err) os.Exit(1) } defer closeFile(csvFile) csvWriter := csv.NewWriter(csvFile) if err := csvWriter.WriteAll(issues); err != nil { fmt.Fprintf(os.Stderr, "ERROR writing CSV output file: %v", err) os.Exit(1) } csvWriter.Flush() }
实体类型的工具迁移
Dialogflow ES 实体类型和 Dialogflow CX 实体类型非常相似,因此它们是最容易迁移的数据类型。该工具只是按原样复制实体类型。
意图的工具迁移
Dialogflow ES intent 和 Dialogflow CX intent 截然不同。
Dialogflow ES 意图用作代理的构建块; 它们包含训练短语、回答、 对话控制上下文、 Webhook 配置、事件、操作和槽填充参数。
Dialogflow CX 已将大部分此类数据移至其他资源。 Dialogflow CX 意图只有训练短语和参数,这使得意图可在整个代理中重复使用。该工具只会将这两类 intent 数据复制到 Dialogflow CX intent。
迁移工具限制
迁移工具不支持以下内容:
- 超级代理:该工具无法从多个分代理中读取数据,但您可以针对每个分代理多次调用该工具。
- 多语言代理:您应修改该工具,以创建多语言训练短语和实体条目。
- 非英语语言的系统实体验证:当该工具发现 Dialogflow CX 不支持的系统实体时,会创建待办事项,并假设英语是默认语言,且使用美国区域。系统实体支持因语言和区域而异。 对于其他语言和区域,您应修改该工具以执行此检查。
基本迁移步骤
以下小节概述了要执行的迁移步骤。 您无需按顺序执行这些手动步骤,甚至可能需要同时或以不同的顺序执行这些步骤。在实际进行更改之前,请仔细阅读这些步骤并开始规划更改。
运行迁移工具后,您可以重新构建 Dialogflow CX 智能体。您仍有相当多的迁移工作要做,但大部分手动输入的数据都会出现在 Dialogflow CX 智能体和 TODO 文件中。
创建 Dialogflow CX 智能体
如果尚未创建,请创建 Dialogflow CX 智能体。请务必使用与 Dialogflow ES 代理相同的默认语言。
运行迁移工具
请按以下步骤执行该工具:
- 如果您尚未安装 Go,请在您的机器上安装 Go。
- 为工具代码创建一个名为
migrate的目录。 - 将上述工具代码复制到此目录中名为
main.go的文件。 - 根据您的具体情况修改代码。
在此目录中创建 Go 模块。例如:
go mod init migrate安装 Dialogflow ES V2 和 Dialogflow CX V3 Go 客户端库:
go get cloud.google.com/go/dialogflow/apiv2 go get cloud.google.com/go/dialogflow/cx/apiv3确保您已设置客户端库身份验证。
运行该工具,并将输出保存到文件:
go run main.go -es-project-id=<ES_PROJECT_ID> -cx-project-id=<CX_PROJECT_ID> \ -cx-region-id=<CX_REGION_ID> -cx-agent-id=<CX_AGENT_ID> -out-file=out.csv
迁移工具问题排查
如果您在运行该工具时遇到错误,请检查以下各项:
| 错误 | 解决方法 |
|---|---|
| RPC 错误:训练短语部分提及了未为 intent 定义的参数。 | 如果您之前使用 Dialogflow ES API 创建了与训练短语不一致的 intent 参数,就可能会发生这种情况。如需解决此问题,请在控制台中重命名 Dialogflow ES 参数,检查训练短语是否正确使用了该参数,然后点击“保存”。如果您的训练短语引用了不存在的参数,也可能会发生这种情况。 |
修正错误后,您需要先清除 Dialogflow CX 智能体中的意图和实体,然后才能再次运行迁移工具。
将 Dialogflow ES intent 数据迁移到 Dialogflow CX
该工具会将意图训练短语和参数迁移到 Dialogflow CX 意图,但还有许多其他 Dialogflow ES 意图字段需要手动迁移。
Dialogflow ES intent 可能需要对应的 Dialogflow CX 页面、对应的 Dialogflow CX intent 或两者。
如果使用 Dialogflow ES intent 匹配将对话从特定对话节点转换到另一个节点,则代理中应有两个与此 intent 相关的页面:
- 包含意图路由的原始页面,该页面将转换到下一页面:原始页面中的意图路由可能具有类似于 Dialogflow ES 意图响应的 Dialogflow CX 执行消息。此页面中可能有许多 intent 路由。 当原始页面处于活跃状态时, 这些意图路由可以将对话 转换到许多可能的路径。 许多 Dialogflow ES intent 将共享同一个对应的 Dialogflow CX 原始页面。
- 下一页面,即原始页面中 intent 路由的转换目标:下一页面的 Dialogflow CX 条目执行方式可能具有与 Dialogflow ES intent 响应类似的 Dialogflow CX 执行方式消息。
如果 Dialogflow ES intent 包含必需参数,您应在表单中创建具有相同参数的相应 Dialogflow CX 页面。
Dialogflow CX intent 和 Dialogflow CX 页面通常会共享相同的参数列表,这意味着单个 Dialogflow ES intent 具有对应的 Dialogflow CX 页面和对应的 Dialogflow CX intent。当匹配到意图路由中包含参数的 Dialogflow CX 意图时,对话通常会转换到具有相同参数的页面。从意图匹配中提取的参数会传播到会话参数,这些参数可用于部分或完全填充页面表单参数。
Dialogflow CX 中不存在后备意图和预定义的后续意图。请参阅内置 intent。
下表介绍了如何将特定 intent 数据从 Dialogflow ES 映射到 Dialogflow CX 资源:
| Dialogflow ES 意图数据 | 对应的 Dialogflow CX 数据 | 需要采取行动 |
|---|---|---|
| 训练短语 | 意图训练短语 | 已通过工具迁移。该工具会检查系统实体支持,并为不受支持的系统实体创建待办事项。 |
| 代理回答 | Fulfillment 响应消息 | 请参阅代理回答。 |
| 对话控制上下文 | 无 | 请参阅结构和对话路径控制。 |
| 网络钩子设置 | 执行方式 webhook 配置 | 请参阅 Webhook。 |
| 事件 | 流级或页面级事件处理脚本 | 查看活动。 |
| 操作 | 执行方式 webhook 标记 | 请参阅 Webhook。 |
| 参数 | 意图参数和/或页面表单参数 | 已由工具迁移到 intent 参数。如果参数是必需的,该工具会创建待办事项,以便可能迁移到页面。请参阅参数。 |
| 形参提示 | 页面表单参数提示 | 请参阅表单填充。 |
创建流程
为每个高级别对话主题创建一个流。 每个流中的主题应有所不同,这样对话就不会频繁地在流之间来回跳转。
如果您使用的是超级代理,则每个分代理都应成为一个或多个流。
从基本对话路径开始
最好在迭代更改时使用模拟器测试代理。 因此,您应该首先关注对话早期的基本对话路径,并在进行更改时进行测试。这些路径正常运行后, 再继续设计更详细的对话路径。
流级状态处理程序与页面级状态处理程序
创建状态处理程序时,请考虑应在流级层还是页面级层应用它们。只要流(以及流中的任何页面)处于活跃状态,流级处理程序就会处于范围内。页面级处理程序仅在特定页面处于活动状态时才在范围内。流级处理程序类似于没有输入上下文的 Dialogflow ES intent。页面级处理程序类似于具有输入上下文的 Dialogflow ES intent。
Webhook 代码
Dialogflow CX 的网络钩子请求和响应属性有所不同。 请参阅 Webhook 部分。
知识连接器
Dialogflow CX 尚不支持知识连接器。您需要将这些意图实现为普通意图,或者等到 Dialogflow CX 支持知识连接器。
智能体设置
查看您的 Dialogflow ES 代理设置,并根据需要调整 Dialogflow CX 智能体设置。
使用 TODO 文件
迁移工具会输出一个 CSV 文件。 此列表中的项目侧重于可能需要注意的特定数据。 将此文件导入电子表格。 在电子表格中逐一解决每个问题, 并使用一列来标记完成情况。
API 用量迁移
如果您的系统使用 Dialogflow ES API 进行运行时或设计时调用,则需要更新此代码以使用 Dialogflow CX API。如果您在运行时仅使用检测 intent 调用,则此更新应该相当简单。
集成
如果您的代理使用集成,请参阅集成部分,并根据需要进行更改。
建议的迁移步骤
以下各小节概述了建议的迁移步骤。
验证
使用代理验证检查您的代理是否遵循最佳实践。
测试
在执行上述手动迁移步骤时,您应该使用模拟器测试代理。当您的代理似乎可以正常工作时,您应该比较 Dialogflow ES 和 Dialogflow CX 代理之间的对话,并验证行为是否相似或有所改进。
使用模拟器测试这些对话时,您应创建测试用例,以防止未来出现回归。
环境
查看您的 Dialogflow ES 环境,并根据需要更新 Dialogflow CX 环境。