万字保姆文:TS+gRPC的AI聚合微服务实现

461 阅读27分钟

在如今处处都充满科技的时代,人工智能(AI)已经成为各行各业的重要工具和推动力。AI技术的进步使得其应用范围不断扩大,从简单的自动化任务到复杂的自然语言处理,AI正在以惊人的速度改变我们的生活和工作方式。然而,随着AI技术的日益复杂和多样化,如何高效地集成和管理各种AI服务成为了一个重要的挑战。

本文旨在探讨如何设计和实现一个聚合AI服务,既能满足普通用户的需求,也能为开发者提供强大的API接口。通过详细的需求分析、技术架构设计和具体实现步骤,我们将展示如何构建一个可靠、灵活且易于扩展的AI服务平台。

在本文中,我们将详细介绍以下内容:

  1. 目标和需求:明确目标用户和他们的需求,确保设计的服务能够真正解决用户的问题。
  2. 技术架构:介绍聚合AI服务的技术架构,包括前端应用、API网关、Chat Service、Model Service和数据库等关键组件。
  3. 服务设计:详细说明每个服务的职责、技术栈和实现逻辑,确保服务之间的高效通信和协作。
  4. 输入/输出规范:定义API网关和gRPC服务的输入输出格式,确保数据传递和处理的一致性和可靠性。
  5. 项目实现:从头开始搭建一个基础的TypeScript和gRPC项目,包含项目结构、依赖安装、服务定义和实现、客户端代码编写等详细步骤。
  6. 高级功能:介绍如何集成OpenAI SDK、使用MongoDB存储会话和消息数据,以及实现一个功能完备的Chat Service。

通过本文的学习,读者老爷将掌握如何设计和实现一个聚合AI服务,从需求分析、系统设计到实际编码和测试的完整流程。希望本文能为您在AI领域的探索和实践提供有价值的参考和指导。

1. 目标和需求

目标用户

  • 普通用户:间接使用AI聚合服务,通过前端应用或其他服务。
  • 开发者:通过API接口将AI功能集成到他们的应用中。

用户需求

  • 普通用户
    • 需要一个可靠的聊天助手来回答问题和生成内容。
  • 开发者
    • 需要详细的API文档和示例代码。
    • 需要高可靠性的服务和技术支持。

核心功能

  • 聊天功能:用户可以通过API与AI进行自然语言对话。
  • 模型切换:用户可以在预定义范围内选择要使用的模型。
  • 输入输出:使用OpenAI的标准输入输出,如果接入其他模型则在这个基础上做加减法。

2. 技术架构

技术架构概述

聚合AI服务的技术架构主要由以下几个部分组成:

  • 前端应用:用户通过前端应用与系统交互。
  • API网关:处理所有外部请求,进行鉴权、路由等操作。
  • Chat Service:处理聊天请求,管理对话上下文。
  • Model Service:与AI模型交互,生成回复。
  • 数据库:存储会话和消息数据。

技术栈

  • TypeScript:用于编写服务端代码,提供类型安全和现代JavaScript特性。
  • gRPC:用于服务间通信,提供高性能和语言无关的RPC框架。
  • MongoDB:作为数据库,存储会话和消息数据。
  • OpenAI SDK:用于调用OpenAI的AI模型。

核心技术讲解

gRPC

gRPC是一个高性能、开源和通用的RPC框架,最初由Google开发。它使用HTTP/2作为传输协议,支持多路复用、流控制、头部压缩和双向流。通过Protocol Buffers(protobuf)进行序列化,gRPC可以在不同语言之间进行高效的通信。

使用gRPC的原因

  • 高性能:gRPC使用HTTP/2和protobuf,提供低延迟和高吞吐量。
  • 多语言支持:gRPC支持多种编程语言,方便不同语言的服务进行通信。
  • 流式传输:支持双向流式传输,适合实时通信场景。

TypeScript

TypeScript是JavaScript的超集,增加了静态类型检查和现代JavaScript特性。使用TypeScript可以提高代码的可维护性和可读性,减少运行时错误。

使用TypeScript的原因

  • 类型安全:提供静态类型检查,减少类型相关的运行时错误。
  • 现代特性:支持ES6/ES7特性,如箭头函数、异步函数等。
  • 工具支持:强大的IDE支持,如VS Code,提供代码补全、重构等功能。

OpenAI SDK

OpenAI SDK提供了与OpenAI模型交互的接口,方便集成各种AI功能,如聊天、文本生成等。本项目中使用OpenAI SDK调用AI模型,生成聊天回复。

使用OpenAI SDK的原因

  • 强大的AI能力:OpenAI提供了领先的AI模型,如GPT-3、GPT-4等,具备强大的自然语言处理能力。
  • 易于集成:SDK提供了简单易用的API接口,方便快速集成AI功能。
  • 持续更新:OpenAI不断更新和优化其模型,提供更好的性能和效果。

MongoDB

MongoDB是一个NoSQL数据库,采用文档存储模型,具有高性能、可扩展性和灵活的数据模型。本项目中使用MongoDB存储会话和消息数据。

使用MongoDB的原因

  • 灵活的数据模型:支持JSON格式的文档,方便存储和查询复杂的数据结构。
  • 高性能:支持水平扩展,提供高性能的数据存储和查询能力。
  • 易于使用:提供简单易用的查询语言和丰富的生态系统。

3. 服务设计

Model Service

职责:负责与AI模型交互,生成回复和回答。

技术栈:TypeScript、gRPC

逻辑描述

  1. 接收请求:通过gRPC接口接收来自其他服务的请求。
  2. 调用AI模型:传递请求数据给AI模型,生成回复或回答。
  3. 返回结果:将生成的结果发送回调用的服务。
  4. 错误处理:处理可能的错误,如模型不可用,返回适当的错误信息。

Chat Service

职责:处理聊天请求,调用Model Service生成回复。

技术栈:TypeScript、gRPC

逻辑描述

  1. 接收请求:通过gRPC接口接收来自API Gateway的聊天请求。
  2. 上下文管理
    • 检查用户的会话ID,获取上下文信息。
    • 如果没有会话ID,则创建新的会话。
  3. 生成回复
    • 调用Model Service,传递用户输入和上下文信息。
    • 接收Model Service返回的回复。
  4. 返回回复:将生成的回复发送回API Gateway。
  5. 错误处理:处理可能的错误,如Model Service不可用,返回适当的错误信息。

数据流向

  1. 用户通过前端应用或API发送聊天请求。
  2. 请求经过API网关,进行鉴权和路由。
  3. API网关将请求通过gRPC转发到Chat Service。
  4. Chat Service调用对话管理模块,处理上下文和多轮对话。
  5. Chat Service通过gRPC调用Model Service,选择合适的模型生成回复。
  6. Model Service通过OpenAI SDK与AI模型交互,生成回复。
  7. Model Service返回生成的回复给Chat Service。
  8. Chat Service将回复通过gRPC发送回API网关。
  9. API网关将回复返回给用户。

image.png

错误处理机制

  • API网关:进行基本的请求验证和鉴权,如果请求无效或鉴权失败,返回适当的错误信息。
  • Chat Service:处理上下文和多轮对话时,如果发生错误,如会话ID无效,返回适当的错误信息。
  • Model Service:调用AI模型时,如果模型不可用或请求失败,返回适当的错误信息。

4. 定义输入/输出规范

这一章的主要目的是定义项目中的输入和输出规范,包括API网关和gRPC服务的输入输出格式。这些规范确保了不同服务之间的数据传递和处理的一致性和可靠性。当然有些数据结构设计的确实拉,可是我又不能成人API

网关的输入输出

这里是api网关上自带的,所有业务都会收到包含这些的请求

// types.d.ts

/**
 * 用户角色类型
 */
type Role = "guest" | "user" | "admin" | "super";

/**
 * 用户认证信息
 */
interface Auth {
  uid: string; // 用户ID
  iat: number; // 签发时间(Unix时间戳)
  exp: number; // 过期时间(Unix时间戳)
  role: Role; // 用户角色
  unverified?: boolean; // 是否未验证(可选)
}

