前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端手写一个人工智能回复小助手

前端手写一个人工智能回复小助手

作者头像
@超人
发布2021-07-29 12:08:11
4790
发布2021-07-29 12:08:11
举报
文章被收录于专栏:Vue中文社区Vue中文社区

前言

说到人工智障,相信大家应该都记得之前的那个仅有几个replace的智能回复吧。

image.png

嗯,非常的智能。

20160417854814_tPmgKw.gif

我们都知道中文博大精深,一句话可以有非常多种的解释。

举个例子:

他赞成我不赞成? 他赞成,我不赞成。 他赞成我不?赞成。 他赞成我?不赞成。

是吧,所以如果我们要实现一个非常聪明的智能是很难的。

不过若干智能助手也是有名的蠢了。

所以,我实现一个蠢但没完全蠢的智能回复应该问题不大。

github项目地址:github.com/lionet1224/…

思路

一开始我是想通过词义来解析一句话。

区分词的类型,如:名词、动词、形容词...等等,然后通过权重将这些词关联起来,最后总结出一个最匹配的回答。

不过实现起来感觉很复杂就放弃了。

image.png

后面就想着,我可以简化这个过程啊,不去区分词的类型,直接就是在所有定义好的句子中取到最匹配的那条。

句子是定义好的回答模板

例如:

  1. 我发送: 我喜欢点赞
  2. 那么我喜欢点赞可以解析为一个数组['我', '喜欢', '点赞']
  3. 然后在一个保存所有句子的数组中取得最匹配的那条句子
  4. 最后调用这条句子的回答方法:我也是并且我已经点了

那么基于这个思路我们就可以开发了。

准备工作

我们需要用到nodejieba这个node库,所以就需要启动一个node服务。

NodeJieba是"结巴"中文分词的 Node.js 版本实现, 由CppJieba提供底层分词算法实现, 是兼具高性能和易用性两者的 Node.js 中文分词组件。- github.com/yanyiwu/nod…

先安装一下koa及其相关的库吧。

npm install koa2 koa-router koa-static nodejieba nodemon --save

然后在根目录中创建一个文件server.js来编写对应的服务代码。

实现的功能不复杂,就是将编写一个api可以将前端输入的句子分解成数组然后返回给前端,并且创建一个静态文件服务器来展示页面。

代码语言:javascript
复制
const nodejieba = require('nodejieba')
const Koa = require('koa2')
const Router = require('koa-router')
const static = require('koa-static')
const app = new Koa();
const router = new Router();

// 一个get请求
router.get('/word', ctx => {
  // cut就是nodejieba来分解句子的方法
  // ctx.query.word 获取链接中"?word=x"的x
  ctx.body = nodejieba.cut(ctx.query.word);
})

// 创建静态文件服务器
app.use(static('.'))
// 应用路由
app.use(router.routes())

// 监听端口启动服务
app.listen(3005, () => {
  console.log('start server *:3005')
})
复制代码

那么我们在package.json中写入对应的启动命令。

代码语言:javascript
复制
{
  // ...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon server.js"
  },
  // ..
}
复制代码

启动成功后,我们可以在终端中看到启动成功的显示。

image.png

编写前端代码

首先我们需要一个界面来展示及操作我们的对话。

这里不管你是仿QQ也好还是仿微信、钉钉什么的,都行,实现了就好。

我就不贴代码了,有兴趣的可以去github中看看。

image.png

实现对应的发送信息的逻辑。

代码语言:javascript
复制
// 人工智障一代
function AImsg1(str){
  return str.replace(/[??]/g, '!')
            .replace(/[吗嘛]/g, '')
            .replace(/[你我]/g, (val) => {
              if(val === '我') return '你'
              else return '我'
            });
}

// 判断一下使用哪一代智能
// 默认使用二代
function getAImsg(str, type = 2){
  return new Promise(resolve => {
    if(type === 1){
      resolve(AImsg1(str))
    } else {
      AImsg2(str).then(res => {
        resolve(res)
      })
    }
  })
}

// 输入框
let inputMsg = document.querySelector('#input-msg')
// 发送按钮
let emitBtn = document.querySelector('#emit')
// 显示信息的容器
let wrapper = document.querySelector('.content');

