【项目准备】大模型对话机器人

1. 如何展示大模型返回的数据?换行符,空格是怎么处理的?

使用pre标签+white-space:pre-warp
pre标签默认保留换行符和空格,但是不会根据容器的宽度进行换行。
所以需要设置white-space:pre-warp根据容器宽度进行换行。
在这里插入图片描述

white-space有哪些取值?

了解这 5 种即可,核心就在于,是否保留换行符,是否合并多个空白符,是否会随着元素宽度而换行:
(1) normal: 换行符和多个空格,都会展示成1个空格,会因为超出宽度而换行
(2) nowrap: 换行符和多个空格符,会展示一个空格,超出容器宽度不换行
(3) pre: 保留换行符和空格,但是不会因为超出宽度而换行
(4) pre-wrap: 保留空格和换行,会因为超出宽度而换行
(5) pre-line: 空格会被合并,保留换行符,会因为超出宽度而换行

实际上对于普通元素来讲,white-space 设置为 pre-wrap,也可以实现类似 pre 标签的效果。在这里也并非一定要使用 pre 标签才行。

那么使用了 white-space 后,还有遇到换行问题吗?

实际上,虽然 white-space: pre-wrap 能够达到我们想要的效果,但是在遇到长的英文单词的时候,还是可能会出现问题(单词超过一行,会超出宽度),这就涉及到另一个属性 word-break. 它是用来控制单词换行的。有几个取值:(可以对照这里,演示查看:https://developer.mozilla.org/zh-CN/docs/Web/CSS/word-break)
(1) normal: 默认值,当最后一个英文单词在本行放不下的时候,会自动换到下一行。如果一行也放不下,那就会超出。
(2) break-all: 直接拆分换行。只要遇到末尾,就会拆开单词换行。一般不常用
(3) keep-all: 除了英文单词,没有被空格分割的汉语,日语等,都会在本行放不下的时候,自动换到下一行。如果下一行放不下,就会超出。几乎不会用到。
(4) break-word: 对于英文单词,本行结尾放不下,且下一行也放不下的时候,会进行拆分。否则不拆分。是一般我们会选择的属性。

关于换行,还会涉及到一个 word-wrap,但是已经属于早期的一些兼容属性,它的作用和 word-break: break-word 一样。知道即可。

为什么要用 pre,不用 code 标签呢?

code 不会保留换行和空格,都会折叠,一般只是用来展示一行代码。而且 code 本身是内联元素,和这里的需求不太一致。
而 pre 保留换行和空格,对服务端返回的数据处理更加友好。且是块级元素,在此处使用起来方便快捷,成本更低。
在这里插入图片描述

那么,除了 pre, 还有更好的方案吗?还有优化的空间吗?

服务端返回的是 markdown 格式的文本,可以使用 markdown.js 对内容处理后进行渲染。可以更好的展示文本中的代码块。
同时可以增加 watch (vue) 来减少 markdown 的不必要的转换。

2. 登录注册的实现方案是 JWT,描述一下过程

在这里插入图片描述

在这里插入图片描述
Koa 实现了基于洋葱模型的中间件,在处理请求时,总是从外层中间件开始,到最内层,最后回到最外层。基于这个结论,我们可以使用一个数组来顺序存储中间件,并依次调用。

中间件,什么是中间件,为什么有中间件,具体跨域和鉴权中间件怎么实现的

(1)中间件是什么?
异步函数,可以执行一些操作,用来处理http请求。有两个参数ctx和next,ctx用来获取请求信息、发送响应内容,next用来将控制权传递给下一个中间件函数。
(2)为什么要有中间件?
处理 Web 请求时,服务端常常需要进行验证请求来源、检查登录状态、确定是否有足够权限、打印日志等操作,而这些重复的操作如果写在具体的路由处理函数中,明显会导致代码冗余,这个时候,我们就可以将这些通用的流程抽象为中间件函数,减少重复代码。
(3)具体跨域和鉴权中间件怎么实现的
跨域中间件:对Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 进行设置,默认情况下,它允许所有域、所有方法和所有请求头的跨域访问(设置为*),但可以通过传递不同的 options 参数进行定制。
鉴权中间件:首先通过请求头的authorization获取token,对获取到的 token 进行验证,验证时使用一个 secretKey(密钥),这个密钥在应用程序中应该是保密的。如果验证成功,token中的用户信息将被解码并存储在 ctx.state.user 中,供后续的中间件或路由处理器使用。验证失败返回401状态码。
主要通过检查 JWT 令牌来实现用户的鉴权,并根据令牌的有效性决定是否允许请求继续处理。
在这里插入图片描述
Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数 ctx 和 next,参数 ctx 是由 koa 传入的封装了 request 和 response 的变量,可以通过它访问 request 和 response,next 就是进入下一个要执行的中间件 。

koa2的中间件可以有好几层,在每一次请求与响应的时候,都可以在中间拦截,
做登录态管理、状态码管理、错误处理…总之每个中间件都可以做一次拦截来搞事情。

中间件功能是可以访问请求对象(request),响应对象(response)和应用程序的请求-响应周期中通过 next 对下一个中间件函数的调用。通俗来讲,利用这个特性在 next 之前对 request 进行处理,在 next 函数之后对 response 处理。Koa 的中间件模型可以非常方便的实现后置处理逻辑。

那么 jwt 的原理是什么呢?

使用层面,我们就使用 json web token 库来完成 token 的生成和验证:

jwt.sign({ userName: username, userId:  user.userId}, secretKey, { expiresIn: '240h' })
jwt.verify(token, secretKey);

除了指定过期时间,还可以指定加密算法。

除此之外,secretKey 是一串字符,可以称之为 密钥,只有服务端知道。其中,加密算法可以采用对称加密(密钥相同,加密解密均是用同一个密钥),或者非对称加密(两个 secretKey 不同,只能用来加密或者解密)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6Im1heGlhb2JvIiwidXNlcklkIjoiNDAxYTQ5NWUtNGZiZC0xMWVmLTk0OGUtNmRkNmViMTdhZjI4IiwiaWF0IjoxNzIyNTkyNTMxLCJleHAiOjE3MjM0NTY1MzF9.6-oy186e7Bcqry0xaF165sdGiTiPnsYc9iMliW9QLWE

base64 解密后:(Base64,就是包括小写字母a-z、大写字母A-Z、数字0-9、符号"+“、”/"一共64个字符的字符集,(任何符号都可以转换成这个字符集中的字符,这个转换过程就叫做base64编码。 Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。采用Base64编码具有不可读性,需要解码后才能阅读。)

{"alg":"HS256","typ":"JWT"}{"userName":"maxiaobo","userId":"401a495e-4fbd-11ef-948e-6dd6eb17af28","iat":1722592531,"exp":1723456531}2Ξ_x0017_*-1h]zF8_x001C_#%oP-a

header, payload, signature,三部分组成
header 就是加密的算法和 token 类型,jwt 统一写为 JWT
payload 就是用户的自定义信息,同时还有一些常用字段,iat 签发时间,exp 过期时间,nbf 生效时间。
这两部分通过 base64 就可以解密。所以不应该放隐私信息,比如用户密码。
最后一部分是签名,是加密最关键的部分。
这个签名,是通过密钥加密算法 对 headerpayload 信息加密后生成。此时服务端拿到 token 后,只需要对 token 进行解密然后对比 headerpayload,即可验证此 token 无误。由于攻击者无法知道密钥,所以可以防止 token 伪造的情况出现

根据用户选择的算法,自定义数据,过期时间,生成 header, payload,然后通过用户选择的算法,对 header.payload 进行加密后,组成新的 header.payload.sign,之后经过 base64 加密。得到 token
由于攻击者不知道加密密钥,所以自己伪造的 token,也只能伪造 header.payload,但是由于签名信息无法伪造,也就无法通过服务器校验。从而保证了 token 的不可伪造性。
通过将用户的 id 信息等存储在 token 中,每次客户端发起请求都携带 token。服务器便知道访问者的 userId,从而实现用户身份的校验。

那么 token 如果泄漏,能够通过校验吗?

能,但是如果用户是正常使用 https 访问,那么信息是加密的,不会被人截取,攻击者是不会有机会获取到的。如果能被截取到,那么用户本身就暴漏在很大的风险中(可能信任了不该信任的证书), 这种情况下,cookie-session 本身也会被获取到。这不是 jwt 算法的问题。

那么这个 token,我只能存在 localStorage 中吗?不能存在 cookie 中吗?

能,可以存在 cookie 中,只是 cookie 存在 csrf 的风险和跨域不能使用的问题。token 可以避免这个问题。

那么用户登出怎么做呢?登出了,此时 token 还有效吗?

一般登出清除客户端的 token 即可。
但是此时 token 没有到有效期,那么确实可以被继续使用。
我们这个程序里确实没有处理这一点,如果想要更加完善,那么可以:
(1) 登出的时候,向服务端发起请求,服务端对 token 进行作废记录
(2) 当遇到 token 时,去数据库中比对,如果在作废表中,那么就不生效。返回 401
(3) 否则,走正常流程即可。

为什么使用 jwt,还有什么其他方案吗?

比较常见的就是 cookie-session 方案。其与 jwt 方案的区别在于:
(1) 客户端处理不同。cookie 是浏览器默认同域携带的,客户端无需干涉。但是与此同时,有 csrf 风险,需要额外处理。而 jwt 的 token 一般放在 localStorage 中,需要每次请求都携带过去,但是好处也是直接杜绝了 csrf 攻击。
(跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。)
(2) 服务端存储不同。session 所对应的信息,需要在服务端进行存储,可能是内存中,数据库中,或者文件中。这些在多机器部署时,信息难以共享,可能需要 redis 进行 session 信息的共享。另一方面,对服务端也有内存压力。而 jwt 信息本身存放在 token 中,服务端无需存储。从而在多机器部署时,不存在共享问题。也没有存储压力。
在这里插入图片描述

中间件用的挺熟呀,能讲讲原理吗?看过 koa 的源码吗?

中间件思想,在服务端开发和前端开发中,都用的非常频繁。
它的本质是一个洋葱模型。请求会经过一个一个的中间件,并在执行完成之后又一个一个执行剩余的代码。
在这里插入图片描述
放弃 for 循环,我们一个一个执行。
通过递归,可以实现想要的洋葱模型。
注意,这里将 next 的控制权,交给了当前的中间件,所以如果我们在中间件中,不调用 next,那么就可以中断后续中间件的执行。

class Koa {
  constructor() {
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  async exec() {
    const context = {};
    const dispatch = async (i) => {
      if (i === this.middlewares.length) {
        return;
      }
      const middleware = this.middlewares[i];
      const next = async () => {
        await dispatch(i + 1);
      };
      await middleware(context, next);
    };
    await dispatch(0);
    return context;
  }
}

const app = new Koa();

app.use(async (ctx, next) => {
  console.log("-->1");
  await next();
  ctx.mid1 = "executed";
  console.log("<--1");
});
app.use(async (ctx, next) => {
  console.log("-->2");
  await next();
  ctx.mid2 = "executed";
  console.log("<--2");
});
app.use(async (ctx, next) => {
  console.log("real logic");
  ctx.result = 1 + 2;
});
app.exec().then(console.log);

在这里插入图片描述

vue-router 用过是吧?知道原理吗?

本质就是前端路由:
有几种方式:
(1) location.hashhashchange 事件
(2) history.pushStatepopState 事件
本质都是,用户跳转,使用 history.pushState/location.hash 修改页面 path/hash,此时会触发 popState/hashchange 事件,router 监听对应事件,渲染不同的组件。即可完成前端路由。
这种路由,由于不会向服务端发起 html 请求,所以页面跳转非常丝滑。用户等待时间很短。

hash 的问题,在于链接比较丑,同时由于使用了 hash,所以页面中不能通过 hash 进行矛点定位。
pushstate 则没有这个问题。但是由于修改了链接。但浏览器刷新的时候,会项服务端发起对应 path 下的 html 请求。
如果服务端没有特殊配置,那么就会出现 404 找不到 html 文件的问题。
这种方案需要服务端做对应支持,如果当前路径下找不到,需要回退到上一 path 寻找,直到找到为止。一直找不到,才返回 404.

3. 为什么要使用流式传输?(流式传输,是什么,好处是什么,缺陷是什么,怎么解决的,有没有更好的方案)

如果返回的数据过长,用户等待时间较久,体验很差。
所以使用了 SSE (Server Send Events) 技术进行一段一段的传输。
SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。
具体流程:
在这里插入图片描述
一些细节:
(1)客户端 new EventSource,因为不能传递自定义头或者 body 信息,所以通过 :clientId 这样的动态路由 params 传递 clientId,而鉴权通过 query 进行传递 token
(2) 惰性建立连接,只有当用户发起聊天时,才会执行建立连接代码。内部通过闭包存储实例,来判断是否曾经建立过连接,如果建立过,则返回旧实例。否则建立新连接。从而达到惰性的效果,减轻服务端压力。
(3)服务端流式信息的遍历: 百度提供的返回 可以通过 for await (const chunk of resp) 来依此读取片段信息。这种方式称为 遍历异步可迭代对象。需要 resp 具备一个 [Symbol.asyncIterator] 方法。具体可不了解,这是百度自己的实例。
(4)client 为什么使用 new stream.PassThrough() ? 接下来重点讲解 node 中的流。

sse 的流,node 怎么实现呢?

百度大模型返回的是一个 异步可迭代对象,我们通过 for await of 的用法,可以拿到一段一段的数据,我们只需要将该数据返回给前端即可。
所以我们需要的是 一个转换流,但是并不需要处理数据,所以我们使用了 Passthrough 直接将数据返回给前端。
除此之外还有几种流:
(1) 可读流
(2) 可写流
(3) 可读可写流
(4) 转换流(特殊的一种,就是 passthrough,是转换流,但是数据并不会转换)
但是 node 流本身是一个复杂的概念,如果面试官深究,建议回答不了解。只是这里用到了,简单了解了一下流的种类,有 xxx, xxx 4种。大概知道,但并没有深入了解。
在这里插入图片描述这里面还有一个细节,就是下面这段代码,可以作为一个需要排查的问题介绍给面试官:
我们在 c 端监听了 onmessage 事件,服务端也发送了 data,但是就是不触发回调。
原因就是数据必须要按照格式来:
第一行,id,第二行 event,第三行 data。第一行可以省略,但是第二行不能,必须指定 event 为 message,onmessage 事件才能正确响应。也算是一个很细的坑点了。
在这里插入图片描述

除了 sse,还有什么方案吗?

为什么我们要用 sse?
http 请求一般由客户端发起,服务端响应,服务端不能主动发起。
而 sse,由客户端发起,服务端不是直接响应,而是可以片段片段的推送消息。从而实现流式的效果。

那么其实还可以使用轮询,每隔 500ms 向服务端请求,每次有新内容就返回,没有新内容或者结束就返回标志停止轮询。
除此之外,还有 websocket,由于 websocket 是双方通信,那么用户消息,也可以直接借助 websocket 发出。它的缺点在于对服务器的压力会稍大一些,可能场景复杂后,使用 websocket 会是更好的方案。

前端如何实现文字的逐字输出

大模型返回的是一段一段的,那么此时,如果不加以处理,前端文字就会一段一段的出现,看起来卡卡的,体验很差。需要优化。

核心原理:
接收外部 props.message,中间有一个字段,stream (实际项目中是通过 isEnd 作为标识,本质一样) 如果需要流式显示,那么内部定义一个 aniContent,每隔 40ms 给其加一个字符。直到长度达到字符长度一样为止。
(为什么是 40ms,自己试的,这种人眼体验最丝滑,requestAnimationFrame 有点快,逐帧输出,大概16.7毫秒)

逐字输出有出现什么问题吗

问题:逐字输出:偶现输出速度越来越快
每次 message 变化后,我们都启动了一个递归,40ms 后,增加一个字符。
当 message 变化频次加快,而上一次的递归并未结束,此时,会再次启动一个递归,也是每隔 40ms 增加一个字符。
如此一来,可能会出现 40ms 增加两个字符的情况,看起来就会速度越来越快。
这种情况,message 越长,返回的频次越高,就越容易复现。
所以我们每次执行前,都将之前的 timer 清除。如此,保证同一时间,永远只有一个递归。

小结:

这个项目,遇到哪些难题?
(1)sse 消息回调无法正确触发
(2)sse 鉴权问题,引申到 jwt, cookie-session 的讨论
(3)文字逐字展示后,速度越来越快的问题

这个项目,你都有哪些优化?
(1) 流式输出
(2) 闭包惰性建立连接
(3) 逐字丝滑动画实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值