/**
 * 分页信息
 */
interface PagingInfo {
  page: number; // 当前页码
  size: number; // 每页大小
}

/**
 * 排序信息
 */
interface SortingInfo {
  [key: string]: "asc" | "desc"; // 排序字段及其顺序(升序或降序)
}

/**
 * 查询参数值类型
 */
type QueryValue =
  | string
  | number
  | undefined
  | null
  | boolean
  | Array<QueryValue>
  | Record<string, any>;

/**
 * 查询对象
 */
type QueryObject = Record<string, QueryValue | QueryValue[]>;

/**
 * 解析后的查询参数
 */
type ParsedQuery = Record<string, string | string[]>;

/**
 * 上下文信息
 */
interface Context {
  auth: Auth | false; // 用户认证信息,如果未认证则为 false
  paging: PagingInfo; // 分页信息
  headers: { [key: string]: string | string[] | undefined }; // 请求头信息
  query: QueryObject; // 查询参数
  traceId: string; // 跟踪ID
  sorting: SortingInfo; // 排序信息
  ip?: string; // 客户端IP地址(可选)
  requestTime: number; // 请求时间(Unix时间戳)
  params?: Record<string, string>; // 路由参数(可选)
}

/**
 * gRPC 请求和响应中的头信息
 */
interface GrpcHeaders {
  headers: { [key: string]: string }; // 请求头键值对
}

/**
 * gRPC 请求和响应中的数据部分
 */
interface GrpcData {
  body: object; // 数据主体(JSON 对象形式)
}

/**
 * gRPC 请求中的用户信息
 */
interface GrpcUser {
  uid: string; // 用户ID
  iat: number; // 签发时间(Unix时间戳)
  exp: number; // 过期时间(Unix时间戳)
  role: Role; // 用户角色
  unverified: boolean; // 是否未验证
}

/**
 * gRPC 请求和响应中的上下文信息
 */
interface GrpcContext {
  traceId: string; // 跟踪ID
  page: number; // 当前页码
  size: number; // 每页大小
  sorting: { [key: string]: string }; // 排序信息
  ip: string; // 客户端IP地址
  requestTime: number; // 请求时间(Unix时间戳)
  params: { [key: string]: string }; // 路由参数
}

/**
 * gRPC 请求类型定义
 */
interface GrpcRequest {
  headers: GrpcHeaders; // 头信息
  data: GrpcData; // 数据部分
  user: GrpcUser; // 用户信息
  context: GrpcContext; // 上下文信息
}

/**
 * gRPC 响应中的状态信息
 */
interface GrpcStatus {
  code: number; // 状态码
  message: string; // 状态信息
}

/**
 * gRPC 响应类型定义
 */
interface GrpcResponse {
  headers: GrpcHeaders; // 头信息
  data: GrpcData; // 数据部分
  status: GrpcStatus; // 状态信息
  context: GrpcContext; // 上下文信息
}

gRPC的输入输出

通用结果定义(common.proto)

syntax = "proto3";

package protos.common;

// 用户角色类型
enum Role {
  GUEST = 0;
  USER = 1;
  ADMIN = 2;
  SUPER = 3;
}

// 用户认证信息
message Auth {
  string uid = 1; // 用户ID
  int64 iat = 2; // 签发时间(Unix时间戳)
  int64 exp = 3; // 过期时间(Unix时间戳)
  Role role = 4; // 用户角色
  bool unverified = 5; // 是否未验证(可选)
}

// 分页信息
message PagingInfo {
  int32 page = 1; // 当前页码
  int32 size = 2; // 每页大小
}

// 排序信息
message SortingInfo {
  map<string, string> sorting = 1; // 排序字段及其顺序(升序或降序)
}

// 查询参数值类型
message QueryValue {
  oneof value {
    string string_value = 1;
    int64 int_value = 2;
    bool bool_value = 3;
    QueryObject object_value = 4;
    QueryArray array_value = 5;
  }
}

// 查询对象
message QueryObject {
  map<string, QueryValue> query = 1;
}

// 查询数组
message QueryArray {
  repeated QueryValue values = 1;
}

// 上下文信息
message Context {
  Auth auth = 1; // 用户认证信息
  PagingInfo paging = 2; // 分页信息
  QueryObject query = 3; // 查询参数
  string traceId = 4; // 跟踪ID
  SortingInfo sorting = 5; // 排序信息
  string ip = 6; // 客户端IP地址(可选)
  int64 requestTime = 7; // 请求时间(Unix时间戳)
  map<string, string> params = 8; // 路由参数(可选)
}

// 通用请求头信息
message CommonHeaders {
  map<string, string> headers = 1; // 头信息键值对
}

示例服务定义 (helloworld.proto)

syntax = "proto3";

package protos.helloworld;

import "common.proto";

// HelloWorld 请求消息
message HelloWorldRequest {
  string name = 1;
}

// HelloWorld 响应消息
message HelloWorldResponse {
  string message = 1;
}

// gRPC 请求类型定义
message GrpcRequest {
  protos.common.CommonHeaders headers = 1; // 头信息
  protos.common.Context context = 2; // 上下文信息
  oneof body {
    HelloWorldRequest helloRequest = 3;
  }
}

// gRPC 响应中的状态信息
message GrpcStatus {
  int32 code = 1; // 状态码
  string message = 2; // 状态信息
}

// gRPC 响应类型定义
message GrpcResponse {
  protos.common.CommonHeaders headers = 1; // 头信息
  GrpcStatus status = 2; // 状态信息
  oneof body {
    HelloWorldResponse helloResponse = 3;
  }
}

// gRPC 服务定义
service HelloWorldService {
  rpc SayHello(GrpcRequest) returns (GrpcResponse);
}

5. 搭建基础TS+gRPC项目

这一章的主要目的内容是如何从头开始搭建一个基础的TypeScript和gRPC项目。通过详细的步骤和代码示例,读者老爷可以学习到如何配置项目结构、定义gRPC服务、生成TypeScript代码、实现gRPC服务以及编写客户端代码。最终,读者老爷能够运行一个完整的gRPC服务和客户端进行通信。

项目结构

grpc-ts-project/
├── protos/                 # 存放 .proto 文件
│   ├── common.proto
│   └── helloworld.proto
├── src/
│   ├── application/        # 应用层,包含服务和用例
│   │   └── services/
│   │       └── greeterService.ts
│   ├── domain/             # 领域层,包含实体和仓库接口
│   │   └── entities/
│   │       └── greeter.ts
│   ├── infrastructure/     # 基础设施层,包含 gRPC 实现和日志配置
│   │   ├── grpc/
│   │   │   └── server.ts
│   │   └── logging/
│   │       └── logger.ts
│   ├── generated/          # 生成的 gRPC 代码
│   ├── client.ts           # 客户端代码
│   └── main.ts             # 应用入口
├── package.json
├── tsconfig.json
└── yarn.lock

初始化项目

mkdir grpc-ts-project
cd grpc-ts-project
yarn init -y

安装必要的依赖:

yarn add typescript ts-node @types/node --dev
yarn add @grpc/grpc-js @grpc/proto-loader google-protobuf
yarn add grpc-tools grpc_tools_node_protoc_ts --dev
yarn add pino pino-pretty

初始化 TypeScript 配置:

npx tsc --init

修改tsconfig.json