// 用户发送信息,并且让AI也发送
function emitMsg(){
  let str = inputMsg.value;
  insertMsg(str, true);
  // 发送后清空输入框
  inputMsg.value = ''

  // 延迟一秒回复
  setTimeout(() => {
    getAImsg(str).then(res => {
      insertMsg(res);
    });
  }, 1000);
}

// 插入到页面中,flag来判断是用户发送的还是电脑发送的
function insertMsg(str, flag){
  let msg = document.createElement('div');
  msg.className = 'msg ' + (flag ? 'right' : '')
  msg.innerHTML = `<div>${str}</div>`;
  wrapper.appendChild(msg);

  wrapper.scrollTop = 100000;
}

emitBtn.onclick = () => {
  emitMsg();
};
// 回车键也可以发送
inputMsg.addEventListener('keyup', ev => {
  if(ev.keyCode === 13) emitMsg();
})
复制代码

通过上面的代码,我们就实现了发送信息的功能。

定义句子

可以将句子理解为一个模板,如果用户发送的话匹配了一条句子,那么人工智能就使用这条句子回复。

接下来,我们来实现句子的定义。

代码语言:javascript
复制
// 定义句子的类
// 句子
class Sentence{
  constructor(keys, answer){
    // 关键词
    this.keys = keys || [];
    // 回答的方法
    this.answer = answer;
    // 存储可变数据
    this.typeVariable = {};
  }
}
复制代码

可以看出,我在其中定义了keys/answer/typeVeriable三个变量。

  • keys就是用来匹配的关键词
  • answer这是一个方法,当匹配了这条句子之后就调用这个方法返回对应的话
  • typeVariable这个是为了让回答不那么死板,可以将一些可变的词提取出来然后在answer进行判断,最后返回合适的回答。

简单的实现

我们先不考虑可变通的回答,先实现一个最简单的问答。

我说: 天气是蓝色的 智能回复: 嗯嗯,天空是蓝色滴

代码语言:javascript
复制
// 我们可以先实现一下AImsg2这个方法,以方便调用
function  AImsg2(str){
  return new Promise(resolve => {
    // 获取前面开发的那个api的数据,将用户输入的文字当做参数。
    axios.get(`http://localhost:3005/word?word=${str}`).then(res => {
      console.log(res.data)
      // 去匹配适合的句子
      let sentences = matchSentence(res.data);

      // 如果没有匹配的回复
      if(sentences.length <= 0){
        resolve('emm,你在说什么呀,没看懂呢')
      } else {
      // 如果有匹配的就去获取回答
        resolve(sentences[0].sentence.get())
      }
    })
  })
}
复制代码

来实现一下matchSentence这个方法。

代码语言:javascript
复制
// 匹配最适合的句子
// 低于30%的匹配当做不匹配
function matchSentence(arr){
  let result = [];
  sentences.map(item => {
    // 用句子类自身的match方法判断是否匹配,返回匹配成功的关键词数量
    let matchNum = item.match(arr);
    // 如果匹配数量低于总关键词数量的1/3就当做没看到
    if(matchNum >= item.keys.length / 3) result.push({
      sentence: item,
      // 这里是匹配的关键词与总关键词数量的比例,为了方便排序最合适的那条
      searchNum: matchNum / item.keys.length
    })
  })
  
  result = result.sort((a, b) => b.searchNum - a.searchNum);
  return result;
}
复制代码

Sentence中实现matchget方法。

代码语言:javascript
复制
class Sentence{
  // ...
  
  // 获取用户发送的语句与定义的句子的匹配程度
  match(arr){
    // 每次匹配都重置数据
    this.typeVariable = {};
    // 将其解构放入一个新数组(浅拷贝功能)
    // 为了数据不会影响去匹配下一个句子
    let userArr = [...arr];
    let matchNum = this.keys.reduce((val, item) => {
      // 关键词是否匹配
      let flag = userArr.find((v, i) => {
        return v === val;
      })
      return val += flag ? 1 : 0;
    }, 0)

    return matchNum;
  }
  
  // 调用回答方法并且将变量传入
  get(){
      return this.answer(this.typeVariable)
  }
}
复制代码

最后添加句子的实例存入数组。

代码语言:javascript
复制
let sentences = [
  new Sentence(['天', '天空', '是', '蓝色', '颜色'], type => {
    let str = '嗯嗯,天空是蓝色滴';

    return str;
  }),
]
复制代码

不出意外的话,我们输入天空是蓝色的就将会得到回复:嗯嗯,天空是蓝色滴

