用一个Flask的小例子来聊聊架构和工具选择

很久没写了,本来打算就“谢邀”,写点爬虫的内容,但似乎这块比较敏感,就不单独开篇写了,以后写其他内容或群里遇到讨论,就瞎聊两句算了。而这篇,也是源自群友一个偶然的“谢邀”(调百度接口,指定用Flask做web服务器做个展示),于是趁周日有空,瞎写一篇,顺便聊聊“架构”、“工具选择”,以及学习方向和学习成本的问题。

 

首先,直接上作业题,给代码,然后再聊废话。

作业题:调用一个免费外部接口,NLP或图像方面的都行,然后用Flask做一个服务器,语言当然是用python,把接口返回的东西展示出来。

需求给的很明确,那咱就挑个简单的,聊天机器人呗,百度、黄小鸭、图灵都有接口,当然是选“大厂”百度了。如果没有百度账号,就注册一个,进百度AI平台:https://ai.baidu.com/,登录好账号之后,在控制台界面,寻找并点击这个红框里面的东西:智能对话定制与服务平台UNIT。

创建应用,拿到API Key和Secret Key:

图片

点击上面图片中的“UNIT配置平台”,并点击下个页面中的“进入UNIT”

图片

然后新建一个机器人,记住ID号:

图片

点击刚刚新建的机器人,在技能管理里面,添加技能,在列表中选取免费的(大部分免费),然后绑定给机器人

图片

之前已经绑定了2个技能了,现在新增一个“电影”技能

图片

选择技能,添加给机器人,实现绑定

图片

技能列表中已经出现新技能,电影,这些技能的ID号要记住,调用接口的时候要用。

图片

准备工作做完了,现在进入写代码环节,一个Flask主程序作为后端,一个前端(使用Flask官方定义的目录结构,放置静态页面和JS脚本,以及CSS样式)。

数据流程是这样的:

1、用JS的ajax技术,发请求(本例请求中主要是对话内容)给Flask后端;

2、再由Flask后端调用百度的API;

3、Flask后端收到百度API的返回数据后,返回给前端的ajax;

4、再由ajax渲染到静态页面上。

这是一个数据流程。N轮及多用户对话,就是重复这个流程。

整个项目的文件结构如下:

图片

jquery-3.5.1.min.js需要自己下载,网上很多资源,下载好了直接用就行,本例用它是因为要用ajax提交请求,否则用form提交的话,稍微有点low,实在拿不出手……,放在static/JS目录下

sytle.css放在static/CSS目录下,文件内容如下:

@charset "utf-8";
body { margin:0; padding:0; width:100%;}
html { padding:0; margin:0;}

