在如今处处都充满科技的时代,人工智能(AI)已经成为各行各业的重要工具和推动力。AI技术的进步使得其应用范围不断扩大,从简单的自动化任务到复杂的自然语言处理,AI正在以惊人的速度改变我们的生活和工作方式。然而,随着AI技术的日益复杂和多样化,如何高效地集成和管理各种AI服务成为了一个重要的挑战。
本文旨在探讨如何设计和实现一个聚合AI服务,既能满足普通用户的需求,也能为开发者提供强大的API接口。通过详细的需求分析、技术架构设计和具体实现步骤,我们将展示如何构建一个可靠、灵活且易于扩展的AI服务平台。
在本文中,我们将详细介绍以下内容:
- 目标和需求:明确目标用户和他们的需求,确保设计的服务能够真正解决用户的问题。
- 技术架构:介绍聚合AI服务的技术架构,包括前端应用、API网关、Chat Service、Model Service和数据库等关键组件。
- 服务设计:详细说明每个服务的职责、技术栈和实现逻辑,确保服务之间的高效通信和协作。
- 输入/输出规范:定义API网关和gRPC服务的输入输出格式,确保数据传递和处理的一致性和可靠性。
- 项目实现:从头开始搭建一个基础的TypeScript和gRPC项目,包含项目结构、依赖安装、服务定义和实现、客户端代码编写等详细步骤。
- 高级功能:介绍如何集成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
逻辑描述:
- 接收请求:通过gRPC接口接收来自其他服务的请求。
- 调用AI模型:传递请求数据给AI模型,生成回复或回答。
- 返回结果:将生成的结果发送回调用的服务。
- 错误处理:处理可能的错误,如模型不可用,返回适当的错误信息。
Chat Service
职责:处理聊天请求,调用Model Service生成回复。
技术栈:TypeScript、gRPC
逻辑描述:
- 接收请求:通过gRPC接口接收来自API Gateway的聊天请求。
- 上下文管理:
- 检查用户的会话ID,获取上下文信息。
- 如果没有会话ID,则创建新的会话。
- 生成回复:
- 调用Model Service,传递用户输入和上下文信息。
- 接收Model Service返回的回复。
- 返回回复:将生成的回复发送回API Gateway。
- 错误处理:处理可能的错误,如Model Service不可用,返回适当的错误信息。
数据流向
- 用户通过前端应用或API发送聊天请求。
- 请求经过API网关,进行鉴权和路由。
- API网关将请求通过gRPC转发到Chat Service。
- Chat Service调用对话管理模块,处理上下文和多轮对话。
- Chat Service通过gRPC调用Model Service,选择合适的模型生成回复。
- Model Service通过OpenAI SDK与AI模型交互,生成回复。
- Model Service返回生成的回复给Chat Service。
- Chat Service将回复通过gRPC发送回API网关。
- API网关将回复返回给用户。
错误处理机制
- 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.proto 和 helloworld.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的好处
- 解耦:将业务逻辑与具体的 AI 服务实现分离,便于维护和管理。
- 灵活扩展:可以轻松替换或添加新的 AI 服务,而无需修改业务逻辑。
- 提高可测试性:可以创建模拟实现进行单元测试,隔离外部依赖。
- 增强团队协作:前后端或不同模块的开发人员可以并行工作,提升效率。
- 统一规范:定义明确的输入输出规范,减少实现不一致导致的错误。
- 代码复用:不同业务模块可以复用同一个接口定义的功能,避免重复实现。
保证与 OpenAI SDK 兼容的好处
- 标准化:使用业界标准的 SDK,确保接口和实现的一致性。
- 可靠性:依赖成熟的 SDK,减少开发和维护成本。
- 兼容性:确保新旧服务的平滑过渡,减少切换成本。
总之,提前抽象接口和保证兼容性可以让系统
更灵活、更稳定、更易于维护和扩展。
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的对象存储的项目中基于mongodb和mongoose封装的我们可以直接拿来使用。
引入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;
};