{
  "compilerOptions": {
    /* 基本选项 */
    "target": "es2020" /* 设置生成的 JavaScript 语言版本,并包含兼容的库声明。 */,
    "module": "commonjs" /* 指定生成的模块代码类型。 */,
    "rootDir": "./src" /* 指定源文件中的根文件夹。 */,
    "outDir": "./dist" /* 指定所有生成文件的输出文件夹。 */,
    "esModuleInterop": true /* 生成额外的 JavaScript 以简化对 CommonJS 模块的导入支持。这使得 'allowSyntheticDefaultImports' 可用于类型兼容性。 */,
    "forceConsistentCasingInFileNames": true /* 确保导入时文件名的大小写正确。 */,
    "strict": true /* 启用所有严格的类型检查选项。 */,
    "skipLibCheck": true /* 跳过对所有 .d.ts 文件的类型检查。 */
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/generated/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

定义 gRPC 服务

创建 protos 目录,并在其中创建 common.protohelloworld.proto 文件。为了不影响阅读体验,这里就不放重复的proto文件了。内容直接复制上面定义输入输出规范中的即可

生成 TypeScript 代码

创建 src/generated 目录:

mkdir -p src/generated

package.json 中添加生成 TypeScript 代码的脚本:

"scripts": {
    "gen:proto": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./src/generated --ts_out=grpc_js:./src/generated --grpc_out=grpc_js:./src/generated -I ./protos ./protos/*.proto",
    "dev": "ts-node src/main.ts",
    "dev:client": "ts-node src/client.ts"
}

运行生成脚本:

yarn gen:proto

API网关的参数注解

// src/infrastructure/api/type.ts

/**
 * 用户角色类型
 */
export type Role = "guest" | "user" | "admin" | "super";

/**
 * 用户认证信息
 */
export interface Auth {
  uid: string; // 用户ID
  iat: number; // 签发时间(Unix时间戳)
  exp: number; // 过期时间(Unix时间戳)
  role: Role; // 用户角色
  unverified?: boolean; // 是否未验证(可选)
}

/**
 * 分页信息
 */
export interface PagingInfo {
  page: number; // 当前页码
  size: number; // 每页大小
}

/**
 * 排序信息
 */
export interface SortingInfo {
  [key: string]: "asc" | "desc"; // 排序字段及其顺序(升序或降序)
}

/**
 * 查询参数值类型
 */
export type QueryValue =
  | string
  | number
  | undefined
  | null
  | boolean
  | Array<QueryValue>
  | Record<string, any>;

/**
 * 查询对象
 */
export type QueryObject = Record<string, QueryValue | QueryValue[]>;

/**
 * 解析后的查询参数
 */
export type ParsedQuery = Record<string, string | string[]>;

/**
 * 上下文信息
 */
export interface APIContext {
  auth: Auth; // 用户认证信息,如果未认证则为 false
  paging: PagingInfo; // 分页信息
  headers: Record<string, string>; // 请求头信息
  query: QueryObject; // 查询参数
  traceId: string; // 跟踪ID
  sorting: SortingInfo; // 排序信息
  ip?: string; // 客户端IP地址(可选)
  requestTime: number; // 请求时间(Unix时间戳)
  params?: Record<string, string>; // 路由参数(可选)
}

/**
 * gRPC 请求和响应中的头信息
 */
export interface GrpcHeaders {
  headers: { [key: string]: string }; // 请求头键值对
}

/**
 * gRPC 请求和响应中的数据部分
 */
export interface GrpcData {
  body: object; // 数据主体(JSON 对象形式)
}

/**
 * gRPC 请求中的用户信息
 */
export interface GrpcUser {
  uid: string; // 用户ID
  iat: number; // 签发时间(Unix时间戳)
  exp: number; // 过期时间(Unix时间戳)
  role: Role; // 用户角色
  unverified: boolean; // 是否未验证
}

/**
 * gRPC 请求和响应中的上下文信息
 */
export interface GrpcContext {
  traceId: string; // 跟踪ID
  page: number; // 当前页码
  size: number; // 每页大小
  sorting: { [key: string]: string }; // 排序信息
  ip: string; // 客户端IP地址
  requestTime: number; // 请求时间(Unix时间戳)
  params: { [key: string]: string }; // 路由参数
}

/**
 * gRPC 请求类型定义
 */
export interface GrpcRequest {
  headers: GrpcHeaders; // 头信息
  data: GrpcData; // 数据部分
  user: GrpcUser; // 用户信息
  context: GrpcContext; // 上下文信息
}

/**
 * gRPC 响应中的状态信息
 */
export interface GrpcStatus {
  code: number; // 状态码
  message: string; // 状态信息
}

/**
 * gRPC 响应类型定义
 */
export interface GrpcResponse {
  headers: GrpcHeaders; // 头信息
  data: GrpcData; // 数据部分
  status: GrpcStatus; // 状态信息
  context: GrpcContext; // 上下文信息
}

实现 gRPC 服务

创建领域层实体

src/domain/entities/greeter.ts 中定义 Greeter 实体:

export class Greeter {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  greet(): string {
    return `Hello ${this.name}`;
  }
}

创建应用层服务

src/application/services/greeterService.ts 中实现 Greeter 服务:

import { Greeter } from "../../domain/entities/greeter";

export class GreeterService {
  sayHello(name: string): string {
    const greeter = new Greeter(name);
    return greeter.greet();
  }
}

创建基础设施层日志配置

src/infrastructure/logging/logger.ts 中配置 Pino 日志:

import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  // 配置传输选项
  transport: {
    // 使用 pino-pretty 作为输出格式化工具
    target: "pino-pretty",
    options: {
      // 启用颜色化输出
      colorize: true,
    },
  },
});

export default logger;

定义 gRPC 服务处理程序

src/infrastructure/grpc/handlers/greeterHandler.ts 中实现 SayHello RPC 方法:

import * as grpc from "@grpc/grpc-js";
import {
  GrpcRequest,
  GrpcResponse,
  HelloWorldResponse,
  GrpcStatus,
} from "../../../generated/helloworld_pb";
import { GreeterService as ApplicationGreeterService } from "../../../application/services/greeterService";
import logger from "../../logging/logger";

// 实现 SayHello RPC 方法
const sayHello: grpc.handleUnaryCall<GrpcRequest, GrpcResponse> = (
  call,
  callback
) => {
  const greeterService = new ApplicationGreeterService();
  const helloRequest = call.request.getHellorequest();

  if (!helloRequest) {
    const errorMessage = "未提供 HelloRequest";
    logger.error(errorMessage);
    callback(
      {
        code: grpc.status.INVALID_ARGUMENT,
        message: errorMessage,
      },
      null
    );
    return;
  }

  const name = helloRequest.getName();
  logger.info(`收到 SayHello 请求,名字: ${name}`);

  try {
    const message = greeterService.sayHello(name);
    const reply = new HelloWorldResponse();
    reply.setMessage(message);

    const response = new GrpcResponse();
    const status = new GrpcStatus();
    status.setCode(0);
    status.setMessage("成功");

    response.setStatus(status);
    response.setHelloresponse(reply);

    logger.info(`发送 SayHello 响应,消息: ${message}`);
    callback(null, response);
  } catch (error: any) {
    logger.error(`处理 SayHello 请求时出错: ${error?.message}`);
    callback(
      {
        code: grpc.status.INTERNAL,
        message: "内部服务器错误",
      },
      null
    );
  }
};

export { sayHello };

创建 gRPC 服务器

src/infrastructure/grpc/server.ts 中实现 gRPC 服务器:

import * as grpc from "@grpc/grpc-js";
import { HelloWorldServiceService } from "../../generated/helloworld_grpc_pb";
import { sayHello } from "./handlers/greeterHandler";
import logger from "../logging/logger";

export function startGrpcServer() {
  const server = new grpc.Server();
  server.addService(HelloWorldServiceService, { sayHello });

  server.bindAsync(
    "0.0.0.0:50051",
    grpc.ServerCredentials.createInsecure(),
    (error, port) => {
      if (error) {
        logger.error(`Server binding error: ${error.message}`);
        return;
      }
      server.start();
      logger.info(`Server running at http://0.0.0.0:${port}`);
    }
  );
}

辅助函数createStandardGrpcObjects

这个函数是一个通用的函数,用于将上下文信息转换为 gRPC 所需的对象。它与生成 gRPC 请求的具体逻辑无关,更像是一个工具函数,可以放在基础设施层中。

src/infrastructure/grpc/utils.ts中定义,代码如下:

import {
  Context as ContextProto,
  Auth as AuthProto,
  PagingInfo as PagingInfoProto,
  QueryObject as QueryObjectProto,
  QueryValue as QueryValueProto,
  SortingInfo as SortingInfoProto,
  CommonHeaders as GrpcHeadersProto,
} from "../../generated/common_pb";
import { APIContext } from "../api/type";


function createStandardGrpcObjects(context: APIContext): {
  headers: GrpcHeadersProto;
  grpcContext: ContextProto;
} {
  // 创建 GrpcHeaders
  const headers = new GrpcHeadersProto();
  for (const [key, value] of Object.entries(context.headers)) {
    if (typeof value === "string") {
      headers.getHeadersMap().set(key, value);
    }
  }

  // 创建 Auth
  const auth = new AuthProto();
  if (context.auth) {
    auth.setUid(context.auth.uid);
    auth.setIat(context.auth.iat);
    auth.setExp(context.auth.exp);
    auth.setRole(
      context.auth.role === "guest"
        ? 0
        : context.auth.role === "user"
        ? 1
        : context.auth.role === "admin"
        ? 2
        : 3
    );
    auth.setUnverified(context.auth.unverified || false);
  }

  // 创建 PagingInfo
  const paging = new PagingInfoProto();
  paging.setPage(context.paging.page);
  paging.setSize(context.paging.size);

  // 创建 QueryObject
  const query = new QueryObjectProto();
  for (const [key, value] of Object.entries(context.query)) {
    const queryValue = new QueryValueProto();
    if (typeof value === "string") {
      queryValue.setStringValue(value);
    } else if (typeof value === "number") {
      queryValue.setIntValue(value);
    } else if (typeof value === "boolean") {
      queryValue.setBoolValue(value);
    }
    // Add more cases as needed for object and array values
    query.getQueryMap().set(key, queryValue);
  }

  // 创建 SortingInfo
  const sorting = new SortingInfoProto();
  for (const [key, value] of Object.entries(context.sorting)) {
    sorting.getSortingMap().set(key, value);
  }

  // 创建 Context
  const grpcContext = new ContextProto();
  grpcContext.setAuth(auth);
  grpcContext.setPaging(paging);
  grpcContext.setQuery(query);
  grpcContext.setTraceid(context.traceId);
  grpcContext.setSorting(sorting);
  grpcContext.setIp(context.ip || "");
  grpcContext.setRequesttime(context.requestTime);
  for (const [key, value] of Object.entries(context.params || {})) {
    grpcContext.getParamsMap().set(key, value);
  }

  return { headers, grpcContext };
}

export { createStandardGrpcObjects };

编写客户端代码

src/client.ts 中创建 gRPC 客户端:

import * as grpc from "@grpc/grpc-js";
import { HelloWorldServiceClient } from "./generated/helloworld_grpc_pb";
import { GrpcRequest, HelloWorldRequest } from "./generated/helloworld_pb";
import { createStandardGrpcObjects } from "./infrastructure/grpc/utils";
import { APIContext } from "./infrastructure/api/type";

// 创建 gRPC 客户端
const client = new HelloWorldServiceClient(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

// 创建 HelloWorldRequest
const helloRequest = new HelloWorldRequest();
helloRequest.setName("大飞机");

// 创建上下文对象
const context: APIContext = {
  auth: {
    uid: "12345",
    iat: 1609459200,
    exp: 1612137600,
    role: "super",
    unverified: false,
  },
  paging: {
    page: 1,
    size: 10,
  },
  headers: {
    "content-type": "application/grpc",
    "user-agent": "grpc-node/1.24.2",
    Authorization: "Bearer some-token",
  },
  query: {
    search: "example search query",
    filter: "active",
  },
  traceId: "abc123",
  sorting: {
    createdAt: "desc",
    name: "asc",
  },
  ip: "192.168.1.1",
  requestTime: 1612137600,
  params: {
    id: "some-id",
  },
};

// 使用工具函数创建标准的 gRPC 对象
const { headers, grpcContext } = createStandardGrpcObjects(context);

// 创建 GrpcRequest
const request = new GrpcRequest();
request.setHeaders(headers); // 这里的 headers 是标准化后的请求头信息
request.setContext(grpcContext); // 这里的 grpcContext 是标准化后的上下文信息
request.setHellorequest(helloRequest);

// 发起 gRPC 请求
client.sayHello(request, (error, response) => {
  if (error) {
    console.error(error);
  } else {
    const helloResponse = response?.getHelloresponse();
    if (helloResponse) {
      console.log("Greeting:", helloResponse.getMessage());
    } else {
      console.error("No HelloResponse received");
    }
  }
});

入口文件

src/main.ts 中启动 gRPC 服务器:

import { startGrpcServer } from "./infrastructure/grpc/server";

startGrpcServer();

运行项目

首先,启动 gRPC 服务器:

yarn dev

然后,在另一个终端窗口中,运行客户端:

yarn dev:client

6. 服务实现

定义ModelService接口

src/application/services/modelService.ts 中定义ModelService接口,保证所有文生文的输出都与OpenAI SDK兼容

这里解释一下!!!

提前抽象接口 ModelService 的好处

  1. 解耦:将业务逻辑与具体的 AI 服务实现分离,便于维护和管理。
  2. 灵活扩展:可以轻松替换或添加新的 AI 服务,而无需修改业务逻辑。
  3. 提高可测试性:可以创建模拟实现进行单元测试,隔离外部依赖。
  4. 增强团队协作:前后端或不同模块的开发人员可以并行工作,提升效率。
  5. 统一规范:定义明确的输入输出规范,减少实现不一致导致的错误。
  6. 代码复用:不同业务模块可以复用同一个接口定义的功能,避免重复实现。

保证与 OpenAI SDK 兼容的好处

  1. 标准化:使用业界标准的 SDK,确保接口和实现的一致性。
  2. 可靠性:依赖成熟的 SDK,减少开发和维护成本。
  3. 兼容性:确保新旧服务的平滑过渡,减少切换成本。

总之,提前抽象接口和保证兼容性可以让系统更灵活更稳定更易于维护和扩展

import OpenAI from "openai";
import { ChatModel } from "openai/resources";

export interface Message {
  role: "system" | "user" | "assistant";
  content: string;
}

// 定义 ModelService 接口
export interface ModelService {
  /**
   * 基础文本对话
   * @param messages - 与模型交互的消息数组
   * @param string - 使用的模型,默认为 "gpt-4o-mini"
   * @returns 响应文本
   */
  basicTextChat(
    messages: Message[],
    model?: ChatModel
  ): Promise<OpenAI.Chat.Completions.ChatCompletion>;
}

实现 OpenAI ModelService

基于 OpenAI Node SDK 实现具体的 ModelService。首先,确保你已经安装了 OpenAI Node SDK:

yarn add openai

注意!!!注意!!!注意!!!

安装了opanai这个包之后很有可能你的proto生成会出现奇怪问题,这时候你可以尝试删除依赖重新安装~

然后在 src/application/services/openAIModelService.ts 中实现具体的服务:

import OpenAI, { ClientOptions } from "openai/index";
import { ChatModel } from "openai/resources";
import { Message, ModelService } from "./modelService";

// 定义 OpenAIHelper 类,实现 ModelService 接口
class OpenAIHelper implements ModelService {
  private client: OpenAI;

  constructor(option: ClientOptions) {
    // 初始化 OpenAI 客户端
    this.client = new OpenAI(option);
  }

  /**
   * 基础文本对话
   * @param messages - 与模型交互的消息数组
   * @param model - 使用的模型,默认为 "gpt-4o-mini"
   * @returns 响应文本
   */
  async basicTextChat(messages: Message[], model: ChatModel = "gpt-4o-mini") {
    try {
      const completion = await this.client.chat.completions.create({
        model,
        messages,
      });

      const message = completion;
      return message || null;
    } catch (error) {
      console.error("Error in basic text chat:", error);
      throw error;
    }
  }
}

export default OpenAIHelper;

到这里,无数据库的情况下基本上无法继续了(当然,如果你硬要做的话也不是不行)。这时就需要请上我们的老朋友baseModel,这个是我在express的对象存储的项目中基于mongodbmongoose封装的我们可以直接拿来使用。

引入mongodb和mongoose

安装依赖:

yarn add mongodb mongoose

新建src/infrastructure/dao/mongodb/baseModel.ts写入baseModel代码(代码我放附件中,为了不影响阅读体验可以划到最后自取

定义领域实体

从这里开始是一个伪领域驱动的设计,为了省事好多设计我都直接封装 到了baseModel。还坚持目前这个目录结构,主要是扩展能力强后面好加东西

1. 会话表(Sessions)

会话表用于存储每个会话的基本信息。

// src/domain/entities/session.ts

import mongoose from 'mongoose';
import BaseModel, { IBaseDocument } from '../../infrastructure/dao/mongodb/baseModel';

export interface ISession extends IBaseDocument {
  userId: mongoose.Types.ObjectId;
  startTime: Date;
  endTime?: Date;
}

const sessionSchema: mongoose.SchemaDefinition = {
  userId: {
    type: mongoose.Types.ObjectId,
    required: true,
    ref: 'User',
  },
  startTime: {
    type: Date,
    required: true,
    default: Date.now,
  },
  endTime: {
    type: Date,
  },
};

class SessionModel extends BaseModel<ISession> {
  constructor() {
    super('Session', sessionSchema);
  }
}

export const Session = new SessionModel();

2. 聊天记录表(Messages)

聊天记录表用于存储每个会话中的所有消息。

// src/domain/entities/message.ts

import mongoose from 'mongoose';
import BaseModel, { IBaseDocument } from '../../infrastructure/dao/mongodb/baseModel';

export interface IMessage extends IBaseDocument {
  sessionId: mongoose.Types.ObjectId;
  sender: 'user' | 'assistant';
  content: string;
  timestamp: Date;
}

const messageSchema: mongoose.SchemaDefinition = {
  sessionId: {
    type: mongoose.Types.ObjectId,
    required: true,
    ref: 'Session',
  },
  sender: {
    type: String,
    required: true,
    enum: ['user', 'assistant'],
  },
  content: {
    type: String,
    required: true,
  },
  timestamp: {
    type: Date,
    required: true,
    default: Date.now,
  },
};

class MessageModel extends BaseModel<IMessage> {
  constructor() {
    super('Message', messageSchema);
  }
}

export const Message = new MessageModel();

定义服务

接下来,我们定义服务层来处理业务逻辑。

// src/application/services/chatService.ts

import mongoose from "mongoose";
import { Session } from "../../domain/entities/session";
import { Message } from "../../domain/entities/message";
import { ISession } from "../../domain/entities/session";
import { IMessage } from "../../domain/entities/message";
import { toObjectId } from "../../infrastructure/dao/mongodb/baseModel";
import OpenAIHelper from "./openAIModelService";
import { Message as AIMessage } from "./modelService";
import { ChatModel } from "openai/resources";
import { getConfig } from "../../infrastructure/config";

class ChatService {
  private openAIHelper: OpenAIHelper;

  constructor() {
    this.openAIHelper = new OpenAIHelper(getConfig("OPENAI"));
  }

  async startSession(userId: mongoose.Types.ObjectId): Promise<ISession> {
    const session = await Session.create({ userId });
    return session;
  }

  async endSession(
    sessionId: mongoose.Types.ObjectId,
    userId: string
  ): Promise<ISession | null> {
    const session = await Session.update(
      sessionId,
      { endTime: new Date() },
      toObjectId(userId)
    );
    return session;
  }

  async addMessage(
    sessionId: mongoose.Types.ObjectId,
    sender: "user" | "assistant",
    content: string,
    userId?: string
  ): Promise<IMessage> {
    const data: any = {
      sessionId,
      sender,
      content,
      timestamp: new Date(),
    };
    if (userId) data.createdBy = toObjectId(userId);
    const message = await Message.create(data);
    return message;
  }

  async getSessionMessages(
    sessionId: mongoose.Types.ObjectId
  ): Promise<IMessage[]> {
    const messages = await Message.find(
      { sessionId },
      {},
      { sort: { timestamp: 1 } }
    );
    return messages.results;
  }

  async chatWithAI(
    sessionId: mongoose.Types.ObjectId,
    userId: string,
    userMessage: string,
    model: ChatModel = "gpt-4o-mini"
  ) {
    // 添加用户消息到数据库
    await this.addMessage(sessionId, "user", userMessage, userId);

    // 获取当前会话的所有消息
    const messages = await this.getSessionMessages(sessionId);

    // 转换消息格式以适配OpenAI API
    const aiMessages: AIMessage[] = messages.map((msg) => ({
      role: msg.sender,
      content: msg.content,
    }));

    // 调用OpenAI API生成回复
    const aiResponse = await this.openAIHelper.basicTextChat(aiMessages, model);

    // 获取AI生成的回复
    const aiMessageContent = aiResponse.choices[0].message.content;

    // 添加AI回复到数据库
    await this.addMessage(
      sessionId,
      "assistant",
      aiMessageContent || "",
      userId
    );

    return aiResponse;
  }
}

export default new ChatService();

定义chat.proto文件

syntax = "proto3";

package protos.chat;

import "common.proto";

// 创建会话请求消息
message CreateSessionRequest {
  string userId = 1;
}

// 创建会话响应消息
message CreateSessionResponse {
  string sessionId = 1;
}

// 结束会话请求消息
message EndSessionRequest {
  string sessionId = 1;
  string userId = 2;
}

// 结束会话响应消息
message EndSessionResponse {
  bool success = 1;
}

// 获取会话聊天记录请求消息
message GetSessionMessagesRequest {
  string sessionId = 1;
}

// 单条消息
message Message {
  string sender = 1;
  string content = 2;
  int64 timestamp = 3;
}

// 获取会话聊天记录响应消息
message GetSessionMessagesResponse {
  repeated Message messages = 1;
}

// 对话请求消息
message ChatRequest {
  string sessionId = 1;
  string userId = 2;
  string userMessage = 3;
  string model = 4; // 可选的模型参数
}

// 对话响应消息
message ChatResponse {
  string data = 1;
}

// gRPC 请求类型定义
message GrpcChatRequest {
  protos.common.CommonHeaders headers = 1; // 头信息
  protos.common.Context context = 2; // 上下文信息
  oneof body {
    CreateSessionRequest createSessionRequest = 3;
    EndSessionRequest endSessionRequest = 4;
    GetSessionMessagesRequest getSessionMessagesRequest = 5;
    ChatRequest chatRequest = 6;
  }
}

// gRPC 响应中的状态信息
message GrpcStatus {
  int32 code = 1; // 状态码
  string message = 2; // 状态信息
}

// gRPC 响应类型定义
message GrpcChatResponse {
  protos.common.CommonHeaders headers = 1; // 头信息
  GrpcStatus status = 2; // 状态信息
  oneof body {
    CreateSessionResponse createSessionResponse = 3;
    EndSessionResponse endSessionResponse = 4;
    GetSessionMessagesResponse getSessionMessagesResponse = 5;
    ChatResponse chatResponse = 6;
  }
}

// gRPC 服务定义
service ChatService {
  rpc CreateSession(GrpcChatRequest) returns (GrpcChatResponse);
  rpc EndSession(GrpcChatRequest) returns (GrpcChatResponse);
  rpc GetSessionMessages(GrpcChatRequest) returns (GrpcChatResponse);
  rpc Chat(GrpcChatRequest) returns (GrpcChatResponse);
}

实现 Chat Service gRPC 接口

src/infrastructure/grpc/handlers 中创建 chatHandler.ts 文件,实现 Chat Service gRPC 接口。

import * as grpc from "@grpc/grpc-js";
import {
  GrpcChatRequest,
  GrpcChatResponse,
  CreateSessionResponse,
  EndSessionResponse,
  GetSessionMessagesResponse,
  ChatResponse,
  GrpcStatus,
  Message,
} from "../../../generated/chat_pb";
import chatService from "../../../application/services/chatService";
import logger from "../../logging/logger";
import { toObjectId } from "../../dao/mongodb/baseModel";

// 创建会话
const createSession: grpc.handleUnaryCall<
  GrpcChatRequest,
  GrpcChatResponse
> = async (call, callback) => {
  const createSessionRequest = call.request.getCreatesessionrequest();
  logger.info(JSON.stringify(createSessionRequest));
  if (!createSessionRequest) {
    const errorMessage = "未提供 CreateSessionRequest";
    logger.error(errorMessage);
    callback(
      {
        code: grpc.status.INVALID_ARGUMENT,
        message: errorMessage,
      },
      null
    );
    return;
  }

  const { userid } = createSessionRequest.toObject();

  logger.info(`收到 CreateSession 请求,用户ID: ${userid}`);

  try {
    const session = await chatService.startSession(toObjectId(userid));

    const createSessionResponse = new CreateSessionResponse();
    createSessionResponse.setSessionid(session._id.toString());

    const response = new GrpcChatResponse();
    const status = new GrpcStatus();
    status.setCode(0);
    status.setMessage("成功");

    response.setStatus(status);
    response.setCreatesessionresponse(createSessionResponse);

    logger.info(`发送 CreateSession 响应,会话ID: ${session._id}`);
    callback(null, response);
  } catch (error: any) {
    logger.error(`处理 CreateSession 请求时出错: ${error?.message}`);
    callback(
      {
        code: grpc.status.INTERNAL,
        message: "内部服务器错误",
      },
      null
    );
  }
};

// 结束会话
const endSession: grpc.handleUnaryCall<
  GrpcChatRequest,
  GrpcChatResponse
> = async (call, callback) => {
  const endSessionRequest = call.request.getEndsessionrequest();

  if (!endSessionRequest) {
    const errorMessage = "未提供 EndSessionRequest";
    logger.error(errorMessage);
    callback(
      {
        code: grpc.status.INVALID_ARGUMENT,
        message: errorMessage,
      },
      null
    );
    return;
  }

  const { sessionid, userid } = endSessionRequest.toObject();

  logger.info(`收到 EndSession 请求,会话ID: ${sessionid}, 用户ID: ${userid}`);

  try {
    const session = await chatService.endSession(toObjectId(sessionid), userid);

    const endSessionResponse = new EndSessionResponse();
    endSessionResponse.setSuccess(!!session);

    const response = new GrpcChatResponse();
    const status = new GrpcStatus();
    status.setCode(0);
    status.setMessage("成功");

    response.setStatus(status);
    response.setEndsessionresponse(endSessionResponse);

    logger.info(`发送 EndSession 响应,成功: ${!!session}`);
    callback(null, response);
  } catch (error: any) {
    logger.error(`处理 EndSession 请求时出错: ${error?.message}`);
    callback(
      {
        code: grpc.status.INTERNAL,
        message: "内部服务器错误",
      },
      null
    );
  }
};

// 获取会话聊天记录
const getSessionMessages: grpc.handleUnaryCall<
  GrpcChatRequest,
  GrpcChatResponse
> = async (call, callback) => {
  const getSessionMessagesRequest = call.request.getGetsessionmessagesrequest();

  if (!getSessionMessagesRequest) {
    const errorMessage = "未提供 GetSessionMessagesRequest";
    logger.error(errorMessage);
    callback(
      {
        code: grpc.status.INVALID_ARGUMENT,
        message: errorMessage,
      },
      null
    );
    return;
  }

  const { sessionid } = getSessionMessagesRequest.toObject();

  logger.info(`收到 GetSessionMessages 请求,会话ID: ${sessionid}`);

  try {
    const messages = await chatService.getSessionMessages(
      toObjectId(sessionid)
    );

    const getSessionMessagesResponse = new GetSessionMessagesResponse();
    messages.forEach((message) => {
      const msg = new Message();
      msg.setSender(message.sender);
      msg.setContent(message.content);
      msg.setTimestamp(message.timestamp.getTime());
      getSessionMessagesResponse.addMessages(msg);
    });

    const response = new GrpcChatResponse();
    const status = new GrpcStatus();
    status.setCode(0);
    status.setMessage("成功");

    response.setStatus(status);
    response.setGetsessionmessagesresponse(getSessionMessagesResponse);

    logger.info(`发送 GetSessionMessages 响应,消息数量: ${messages.length}`);
    callback(null, response);
  } catch (error: any) {
    logger.error(`处理 GetSessionMessages 请求时出错: ${error?.message}`);
    callback(
      {
        code: grpc.status.INTERNAL,
        message: "内部服务器错误",
      },
      null
    );
  }
};

// 对话
const chat: grpc.handleUnaryCall<GrpcChatRequest, GrpcChatResponse> = async (
  call,
  callback
) => {
  const chatRequest = call.request.getChatrequest();

  if (!chatRequest) {
    const errorMessage = "未提供 ChatRequest";
    logger.error(errorMessage);
    callback(
      {
        code: grpc.status.INVALID_ARGUMENT,
        message: errorMessage,
      },
      null
    );
    return;
  }

  const { sessionid, userid, usermessage, model } = chatRequest.toObject();

  logger.info(`收到 Chat 请求,用户ID: ${userid}, 会话ID: ${sessionid}`);

  try {
    const aiResponse = await chatService.chatWithAI(
      toObjectId(sessionid),
      userid,
      usermessage,
      model as any // 这里假设 model 是 ChatModel 类型
    );

    const aiMessageContent = aiResponse;

    const chatResponse = new ChatResponse();
    chatResponse.setData(JSON.stringify(aiMessageContent));

    const response = new GrpcChatResponse();
    const status = new GrpcStatus();
    status.setCode(0);
    status.setMessage("成功");

    response.setStatus(status);
    response.setChatresponse(chatResponse);

    logger.info(`发送 Chat 响应,消息: ${aiMessageContent}`);
    callback(null, response);
  } catch (error: any) {
    logger.error(`处理 Chat 请求时出错: ${error?.message}`);
    callback(
      {
        code: grpc.status.INTERNAL,
        message: "内部服务器错误",
      },
      null
    );
  }
};

export { createSession, endSession, getSessionMessages, chat };

创建 gRPC 服务器

src/infrastructure/grpc/server.ts 中实现 gRPC 服务器:

import * as grpc from "@grpc/grpc-js";
import { HelloWorldServiceService } from "../../generated/helloworld_grpc_pb";
import { ChatServiceService } from "../../generated/chat_grpc_pb";
import { sayHello } from "./handlers/greeterHandler";
import { chat } from "./handlers/chatHandler";
import logger from "../logging/logger";

export function startGrpcServer() {
  const server = new grpc.Server();
  server.addService(HelloWorldServiceService, { sayHello });
  server.addService(ChatServiceService, { chat });

  server.bindAsync(
    "0.0.0.0:50051",
    grpc.ServerCredentials.createInsecure(),
    (error, port) => {
      if (error) {
        logger.error(`Server binding error: ${error.message}`);
        return;
      }
      server.start();
      logger.info(`Server running at http://0.0.0.0:${port}`);
    }
  );
}

启动 gRPC 服务器

src/main.ts 中启动 gRPC 服务器:

import { initializeMongoose } from "./infrastructure/dao/mongodb";
import { startGrpcServer } from "./infrastructure/grpc/server";
initializeMongoose()
startGrpcServer();

启动 gRPC 服务器:

yarn dev

创建 SDK 类

首先,我们在 src/sdk 目录下创建一个文件 ChatServiceClient.ts,并将所有客户端逻辑封装在这个类中。

import * as grpc from "@grpc/grpc-js";
import { ChatServiceClient as GrpcChatServiceClient } from "../generated/chat_grpc_pb";
import {
  GrpcChatRequest,
  ChatRequest,
  CreateSessionRequest,
  EndSessionRequest,
  GetSessionMessagesRequest,
} from "../generated/chat_pb";
import { createStandardGrpcObjects } from "../infrastructure/grpc/utils";
import { APIContext } from "../infrastructure/api/type";

class ChatServiceClient {
  private client: GrpcChatServiceClient;
  private context: APIContext;

  constructor(address: string, context: APIContext) {
    this.client = new GrpcChatServiceClient(
      address,
      grpc.credentials.createInsecure()
    );
    this.context = context;
  }

  async createSession(userId: string): Promise<string> {
    return new Promise((resolve, reject) => {
      const createSessionRequest = new CreateSessionRequest();
      createSessionRequest.setUserid(userId);

      const { headers, grpcContext } = createStandardGrpcObjects(this.context);

      const grpcRequest = new GrpcChatRequest();
      grpcRequest.setHeaders(headers);
      grpcRequest.setContext(grpcContext);
      grpcRequest.setCreatesessionrequest(createSessionRequest);

      this.client.createSession(grpcRequest, (error, response) => {
        if (error) {
          reject(error);
        } else {
          const sessionId = response.getCreatesessionresponse()?.getSessionid();
          if (sessionId) {
            resolve(sessionId);
          } else {
            reject(new Error("Failed to create session"));
          }
        }
      });
    });
  }

  async endSession(sessionId: string, userId: string): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const endSessionRequest = new EndSessionRequest();
      endSessionRequest.setSessionid(sessionId);
      endSessionRequest.setUserid(userId);

      const { headers, grpcContext } = createStandardGrpcObjects(this.context);

      const grpcRequest = new GrpcChatRequest();
      grpcRequest.setHeaders(headers);
      grpcRequest.setContext(grpcContext);
      grpcRequest.setEndsessionrequest(endSessionRequest);

      this.client.endSession(grpcRequest, (error, response) => {
        if (error) {
          reject(error);
        } else {
          const success = response.getEndsessionresponse()?.getSuccess();
          resolve(success || false);
        }
      });
    });
  }

  async getSessionMessages(sessionId: string): Promise<Array<{ sender: string, content: string, timestamp: number }>> {
    return new Promise((resolve, reject) => {
      const getSessionMessagesRequest = new GetSessionMessagesRequest();
      getSessionMessagesRequest.setSessionid(sessionId);

      const { headers, grpcContext } = createStandardGrpcObjects(this.context);

      const grpcRequest = new GrpcChatRequest();
      grpcRequest.setHeaders(headers);
      grpcRequest.setContext(grpcContext);
      grpcRequest.setGetsessionmessagesrequest(getSessionMessagesRequest);

      this.client.getSessionMessages(grpcRequest, (error, response) => {
        if (error) {
          reject(error);
        } else {
          const messages = response.getGetsessionmessagesresponse()?.getMessagesList().map(msg => ({
            sender: msg.getSender(),
            content: msg.getContent(),
            timestamp: msg.getTimestamp(),
          })) || [];
          resolve(messages);
        }
      });
    });
  }

  async chat(sessionId: string, userId: string, userMessage: string, model: string = "gpt-4o-mini"): Promise<string> {
    return new Promise((resolve, reject) => {
      const chatRequest = new ChatRequest();
      chatRequest.setSessionid(sessionId);
      chatRequest.setUserid(userId);
      chatRequest.setUsermessage(userMessage);
      chatRequest.setModel(model);

      const { headers, grpcContext } = createStandardGrpcObjects(this.context);

      const grpcRequest = new GrpcChatRequest();
      grpcRequest.setHeaders(headers);
      grpcRequest.setContext(grpcContext);
      grpcRequest.setChatrequest(chatRequest);

      this.client.chat(grpcRequest, (error, response) => {
        if (error) {
          reject(error);
        } else {
          const aiMessageContent = response.getChatresponse()?.getData();
          if (aiMessageContent) {
            resolve(aiMessageContent);
          } else {
            reject(new Error("Failed to get AI response"));
          }
        }
      });
    });
  }
}