.shadow-box {width: 100%;height: 100%;position: fixed;top: 0;left: 0;background: rgba(0,0,0,0.3);z-index: 9999;}
.shadow {position: absolute;top: 50%;left: 50%;margin: -380px 0 0 -400px;}
.shadow-bg {width: 800px;height: 800px;position: absolute;background: #ffffff;border-radius: 5px;padding: 5px 5px 5px 5px;}
.shadow-bg .echart_bar{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;}
.shadow-bg .echart_block_1{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;float: left;width: 286px;height:440px;}
.shadow-bg .echart_block_1 ul { text-align:right; padding:0; margin:0; list-style:none; border:0;}
.shadow-bg .echart_block_1 ul li { float:left; margin:0; padding:0 5px; border:0; height:30px;font-size:12px;}
.shadow-bg .echart_block_2{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;float: left;width: 784px;height:300px;}
.shadow-bg .title{float: left;width: 800px;height: 30px;line-height: 30px;position: relative;}

.shadow-assis {position: absolute;top: 50%;left: 50%;margin: -180px 0 0 -200px;}
.shadow-bg-assis {width: 400px;height: 360px;position: absolute;background: #ffffff;border-radius: 5px;padding: 5px 5px 5px 5px;}
.shadow-bg-assis .title{float: left;width: 400px;height: 30px;line-height: 30px;position: relative;}
/* .assis-box {position: relative;width: 300px;height: 310px;margin: 10px 0 0 10px;border: 1px solid cornflowerblue;padding-top: 20px;} */
/* .shadow-bg-assis .assis-box {position: relative;width: 300px;height: 310px;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;} */
.shadow-bg-assis .assis-box01 {width: 380px;height: 250px;overflow: auto;margin: 10px;border: 1px solid #CECECE;padding-top: 5px;}
.shadow-bg-assis .assis-txt {width: 330px;height: 18px;margin-left: 10px;}

如果不怎么讲究的话,完全可以不用CSS,都是样式,没啥好说的

index.html文件,放置在templates目录下,内容如下:

<html>
    <head>
        <title>proj_1_flask</title>
        <link rel="stylesheet" type="text/css" href="/CSS/style.css"/>
        <script src="/JS/jquery-3.5.1.min.js"></script> 
    </head>
    <body>
        <!-- 百度UNIT调用 -->
        <div class="shadow-box" id="assistantArea" style="display: block;">
            <div class="shadow-assis">
                <div class="shadow-bg-assis" id="containerArea">
                    <div class="title-assis">
                        <span class="t_txt" id="assistantArea_title">百度UNIT调用</span>
                    </div>
                    
                    <div id="box01" class="assis-box01"></div>
                    <input type="text" id="txt" class="assis-txt" placeholder="ctrl+Enter发送" />
                    <input type="button" id="btn" value="发送" />

                </div>
            </div>
        </div>
        <script type="text/javascript">
            var oBox = document.getElementById("box01");
            var oTxt = document.getElementById("txt");
            var oBtn = document.getElementById("btn");
        
            function msg() {
                var val = oTxt.value;
                
                // oBox.innerHTML += '<p>' + val + '</p>'
                // oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
                
                oTxt.value=""; //发送完成侯情况输入框
                $.ajax({
                    url: "/Baidu_Unit_Chat",
                    type: "POST", 
                    dataType: "json",
                    async: true,
                    data: {"sender_name": 'your_name', "send_text": val},
                    beforeSend: function(XMLHttpRequest){
                        // Handle the beforeSend event
                        var p = document.createElement("p");
                        p.setAttribute("style", "color:#0080C1;");  
                        p.innerHTML = 'From me:' + val
                        oBox.appendChild(p); 
                        oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
                        console.time('global');
                    },
                    success: function(res){
                        console.log(res);
                        var str = res.text;
                        var p = document.createElement("p");
                        // p.setAttribute("style", "color:#0080C1;");  
                        p.innerHTML = 'From AI:' + str
                        oBox.appendChild(p);
                        oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
                        console.timeEnd('global');
                    }
                });
            }
            oBtn.onclick = msg;
            document.onkeydown = function(e) {
                var e = e || window.event;
                if (e.ctrlKey && e.keyCode == 13 ||e.keyCode ==13) {
                    msg();
                }
            }
</script>
    </body>
</html>

 这段里面,ajax的部分稍微讲讲,对ajax不熟的人可以了解一下

图片

红框和线条画出来的地方注意一下:

冒号前面的,是ajax内置的属性,不能改,照写上去。

url是待会提交给Flask处理的地址

type是post,这是百度api要求的

async: true,代表是“异步”提交请求。效果是你说的话,提交后马上显示在页面上,百度api返回的话,等它返回之后再显示在页面上。

如果是同步,效果就是,你要等百度返回值之后,把你说的话和百度返回的话,一起显示出来。

data就是你提交的一个字典结构,可以定义你想定义的所有内容进去,这里只定义2个作为示例,而且主要是send_text有用,send_name提交后并未做处理,就直接返回了,如果要做用户管理和鉴权,需要用到这个,可以自己扩充一下。

beforeSend和async: true配合,就是上面说的那个效果。

success是返回后发生的事,参数res就是Flask处理后,返回的内容。

最后,就是Flask了,这里的文件是Flask_Main.py,代码如下:

# coding:utf-8

import requests
import json
from flask import Flask, render_template, request
app = Flask(__name__, static_url_path='')

class UNIT:
    def __init__(self, api_key, api_secret):
        self.access_token = None
        self.url = None
        self.set_access_token(api_key, api_secret)

    def set_access_token(self, api_key, api_secret):
        host = 'https://aip.baidubce.com/oauth/2.0/token?' \
               'grant_type=client_credentials&' \
               'client_id={0}&' \
               'client_secret={1}'.format(api_key, api_secret)
        response = requests.post(host)
        if response:
            self.access_token = response.json()['access_token']

    def query(self, query_text):
        self.url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + self.access_token
        '''
        提交字段说明:详情见百度AI开发中心UNIT文档中的【请求参数详细说明】
                        "log_id": "UNITTEST_10000",  # 随便写
                        "version": "2.0",  # 必须写2.0
                        "service_id": "Sxxxx",  # 填你自己百度AI平台申请UNIT的机器人id
                        "skill_ids": ["1076657", "1076658"],  # 申请机器人的技能,把技能id写这里,这两个技能是尬聊和问候,需要其他技能自己去百度拿,大部分免费
                        "session_id": "",  # ssession保存机器人的历史会话信息,由机器人创建,客户端从上轮应答中取出并直接传递,不需要了解其内容。如果为空,则表示清空session
                        "request": {
                            "query": "%s",  # 你的输入
                            "user_id": "88888"  # 与技能对话的用户id
                        },
                        "dialog_state": {
                            "contexts": {"SYS_REMEMBERED_SKILLS": ["1076657"]}  # dialog_state.contexts["SYS_REMEMBERED_SKILLS"]中设定一个技能ID列表,避免处于列表中的技能session被清空,就可以基于历史会话信息与机器人进行多轮对话,而无需接触复杂的原始session。
                        }
        '''
        post_data = """{
                        "log_id": "UNITTEST_10000",
                        "version": "2.0",
                        "service_id": "Sxxxx",
                        "skill_ids": ["1076657", "1076658"],
                        "session_id": "",
                        "request": {
                            "query": "%s",
                            "user_id": "88888"
                        },
                        "dialog_state": {
                            "contexts": {"SYS_REMEMBERED_SKILLS": ["1076657"]}
                        }
                }""" % query_text
        post_data = post_data.encode('utf-8')
        headers = {'content-type': 'application/x-www-form-urlencoded'}
        response = requests.post(self.url, data=post_data, headers=headers)
        print(response.json())
        if response:
            return response.json()['result']['response_list'][0]['action_list'][0]['say']

unit = UNIT('your_api_key', 'your_api_secret')

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/Baidu_Unit_Chat', methods=['post'])
def ai_talk():
    rst_dict = {}
    send_text = request.form.get('send_text')
    sender_name = request.form.get('sender_name')
    out_str = unit.query(send_text)  # 调用api的返回值
    if out_str is None:
        rst_dict['status'] = 'failure'
    else:
        rst_dict['status'] = 'success'
    rst_dict['from_whom'] = sender_name
    rst_dict['text'] = out_str
    ai_say = json.dumps(rst_dict)
    return ai_say


if __name__ == '__main__':
    app.run(debug=True)

 

注释都写得很清楚了,需要找百度拿的就是下面几个值

类UNIT内的函数:

your_api_key和your_api_secret,文章开头有怎么拿的方法

函数set_access_token在UNIT类实例化的时候执行,根据上面两个值,生成token,这个token下面要用,这个token值用函数获取,是实时变化的。

函数query的参数query_text,就是前端ajax送过来的对话信息

类UNIT在Flask运行之初即实例化加载,然后提供持久服务。

类UNIT外的函数:

index函数不用说了

ai_talk函数,处理前端提交到/Baidu_Unit_Chat的请求,本例中,是和ajax配合工作。该函数中send_text = request.form.get('send_text')获得ajax送过来的对话信息,交给unit.query(send_text)处理,并赋值给out_str,返回的字典经过json打包后,发送给前端ajax,并由ajax解析后渲染到页面上。

至此,完成一个数据流程。(对应看文章前半部分关于流程的描述,观察程序实现的过程)。

 

最后效果放一个,用图片形式偷偷说下“爬虫”,悟一下……

图片

因为加了电影技能,试了下,然后……百度api的效果怎样,就不讨论了……

Flask上的提示

图片

 

至此,正文就完了,下面开始讲废话:

简单说说这个nlp的制作原理,我也做过内核部分,流程类似,算是番外篇吧,有兴趣就了解一下:

1、设置一些列场景,就是百度定义的“技能”,在每个场景中,找打量相关语料,进行训练,每个场景一个对话模型

2、对输入的文本,先进行场景判断,场景的数量是有限的,用文本分类做模型,判断输入文本属于哪个场景,就调用哪个场景的对话模型

3、多轮对话方面,设置session保存,同一个用户,保存前几轮的对话内容,以便判断聊天主题需要转换,也可以提供同一用户在停止聊天后,保持上一轮的语境,以便下次聊天时迅速切入,或判断是否需要转换话题

4、以上是用模型进行的选择和对话,我称之为“软切换”

5、设计“词槽”,做关键字匹配,我称之为“硬切换”,这个可以迅速提升效果,缺点是,太low……

6、还有其他很多办法,各自开脑洞

……

差不多就这样

1、Flask是不是一个好web服务器?

是个好web服务器,但由于和python绑定太深,缺点也很多,比如如果一个项目是由多人合作完成,项目组内有人用java,有人用jsp,有人用C/C++,Flask就不是一个好的选择了,而且速度方面,一直是个问题。当然,有人觉得,对于学习来说,无所谓,可以用就行。嗯,学习阶段是可以用,但熟练以后,我相信自然就会放弃Flask,至少是主框架不会再用Flask。

2、前面我重点介绍的其实是数据流这个问题,无论是对这种小demo,还是非常大的项目,都要习惯用数据流来思考架构问题,把复杂的问题,拆解成简单问题,然后每个问题,用最适合的工具去处理,最后就形成了所谓的架构

3、在选用工具方面,要尽量的选择通用性较强的工具,学习新工具,也要学习通用性强的工具,长期坚持下去,自己的工具箱才会强大,才会在处理问题的时候,信手拈来,搭建靠谱的架构。因为,无论是通用性强还是弱,对于工程问题来说,学习成本差别不会太大。

废话也讲完了,打完收工。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值