如何管理 Dialogflow / Api.ai 中的 5 秒响应超时限制?

Posted

技术标签:

【中文标题】如何管理 Dialogflow / Api.ai 中的 5 秒响应超时限制?【英文标题】:How to manage the 5 seconds response timeout limit in Dialogflow / Api.ai? 【发布时间】:2019-01-21 05:52:23 【问题描述】:

我正在使用 Dialogflow 创建一个代理/机器人,该代理/机器人通过诸如“我需要从 HR 处获取地址证明”之类的操作项来响应不同类型的用户查询。这需要机器人从公司的数据库中获取一些信息,并通过将检索到的信息填充到人力资源提供的模板化信件文件中来生成文档/信件。执行此操作的逻辑已写入 python 文件。数据库集成是使用 Webhooks 完成的。

问题在于,这个完整的解释用户请求、打开数据库和检索所需信息的过程需要超过 5 秒,这恰好是 Dialogflow 代理的响应超时限制。我对此进行了一些研究,发现我们无法增加此限制,但我们可以通过异步调用保持会话处于活动状态。我无法找到提供答案的正确资源。

所以,我的问题是——

我们可以在对话流中进行异步调用吗?

如果是,那么我们如何通过json向Dailogflow代理发送异步数据?

还有其他方法可以解决这个 5 秒响应超时限制吗?

提前致谢!

【问题讨论】:

你最终是如何解决这个问题的?你走异步方式了吗?对用户体验有影响吗? 【参考方案1】:

降低代码的复杂性以使其更快;你们中的一些人正在使用微服务或纳米服务架构,例如 firebase 函数、AWS lambda 或 Kubernetes,尝试通过在函数内部而不是全局范围内初始化库来减少死启动和冷启动,

如果您有多个 API 调用,请尝试使其并行而不是一个接一个地减少。例如promise.all 方法

也可以通过数据库或上下文来解决问题。

例如用户问:我的余额是多少

Bot:我正在检查你的余额。过几秒再问

并在后台获取耗时API并将数据保存在MongoDB等高速数据库中(相对于慢速Web服务API),并在上下文菜单或数据库中标记一个标志。

当用户几秒钟后再次询问时,检查标志是否为肯定从高速数据库中获取数据并提供给用户

提示:如果您使用的是谷歌助手,您可以在从 API 获取数据完成时发送推送通知

更新:

回复评论:“您能解释一下“在函数内部而不是全局范围内初始化库”是什么意思吗?”

例如,在 firebase 函数的情况下,它实际上是在容器化环境中执行的,当您暂时不调用该函数时,它只会从内存中释放函数的容器,当您再次调用它时,它会初始化在实际执行之前再次调用容器,该初始化称为冷启动,因此第一次调用需要更多时间,随后调用需要更少,即使第一次调用的执行时间相同但函数无法执行直到容器初始化完成,容器的初始化包括所有的库和数据库连接初始化等。这一切都很好,您无法摆脱微/纳米服务架构中的冷启动,但有时它会花费越来越多的时间并导致用户感到沮丧和糟糕的体验,并且像 dialogflow first call 这样的服务每次都会失败,这不好,这里还有更多:像firebase这样的服务实际上为每个函数创建了单独的容器,例如,如果你有多个函数,firebase实际上将每个函数部署在一个单独的容器中,所以调用每个函数只初始化那个函数的容器而不是所有其他函数容器,这里是真正的问题来了,您调用一个函数并在全局范围内初始化所有内容,无论您的函数是否使用它,大多数开发人员都会犯错误,他们在全局范围内初始化数据库,这意味着每个函数都必须在冷启动时对其进行初始化,但不是全部你的函数实际上使用数据库连接,所以我们需要在每个函数的主体中分别初始化数据库,然后不在函数之外,事实上我做的是我做一个可重用的函数来检查数据库是否尚未连接,否则什么都不做,这个检查是为了避免在每个函数调用中初始化数据库,这可能会导致执行时间增加。

