# 《ChatGPT 微服务应用体系构建》,关于面试中的技能、简历、问题汇总

作者:小傅哥
博客:https://bugstack.cn (opens new window)
课程:https://t.zsxq.com/0d7K7hJ0i (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

此部分主要用于向读者提供星球项目之一的 ChatGPT 微服务项目如何体现到简历中,包括;专业技能、项目经验。

# 一、专业技能

  • 【前端】熟练使用 Next.js 构建的 React、Typescript 语言,构建的前端工程。
  • 【前端】熟练使用 React Route 路由子页面的开发技术,以及相应的信息传递。
  • 【前端】熟练掌握,跨域接口的对接使用,以及本地 json 数据加载。
  • 【前端】熟练使用本地浏览器内存,存储 Token、配置、对话等信息。
  • 【前端】熟练掌握前端页面数据的存放、使用以及和后端接口的交互方式。
  • 【后端】熟练构建 DDD 工程架构,分层模块,职责体系。并掌握 DDD 架构的开发模式以及微服务设计思想。
  • 【后端】熟练掌握 Spring、SpringBoot、MyBatis 等开发框架技术,并对其使用源码所提供的接口、类、SPI标准开发各类组件,有一定的设计思路和落地能力。—— 因为这里包含了 SDK 的设计、开发和使用。
  • 【后端】熟练使用多种设计模式、设计原则,对各类场景的方案设计和落地能力,深度提高自身编码思维和开发技术能力。—— 同时包括非常重要的会话模型OpenAi-SDK的架构设计。
  • 【后端】熟练对接微信公众号 SDK,属性验签流程和对话流程。以及完成 JWT Token 的生成和校验。
  • 【后端】熟练掌握 Nginx Auth 验证模块的开发和使用,以用于对接口的校验和拦截。
  • 【后端】熟练使用流式异步响应式框架开发应答接口,完成前端动态展示应答数据。
  • 【后端】熟悉微信支付渠道对接,掌握商品、交易、下单、发货、掉单补偿等核心流程开发。(这是一个真实支付场景,在DDD架构下实战)
  • 【后端】熟练使用 okhttp3、retrofit2 框架,对接 ChatGPT 完成通用 SDK 的开发。有了这项技能,以后你可以方便的对接任何一个 HTTP 请求服务。
  • 【后端】熟练掌握异常、枚举、错误码的定义和使用,并学习如何合理打印服务日志,便于问题排查。
  • 【运维】熟练使用 Docker 在本地和服务端的配置和部署应用,以及在本地构建前后端镜像。
  • 【运维】熟练掌握 Git、GitCode,对工程代码的管理,推送、拉取、切换分支、合并代码等操作。
  • 【运维】熟练申请和使用 SSL 配置 Nginx 域名 HTTPS 服务。

# 二、简历模板

  • 项目名称:OpenAI 应用服务 - 辅助工作提效工具开发
  • 项目架构:微服务架构设计,OpenAI-SDK 多模型组件【ChatGLM、ChatGPT】、DDD 应用服务API封装、WEB REACT 前端界面【按需编写】
  • 核心技术:SpringBoot、MyBatis、Redis/Guava、OKHttp3、OpenAI 大模型【可对接ChatGLM、腾讯混元等】、React、TypeScript
  • 项目描述:此项目以应用OpenAI技术,对接多种大模型提供生成式服务,为XXX场景提效。项目的架构设计实现以微服务进行拆分,涵盖;OpenAI-SDK、OpenAI-API、公众号鉴权、企业支付【暂时申请中,如果你对接了可以写】等。并以模块化设计,积木式构建应用,让不同的场景诉求都可以配置化对接。
  • 核心职责:
    • 首先这是我们xxx公司/实践/实训的第一个OpenAI项目,后续势必会有更多的场景以不同的方式接入。包括 SDK 独立接入、API 接口接入、MQ 消息接入,以及使用提供好的服务类接入。所以在这部分设计的时候,我采用了微服务的架构设计,按照职责边界进行拆分设计。
    • 采用 DDD 架构 API,以及便于不同领域模块的独立设计,一个领域就是一个功能域。在功能域中提供模型、仓储、事件、服务。这样可以更好扩展。
    • 鉴于生成式服务的文本生成可能会有不可靠信息,所以对这部分内容添加了敏感词的过滤。并可根据不同场景选择不同范围的敏感词处理。
    • OpenAI 大模型有多种,这部分在架构上设计独立的 SDK,在实现上采用了 Session 会话模型进行处理,以及通过工厂处理服务。在细节对上,采用了 OKHttp3 框架完成模型对接,这样的方式更好扩展,代码也更易于维护。
    • 在整套工程的设计实现中,采用了较多的分治、抽象的思想和设计模式和设计原则知识的运用,来解决各类场景问题。
    • 对接微信扫码支付,完成从商品库、下单支付、异步发货、掉单补偿等核心流程实现。让用户可以在线购买对话额度。
    • 注意:你还可以根据项目中提到的各类技术和章节,来编写你的职责。