export default ChatServiceClient;

使用 SDK 类

接下来,我们在 src/client.ts 中使用这个 SDK 类来进行操作:

import ChatServiceClient from "./sdk/ChatServiceClient";
import { APIContext } from "./infrastructure/api/type";

const userid = "66c4911740bdd8adbc8c1dde"

// 创建上下文对象
const context: APIContext = {
  auth: {
    uid: userid,
    iat: 1609459200,
    exp: 1612137600,
    role: "super",
    unverified: false,
  },
  paging: {
    page: 1,
    size: 10,
  },
  headers: {
    "content-type": "application/grpc",
    "user-agent": "grpc-node/1.24.2",
    Authorization: "Bearer some-token",
  },
  query: {
    search: "example search query",
    filter: "active",
  },
  traceId: "abc123",
  sorting: {
    createdAt: "desc",
    name: "asc",
  },
  ip: "192.168.1.1",
  requestTime: 1612137600,
  params: {
    id: "some-id",
  },
};

// 创建 ChatServiceClient 实例
const chatClient = new ChatServiceClient("localhost:50051", context);

async function main() {
  try {
    // 创建会话
    const sessionId = await chatClient.createSession(userid);
    console.log("Created Session ID:", sessionId);

    // 发起聊天请求
    const aiResponse = await chatClient.chat(
      sessionId,
      userid,
      "你好,我应该怎么赚钱呢作为一个程序员?"
    );
    console.log("AI Response:", aiResponse);

    // 获取会话消息
    const messages = await chatClient.getSessionMessages(sessionId);
    console.log("Session Messages:", messages);

    // 结束会话
    const success = await chatClient.endSession(sessionId, userid);
    console.log("End Session Success:", success);
  } catch (error) {
    console.error("Error:", error);
  }
}