稍后我会尝试添加 firebase 函数代码示例。

更新 2:

这里是代码示例

传统方式:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as _cors from 'cors';
import firestore from './../db'
import * as mongoose from "mongoose";

const defaultApp = admin.initializeApp(functions.config().firebase)


const dbURI = `mongodb://xxxxxx:xxxxxx@ds00000.mlab.com:123456/mydb`;
// const dbURI = `mongodb://localhost:27017/mydb`;


mongoose.connect(dbURI, 
        useNewUrlParser: true, useUnifiedTopology: true
    ).catch(e => 
        console.log("mongo connection failed for reason: ", e);
    )



var cors = _cors( origin: true );// set these options appropriately According to your case,
// see document: https://www.npmjs.com/package/cors#configuration-options
// true means allow everything


// http example
export const addMessage = functions.https.onRequest((req, res) => 
    const original = req.query.text;
    admin.database().ref('/messages').push( original: original ).then(snapshot => 
        res.redirect(303, snapshot.ref);
    );
);


export const signup = functions.https.onRequest(async (req, res) => 

    ... signup stuff using mongodb

    res.send("user signed up");
)

//databse trigger example
export const makeUppercase = functions.database.ref('/messages/pushId/original')
    .onWrite(event => 
        const original = event.data.val();
        console.log('Uppercasing', event.params.pushId, original);
        const uppercase = original.toUpperCase();
        return event.data.ref.parent.child('uppercase').set(uppercase);
    );

//cors example
export const ping = functions.https.onRequest(async (req, res) => 
    cors(req, res, () => 
        res.send("this is a function");
    )
)

在上面的代码中,你可能会注意到 4 个函数

    HTTP 触发器 addMessage 用于在 Firebase DB 中添加消息 HTTP 注册功能,使用 MongoDB 数据库触发器将条目设为大写,不使用任何数据库 HTTP 触发 ping 功能,不使用任何数据库

您可能还会注意到两个数据库初始化,firebase 和 MongoDB

假设你在一段时间后第一次调用一个函数并且该函数是冷的,所以它会初始化这两个数据库,不仅是一次而且分别对所有四个函数进行初始化,假设每个数据库初始化需要 400 毫秒,所以这两个需要 800 英里,所以当你调用第一个函数来添加一条消息时,它会初始化两个 db(800ms) 然后它会实际执行函数(比如说 150ms) 所以 800ms+150ms 所以大约需要 950ms第一次,不管它没有使用 mongodb,它都会初始化它,因为初始化是写在全局范围内的

如果您在 addMessage 函数之后调用注册函数,它将为 db init 执行相同的 800 毫秒,然后注册函数执行假设它需要 200 毫秒,因此总共 800+200=1000 毫秒,您可能会认为 db 已经初始化所以为什么再说一次,正如我在最初的回答中已经提到的那样,每个函数可能存在于单独的容器中(并非总是如此,但确实如此)这意味着注册函数可能不知道 addMessage 函数中发生了什么,因此它将为其容器初始化数据库所以第一次通话会比后续通话花费更多时间

函数 3 是一个 db 触发器,它没有使用数据库,但是当它被调用时,它会接收到数据库的句柄并使用该句柄在数据库中进行更改,但在这种情况下,当函数是冷的并且你创建了一个在 db 中的条目它实际上像任何其他函数一样初始化函数,这意味着第一次仍然存在 800ms 开销,这就是大多数人讨厌 db 触发器但他们不知道为什么会发生的原因(此时我想提一下他们的设计中除了冷启动之外几乎没有什么东西,而且 github 上也有问题,但相信我优化冷启动会解决你的问题 50%)

函数 4 只不过是一个 ping 函数,但它也会初始化数据库,800 毫秒的开销

现在看看以下经过一些优化的代码:

您可能会注意到,我没有在全局范围内直接初始化数据库,而是在全局范围内注册了一个名为 initMongodb 的子例程函数,其中包含数据库初始化逻辑,因此当您调用 firebase 函数时,它不会在冷启动期间初始化数据库,而只会注册此子例程函数在全局范围内,因此您可以通过任何 firebase 函数访问它,

现在,如果您观察第二个函数,即注册,您可能已经注意到我使数据库初始化进一步有条件,因为如果函数没有接收到正确的数据来执行注册,那么初始化数据库有什么意义,此时我会想提一下,如果数据库初始化完成一次,那么在后续调用中它实际上不会再次初始化数据库,实际上当firebase函数执行完成时,它会破坏该firebase函数范围内的所有变量,但它会保留全局变量(直到下一个冷启动),您可能会注意到我需要 mongodb 作为变量名称 mongoose 和 firebase 作为变量名称 admin 在全局范围内,初始化会对这些变量进行一些更改,这就是为什么初始化逻辑是有条件的,如果 db未初始化则初始化,否则什么也不做。

这里要注意的另一点是“不要”尝试将所有内容保留在 firebase 函数本地范围内(例如导入 mongoose 以及初始化 mongoose 和其他 DB),这将使开销永久化,并将导入和每次调用都从头开始初始化数据库,因为所有局部变量在执行完成后都会被销毁,所以它比冷启动本身更危险

最后,如果您观察函数 3 和 4,将不会进行数据库初始化,但这并不意味着在冷启动和后续调用中会花费相同的时间,在导入等过程中仍然会发生一些事情它将库文件从磁盘加载到内存,但与 db 初始化(向互联网上的其他计算机发出 https/socket 请求)相比,这并不需要那么长的时间,导入都发生在同一台计算机上,仍然更好以避免生产中不必要的导入。

冷启动优化方式(推荐)

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as _cors from 'cors';
import firestore from './../db'
import * as mongoose from "mongoose";

const dbURI = `mongodb://xxxxxx:xxxxxx@ds00000.mlab.com:123456/mydb`;
// const dbURI = `mongodb://localhost:27017/mydb`;


export functions initFirebase()
    if (admin.apps.length === 0) 
        console.log("initializing firebase database");
        admin.initializeApp(functions.config().firebase)
    else
        console.log("firebase is already initialized");
    


export function initMongoDb() 

    if (mongoose.connection.readyState !== mongoose.STATES.connected
        && mongoose.connection.readyState !== mongoose.STATES.connecting) 

        console.log("initializing mongoose");

        mongoose.connect(dbURI, 
            useNewUrlParser: true, useUnifiedTopology: true
        ).catch(e => 
            console.log("mongo connection failed for reason: ", e);
        )
     else 
        console.log("mongoose already connected: ", mongoose.STATES[mongoose.connection.readyState]);
    




var cors = _cors( origin: true );// set these options appropriately According to your case,
// see document: https://www.npmjs.com/package/cors#configuration-options
// true means allow everything


// http example
export const addMessage = functions.https.onRequest((req, res) => 
    initFirebase()

    const original = req.query.text;
    admin.database().ref('/messages').push( original: original ).then(snapshot => 
        res.redirect(303, snapshot.ref);
    );
);



export const signup = functions.https.onRequest(async (req, res) => 
    

    if(req.body.name && req.body.email && req.body.password)
        initMongoDb();

        ... signup stuff using mongodb

        res.send("user signed up");
    else
        res.status(400).send("parameter missing");
    
)

//database trigger example
export const makeUppercase = functions.database.ref('/messages/pushId/original')
    .onWrite(event =>         
        const original = event.data.val();
        console.log('Uppercasing', event.params.pushId, original);
        const uppercase = original.toUpperCase();
        return event.data.ref.parent.child('uppercase').set(uppercase);
    );

//cors example
export const function3 = functions.https.onRequest(async (req, res) => 
    cors(req, res, () => 
        res.send("this is a function");
    )
)