# 三、面试问题

谢二机同学你好,你做的这个项目我非常感兴趣,我们公司也有这样的OpenAI 业务对接,正好可以结合你做的这个《OpenAI大模型项目》 (opens new window)聊一下。

# 1. 你的这个项目的背景和需求来自哪里?

校招身份举例

面试官您好,此项目的最核心背景和诉求,是我希望找到一个可以真实锻炼技术应用的场景。并且可以基于此项目的设计、开发、上线、运维等一系列动作,提高编程思维和落地能力。而此项目的只是一个载体,在项目中我所运用到的微信对接、登录鉴权、异步接口、下单支付、异步发货、账户管理等场景可以运用到其他任何一个项目中使用。所以我选择开发 这样一个项目。

并且此项目运用了DDD分层架构,领域驱动设计实现,对于各个场景都遵守了设计原则和设计模式,解决各类复杂场景的实现。如;生成式服务流程中运用模板模式、策略模式、工厂模式,解决对话过程中所需的规则过滤、模型校验、账户状态、账户扣减等开发流程。【项目提问过程中,有时候会让绘制下项目的分层结构、核心流程,之后会基于你画的这些进行提问】

通过以上项目的学习我锻炼到了相关项目所用到的核心技术使用,架构设计和落地实现。而此项目的学习,也为我以后在工作中解决实际场问题,打下了牢固的基础。

# 2. 项目为什么使用DDD架构,有什么好处?

首先是 DDD 的结构分层更加清晰(DDD 是一种软件设计方法,软件设计方法涵盖了范式、模型、框架、方法论),与 MVC 相比避免了 PO、VO 对象被乱用的可能。而这也是在 DDD 中将行为和逻辑封装到一个领域内进行处理,也就是常说的充血模型结构。这样的结构方式,就可以更好的做到业务流程松耦合,功能实现高内聚。

那么在这个 OpenAI 项目中,按照业务流程涵盖了鉴权登录OpenAI下单微信,4个大的核心场景。这4个核心场景恰好是4个领域,每个领域可以独立开发设计,再通过上层进行编排使用。如果是小项目直接由 trigger 触发器层的进行编排,如果是较大型复杂项目可以增加 case 编排层,再由 trigger 层调用。【层之间是防腐,防腐避免了对象和服务被污染】

相对比与MVC结构,这样的架构设计,不会因为引入更多的功能,系统的复杂度也随之提高。因为所有的流程都被拆解了,每个功能都聚合到自己的领域内,这样的复杂度不会增加,并且也更好维护。

# 3. 充血模型和贫血模型分别合适什么场景?

在 DDD 架构中,充血模型,主要的价值在于解决具有生命周期的流程。如生成式对话、商品下单支付、用户登录授权等流程。因为这些流程中具有较复杂的场景模型和唯一ID,所以采用充血模型结构,会很适合的将状态和行为封装到同一的领域包中进行聚合开发实现。这样的实现方式,也为以后扩展、迭代、维护做了良好的基建,避免工程过于腐化。—— 注意关于DDD的知识,可以把这里的5个视频都刷下。 https://bugstack.cn/md/road-map/mvc.html(opens new window) (opens new window)

