☰
NestJs+MongoDB+Deepseek+Langchain 实现 AI 聊天助手:TaoToken 统一 Key 接入与配置骨架
2026/9/26 16:05:08 网站建设 项目流程

1. 为什么要在 NestJs 里给 Deepseek 加一层统一 Key

做 AI 聊天助手,最容易被低估的不是模型效果,而是凭证管理。你一开始可能只在.env里写一个DEEPSEEK_API_KEY,跑得挺顺。等到项目里同时出现 Deepseek、Claude、GPT 多个模型,或者前端、后端、定时任务、Agent 各要一份 Key 时,麻烦就来了:Key 散落在多个文件、换模型要改代码、额度用超了不知道是哪个服务烧的、团队协作时还得把 Key 发来发去。

我这次要搭的是一个 NestJs + MongoDB + Deepseek + Langchain 的 AI 聊天助手,核心诉求有三个:第一,模型凭证统一走 TaoToken 的 API 通道,业务代码只认一个 baseURL 和一个 Key;第二,会话上下文持久化到 MongoDB,刷新页面还能接着聊;第三,配置骨架可复制,换模型只改配置不改逻辑。

TaoToken 在这里扮演的角色是「统一 Key / API 通道管理」。你可以把它理解成一个模型调用的统一入口:官网在 https://taotoken.net,API 端点是 https://taotoken.net/api。它兼容 OpenAI 风格的接口协议,所以 Langchain 的ChatOpenAI可以直接对接,不用为每个模型写一套 SDK。对 NestJs 这种模块化框架来说,这意味着AiModule只需要维护一份配置,ChatModule只管业务,职责非常干净。

这篇文章适合谁:已经会一点 NestJs、想跑通带持久化上下文的聊天接口的后端同学;正在纠结多模型 Key 怎么管的开发者;以及想用 Langchain 但不想被各家 SDK 差异折腾的人。下面从环境准备一路写到端到端验证,配置骨架可以直接抄。

2. TaoToken 前置:拿 Key、认端点、定模型名

在写代码之前,先把凭证和端点确认清楚,这一步做扎实,后面能省掉大量 401 和 404 排查。

2.1 获取 API Key

登录 TaoToken 控制台,在 API Keys 页面创建一个新的 Key。建议按用途命名,比如nest-chat-dev,方便后续区分开发和生产。创建后立刻复制保存,页面刷新后通常不再完整显示。

控制台地址:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console

API Keys 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys

2.2 确认 API 端点与模型名

TaoToken 的 API 基础地址是https://taotoken.net/api,注意这里不加 UTM 参数,它是给程序调用的。Langchain 的ChatOpenAI需要的是baseURL,填这个地址即可。

模型名方面,Deepseek 对话模型一般用deepseek-chat。如果你不确定当前账号下有哪些可用模型,可以先用模型对话页面手动发一条消息验证,确认模型名和额度都正常,再写进代码。

模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat

2.3 环境与依赖版本

本地需要 Node 20+、MongoDB 8.0。NestJs 用 11.x,Langchain 用 1.x 系列。关键依赖如下:

{ "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/mongoose": "^11.0.4", "@nestjs/platform-express": "^11.0.1", "langchain": "^1.3.0", "mongoose": "^9.4.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" } }

这里没有引入 Deepseek 官方 SDK,因为走 TaoToken 的 OpenAI 兼容通道后,@langchain/openai一个包就够了。少一个依赖,就少一处版本冲突。

3. 可复制配置骨架:settings.json 与 config.toml

配置这块我给了两种格式,你可以按团队习惯选。NestJs 项目本身用.env最顺,但如果你有跨语言服务或需要结构化配置,settings.json和config.toml更清晰。

3.1 .env 版本(NestJs 直接可用)

# 服务端口 PORT=3000 # MongoDB 8.0 MONGODB_URI=mongodb://localhost:27017/ai_chat # TaoToken 统一通道 TAOTOKEN_API_KEY=sk-你的TaoToken密钥 TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=deepseek-chat # CORS CORS_ORIGIN=http://localhost:5173,http://localhost:8080

注意TAOTOKEN_BASE_URL结尾不要带/v1,Langchain 的ChatOpenAI会自己拼接路径。这一点踩过坑:多写一层/v1会变成/v1/v1/chat/completions,直接 404。