Update: a ping call to function on start of mobile app or on page load in web also works well



Inzamam Malik, 
Web & Chatbot developer. 
malikasinger@gmail.com

【讨论】:

你能解释一下“在函数内部而不是全局范围内初始化库”是什么意思吗? 真的好用,期待看到代码示例:)【参考方案2】:

我刚刚检查了Actions on Google documentation 和Fulfillment documentation 页面,确实有5 秒的超时限制。

这可能不是最好的解决方案,也可能不适合您的情况,但考虑到给定的严格 5 秒窗口(我们希望确保动态对话,而不会让用户等待太久)

您以异步方式开始计算,然后返回给用户并告诉他们在几秒钟内请求结果,同时计算完成。它将保存在用户的私人空间中,此时用户将触发第二个意图,该意图将请求同时已预先计算的结果,因此您可以获取并返回它们。

【讨论】:

是的,我也有同样的想法。这可能不是一个好方法,因为用户必须两次发出相同的请求,但我想现在,这可能是最好的选择。谢谢! 文档中哪里有“5 秒限制”?我在提供的链接中没有找到它 似乎也找不到 你是对的,5 秒的超时限制不再存在于共享的链接中。文档在不断发展和改进。 5 秒超时。为了实现,我已经检查了这个添加了 6 秒的超时。 :(。我们不能有一个等待消息吗..?【参考方案3】:

您可以通过设置多个跟进事件将 5 秒的 Intent 限制延长至 15 秒。 目前只能依次设置3个后续事件(可以将超时时间延长至15秒)。

以下是如何在履行中心执行此操作的示例:

  function function1(agent)
      //This function handles your intent fulfillment
      //you can initialize your db query here.
      //When data is found, store it in a separate table for quick search
      
      //get current date
      var currentTime = new Date().getTime(); 
      
      while (currentTime + 4500 >= new Date().getTime()) 
        /*waits for 4.5 seconds
          You can check every second if data is available in the database
          if not, call the next follow up event and do the 
          same while loop in the next follow-up event 
          (up to 3 follow up events)
        */
        
         /* 
         if(date.found)
            agent.add('your data here');//Returns response to user
         
          */
        
       
      
      //add a follow-up event
      agent.setFollowupEvent('customEvent1'); 
      
      //add a default response (in case there's a problem with the follow-up event)
      agent.add("This is function1");
  


  let intentMap = new Map();
  intentMap.set('Your intent name here', function1);;
  agent.handleRequest(intentMap);

要了解有关自定义事件的更多信息,请访问此页面:https://dialogflow.com/docs/events/custom-events

【讨论】:

这些 customEvent1 是否必须存在于 dialogflow 中,或者设置 agent.setFollowupEvent 是否足够? @Egghead 这些自定义事件必须存在于您正在使用的意图的事件字段中 是否还需要在一段时间(4,5 秒)后添加此事件,因为如果您直接放置(没有延迟)它会不起作用吗? @Egghead,没有必要添加 4.5 秒的延迟。它会毫不拖延地工作。这篇文章的原因是帮助人们延长 Dialogflow 的 5 秒限制。 @IgorNefedov 由于 JS 的阻塞性质,这并没有帮助。如果响应可用,我将每 500 毫秒调用一次 API 并检查一次。但是我的回调没有调用,因为主线程中的while循环阻塞了执行!请指教。

以上是关于如何管理 Dialogflow / Api.ai 中的 5 秒响应超时限制?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 node.js MySQL 从 MySql DB 获取结果并将它们发送回 API.ai - DialogFlow

使用 DialogFlow 进行 webhook 调试的 ngrok 不起作用

Rails 和 Api.ai 的“找不到 404 帖子”

Android Studio 3.0 Unresolved reference: SupportedLanguages for AIConfiguration class in Dialogflow(

getaddrinfo ENOTFOUND API 谷歌云

可以100%以编程方式创建,更新和删除(管理)Dialogflow代理吗?