而贫血模型则比较适合 Querys 场景,因为这些场景只是数据的汇总查询,没有唯一ID和生命周期。所以比较适合在工程中提供 query 模块,使用贫血模型开发。

# 4. 项目开发中你是怎么提交代码的?

首先我是在自己的Gitcode仓库中创建了一个空的项目地址,之后复制项目地址 https://xxxx.git。在本地创建 OpenAI SpringBoot 工程,以及工程中 app、domain、infrastructure、trigger、types 5个 modules 模块。在之后在 idea 点开 Git 菜单创建本地仓库,再把仓库中需要提交的内容右键 Add 添加。最后 Ctrl + Shift + K 推送代码,推送时配置复制的 git 地址。

这个过程中我会注意不要把一些本地文件如 .idea编译文件 等提交上去,这些文件可以添加到 .gitignore 文件中忽略提交。后续的开发中,会按照时间-姓名-功能的规范拉取开发分支,编写代码和提交。开发验证完毕后合并 master 分支进行部署。【如果多人开发,可以把开发分支合并到 test 分支进行部署测试】

# 5. 这个项目有部署上线吗?怎么部署的,部署后内存占用如何,请描述下。

项目上线了,并且对项目部署了 Prometheus + Grafana 监控组件,以及配置了前端百度统计 PV、UV。部署的方式是先购买了一台2c4g 5M带宽的云服务器以及申请备案了域名,之后在前后端应用配置 Dockerfile 打包镜像推送到 DockerHub,之后编写 docker compose 含环境(MySQL、Redis等)脚本,在云服务器执行部署的。因为本项目的登录采用了公众号登录,所以在部署后把服务端接口地址,配置到公众号控制台进行验签和接收验证码登录。

整个项目部署完成后,占用了53%~61%的服务器内存,Tomcat 配置了最大连接数是250,OpenAI 调用接口熔断为6秒【OpenAi有时候会有超时】,同时在前端也配置了50秒主动断开。压测过接口的响应,按照目前的服务器可以压测到 50 ~ 80 TPS【随着不同的文本量提问,会有波动。OpenAI 响应有时候会需要7-10秒完成】,日常测试接口响应平均值在3~10秒。—— 这些数据来自于压测后 Grafana 监控的展示【星球内提供了 Grafana 监控配置面板】。

这里为什么后端有超时熔断,前端也加了50秒超时主动断开。因为 OpenAI 接口的响应是 ResponseBodyEmitter 异步应答,当返回一个数据块以后,代表已经有响应,则不会计算服务端的超时。但第一次应答后,后面的数据如果卡主了,仍可能一直无应答。所以前端加上主动超时断开会更稳妥。【一般情况下,1~5秒,服务端开始应答】

# 6. 你的项目对接了哪些 OpenAI?怎么对接的?

在项目的开发阶段对接了 ChatGPT 和国内智谱AI ChatGLM,两款AI产品,都有文生文、文生图、多模态的图文理解等功能。对接的方式是采用会话模型(MyBatis Session)流程,使用 retrofit2 + OkHttp3 框架,封装 OpenAI HTTP 接口。提供统一的出入参,并使用工厂模式,构建 OpenAI 启动阶段所需的验证、日志、执行流程。并最终对外提供 OpenAI 会话服务。

星球大模型项目的 OpenAI SDK 提供了两种,一种是独立的 ChatGPT-SDK-Java、ChatGLM-SDK-Java,以及一个综合的 OpenAI-SDK-Java 对接了国内外90%的大模型。这里你可以按照自己的学习积累程度来描述。此外这些 SDK 也已经发布到了 Maven 中央仓库,外部人员也可以使用【有不少伙伴参与了 SDK 开发,你也可以讲自己参与了一起开发或者是你独立内部开发的】。

# 7. 因为你的项目是前后端分离的,接口跨域怎么做的?