main();

总结

本文详细介绍了一个聚合AI服务的设计与实现过程,主要面向普通用户和开发者,旨在提供一个可靠的聊天助手和API接口。

核心功能包括支持多轮对话和上下文保持的聊天接口,确保用户体验的连贯性和智能化。服务设计分为Model Service和Chat Service,采用TypeScript和gRPC技术栈,以实现高效的通信和扩展性。项目实现过程涵盖了基础项目搭建、MongoDB集成、gRPC服务定义与实现以及SDK封装,展示了从需求分析、系统设计到实际编码和测试的完整流程。最终,本文展示了如何通过这些技术和步骤构建一个功能完备、易于使用的AI聊天服务。

附件1——baseModel.ts

// src/infrastructure/dao/mongodb/baseModel.ts

import { FindOptions } from "mongodb";
import mongoose, {
  Schema,
  Document,
  Model,
  FilterQuery,
  QueryOptions,
  Query,
  Aggregate,
} from "mongoose";

export interface IBaseDocument extends Document<mongoose.Types.ObjectId> {
  _id: mongoose.Types.ObjectId;
  deletedAt?: Date;
  createdAt: Date;
  updatedAt: Date;
  createdBy?: mongoose.Types.ObjectId;
  updatedBy?: mongoose.Types.ObjectId;
  version: number;
}