3.2 settings.json 版本

{ "server": { "port": 3000, "corsOrigin": ["http://localhost:5173", "http://localhost:8080"] }, "mongodb": { "uri": "mongodb://localhost:27017/ai_chat" }, "taotoken": { "apiKey": "sk-你的TaoToken密钥", "baseUrl": "https://taotoken.net/api", "model": "deepseek-chat", "temperature": 0.7, "maxTokens": 1024 } }

3.3 config.toml 版本

[server] port = 3000 cors_origin = ["http://localhost:5173", "http://localhost:8080"] [mongodb] uri = "mongodb://localhost:27017/ai_chat" [taotoken] api_key = "sk-你的TaoToken密钥" base_url = "https://taotoken.net/api" model = "deepseek-chat" temperature = 0.7 max_tokens = 1024

三种格式字段含义一致,核心就四个:apiKey、baseUrl、model、生成参数。把模型凭证收敛到这一处,后面换模型只改model字段。

3.4 AiModule 配置骨架

import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AiService } from './ai.service'; @Module({ imports: [ConfigModule], providers: [AiService], exports: [AiService], }) export class AiModule {}

AiService负责把配置转成 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>('TAOTOKEN_API_KEY')!; const baseURL = this.configService.get<string>('TAOTOKEN_BASE_URL')!; const model = this.configService.get<string>('TAOTOKEN_MODEL')!; this.llm = new ChatOpenAI({ apiKey, model, streaming: true, temperature: 0.7, maxTokens: 1024, configuration: { baseURL }, }); } async streamChat(prompt: string) { return this.llm.stream([['user', prompt]]); } }

这里configuration.baseURL就是 TaoToken 通道的接入点。业务层完全不知道底层是 Deepseek 还是别的模型,只调用streamChat。

4. MongoDB 会话存储字段设计

聊天助手要「记得住」,就得把每轮对话落库。我设计了两张表:Chat存消息明细,HumanSession存会话状态(AI 模式还是人工模式)。这样后续要加人工客服接管,不用改消息表结构。

4.1 Chat 消息表

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, index: true }) sessionId: string; @Prop({ required: false }) userMessage: string; @Prop({ required: false }) aiResponse: string; @Prop({ default: false }) isHuman: boolean; } export const ChatSchema = SchemaFactory.createForClass(Chat);

字段说明:sessionId建索引,因为查询历史永远按会话过滤;userMessage和aiResponse分开存,方便前端左右气泡渲染;isHuman标记这条回复是否来自人工,为后续接管留口子;timestamps自动生成createdAt,排序历史时直接用它。

4.2 HumanSession 会话状态表

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; @Prop({ default: 'ai' }) status: 'ai' | 'human'; @Prop({ default: null }) adminId: string; } export const HumanSessionSchema = SchemaFactory.createForClass(HumanSession);

status只有两个值,ai表示正常走模型,human表示已转人工。adminId记录接管的管理员,方便审计。

4.3 上下文拼装逻辑

每次请求进来,先按sessionId查历史,按createdAt升序拼成对话文本,再和当前问题一起塞进 prompt:

async getHistory(sessionId: string) { return this.chatModel.find({ sessionId }).sort({ createdAt: 1 }); } private buildHistoryText(docs: ChatDocument[]) { return docs .map((item) => `用户:${item.userMessage}\n助手:${item.aiResponse}`) .join('\n'); }

这里有个细节:历史不能无限拼。Deepseek 的上下文窗口有限,我一般只取最近 20 条,或者按字符数截断到 6000 字以内。否则聊久了 token 消耗会失控。

5. 端到端验证:跑通带持久化上下文的聊天接口

配置和存储都就位后,用一个最小闭环验证:发消息 → 模型流式返回 → 落库 → 再发一条能带上文。

5.1 ChatService 核心流式逻辑