首先我们知道,Web跨域(Cross-Origin Resource Sharing,CORS)是一种安全机制,用于限制一个域下的文档或脚本如何与另一个源的资源进行交互。这是一个由浏览器强制执行的安全特性,旨在防止恶意网站读取或修改另一个网站的数据,这种攻击通常被称为“跨站点脚本”(Cross-Site Scripting,XSS)。

所以在我的前后端分离项目中,通过配置 @CrossOrigin 注解来解决跨越问题。开发阶段 Access-Control-Allow-Origin: *、上线阶段 Access-Control-Allow-Origin: https://gaga.plus

# 8. 请描述下商品下单支付场景。以及怎么保证的补偿。

下单支付场景在整个项目中是一块非常核心的流程,首先是系统设计采用了 DDD 架构,对商品下单按照业务流程拆解出来了单独的领域模块。在设计实现上,以购物车为下单入参的实体对象,出参为支付单实体。保存的是订单的聚合信息。

因为本身下单是一个较为复杂的流程,所以在编码实现上采用了模板模式,定义出下单的标准过程。商品下单的流程需要先查询是否存在已下单未支付已下单但无支付单的订单,对这样两种订单分别进行处理。已下单未支付,在有效期内未关单的直接返回给用户直接支付。已下单但无支付单则调用(微信/支付宝沙箱/蓝兔)创建支付订单,保存库表记录后返回给前端用户进行支付。此外的流程则直接执行购物车商品ID查询,组装聚合订单数据,创建支付单,保存库表记录和返回给用户。在用户支付完成后,接收支付回调消息,推送(MQ/Redis发布订阅/Guava事件)信息,由系统中 trigger 模块下的监听处理接收支付成功消息,完成商品的发货处理。

那么这里可能发生的调单情况,接收回调消息处理失败。则由定时任务扫描库表订单超过15分钟未支付的订单,查询支付平台(微信/支付宝沙箱/蓝兔)是否已支付,如果支付则发送事件消息走后续的补货流程。另外如果超过15分钟以上未支付则进行本地关单,不对支付平台关单(支付平台有自己的时间),这样可以让用户的体验更加舒服。用户超时后支付,仍可以走后续的发货流程。但如果用户刷新界面,则获取创建新的支付单。

# 9. 你的订单表也是一个频繁使用的表,那么库表有哪些核心字段,索引有哪些?

有用户ID(用的公众号创建的 openid)、商品ID、商品名称、商品金额、订单ID、下单时间、订单状态、支付单类型(微信、支付宝)、支付单号、支付时间、交易单号、交易状态等。表中对订单ID、用户唯一创建了唯一索引,对订单状态和订单时间创建组合索引(提高扫描效率)。

# 10. 我看到OpenAI的体验,都是渐进式展示的,这块后端使用了什么接口形式?

这部分是使用 SpringBoot 应用提供的 HTTP 接口,返回的是 ResponseBodyEmitter 异步请求处理协议。它是 SpringMVC 所提供的一个异步响应提,当你控制器中返回一个 ResponseBodyEmitter 实例时,Spring MVC 会开启一个异步请求处理,这样就可以在单个请求中发送多个 OpenAI 应答数据块。这样的应答方式是非常适合 OpenAI 这样需要大批量应答数据的场景。此外因为服务端的接口需要 Nginx 转发,所以在 Nginx 端还需要关闭分块解码(chunked_transfer_encoding)、关闭转发缓冲(proxy_buffering)、关闭应答缓存(proxy_cache),这样最终才能是一个渐进式的展示效果。

# 11. 你的项目中对接了 ChatGPT 和 ChatGLM 两个模型,那么使用了什么设计模式?

关于 ChatGPT、ChatGLM 两个 OpenAI 服务,在项目中定义了一个通信渠道策略接口,接口方法返回统一的格式数据。两款 OpenAI 服务分别实现自己接口处理。之后两个实现的策略模式注入到 Map 中,Key 是一个枚举值。当前端选择不同的模型进行问答时,则根据模型的枚举值从 Map 中选择出对应的策略处理类。这样即使后续拓展其他的 OpenAI 服务也非常容易扩展。