export type ExtendedFilterQuery<T> = FilterQuery<T> & {
  deletedAt?: { $exists: boolean };
};

export function toObjectId(
  id: mongoose.Types.ObjectId | string
): mongoose.Types.ObjectId {
  return typeof id === "string" ? new mongoose.Types.ObjectId(id) : id;
}

export const baseSchemaDict = {
  createdAt: {
    type: Date,
    default: Date.now,
  },
  updatedAt: {
    type: Date,
    default: Date.now,
  },
  deletedAt: {
    type: Date,
  },
  createdBy: {
    type: mongoose.Types.ObjectId,
    ref: "User",
  },
  updatedBy: {
    type: mongoose.Types.ObjectId,
    ref: "User",
  },
  version: {
    type: Number,
    default: 0,
  },
};

class BaseModel<T extends IBaseDocument> {
  private model: mongoose.Model<T>;
  schema: mongoose.Schema<T>;

  constructor(
    modelName: string,
    schemaDef: mongoose.SchemaDefinition,
    options: mongoose.SchemaOptions = {}
  ) {
    this.schema = new mongoose.Schema(
      {
        ...schemaDef,
        ...baseSchemaDict,
      },
      options
    );

    this.initMiddleware();
    this.model = mongoose.model<T>(modelName, this.schema);
  }