image.png

为什么关键词是['天', '天空', '是', '蓝色', '颜色']这样的?

因为我们可能的问法有天是什么颜色/天空的颜色是蓝色的,所以我们可以将更多的关键词加入,以方便匹配。

变通的回答

如果仅仅是上面的写法,我们就只能非常死板的回答,所以,我们可以给一些关键词定义为可变的数据,最后取到这些数据来灵活的回答。

例如:爸爸的爸爸叫什么?

我们先来定义一个类,来保存一个种类的关键词。

代码语言:javascript
复制
// 种类,为一个系列的文字,如颜色的赤橙黄绿青蓝紫、时间的今天明天后天
class Type{
  // key是关键词
  // arr是这个种类下的词
  // exclude 排除关键词
  constructor(key, arr, exclude){
    this.key = key;
    this.arr = arr;
    this.exclude = exclude || [];
  }

  // 判断是否匹配
  match(str){
    return this.arr.find(item => {
      return str.indexOf(item) > -1;
    }) && this.exclude.indexOf(str) <= -1
  }
}
复制代码

然后创建对应语句的实例:

代码语言:javascript
复制
let sentences = [
  // 使用%x%的语法来表示可变数据
  new Sentence(['%family%', '的', '%family%', '叫', '是', '什么'], type => {
    let data = {
      '爸爸': {
        '爸爸': '爷爷',
        '妈妈': '奶奶'
      },
      '妈妈': {
        '爸爸': '姥爷',
        '妈妈': '姥姥'
      },
    }

    // 判断是否拥有这个叫法
    let result = data[type.family[0]] && data[type.family[0]][type.family[1]]

    // 最后返回
    if(result){
      return `${type.family[0]}的${type.family[1]}叫${result}喔`
    } else {
      return '咳咳,我不知道诶'
    }
  }),
]

let types = {
  // 创建family这个种类
  family: new Type('family', ['爸爸', '妈妈', '哥哥', '姐姐', '妹妹', '弟弟', '外公', '外婆', '婆婆', '爷爷'])
}
复制代码

当然定义好了之后还需要在Sentence中编写获取可变数据的方法。

代码语言:javascript
复制
class Sentence{
  // ...

  // 获取用户发送的语句与定义的句子的匹配程度
  match(arr){
    this.typeVariable = {};
    let userArr = [...arr];
    let matchNum = this.keys.reduce((val, item) => {
      let flag = userArr.find((v, i) => {
        // 使用正则匹配%x%的写法,并且获取x的数据
        let isType = /^%(.*)%$/.exec(item);
        
        if(isType){
          // 判断关键词是否在这个种类中
          let matchType = types[isType[1]].match(v)

          if(matchType){
            // 存入typeVariable中
            if(!this.typeVariable[isType[1]]) this.typeVariable[isType[1]] = [];
            this.typeVariable[isType[1]].push(v);
            // 匹配过后,这个存入的数据应该删除,不然后面匹配的时候会将第一个数据重复输入
            userArr.splice(i, 1)
          }

          return matchType;
        } else {
          return item === v;
        }
      })
      return val += flag ? 1 : 0;
    }, 0)


    return matchNum;
  }

  // ...
}
复制代码

到这里变通回答的功能就实现了,我们来看看效果。

image.png

更多的功能

可以增加的功能还是挺多的,如:

  • 回复可以增加一些随机性,我可以回复你:我有点喜欢你,也可以说:我超级喜欢你。
  • 可变的数据获取可以增加更多的选择,不只是匹配Type,还可以指定某个关键词后面接着的关键词,如:我喜欢点赞,那么我就取到点赞这个关键词。
  • 在调用answer时调用其他的API,以实现更好的功能,如:获取天气、获取地点、获取土味情话。
  • 区分不同性格的回答,内向的人、外向的人、热情的人、冷淡的人拥有不一样的语气。

还有更多的想法就不一一列举了。

最后

感谢大家的阅读,此文仅为抛砖引玉,代码质量不佳请勿见怪。

(诚挚的眼神)

关于本文

作者:我系小西几呀

https://juejin.cn/post/6963781192428552205

代码语言:javascript
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Vue中文社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 思路
  • 准备工作
  • 编写前端代码
  • 定义句子
  • 简单的实现
  • 变通的回答
  • 更多的功能
  • 最后
  • 作者:我系小西几呀
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档