# 12. 你在项目中有支付购买的次数,那么对应就会有额度扣减、账户的校验、还有模型的使用类型,这些是怎么实现的?

其实除了账户、额度、模型,还有敏感词过滤,在这部分实现中我定义了统一的 ILogicFilter 过滤接口,分别实现不同的过滤诉求。在通过工厂的封装,装配上不同的过滤规则类。用户进行问答后,会对用户的信息分别进行校验。这部分也就是整体 OpenAI 应答中使用的模板、工厂、策略三个设计模式。另外像是这样的调用验证方式,也可以使用责任链的方式处理。

之后这里在说下敏感词,敏感词对接了通用的敏感词库,但校验的过于严格同时不是动态的,所以可能不准。后来又对接了云服务厂商的敏感词过滤服务+图片审核服务 (opens new window)。可以配置广告、舆情、敏感类等都可以配置过滤。

# 13. 你是怎么处理异常的?

项目中在对接 OpenAI 接口、数据库、缓存、下单调用等,是有可能出现超时、数据接口更新、数据库连接、缓存数据问题等异常,这些异常属于功能异常。是在每个领域模型内可能发生的问题情况。为了让异常保持统一,具备业务语意,所以在 types 层了定义业务异常类 ChatGPTException 和对应的异常码枚举【0000 成功、0001 失败、0002 非法参数、0003 未登录、OE001 商品以下架等异常码】这样就可以标准化返回给前端,针对不同的异常进行信息展示。

# 14. 对于返回给前端的接口,返回从出参结果怎么定义的?

定义一个 Response<T> 添加 code、info、data(枚举类型)参数,统一封装返回结果。这是一个通用设计,应该不止我开发的系统,在我去F12看各个网站的时候,也都是这样统一标准的接口出参。

# 15. 公众号里可以对接 OpenAI 自动回复吗?

技术上可以对接的,在公众号配置完接口地址验签成功后,就可以接收用户在公众号发送的消息了,之后根据消息内容请求 OpenAI 同步响应接口(也可以是异步接口用 Future 封装)。但这里要注意一点,我是个人公众号开发,不能直接根据用户ID给用户返回信息,只能随着请求一次返回。

那么就有可能用户提问后,我调用 OpenAI 接口,因为会需要较长时间返回数据,超过公众号5秒限制,就会得不到数据。所以这块利用公众号的回调机制,5秒+3次,我再后端使用了 CountDownLatch 进行等待,每次都耗时5秒,让公众号第3次在从我的后端获取数据返回。这样可以最大限度保证,一次提问就能获得数据。如果仍然没有获得数据,则提示给用户需要再次提问同样的问题。这个时候我会以问题作为key,获取OpenAI的结果缓存回答给用户。

# 16. 应用程序使用了什么数据库连接池,连接数配置如何?

配置了 SpringBoot 默认提供的 hikari 连接池。在这之前我也有压测过 c3p0、dbcp、druid、hikari (opens new window) 这四款连接池,其中 hikari 是效率最高也是最稳定的,dbcp 相对较差,但如果不使用连接池那么会更差。

在我的应用中,配置最小空闲连接数(minimum-idle)是15个,最大连接数(maximum-pool-size)是25个。本身下单接口的平均响应时间是48毫秒,那么 1秒 / 0.048秒/事务 = 约20.83事务/秒/连接 也就是 20.83事务/秒/连接 * 25连接 = 约520.75事务/秒 这个量级是非常够用的。如果不够可以适当调整连接数大小。

# 17. 是否支持分布式部署?

是的,一个项目是否支持分布式部署的标识在于它的数据处理是基于单体的还是基于分布式架构的,当一台机器宕机用户在访问时候轮训到下一台机器是否还可以保证业务进行。像是这套项目使用数据库存储用户、账户、商品、下单,在 Redis 存放用户的登录鉴权,那么就可以基于 Nginx 轮训的方式配置多态应用的负载,以此支持分布式架构。