  private initMiddleware() {
    this.schema.pre<T>("save", function (next) {
      if (!this.isNew) {
        this.updatedAt = new Date();
        this.version += 1;
      }
      next();
    });

    this.schema.pre<Query<T, T>>(["find", "findOne"], function (next) {
      this.where({ deletedAt: { $exists: false } });
      next();
    });

    this.schema.pre<any>(
      [
        "updateOne",
        "findOneAndUpdate",
        "findOneAndDelete",
        "findOneAndReplace",
      ],
      function (next) {
        this.where({ deletedAt: { $exists: false } });

        const update = this.getUpdate();
        if (!update.$set) {
          update.$set = {};
        }

        update.$set.updatedAt = new Date();
        update.$inc = update.$inc || {};
        update.$inc.version = 1;

        this.setUpdate(update);
        next();
      }
    );

    this.schema.pre<any>("updateMany", function (next) {
      const update = this.getUpdate();
      if (update.$set) {
        update.$set.updatedAt = new Date();
        update.$inc = update.$inc || {};
        update.$inc.version = 1;
      } else {
        this.setUpdate({
          $set: { updatedAt: new Date() },
          $inc: { version: 1 },
        });
      }
      next();
    });

    this.schema.pre<Aggregate<T>>("aggregate", function (next) {
      const pipeline = this.pipeline();
      pipeline.unshift({ $match: { deletedAt: { $exists: false } } });
      next();
    });
  }