import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Observable } from 'rxjs'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { AiService } from '../ai/ai.service'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, private aiService: AiService, ) {} streamMessage(sessionId: string, message: string): Observable<string> { return new Observable((subscriber) => { void (async () => { try { const historyDocs = await this.getHistory(sessionId); const history = this.buildHistoryText(historyDocs); const prompt = `以下是历史对话:\n${history}\n用户现在问:${message}`; const stream = await this.aiService.streamChat(prompt); let full = ''; for await (const chunk of stream) { if (typeof chunk.content === 'string') { full += chunk.content; subscriber.next(chunk.content); } } await this.chatModel.create({ sessionId, userMessage: message, aiResponse: full, isHuman: false, }); subscriber.complete(); } catch (err) { subscriber.error(err); } })(); }); } async getHistory(sessionId: string) { return this.chatModel.find({ sessionId }).sort({ createdAt: 1 }).limit(20); } private buildHistoryText(docs: ChatDocument[]) { return docs .map((item) => `用户:${item.userMessage}\n助手:${item.aiResponse}`) .join('\n'); } }

5.2 Controller 暴露 SSE 接口

import { Controller, Get, Query, Res } from '@nestjs/common'; import type { Response } from 'express'; import { ChatService } from './chat.service'; @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'); 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(), }); } }

5.3 验证动作

启动服务:

npm run start:dev

第一条请求,问一个需要记忆的问题:

curl -N "http://localhost:3000/chat/stream?sessionId=test-001&message=我叫小明,喜欢喝美式"

你应该看到 SSE 流式返回,最后以data: [DONE]结束。接着第二条,同一个sessionId:

curl -N "http://localhost:3000/chat/stream?sessionId=test-001&message=我刚才说我叫什么?喜欢喝什么?"

如果模型回答里出现「小明」和「美式」,说明上下文持久化生效了。再去 MongoDB 里查一下:

mongosh use ai_chat db.chats.find({ sessionId: "test-001" }).sort({ createdAt: 1 })

应该能看到两条记录,每条都有userMessage和aiResponse。到这一步,带持久化上下文的聊天接口就跑通了。

6. 本篇常见错排查

6.1 401 Unauthorized

最常见的原因是 Key 没读到。检查.env里变量名和configService.get的字符串是否完全一致,大小写敏感。另一个原因是 Key 前后有空格,复制时容易带上。可以在AiService构造里打印apiKey.slice(0, 6)确认前缀。

6.2 404 Not Found

八成是baseURL写错了。正确值是https://taotoken.net/api,不要加/v1,不要加结尾斜杠。Langchain 内部会拼/chat/completions。如果你用的是settings.json,确认 JSON 里没有多余逗号导致解析失败。

6.3 模型名不识别

model字段要和 TaoToken 通道支持的名称一致。先用模型对话页面手动验证一次,确认能正常返回,再把模型名抄进配置。不要凭记忆写。

6.4 上下文丢失

如果第二条消息模型不记得上文,先查 MongoDB 里有没有落库。没有记录说明chatModel.create没执行,可能是流式循环里抛错被吞了。有记录但模型不记得,检查getHistory的排序和limit,以及buildHistoryText是否真的把历史拼进了 prompt。

6.5 流式返回卡住不结束

SSE 接口忘记res.end(),或者subscriber.complete()没触发。检查for await循环是否正常退出。另外 NestJs 默认的响应超时也可能干扰,长连接场景建议在 Controller 里显式设置 header。

6.6 MongoDB 连接失败

确认本地 MongoDB 8.0 已启动,MONGODB_URI里的库名和端口正确。如果用了 Docker,注意容器网络里localhost指向容器自身,要换成宿主机地址或服务名。

7. 下一步:把统一 Key 用到 Coding 与 Agent 场景

跑通这个聊天助手后,你会发现 TaoToken 统一 Key 的价值不止在聊天。同一套凭证可以复用到代码补全、Agent 工具调用等场景。如果你打算把模型接进日常编码流程,可以看看 Coding Plan,它把模型调用和编码工作流结合得更紧:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan

接入文档里有各语言和框架的对接示例,NestJs 之外的服务也能参考:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc

如果你在配置过程中遇到 401 或 404,优先回到 API Keys 页面确认 Key 状态,再对照接入文档检查 baseURL:

https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys

我自己的习惯是:每接一个新模型,先用模型对话页面发一条「你好」确认通道通,再写进代码。这一步花 30 秒,能省掉半小时的排查。配置骨架抄完之后,先把sessionId固定成test-001跑两条 curl,确认上下文和落库都正常,再往前端接。这样出问题时,你能立刻判断是模型通道的问题还是业务逻辑的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询