# 18. 登录这个业务过程是什么样?

企业公众号登录的模型是通过 AccessToken 创建二维码凭证 ticket,并让前端根据凭证创建带参登录二维码。当用户扫码后,对接公众号的服务端会收到回调信息,里面含带了 ticket、openid,那么这个时候就可以创建出 jwt token,前端页面不断通过 ticket 轮训接口获取绑定登录信息即可。

另外是个人公众号,它是没有这个权限的。所以只能是让用户在公众号中回复一个固定编号(405),之后服务端接受到固定编号后创建一个唯一登录验证码分配给 openid,用户在通过在页面填写验证码的方式进行登录。填写验证码后,调用服务端接口进行鉴权下发 jwt token 即可。

# 19. 如果现在需要让你扩展一个功能,比如接入图片转代码/根据流程图生成代码你会怎么做?

这种问题是比较偏开放性的,如果做完项目又结合项目扩展了新的功能,那么回答这类问题会更加得心应手。因为如果你扩展了新的功能,那么你就会知道怎么先去了解需要对接功能的文档【技术调研】,之后结合文档来做对应的案例,让案例覆盖80%你要实现的功能。这样你大概知道了这样东西是可以使用的。

接下来梳理代码,设计接入方式和流程,并设计这部分的领域功能,开发完成功能以后开始进行测试验证。这部分是单元测试。测试通过后,开始提供对应的服务的接口,如果是旧的功能扩展,则要考虑兼容性,比如增加了新的模型枚举。可参考案例:OpenAI + TLDraw 设计图转前端代码 (opens new window)

# 20.(开放问题)你在做项目中,什么问题难住你的时间最长,为什么?

这是一个开放问题,重点考察你对项目的开发中个人的积累。你可以针对自己的学习过程中,有哪个流程的实现,让你最为有感触,即可回答。

这还是比较多的,这个项目有很多的创新设计让代码更好的维护,也有一些取巧的架构方案。比如提到的个人公众号,没有带参二维码就只能通过让用户主动回复生产验证码绑定openid解决,用户在输入验证码进行登录。

另外是一个下单的场景,这块的DDD领域设计,到底要用什么作为入参,什么做为出参,进行下单。经过了大量的思考,应该模拟生活中的超时,推着购物车到收银台,创建支付单,扫码登录。所以在系统设计上,以购物车为入参的实体对象,出参为支付单信息【可以扫码支付】,中间的流程用聚合对象保存订单信息。这块的设计,非常清晰和好维护。所以也是一个记忆非常深刻的点。

# 四、公司面试

# 1. 面试被问openai项目有被问到为什么要使用okhttp3和retrofit,我回答的是用他们来封装对http的调用,面试官说是为什么要选择okhttp3而不是其他的框架?这块我没有接触过其他框架所以回答的不是很好,想问下小傅哥这个问题应该怎么回答

  1. 面试官要问的是,对于 http 的封装调用,你都了解哪些框架。如;Apache HttpClient、OkHttp、Retrofit、Spring RestTemplate、Spring WebClient、Feign,甚至 hutool 也提供了封装框架。
  2. 之后你要说明,这些框架的优劣势对比,并重点介绍 OkHttp、Retrofit
    • 2.1 OkHttp - OkHttp是一个现代的HTTP客户端,它提供了简洁的API和高效的性能。它支持同步阻塞调用和异步调用,并且能够处理SPDY、HTTP/2和WebSocket等协议。
    • 2.2 Retrofit - Retrofit是一个类型安全的HTTP客户端,它通过注解将HTTP API转换为Java接口。它通常与OkHttp一起使用,提供了一个高层次的抽象来处理网络请求。
  3. 对于2中使用的框架,还可以提到目前在微信支付sdk、支付宝sdk,都有使用现代简洁高效的 okttp 框架,对于一些标准度更高的调用则使用 Retrofit,因为他可以简单配置即可使用,屏蔽了对接细节。这与以往的 httpclient 对接方式相比,代码更加简洁,维护成本更低。