  getModel(): mongoose.Model<T> {
    return this.model;
  }

  async create(
    doc: Partial<T>,
    userId?: mongoose.Types.ObjectId | null
  ): Promise<T> {
    if (userId) {
      doc.createdBy = userId;
      doc.updatedBy = userId;
    }
    return await this.model.create(doc);
  }

  async update(
    id: mongoose.Types.ObjectId | string,
    update: Partial<T>,
    userId: mongoose.Types.ObjectId
  ): Promise<T | null> {
    update.updatedBy = userId;
    return await this.model
      .findByIdAndUpdate(toObjectId(id), update, { new: true })
      .exec();
  }

  async delete(
    id: mongoose.Types.ObjectId | string,
    userId?: mongoose.Types.ObjectId
  ): Promise<T | null> {
    try {
      return await this.model
        .findByIdAndUpdate(
          toObjectId(id),
          {
            deletedAt: new Date(),
            updatedBy: userId,
          },
          { new: true }
        )
        .exec();
    } catch (error) {
      throw new Error("Failed to delete document");
    }
  }

  async find(
    query: ExtendedFilterQuery<T>,
    options: QueryOptions = {},
    findOptions: FindOptions = {}
  ) {
    const { skip = 0, limit = 10, sort = {} } = findOptions;

    const aggregationPipeline: any[] = [{ $match: query }];

    if (Object.keys(sort).length > 0) {
      aggregationPipeline.push({ $sort: sort });
    }

    if (Object.keys(options).length > 0) {
      aggregationPipeline.push({ $project: options });
    }

    aggregationPipeline.push({
      $facet: {
        results: [{ $skip: skip }, { $limit: limit }],
        total: [{ $count: "count" }],
      },
    });

    const result = await this.model.aggregate(aggregationPipeline).exec();

    const results = result[0].results as T[];
    const total = result[0].total[0] ? result[0].total[0].count : 0;

    return { results, total };
  }

  async findById(
    id: mongoose.Types.ObjectId | string,
    options: QueryOptions = {}
  ): Promise<T | null> {
    const query: ExtendedFilterQuery<T> = {
      _id: toObjectId(id),
      deletedAt: { $exists: false },
    };
    return await this.model.findOne(query, options).exec();
  }

  async findOne(
    query: ExtendedFilterQuery<T>,
    options: QueryOptions = {}
  ): Promise<T | null> {
    return await this.model.findOne(query, options).exec();
  }

  async findAllIncludingDeleted(
    query: FilterQuery<T>,
    options: QueryOptions = {}
  ) {
    return await this.model.find(query, options).exec();
  }

  async findByIdAndUpdate(
    id: mongoose.Types.ObjectId | string,
    update: Partial<T>,
    options: QueryOptions = {}
  ): Promise<T | null> {
    const doc = await this.model.findById(toObjectId(id)).exec();
    if (!doc) {
      throw new Error("Document not found");
    }
    return await this.model
      .findOneAndUpdate({ _id: toObjectId(id) }, update, {
        new: true,
        ...options,
      })
      .exec();
  }

  async findByIdAndDelete(
    id: mongoose.Types.ObjectId | string,
    options: QueryOptions = {}
  ): Promise<T | null> {
    return await this.model
      .findByIdAndUpdate(
        toObjectId(id),
        { deletedAt: new Date() },
        { new: true, ...options }
      )
      .exec();
  }
  async upsert(
    filter: ExtendedFilterQuery<T>,
    update: Partial<T>,
    userId: mongoose.Types.ObjectId
  ): Promise<T | null> {
    update.updatedBy = userId;
    const options = { new: true, upsert: true, setDefaultsOnInsert: true };
    return await this.model.findOneAndUpdate(filter, update, options).exec();
  }

  async createMany(
    docs: Partial<T>[],
    userId?: mongoose.Types.ObjectId | null
  ) {
    if (userId) {
      docs = docs.map((doc) => ({
        ...doc,
        createdBy: userId,
        updatedBy: userId,
      }));
    }
    return await this.model.insertMany(docs);
  }

  async aggregate(pipeline: any[]): Promise<any[]> {
    return await this.model.aggregate(pipeline).exec();
  }

  async byIds(
    ids: (mongoose.Types.ObjectId | string)[],
    options: QueryOptions = {}
  ): Promise<T[]> {
    const objectIds = ids.map((id) => toObjectId(id));
    const query: ExtendedFilterQuery<T> = {
      _id: { $in: objectIds },
      deletedAt: { $exists: false },
    };
    return await this.model.find(query, options).exec();
  }
}

export default BaseModel;

附件2——MongoDB连接和初始化函数initializeMongoose

// src/infrastructure/dao/mongodb/index.ts

import { MongoClient } from "mongodb";
import mongoose, { ConnectOptions } from "mongoose";
import { getConfig } from "../../config";
import logger from "../../logging/logger";

const mongodbConfig = getConfig("MONGODB");
const mongoUrl = mongodbConfig.url;

const options: ConnectOptions = {
  dbName: mongodbConfig.dbName,
  connectTimeoutMS: 10000,
  socketTimeoutMS: 45000,
  maxPoolSize: 10,
  minPoolSize: 5,
  autoIndex: true,
  retryWrites: true,
  w: "majority",
  readPreference: "primary",
  authSource: "admin",
};

let isConnected = false;

const initializeMongoose = async () => {
  if (isConnected) {
    return;
  }

  try {
    await mongoose.connect(mongoUrl, options);
    isConnected = true;
    logger.info("MongoDB 连接成功...");
  } catch (err) {
    logger.error("MongoDB 连接错误:", err);
    throw err;
  }

  mongoose.connection.on("connected", () => {
    logger.info("Mongoose 已连接到数据库");
  });

  mongoose.connection.on("error", (err) => {
    logger.error("Mongoose 连接错误:", err);
  });

  mongoose.connection.on("disconnected", () => {
    isConnected = false;
    logger.info("Mongoose 已断开与数据库的连接");
  });

  process.on("SIGINT", async () => {
    await mongoose.connection.close();
    logger.info("应用程序终止,Mongoose 已断开连接");
    process.exit(0);
  });
};

export { initializeMongoose };

附件3——config.ts

// src/infrastructure/config/index.ts

import { readFileSync } from "fs";
import { join } from "path";
import { parse } from "yaml";

// 获取项目运行环境
export const getEnv = () => {
  return process.env.RUNNING_ENV || "dev";
};

// 读取项目配置
export const getConfig = (type?: string): any => {
  const environment = getEnv();
  const yamlPath = join(process.cwd(), `/.config/.config.yaml`);
  const file = readFileSync(yamlPath, "utf8");
  const config = parse(file);
  if (type) {
    return config[type];
  }
  return config;
};