NestJs+MongoDB+Deepseek+Langchain实现ai聊天助手
目录
一、思路
分为ai模块、聊天模块、如果有登录系统就在加入jwt,管理员等,源码在:AI-customer/ai-customer-service-backend at master · BI-shi-qiang/AI-customer
二、准备环境
node20+、mongodb8、本地安装@nestjs、提前注册deepsee开放平台获取api、还有项目中所用到的一些依赖如下
(1)package.json
{
"name": "ai-customer-service-backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@langchain/core": "^1.1.39",
"@langchain/openai": "^1.4.2",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/mongoose": "^11.0.4",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.1.18",
"@nestjs/websockets": "^11.1.18",
"bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"langchain": "^1.3.0",
"mongoose": "^9.4.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcryptjs": "^3.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
(2).env
# 端口
PORT=3000
# MongoDB 8.0
MONGODB_URI=mongodb://localhost:27017/~
# DeepSeek API
DEEPSEEK_API_KEY=~
DEEPSEEK_API_URL=https://api.deepseek.com/v1
DEEPSEEK_MODEL=deepseek-chat
# CORS
CORS_ORIGIN=http://localhost:5173,http://localhost:8080
# JWT
JWT_SECRET=~
三、AI助手模块实现
(1)src/ai/ai.service.ts主要是利用langchain框架导入大模型,便于多模型切换
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ChatOpenAI } from '@langchain/openai';
@Injectable()
export class AiService {
private llm: ChatOpenAI;
constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get<string>('DEEPSEEK_API_KEY')!;
const baseURL = this.configService.get<string>('DEEPSEEK_API_URL')!;
const model = this.configService.get<string>('DEEPSEEK_MODEL')!;
this.llm = new ChatOpenAI({
apiKey,
model,
streaming: true,
temperature: 0.7,
maxTokens: 1024,
configuration: { baseURL },
});
}
async streamChat(message: string) {
return this.llm.stream([['user', message]]);
}
}
(2)src/ai/gaiban.ts
export const MY_GAIBAN = {
// ...你原来的内容
lifeFullStory: `
给这个ai的知识库,让他知道你的产品是干啥的,常问的问题让ai能够根据知识库内容回答
`,
answerRulesFull: `
1. 回话要求那些,比如以什么风格回复,那些不能说等
2.
3.
`,
};
(3)src/ai/ai.module.ts将这个ai模块导出
import { Module } from '@nestjs/common';
import { AiService } from './ai.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule],
providers: [AiService],
exports: [AiService],
})
export class AiModule {}
四、聊天模块实现
(1)src/chat/chat.service.ts
聊天的主要逻辑部分,就是ai回复功能,人工聊天功能,聊天内容存数据库等
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Chat, ChatDocument } from '../schemas/chat.schema';
import { AiService } from '../ai/ai.service';
import { MY_GAIBAN } from '../ai/gaiban';
import { PromptTemplate } from '@langchain/core/prompts';
import { Observable } from 'rxjs';
import { ChatGateway } from './chat.gateway';
import {
HumanSession,
HumanSessionDocument,
} from '../schemas/human-session.schema';
const EMOJI_MAP = {
哈哈哈哈哈: '🤣',
牛批: '👍',
没得问题: '👌',
懂嘞: '👌',
莫为难: '🙏',
劳资: '🤬',
嘿嘿嘿: '😃',
};
@Injectable()
export class ChatService {
private readonly ASSISTANT_PROMPT = PromptTemplate.fromTemplate(`
你是~。
说话风格:搞笑、耿直、有点皮、口语化,带口头禅~。
以下是你的所有资料,必须严格按照这个回答,不准瞎编:
{knowledge}
规则:
1. 问什么答什么,简短自然,控制在3~5句话以内,尽量用谦虚的语气回答,不要自以为是。
2. 可以轻松闲聊,但不懂就说不懂
3. 不准输出模板、不准解释规则、不准加括号、可以加括号描述下内心的调皮想法,但不过分
4. 自我介绍:可以用这个:hello,~,;也不要一直都是这句话开头,可时不时修改一下,就是保持自然度
5. 不懂就说:哎呀,我的Token是不是不要钱,这些问题国人跑去问豆包~~
6. 自然聊天,不频繁提网站
7. 表情包不要多用,一句话最多加1个,偶尔才用,不要每个词都加
~
聊天历史:
{history}
用户现在问:
{input}
`);
constructor(
@InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
@InjectModel(HumanSession.name)
private humanSessionModel: Model<HumanSessionDocument>,
private aiService: AiService,
private readonly chatGateway: ChatGateway,
) {}
// ====================== 核心:用户发消息(自动判断 AI / 人工)======================
streamMessage(sessionId: string, message: string): Observable<string> {
return new Observable((subscriber) => {
void (async () => {
try {
const session = await this.getOrCreateSession(sessionId);
// ============== 人工模式:不调用AI,只存消息,等后台回复 ==============
if (session.status === 'human') {
const newMsg = await this.saveHistory(
sessionId,
message,
'',
false,
);
this.chatGateway.sendMessageToSession(sessionId, newMsg);
setTimeout(() => {
subscriber.next('');
subscriber.complete();
}, 100);
return;
}
// ============== AI 模式:正常流式回复 ==============
const historyDocs = await this.getHistory(sessionId);
const history = historyDocs
.map(
(item) => `用户:${item.userMessage}\n阿毕:${item.aiResponse}`,
)
.join('\n');
const safeKnowledge = JSON.stringify(MY_GAIBAN, null, 2) //MY_PERSONAL_PROFILE
.replace(/{/g, '【')
.replace(/}/g, '】');
const prompt = await this.ASSISTANT_PROMPT.format({
knowledge: safeKnowledge,
history,
input: message,
});
const stream = await this.aiService.streamChat(prompt);
let fullAIResponse = '';
for await (const chunk of stream) {
if (typeof chunk.content === 'string') {
const token = chunk.content;
fullAIResponse += token;
subscriber.next(token);
}
}
let finalText = fullAIResponse;
const keys = Object.keys(EMOJI_MAP);
const hasKeyword = keys.some((k) => finalText.includes(k));
if (hasKeyword && Math.random() > 0.5) {
for (const k of keys) {
if (finalText.includes(k)) {
finalText = finalText.replace(k, `${k}${EMOJI_MAP[k]}`);
break;
}
}
}
subscriber.next('\n' + finalText.slice(fullAIResponse.length));
// 保存并推送
const newMessage = await this.saveHistory(
sessionId,
message,
finalText,
false,
);
// 👇 实时推送
this.chatGateway.sendMessageToSession(sessionId, newMessage);
subscriber.complete();
} catch (err) {
console.error('聊天错误:', err);
subscriber.error(err);
}
})();
});
}
// ====================== 管理员:人工发送消息 ======================
async humanSendMessage(sessionId: string, adminId: string, content: string) {
const newMessage = await this.saveHistory(sessionId, '', content, true);
// 👇 实时推送
this.chatGateway.sendMessageToSession(sessionId, newMessage);
return { success: true, content };
}
// ====================== 用户:人工发送消息 ======================
async humanUserMessage(sessionId: string, message: string) {
const newMsg = await this.saveHistory(
sessionId,
message, // 用户消息
'', // 回复为空
false,
);
try {
// 包裹 try/catch,永远不会 500
this.chatGateway.sendMessageToSession(sessionId, newMsg);
} catch (err) {
console.error('发送人工消息错误:', err);
}
return newMsg;
}
// ====================== 切换到人工 ======================
async switchToHuman(sessionId: string, adminId?: string) {
await this.humanSessionModel.findOneAndUpdate(
{ sessionId },
{ status: 'human', adminId },
{ upsert: true },
);
return { success: true, msg: '已切换为人工服务' };
}
// ====================== 切换回AI ======================
async switchToAi(sessionId: string) {
await this.humanSessionModel.findOneAndUpdate(
{ sessionId },
{ status: 'ai', adminId: null },
{ upsert: true },
);
return { success: true, msg: '已切换回AI助手' };
}
// ====================== 获取会话状态 ======================
async getSessionStatus(sessionId: string) {
const s = await this.getOrCreateSession(sessionId);
return { status: s.status, adminId: s.adminId };
}
// ====================== 工具方法 ======================
private async getOrCreateSession(sessionId: string) {
let s = await this.humanSessionModel.findOne({ sessionId });
if (!s) s = await this.humanSessionModel.create({ sessionId });
return s;
}
async saveHistory(
sessionId: string,
userMessage: string,
aiResponse: string,
isHuman = false,
) {
return this.chatModel.create({
sessionId,
userMessage,
aiResponse,
isHuman,
});
}
async getHistory(sessionId: string) {
return this.chatModel.find({ sessionId }).sort({ createdAt: 1 });
}
async deleteSession(sessionId: string) {
await this.chatModel.deleteMany({ sessionId });
await this.humanSessionModel.deleteOne({ sessionId });
return { success: true };
}
// 会话列表
getAllSessions() {
return this.chatModel.aggregate([
{
$group: {
_id: '$sessionId',
lastMsg: { $last: '$userMessage' },
time: { $max: '$createdAt' },
},
},
{ $project: { sessionId: '$_id', lastMsg: 1, time: 1 } },
{ $sort: { time: -1 } },
]);
}
// 所有人工会话
getHumanSessions() {
return this.humanSessionModel.find({ status: 'human' }).populate('adminId');
}
}
(2)src/chat/chat.gateway.ts处理聊天过程中实时聊天的处理,运用websockets
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: true,
credentials: true,
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log('✅ 客户端已连接:', client.id);
}
handleDisconnect(client: Socket) {
console.log('❌ 客户端已断开:', client.id);
}
@SubscribeMessage('joinSession')
async handleJoinSession(client: Socket, sessionId: string) {
// 👈 加 async
try {
await client.join(sessionId); // 👈 加 await
console.log('📩 加入会话:', sessionId);
} catch (err) {
console.error('加入房间失败:', err);
}
}
// 推送新消息到会话
sendMessageToSession(sessionId: string, message: any) {
this.server.to(sessionId).emit('newMessage', message);
}
}
(3)src/chat/chat.controller.ts将聊天功能封装成接口便于前端调用
import {
Controller,
Get,
Delete,
Param,
Query,
Res,
UseGuards,
Post,
Body,
Request,
} from '@nestjs/common';
import { ChatService } from './chat.service';
import type { Response } from 'express';
import { JwtGuard } from '../auth/jwt.guard';
interface UserPayload {
id: string;
username: string;
}
@Controller('chat')
export class ChatController {
constructor(private chatService: ChatService) {}
// 用户聊天流
@Get('stream')
stream(
@Query('sessionId') sessionId: string,
@Query('message') message: string,
@Res() res: Response,
) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
this.chatService.streamMessage(sessionId, message).subscribe({
next: (token) => res.write(`data: ${token}\n\n`),
complete: () => {
res.write('data: [DONE]\n\n');
res.end();
},
error: () => res.end(),
});
}
// ====================== 【用户】请求转人工 ======================
@Get('request-human')
async requestHuman(@Query('sessionId') sessionId: string) {
return this.chatService.switchToHuman(sessionId);
}
// ====================== 【管理员】登录后可用 ======================
@UseGuards(JwtGuard)
@Post('human-send')
humanSend(
@Body('sessionId') sessionId: string,
@Body('content') content: string,
@Request() req: { user: UserPayload },
) {
return this.chatService.humanSendMessage(sessionId, req.user.id, content);
}
// ====================== 【用户】人工发送消息 ======================
@Post('user-send-message')
async userSendMessage(@Body() body: { sessionId: string; message: string }) {
return this.chatService.humanUserMessage(body.sessionId, body.message);
}
// 会话状态
@Get('session-status/:sessionId')
async getSessionStatus(@Param('sessionId') sessionId: string) {
return this.chatService.getSessionStatus(sessionId);
}
// @UseGuards(JwtGuard)
@Get('switch-human/:sessionId')
switchToHuman(
@Param('sessionId') sessionId: string,
// @Request() req: { user: UserPayload },
) {
return this.chatService.switchToHuman(sessionId);
}
// @UseGuards(JwtGuard)
@Get('switch-ai/:sessionId')
switchToAi(@Param('sessionId') sessionId: string) {
return this.chatService.switchToAi(sessionId);
}
@UseGuards(JwtGuard)
@Get('human-sessions')
getHumanSessions() {
return this.chatService.getHumanSessions();
}
// 原有接口
@Get('history/:sessionId')
getHistory(@Param('sessionId') sessionId: string) {
return this.chatService.getHistory(sessionId);
}
@Delete('delete/:sessionId')
deleteSession(@Param('sessionId') sessionId: string) {
return this.chatService.deleteSession(sessionId);
}
@Get('sessions')
getAllSessions() {
return this.chatService.getAllSessions();
}
@Get('status/:sessionId')
getStatus(@Param('sessionId') sessionId: string) {
return this.chatService.getSessionStatus(sessionId);
}
}
(4)src/chat/chat.module.ts将聊天模块导出
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
import { Chat, ChatSchema } from '../schemas/chat.schema';
import { AiModule } from '../ai/ai.module';
import { ChatGateway } from './chat.gateway';
import {
HumanSession,
HumanSessionSchema,
} from '../schemas/human-session.schema';
@Module({
imports: [
MongooseModule.forFeature([
{ name: Chat.name, schema: ChatSchema },
{ name: HumanSession.name, schema: HumanSessionSchema },
]),
AiModule,
],
controllers: [ChatController],
providers: [ChatService, ChatGateway],
exports: [ChatGateway],
})
export class ChatModule {}
五、jwt处理模块(后台管理员登录准备)
(1)arc/auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('JWT_SECRET') || 'bsq_ai_secret',
ignoreExpiration: false,
passReqToCallback: false,
});
}
validate(payload: { sub: string; username: string }) {
return {
id: payload.sub,
username: payload.username,
};
}
}
(2)src/auth/jwt.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}
(3)src/auth/auth.module.ts将此模块导出
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET', 'bsq_ai_secret'),
signOptions: { expiresIn: '7d' },
}),
inject: [ConfigService],
}),
],
providers: [JwtStrategy],
exports: [JwtModule],
})
export class AuthModule {}
六、管理员登录模块
因为这个聊天系统就是任何用户都可以用,所以就只有管理员登录后台查看回复一下这个,就只做了后台管理员的登录
(1)src/admin/dto/login.dto.ts
export class LoginDto {
username: string;
password: string;
}
(2)src/admin/admin.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import * as bcrypt from 'bcryptjs';
import { Admin, AdminDocument } from '../schemas/admin.schema';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AdminService {
constructor(
@InjectModel(Admin.name) private adminModel: Model<AdminDocument>,
private jwtService: JwtService,
) {}
// 登录
async login(loginDto: LoginDto) {
const { username, password } = loginDto;
const admin = await this.adminModel.findOne({ username });
if (!admin) throw new UnauthorizedException('账号不存在');
const isMatch = await bcrypt.compare(password, admin.password);
if (!isMatch) throw new UnauthorizedException('密码错误');
const token = this.jwtService.sign({
sub: admin._id.toString(), // 转字符串,避免类型问题
username: admin.username,
});
return {
admin: { id: admin._id.toString(), username: admin.username },
token,
};
}
// 初始化超级管理员(第一次运行自动创建)
async initAdmin() {
const exist = await this.adminModel.findOne({ username: 'admin' });
if (!exist) {
const hash = await bcrypt.hash('123456', 10);
await this.adminModel.create({ username: 'admin', password: hash });
console.log('✅ 初始管理员创建成功:admin / 123456');
}
}
}
(3)src/admin/admin.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AdminService } from './admin.service';
import { LoginDto } from './dto/login.dto';
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post('login')
login(@Body() loginDto: LoginDto) {
return this.adminService.login(loginDto);
}
}
(4)src/admin/admin.module.ts将此模块导出
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AdminService } from './admin.service';
import { AdminController } from './admin.controller';
import { Admin, AdminSchema } from '../schemas/admin.schema';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [
MongooseModule.forFeature([{ name: Admin.name, schema: AdminSchema }]),
AuthModule,
],
controllers: [AdminController],
providers: [AdminService],
exports: [AdminService],
})
export class AdminModule {}
七、实体类
(1)src/schemas/chat.schema.ts聊天表
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type ChatDocument = Chat & Document;
@Schema({ timestamps: true })
export class Chat {
@Prop({ required: true })
sessionId: string;
@Prop({ required: false })
userMessage: string;
@Prop({ required: false })
aiResponse: string;
// 新增:是否人工回复
@Prop({ default: false })
isHuman: boolean;
}
export const ChatSchema = SchemaFactory.createForClass(Chat);
(2)src/schemas/admin.schema.ts管理员
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type AdminDocument = Admin & Document;
@Schema({ timestamps: true })
export class Admin {
@Prop({ required: true, unique: true })
username: string;
@Prop({ required: true })
password: string;
}
export const AdminSchema = SchemaFactory.createForClass(Admin);
(3)src/schemas/human-session.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type HumanSessionDocument = HumanSession & Document;
@Schema({ timestamps: true })
export class HumanSession {
@Prop({ required: true, unique: true })
sessionId: string;
// 状态:ai / human
@Prop({ default: 'ai' })
status: 'ai' | 'human';
// 当前接待的管理员ID
@Prop({ default: null })
adminId: string;
}
export const HumanSessionSchema = SchemaFactory.createForClass(HumanSession);
八、将各模块汇总
(1)src/app.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import { ChatModule } from './chat/chat.module';
import { AiModule } from './ai/ai.module';
import { AdminModule } from './admin/admin.module';
import { AuthModule } from './auth/auth.module';
import { AdminService } from './admin/admin.service';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
MongooseModule.forRoot(process.env.MONGODB_URI!),
AuthModule,
AdminModule,
ChatModule,
AiModule,
],
})
export class AppModule implements OnModuleInit {
constructor(private adminService: AdminService) {}
async onModuleInit() {
await this.adminService.initAdmin();
}
}
(2)main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
最后npm run start:dev运行
至此!一个简易的ai助手完成
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)