🐳 NestJS × LangChain AI 知识库问答实战教程

DeepSeek 版 + 通义版 双轨整合 · NestJS 11 → LangChain.js → pgvector RAG → 综合项目上线

2教程版本
11选项卡
108学习节
8可运行代码工程
190K字符教程

📊 我的学习进度 本地自动保存 · 数据不出浏览器

加载中…
D0 · 前置环境搭建0/0
D1 · NestJS 工程化0/0
D2 · LangChain 服务0/0
D3 · pgvector RAG0/0
D4 · 综合实战项目0/0
D5 · 教程编排计划0/0
Q1 · NestJS 工程化0/0
Q2 · LangChain 服务0/0
Q3 · pgvector RAG0/0
Q4 · 综合实战项目0/0
💡 用法:顶部选项卡切换「总览 / D0~D5(DeepSeek 版)/ Q1~Q4(通义版)」;每个小节卡片右上角「✓ 标记已学」记录进度;重开页面自动回到上次的篇。

⚖️ 两个版本怎么选 整合说明

本手册由 tutorial-dsv4(DeepSeek 优先版,D 系列)与 tutorial-qw3.8(通义/新手友好版,Q 系列)两套教程合并而成, 同一门课:NestJS 11 + LangChain.js 1.x + PostgreSQL 16 / pgvector,从零搭一个可上线的 AI 知识库问答系统。

阶段🐳 DeepSeek 版(D)🐫 通义版(Q)
0 环境D0 前置环境搭建(专篇)并入各篇前置章节
1 NestJS 工程化D1Q1
2 LangChain 大模型服务D2Q2
3 pgvector 向量检索 RAGD3Q3
4 综合实战上线D4Q4
附加D5 教程编写计划(决策全记录)每节固定七段结构:目标→原理→最小示例→完整实现→验证→坑点→练习

选版建议:纯新手先走 Q 版(讲解更细、统一课节结构);主力用 DeepSeek/字节模型或对决策过程感兴趣读 D 版(含 00 环境专篇与 plan 全案)。两版代码目录互相独立,均可运行。

📚 配套学习路线可交叉参阅:AI Agent 开发学习手册(171 天)

🐳 tutorial-dsv4 · 原版 README DeepSeek 版

从零到一,用 NestJS + LangChain.js + PostgreSQL/pgvector 搭建一个完整可上线的 AI 知识库问答后端系统。

全部使用开源组件,大模型优先适配国产模型(DeepSeek、通义千问、字节 Seed),统一走 OpenAI 兼容协议接口。


一、教程能学到什么

阶段 主题 交付物
阶段 1 NestJS 后端工程化全流程 01-nestjs-complete-guide.md + code/01-nest-basic/
阶段 2 LangChain 大模型服务开发 02-langchain-ai-service.md + code/02-langchain-module/
阶段 3 PostgreSQL 进阶与向量检索 RAG 03-postgresql-vector-rag.md + code/03-pg-vector-rag/
阶段 4 综合实战:AI 知识库问答系统 04-full-project.md + code/04-full-project/

学习顺序:严格按 1 → 2 → 3 → 4 递进,每阶段独立可运行。前序阶段产出是后续的依赖骨架。


二、技术栈

层 技术 版本
运行时 Node.js 22 LTS(LangChain 1.x 硬性要求)
后端框架 NestJS(Express 引擎) 11.x
语言 TypeScript(strict,禁用 any) 5.7+
ORM TypeORM 0.3.x
数据库 PostgreSQL 16.x
向量插件 pgvector 0.8.x
大模型框架 LangChain.js 1.x 最新线
大模型 DeepSeek / 通义千问 / 字节 Seed OpenAI 兼容协议
嵌入模型 通义千问 text-embedding-v3 DashScope 兼容端点

三、快速开始

1. 准备环境

先阅读 00-environment-setup.md,完成 Node 22、PostgreSQL 16、pgvector 的安装,并(可选)申请国产大模型 API Key。

2. 运行某个阶段

以阶段 1 为例:

cd tutorial/code/01-nest-basic
cp .env.example .env        # 按需修改数据库连接
npm install
npm run start:dev           # 开发模式启动

启动后:

  • Swagger 文档:http://localhost:3000/api-docs
  • 各接口 curl 测试命令见每个 code/*/README.md

说明:本教程大模型调用采用 Mock LLM 兜底,无 API Key 也能离线跑通全部链路;配置真实 Key 后一键切换到真实模型。


四、目录结构

tutorial/
├── plan.md                      # 教程总纲(本计划)
├── README.md                    # 本文件
├── 00-environment-setup.md      # 前置环境搭建
├── 01-nestjs-complete-guide.md  # 阶段1文档
├── 02-langchain-ai-service.md   # 阶段2文档
├── 03-postgresql-vector-rag.md  # 阶段3文档
├── 04-full-project.md           # 阶段4文档
└── code/
    ├── 01-nest-basic/
    ├── 02-langchain-module/
    ├── 03-pg-vector-rag/
    └── 04-full-project/

五、学习时长参考

阶段 业余 全职
阶段 1 1 周 3–4 天
阶段 2 1 周 4–5 天
阶段 3 5–7 天 3–4 天
阶段 4 1 周 4–5 天
合计 约 4 周 约 2 周

六、常见坑点速览

  1. LangChain 1.x 大改版:旧资料里的 .call()/.run()/langchain/chains 写法已废弃,本教程统一 1.x 范式(.invoke()/.stream()/LCEL .pipe())。
  2. Node 必须 22+:LangChain 1.x 已放弃 Node 18,环境不对会安装/运行报错。
  3. DeepSeek 无 embedding:向量化使用通义千问 text-embedding-v3,聊天模型与嵌入模型需分开配置。
  4. TypeORM 不原生支持 vector 类型:需自定义列类型 + pg 原生 SQL 做距离检索。
  5. 更完整的风险清单见 plan.md 第 7 节。

七、许可与说明

  • 全部依赖为开源组件,无付费商业组件。
  • 教程仅供学习交流,生产部署请自行做好密钥管理与安全加固。

🐫 tutorial-qw3.8 · 原版 README 通义版

一套面向初学者的中文教程,从零搭建到完整上线一个 AI 知识库问答系统。全程配套可运行代码,每个知识点遵循统一结构:

🎯 学习目标 → 📖 原理讲解 → 🔨 最小示例 → 🏗️ 完整实现 → ✅ 验证步骤 → ⚠️ 常见坑点 → 📝 课后练习

学习路径

阶段 1 NestJS 工程化 ──► 阶段 2 LangChain 服务化 ──► 阶段 3 向量检索与 RAG ──► 阶段 4 综合项目
   (后端地基)            (AI 能力层)                 (数据与检索层)           (全部整合上线)
阶段 主题 文档 代码目录 建议时长(每天 2~3 小时)
1 NestJS 后端工程化全流程 docs/01-nestjs-complete-guide.md code/01-nest-basic/ 4~5 天
2 LangChain 大模型服务开发 docs/02-langchain-ai-service.md code/02-langchain-module/ 4~5 天
3 PostgreSQL 进阶与向量检索 RAG docs/03-postgresql-vector-rag.md code/03-pg-vector-rag/ 3~4 天
4 综合实战:AI 知识库问答系统 docs/04-full-project.md code/04-full-project/ 4~6 天

请严格按 1 → 2 → 3 → 4 的顺序学习。每个阶段项目独立可运行,学完当阶段即可单独启动验证。合计约 2~3 周;有 NestJS 经验者可压缩到 1~2 周。

学习建议

  • 阶段 1:每节先跑通最小示例再做练习;JWT 与 TypeORM 两节是后续地基,务必吃透。
  • 阶段 2:先配好一个真实 API Key;MCP 与 Skill 两节概念多,配合 curl 反复实验。
  • 阶段 3:先用 Docker 起 pgvector 环境;重点理解"向量维度一致性"与索引参数。
  • 阶段 4:按文档顺序逐模块启动,最后走一遍 Docker 部署全流程。

学习者画像

  • 会基础 JavaScript/TypeScript 语法,了解 HTTP 与 REST 概念
  • 不要求有 NestJS / LangChain / PostgreSQL 经验(教程从零讲起)
  • 不要求有 AI/机器学习背景(RAG 原理用类比讲清)

前置环境安装

1. Node.js(≥ 20 LTS,推荐 22)

# macOS
brew install node@22
# 或使用 nvm(推荐,便于多版本管理)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 22 && nvm use 22

# 验证
node -v   # >= v20
npm -v

2. PostgreSQL 16 + pgvector

方式 A(推荐):Docker 一键启动,镜像自带 pgvector 扩展,免去编译麻烦:

docker run -d --name tutorial-pg \
  -e POSTGRES_USER=tutorial \
  -e POSTGRES_PASSWORD=tutorial123 \
  -e POSTGRES_DB=tutorial \
  -p 5432:5432 \
  pgvector/pgvector:pg16

方式 B:brew 本地安装(macOS):

brew install postgresql@16 pgvector
brew services start postgresql@16

方式 C:Linux 源码编译 pgvector(需先装 postgresql-server-dev-16、build-essential):

git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector && make && sudo make install

验证 pgvector(任一方式安装后执行):

# 进入 psql(Docker 方式)
docker exec -it tutorial-pg psql -U tutorial -d tutorial

# 在 psql 中执行
CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- 输出版本号(如 0.8.0)即安装成功
\q

3. NestJS CLI

npm i -g @nestjs/cli
nest -v   # 应输出 11.x

4. 大模型 API Key(阶段 2 起需要)

本教程统一走 OpenAI 兼容协议,以下任一平台均可(按自己情况选一个即可):

平台 Base URL 推荐模型
DeepSeek https://api.deepseek.com deepseek-chat / deepseek-reasoner
阿里云百炼(通义千问) https://dashscope.aliyuncs.com/compatible-mode/v1 qwen-plus / qwen-turbo / text-embedding-v3
火山方舟(字节 Seed) https://ark.cn-beijing.volces.com/api/v3 doubao 系列

没有 API Key 也能学:阶段 3 的向量检索支持本地 Embedding 模型离线跑通,详见各阶段 README。

5. Docker(阶段 4 部署需要,可选)

从 docker.com 安装 Docker Desktop 即可。

常见问题(环境篇)

Q:5432 端口被占用?

# 查看占用进程
lsof -i :5432
# 或让 Docker 容器改用其他端口:-p 5433:5432,同时修改项目 .env 中 DB_PORT=5433

Q:brew 安装的 postgresql 版本冲突?

brew services stop postgresql@14   # 停掉旧版本
brew services start postgresql@16

Q:各阶段项目端口是多少?

为避免同时运行时冲突,四个项目默认端口已错开,均可通过 .env 的 PORT 覆盖:

项目 默认端口
01-nest-basic 3101
02-langchain-module 3102
03-pg-vector-rag 3103
04-full-project 3104

目录结构

tutorial/
├── README.md                  # 本文件:学习路径与环境安装
├── docs/                      # 四个阶段的教程文档
│   ├── 01-nestjs-complete-guide.md
│   ├── 02-langchain-ai-service.md
│   ├── 03-postgresql-vector-rag.md
│   └── 04-full-project.md
└── code/                      # 四个阶段的配套代码(各自独立可运行)
    ├── 01-nest-basic/
    ├── 02-langchain-module/
    ├── 03-pg-vector-rag/
    └── 04-full-project/

快速开始

# 以阶段 1 为例
cd code/01-nest-basic
cp .env.example .env       # 按需修改数据库连接
npm install
npm run start:dev          # 启动开发服务器
# 另开终端,按 README 中的 curl 命令逐个验证接口

祝学习顺利!遇到报错不要慌——每章的"常见坑点"小节都列出了原始报错信息与解决方法。

🧭 前置环境搭建 · 篇首 DeepSeek 版 · tutorial-dsv4

本教程四个阶段的代码都要依赖以下环境。请先按本文完成安装,再进入各阶段。


📌 1. Node.js 22+

为什么必须 22+:LangChain.js 1.x 已放弃 Node 18(Node 18 于 2025 年 3 月 EOL),安装或运行低于 22 的版本会直接报错。本教程统一使用 Node 22 LTS。

macOS(推荐 nvm)

# 安装 nvm(若已安装可跳过)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# 重新加载 shell 配置
source ~/.zshrc

# 安装并启用 Node 22
nvm install 22
nvm use 22
nvm alias default 22

验证

node -v   # 期望 v22.x.x
npm -v    # 期望 10.x

若 node -v 显示的不是 22,用 nvm use 22 切换后再继续。


📌 2. PostgreSQL 16

macOS(Homebrew)

brew install postgresql@16
brew services start postgresql@16      # 开机自启并立即启动

注意:postgresql@16 是 keg-only 安装,默认不会软链到 PATH。如需在终端直接使用 psql,执行:

echo 'export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Linux(Ubuntu/Debian)

sudo apt update
sudo apt install -y postgresql-16
sudo systemctl enable postgresql
sudo systemctl start postgresql

创建数据库与账号

# 进入 psql(macOS 默认超级用户是你的系统用户名)
psql postgres

# 在 psql 内执行:
CREATE USER tutorial WITH PASSWORD 'tutorial123';
CREATE DATABASE tutorial OWNER tutorial;
\q

教程所有阶段的 .env 默认连接串: postgres://tutorial:tutorial123@localhost:5432/tutorial 请按你实际创建的用户名/密码/库名修改 .env。


📌 3. pgvector 向量插件

pgvector 让 PostgreSQL 支持向量存储与相似度检索,是阶段 3 / 4 的核心。

macOS(Homebrew,推荐)

brew install pgvector

⚠️ macOS + PostgreSQL 16 特别坑:新版 Homebrew 的 pgvector bottle(如 0.8.6)只预编译了 PostgreSQL 17/18 的扩展文件,不再包含 PG16。如果你的 PostgreSQL 是 16,CREATE EXTENSION vector 会报 extension "vector" is not available。此时必须源码编译:

brew install git make    # 确保有编译工具(Xcode CLT)
git clone --depth 1 --branch v0.8.6 https://github.com/pgvector/pgvector.git
cd pgvector
make PG_CONFIG=/opt/homebrew/opt/postgresql@16/bin/pg_config
make PG_CONFIG=/opt/homebrew/opt/postgresql@16/bin/pg_config install

Linux(Ubuntu/Debian)

sudo apt install -y postgresql-16-pgvector

源码编译(备选,当包管理器安装失败时)

git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install   # 需要 postgresql-server-dev(Linux)或 Xcode CLT(macOS)

在数据库中启用扩展

CREATE EXTENSION vector 需要超级用户权限(会安装 C 函数到 pg_catalog)。macOS 上 Homebrew 安装的超级用户是你的系统用户名,直接 psql -d tutorial 连接即可。

# macOS:直接用系统超级用户连接(无需 -U)
psql -d tutorial

# Linux:用 postgres 超级用户
# sudo -u postgres psql -d tutorial

在 psql 内执行:

CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname = 'vector';   -- 期望返回 0.8.x
\q

常见坑

现象 原因 解决
ERROR: extension "vector" has no installation script pgvector 未安装成功 重装 pgvector,确认 pg_config 指向 PostgreSQL 16
ERROR: could not open extension control file pgvector 与 PG 版本不匹配 用与 PostgreSQL 同版本的 pgvector 重新编译
brew install pgvector 装到了别的 PG 版本 存在多个 PG 版本 用源码编译并 make PG_CONFIG=/opt/homebrew/opt/postgresql@16/bin/pg_config install

📌 4. 国产大模型 API Key(可选)

本教程大模型调用用 Mock 兜底:不配置任何 Key 也能离线跑通全部链路。要体验真实大模型效果,任选一家申请:

厂商 申请入口 OpenAI 兼容 baseURL 常用模型
DeepSeek https://platform.deepseek.com https://api.deepseek.com/v1 deepseek-chat、deepseek-reasoner
阿里云通义千问 https://dashscope.console.aliyun.com https://dashscope.aliyuncs.com/compatible-mode/v1 qwen-turbo、qwen-plus、qwen-max
字节火山方舟 https://console.volcengine.com/ark https://ark.cn-beijing.volces.com/api/v3 doubao-*、deepseek-* 端点

重要:DeepSeek 官方不提供 embedding(向量化)接口,因此本教程的向量化统一使用通义千问 text-embedding-v3(与聊天模型共用 DashScope 兼容端点)。配置 .env 时注意聊天模型与嵌入模型分开配置:

# 聊天模型(示例:DeepSeek)
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-xxxx
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1

# 嵌入模型(通义千问)
EMBEDDING_API_KEY=sk-xxxx
EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDING_MODEL=text-embedding-v3

真实调用会产生流量费用,请留意各家计费规则。


📌 5. 环境自检清单

依次执行,全部通过即可开始学习:

node -v                                    # v22.x.x
psql --version                             # PostgreSQL 16.x
psql -U tutorial -d tutorial -c "SELECT extversion FROM pg_extension WHERE extname='vector';"   # 0.8.0

三项全部符合预期,即可进入 阶段 1:NestJS 后端工程化。

🧭 NestJS 工程化 · 篇首 DeepSeek 版 · tutorial-dsv4

配套代码:code/01-nest-basic/(完整可运行项目) 本章覆盖后端开发最常用的 9 大能力,每个知识点按「原理 → 代码 → 验证」展开。


📌 1. 环境搭建与项目初始化

1.1 原理

NestJS 通过官方 CLI(@nestjs/cli)脚手架项目。CLI 底层用 Webpack 打包,提供 generate(生成模块/控制器/服务)、build、start 等命令,让开发者专注于业务而非配置。

1.2 安装 CLI 与创建项目

# 全局安装 CLI(用 npm 或你偏好的包管理器)
npm i -g @nestjs/cli

# 创建项目(nest new 项目名)
nest new 01-nest-basic
# 选择 npm 作为包管理器,等待安装完成

1.3 启动命令

命令 说明
npm run start:dev 开发模式(监听文件变化自动重启,最常用)
npm run start:debug 调试模式(可断点)
npm run start:prod 生产模式(先 build 再运行)
npm run build 编译到 dist/
npm run lint ESLint 检查
npm run test / test:e2e 单元 / E2E 测试

1.4 目录结构详解

01-nest-basic/
├── package.json              # 依赖与脚本声明
├── tsconfig.json             # TS 编译配置(含 decorator 支持)
├── tsconfig.build.json       # 构建用(排除测试文件)
├── nest-cli.json             # Nest CLI 配置(sourceRoot 等)
├── .eslintrc.js / .prettierrc# 代码规范
├── .env.example              # 环境变量模板(需复制为 .env)
└── src/
    ├── main.ts               # 应用入口:装配全局管道/过滤器/Swagger/端口
    ├── app.module.ts         # 根模块:聚合所有功能模块
    ├── common/               # 通用能力:守卫/拦截器/过滤器/装饰器/中间件
    ├── config/               # 多环境配置与校验
    ├── entities/             # TypeORM 实体
    ├── migrations/           # 数据库迁移脚本
    └── modules/              # 业务功能模块(auth/users/upload/chat/health)
        ├── auth/             #   鉴权模块
        ├── users/            #   用户 CRUD 模块
        ├── upload/           #   文件上传下载模块
        ├── chat/             #   WebSocket 网关模块
        └── health/           #   健康检查模块

关键文件作用:

文件 作用
main.ts 创建 NestFactory 应用实例,注册全局中间件/管道/过滤器,监听端口
app.module.ts 根模块 @Module,通过 imports 挂载各功能模块,装配 TypeORM 连接
*.module.ts 模块定义:声明该模块的 controller/provider,导入依赖模块
*.controller.ts 路由层:处理 HTTP 请求、参数解析、返回响应
*.service.ts 业务层:被 Controller 注入调用,承载业务逻辑
*.entity.ts TypeORM 实体:映射数据库表
*.dto.ts 数据传输对象:定义入参结构 + 校验规则

📌 2. 核心概念:模块化 / DI / 分层

2.1 原理

  • 模块(Module):Nest 用 @Module 装饰器组织代码,是依赖注入的边界。每个功能放一个模块,imports 引入依赖、controllers 声明路由、providers 声明可注入对象、exports 对外暴露。
  • 依赖注入(DI):Nest 内置 IoC 容器。在构造函数声明类型,容器自动实例化并注入,解耦了「创建」与「使用」。
  • 分层:Controller(路由/参数)→ Service(业务)→ Repository(数据,TypeORM 提供),职责单一、可测试。

2.2 代码示例

// users.module.ts —— 模块定义
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from '../../entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])], // 注册实体,注入 Repository
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],                     // 供 auth 模块复用
})
export class UsersModule {}
// users.service.ts —— 通过构造函数注入 Repository
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.userRepo.find();
  }
}

2.3 验证

npm run start:dev
# 另开终端:
curl http://localhost:3000/users

📌 3. 配置管理:多环境与校验

3.1 原理

@nestjs/config 封装 dotenv,ConfigModule.forRoot() 按 NODE_ENV 加载不同 .env 文件(.env.development / .env.test / .env.production)。用 joi 或 class-validator 对配置做启动期校验,配置缺失/非法时立即报错,避免运行时才暴露问题。

3.2 代码示例

// config/configuration.ts —— 命名空间化配置
export default () => ({
  port: parseInt(process.env.PORT ?? '3000', 10),
  database: {
    host: process.env.DB_HOST ?? 'localhost',
    port: parseInt(process.env.DB_PORT ?? '5432', 10),
    username: process.env.DB_USER ?? 'tutorial',
    password: process.env.DB_PASSWORD ?? 'tutorial123',
    name: process.env.DB_NAME ?? 'tutorial',
  },
  jwt: {
    secret: process.env.JWT_SECRET ?? 'dev-secret',
    accessTtl: process.env.JWT_ACCESS_TTL ?? '15m',
    refreshTtl: process.env.JWT_REFRESH_TTL ?? '7d',
  },
});
// config/env.validation.ts —— Joi 校验
import * as Joi from 'joi';
export const validationSchema = Joi.object({
  NODE_ENV: Joi.string().valid('development', 'test', 'production').default('development'),
  PORT: Joi.number().default(3000),
  DB_HOST: Joi.string().required(),
  DB_PORT: Joi.number().default(5432),
  DB_USER: Joi.string().required(),
  DB_PASSWORD: Joi.string().required(),
  DB_NAME: Joi.string().required(),
  JWT_SECRET: Joi.string().required(),
});
// app.module.ts —— 装配
ConfigModule.forRoot({
  isGlobal: true,
  load: [configuration],
  validationSchema,
  envFilePath: [`.env.${process.env.NODE_ENV ?? 'development'}`, '.env'],
}),

3.3 验证

cp .env.example .env
npm run start:dev           # 若 DB_PASSWORD 缺失,启动即报错

📌 4. TypeORM 接入 PostgreSQL

4.1 原理

TypeORM 是 TS 生态最成熟的 ORM,通过装饰器把类映射成表,Repository 提供类型安全的 CRUD。@nestjs/typeorm 将其接入 Nest 的 DI 容器,TypeOrmModule.forFeature([Entity]) 后即可 @InjectRepository 注入。

4.2 实体定义

// entities/user.entity.ts
@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true, length: 100 })
  email: string;

  @Column({ select: false })       // 默认查询不返回密码
  password: string;

  @Column({ length: 50, nullable: true })
  nickname: string;

  @Column({ type: 'varchar', array: true, default: '{}' })
  roles: string[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

4.3 CRUD / 关联 / 事务 / 迁移

// Repository CRUD
await this.userRepo.save(this.userRepo.create(dto));   // 增
await this.userRepo.find();                            // 查全部
await this.userRepo.findOneBy({ id });                 // 查单个
await this.userRepo.update(id, dto);                   // 改
await this.userRepo.delete(id);                        // 删
// 事务:dataSource.transaction
await this.dataSource.transaction(async (manager) => {
  const user = await manager.save(User, userData);
  await manager.save(Profile, { userId: user.id, ...profileData });
});
# 迁移脚本(package.json 中配置了 typeorm 脚本)
npm run migration:generate -- src/migrations/Init   # 根据实体生成迁移
npm run migration:run                                # 执行迁移
npm run migration:revert                             # 回滚

4.4 验证

curl -X POST http://localhost:3000/users -H 'Content-Type: application/json' \
  -d '{"email":"a@b.com","password":"123456","nickname":"张三"}'
curl http://localhost:3000/users

📌 5. JWT 鉴权体系

5.1 原理

登录成功后签发 Access Token(短期,用于接口鉴权)+ Refresh Token(长期,用于换发新 Access)。JwtStrategy 校验 Access Token 并解析出用户信息挂到 request.user,JwtAuthGuard 拦截未携带有效 Token 的请求。@Roles 装饰器 + RolesGuard 实现接口级权限控制。

5.2 关键实现

// strategies/jwt.strategy.ts —— 校验 Access Token
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.get<string>('jwt.secret'),
    });
  }
  async validate(payload: JwtPayload) {
    return { id: payload.sub, email: payload.email, roles: payload.roles };
  }
}
// common/guards/jwt-auth.guard.ts —— 复用 Passport 守卫
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// common/decorators/public.decorator.ts —— 标记公开接口
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 使用:登录接口公开,用户列表需鉴权,删除需 admin 角色
@Public()
@Post('login')
login(@Body() dto: LoginDto) { ... }

@Get('profile')
getProfile(@Req() req) { return req.user; }        // 需登录

@Roles('admin')
@Delete(':id')
remove(@Param('id') id: string) { ... }            // 需 admin 角色

5.3 验证

# 注册
curl -X POST http://localhost:3000/auth/register -H 'Content-Type: application/json' \
  -d '{"email":"a@b.com","password":"123456","nickname":"张三"}'

# 登录,获取 accessToken / refreshToken
curl -X POST http://localhost:3000/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"a@b.com","password":"123456"}'

# 刷新
curl -X POST http://localhost:3000/auth/refresh -H 'Content-Type: application/json' \
  -d '{"refreshToken":"<上一步的 refreshToken>"}'

# 带 Token 访问受保护接口
curl http://localhost:3000/auth/profile -H 'Authorization: Bearer <accessToken>'

📌 6. 中间件 / 拦截器 / 过滤器 / 管道

6.1 原理与区别

组件 时机 典型用途
中间件 Middleware 路由处理之前 日志、跨域、鉴权前置、请求预处理
管道 Pipe 参数交给 Controller 之前 数据转换、校验(ValidationPipe)
拦截器 Interceptor 处理前后(AOP) 统一响应格式、性能计时、缓存
异常过滤器 Filter 抛异常之后 统一错误响应结构

执行顺序:Middleware → Guard → Interceptor(前) → Pipe → Controller → Interceptor(后) → Filter(若异常)。

6.2 关键实现

// common/middleware/logger.middleware.ts —— 记录请求耗时
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();
    res.on('finish', () =>
      console.log(`[${req.method}] ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`),
    );
    next();
  }
}
// common/interceptors/transform.interceptor.ts —— 统一响应 { code, data, message }
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map((data) => ({ code: 0, data, message: 'ok' })));
  }
}
// common/filters/http-exception.filter.ts —— 统一错误结构
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse<Response>();
    const status = exception instanceof HttpException ? exception.getStatus() : 500;
    res.status(status).json({ code: status, data: null, message: /* 提取错误信息 */ });
  }
}
// main.ts 装配
app.use(new LoggerMiddleware().use.bind(new LoggerMiddleware()));  // 或 app.use(logger)
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useGlobalInterceptors(new TransformInterceptor());
app.useGlobalFilters(new AllExceptionsFilter());

6.3 验证

# 观察日志中间件输出
curl http://localhost:3000/health

# 触发校验错误(缺少必填字段),观察统一错误结构
curl -X POST http://localhost:3000/users -H 'Content-Type: application/json' -d '{}'

📌 7. 文件上传下载

7.1 原理

Nest 内置 multer。@UploadedFile 配合 FileInterceptor 接收单文件,FilesInterceptor 接收多文件。用 MulterModule 或拦截器的 limits/fileFilter 限制大小与类型。下载/静态资源用 @nestjs/serve-static 的 ServeStaticModule。

7.2 关键实现

// upload.controller.ts
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
  limits: { fileSize: 5 * 1024 * 1024 },                  // 5MB
  fileFilter: (req, file, cb) => {                          // 仅允许图片
    if (!file.mimetype.match(/\/(jpeg|png|jpg|gif|webp)$/)) {
      return cb(new BadRequestException('仅支持图片文件'), false);
    }
    cb(null, true);
  },
}))
upload(@UploadedFile() file: Express.Multer.File) {
  return { filename: file.filename, originalname: file.originalname, size: file.size, url: `/static/${file.filename}` };
}
// app.module.ts —— 静态资源服务
ServeStaticModule.forRoot({
  rootPath: join(__dirname, '..', 'uploads'),
  serveRoot: '/static',
}),

7.3 验证

# 生成测试图并上传
curl -X POST http://localhost:3000/upload/upload -F "file=@./test.png"
# 访问返回的 url 即可下载/预览
curl -O http://localhost:3000/static/<filename>

📌 8. WebSocket 实时通信

8.1 原理

@nestjs/websockets + socket.io 提供 @WebSocketGateway 网关。服务端用 @SubscribeMessage 监听事件,用 @WebSocketServer 拿到 server 实例做房间管理与广播。握手时校验 Token 实现鉴权。

8.2 关键实现

@WebSocketGateway({ cors: { origin: '*' }, namespace: '/chat' })
export class ChatGateway implements OnGatewayConnection {
  @WebSocketServer() server: Server;

  // 握手鉴权
  async handleConnection(client: Socket) {
    const token = client.handshake.auth?.token ?? client.handshake.headers.authorization;
    if (!token || !this.jwtService.verify(token)) client.disconnect();
  }

  @SubscribeMessage('joinRoom')
  joinRoom(@MessageBody('room') room: string, @ConnectedSocket() client: Socket) {
    client.join(room);
    return { event: 'joined', data: room };
  }

  @SubscribeMessage('sendMessage')
  sendMessage(@MessageBody() payload: { room: string; message: string }, @ConnectedSocket() client: Socket) {
    this.server.to(payload.room).emit('newMessage', { from: client.id, message: payload.message });
  }
}

8.3 验证

# 用 socket.io 客户端或 curl 探测(生产可用 wscat)
npm i -g wscat
wscat -c "ws://localhost:3000/chat" --auth "Bearer <token>"
# 发送:{"event":"joinRoom","data":{"room":"room1"}}

📌 9. Swagger 接口文档

9.1 原理

@nestjs/swagger 用装饰器(@ApiTags/@ApiOperation/@ApiProperty/@ApiBearerAuth)从代码自动生成 OpenAPI 文档,SwaggerModule 挂载到路由。

9.2 关键实现

// main.ts
const config = new DocumentBuilder()
  .setTitle('NestJS 基础项目 API')
  .setDescription('阶段1完整示例')
  .setVersion('1.0')
  .addBearerAuth()
  .build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
// dto 示例
export class RegisterDto {
  @ApiProperty({ example: 'a@b.com' })
  @IsEmail()
  email: string;

  @ApiProperty({ example: '123456', minLength: 6 })
  @MinLength(6)
  password: string;
}

9.3 验证

启动后访问 http://localhost:3000/api-docs 即可查看并在线调试全部接口。


📌 附录:项目启动与完整 curl 测试

完整启动说明、.env 配置、以及覆盖全部接口的 curl 命令清单,见 code/01-nest-basic/README.md。

📁 配套代码目录 tutorial-dsv4/code/01-nest-basic

    01-nest-basic/
      .env
      .env.example
      .prettierrc
      README.md
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      src/
        app.module.ts
        main.ts
        common/
        config/
          configuration.ts
          data-source.ts
          env.validation.ts
        entities/
          user.entity.ts
        modules/

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 LangChain 服务 · 篇首 DeepSeek 版 · tutorial-dsv4

配套代码:code/02-langchain-module/(完整可运行模块) 在 NestJS 框架内模块化集成 LangChain.js,覆盖大模型后端开发的全部常用能力。


📌 0. 重要:LangChain.js 1.x 变化速览

本教程使用 LangChain.js 1.x 最新稳定线(@langchain/core ^1.2、@langchain/openai ^1.5、langchain ^1.5)。它相比旧版(0.x)有重大变化,网上大量旧资料已失效:

旧写法(0.x,已废弃) 1.x 写法
import { ... } from "langchain/chains" 旧 chains 迁至 @langchain/classic
model.call() / model.predict() model.invoke()
ChatPromptTemplate.fromPromptMessages() ChatPromptTemplate.fromMessages()
Runnable.bind() .bindTools() 等专用方法
retriever getRelevantDocuments() .invoke()
Node 18 可运行 要求 Node 22+

包结构:核心抽象在 @langchain/core;OpenAI 兼容模型在 @langchain/openai;Agent 运行时在 langchain + @langchain/langgraph。


📌 1. LangChain 基础与 Nest 模块化封装

1.1 原理

LangChain 的核心抽象:

  • Model(模型):BaseChatModel,统一 .invoke() / .stream() 接口。
  • Message(消息):SystemMessage / HumanMessage / AIMessage / ToolMessage,从 @langchain/core/messages 导入。
  • Prompt(提示词):ChatPromptTemplate.fromMessages()。
  • LCEL(LangChain Expression Language):用 .pipe() 组合成 Runnable 链。
  • Tool(工具):tool() 工厂 + zod schema 定义结构化入参。

在 Nest 中,用 Provider 封装模型服务,通过依赖注入供各模块使用,天然契合 DI 体系。

1.2 关键代码

// llm/model-factory.service.ts —— 用 ChatOpenAI 接入国产模型
import { ChatOpenAI } from '@langchain/openai';

const model = new ChatOpenAI({
  model: 'deepseek-chat',
  apiKey: 'sk-xxx',
  configuration: { baseURL: 'https://api.deepseek.com/v1' }, // 关键:指向国产 OpenAI 兼容端点
  temperature: 0.7,
});

核心要点:国产大模型(DeepSeek / 通义千问 / 字节 Seed)均提供 OpenAI 兼容接口,只需把 configuration.baseURL 指到对应端点即可,无需额外适配包。

三个端点:

厂商 baseURL 模型名示例
DeepSeek https://api.deepseek.com/v1 deepseek-chat / deepseek-reasoner
通义千问 https://dashscope.aliyuncs.com/compatible-mode/v1 qwen-turbo / qwen-plus / qwen-max
字节方舟 https://ark.cn-beijing.volces.com/api/v3 doubao-* / deepseek-*

📌 2. 多模型加载与管理

2.1 原理

用 模型工厂(Factory)+ 注册表 模式:工厂统一创建、缓存各 provider 模型实例,对外暴露统一的 getModel(name),实现「同时加载多个国产模型 + 运行时热切换」。

2.2 关键代码

// llm/model-factory.service.ts
getModel(name: ModelProviderName, params?: ModelParams): BaseChatModel {
  if (this.config.get<boolean>('llm.mock', true)) {
    return new MockChatModel(`${name}:${params?.model ?? 'default'}`); // Mock 兜底
  }
  const provider = this.config.get<ProviderConfig>(`llm.providers.${name}`, ...);
  return new ChatOpenAI({
    model: params?.model ?? provider.model,
    apiKey: provider.apiKey,
    configuration: { baseURL: provider.baseURL },
    ...
  });
}

ModelProviderName = 'deepseek' | 'qwen' | 'ark'。Mock 模式通过环境变量 LLM_MOCK=true 切换,无任何 key 也能离线跑通。

2.3 验证

# 显式指定 provider
curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' \
  -d '{"message":"你好","provider":"deepseek"}'
# 响应 route.reason 为「用户显式指定 provider」

📌 3. 模型参数详解

3.1 参数表与调优建议

参数 作用 建议
temperature 采样温度(0-2),越高越随机 创意任务 0.8-1.2;事实问答 0-0.3
maxTokens 最大输出 token 数 按任务设上限,防超长/超费
topP 核采样,取累积概率前 p 的 token 与 temperature 二选一调,勿同时大调
frequencyPenalty 降低重复词概率 生成长文时 0.3-0.5
presencePenalty 鼓励引入新词 需要多样性时调高
stop 停止序列 结构化输出时用 ["\n\n"] 等
seed 随机种子(部分模型支持) 需要结果可复现时设置
thinking_mode 推理模式(deepseek-reasoner 等) 复杂推理开启,会先输出思考链

3.2 原理

这些参数在请求时通过 ChatOpenAI 构造或 .withConfig() 传入,最终映射到各家 OpenAI 兼容 API 的 temperature、max_tokens、top_p 等字段。


📌 4. 动态参数配置

4.1 原理

请求 DTO 携带可覆盖参数,服务层把它们透传给模型工厂。这样同一个接口能按需调整模型行为,也支持「场景预设配置」。

4.2 关键代码

// llm/dto/chat.dto.ts —— 可覆盖参数
export class ChatDto {
  message: string;
  conversationId?: string;
  provider?: 'deepseek' | 'qwen' | 'ark';
  strategy?: 'auto' | 'simple' | 'medium' | 'complex';
  @Min(0) @Max(2) temperature?: number;
  @Min(1) maxTokens?: number;
  @Min(0) @Max(1) topP?: number;
}
// chat/chat.service.ts —— 动态参数透传
this.modelFactory.getModel(route.provider, {
  model: route.model,
  temperature: dto.temperature,
  maxTokens: dto.maxTokens,
  topP: dto.topP,
});

4.3 验证

# 指定温度与 maxTokens
curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' \
  -d '{"message":"写一首诗","temperature":1.2,"maxTokens":200}'

📌 5. 模型路由策略

5.1 原理

根据任务复杂度 / 成本自动选择模型:简单问题用轻量模型(省成本、低延迟),复杂问题用强模型(保质量)。核心是一个 route() 决策函数。

5.2 关键代码

// llm/model-router.service.ts
route(input): RouteDecision {
  if (input.provider) return { provider: input.provider, reason: '用户显式指定 provider' };
  const complexity = input.complexity ?? this.detectComplexity(input.message ?? '');
  switch (complexity) {
    case 'simple':  return { provider: 'qwen', model: 'qwen-turbo', ... };
    case 'medium':  return { provider: 'qwen', model: 'qwen-plus', ... };
    case 'complex': return { provider: 'deepseek', model: 'deepseek-reasoner', ... };
  }
}

复杂度检测用「长度 + 关键词」启发式:消息含「分析/对比/方案/代码」等词或超 100 字判为 complex。

5.3 验证

curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' -d '{"message":"你好"}'
# route: qwen / qwen-turbo(简单)

curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' \
  -d '{"message":"请详细分析对比微服务和单体架构优缺点并给出设计方案"}'
# route: deepseek / deepseek-reasoner(复杂)

📌 6. MCP 协议

6.1 原理

MCP(Model Context Protocol)是让 AI 应用连接外部工具/数据的开放协议,三要素:

  • Host:承载 AI 能力的宿主(本教程的 Nest 服务)
  • Client:与 Server 建立连接的客户端
  • Server:暴露工具(tool)/资源(resource)/提示词(prompt)

本教程用 @modelcontextprotocol/sdk 开发自定义 MCP Server,通过 Streamable HTTP 暴露。

6.2 关键代码

// mcp/mcp.service.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

async handleRequest(req, res, parsedBody) {
  const server = new McpServer({ name: 'tutorial-mcp-server', version: '1.0.0' });
  server.registerTool('echo', {
    title: 'Echo 回显',
    description: '回显输入的文本',
    inputSchema: { text: z.string() },
  }, async (input) => ({ content: [{ type: 'text', text: `echo: ${input.text}` }] }));

  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
  await server.connect(transport);
  await transport.handleRequest(req, res, parsedBody);
}

接入第三方 MCP:用 @langchain/mcp-adapters 的 MultiServerMCPClient 连接远程 MCP Server,把其工具加载为 LangChain Tool,再 bindTools 挂载给模型。

6.3 验证

curl -X POST http://localhost:3001/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# 返回 serverInfo: tutorial-mcp-server,capabilities.tools.listChanged=true

📌 7. Skill 开发

7.1 原理

Skill 是「封装了业务逻辑、可被大模型调用的工具」。用 tool() 工厂 + zod schema 定义入参,模型通过 Function Calling 结构化调用。Skill 注册表统一管理,按需挂载。

7.2 关键代码

// skills/skills/calculator.skill.ts
import { tool } from '@langchain/core/tools';
import { z } from 'zod';

toTool(): StructuredToolInterface {
  return tool(
    async (input) => String(this.safeEval(input.expression)),
    {
      name: 'calculator',
      description: '计算数学表达式,支持加减乘除与括号',
      schema: z.object({ expression: z.string().describe('数学表达式') }),
    },
  );
}
// skills/skills.registry.ts —— 注册表
listTools(): StructuredToolInterface[] {
  return [this.calculator.toTool(), this.databaseQuery.toTool()];
}

7.3 验证

工具的真实调用需真实模型(Mock 模型不发起 Function Calling)。配置真实 key 后,提问「1+2*3 等于多少」即可看到模型调用 calculator 工具。


📌 8. 本地 API 接入(工具调用闭环)

8.1 原理

让大模型通过工具调用本地 Nest 业务接口 / 数据库,形成「模型 → 工具 → 本地数据 → 模型」的闭环。实现要点是 agent loop:

  1. 模型返回 tool_calls
  2. 服务端执行对应工具(查数据库等)
  3. 把结果以 ToolMessage 回传给模型
  4. 模型综合后给出最终回答

8.2 关键代码

// skills/skills/database-query.skill.ts —— 调用本地数据库
toTool(): StructuredToolInterface {
  return tool(
    async (input) => {
      const count = await this.messageRepo.countBy({ conversationId: input.conversationId });
      return `会话 ${input.conversationId} 当前共有 ${count} 条历史消息`;
    },
    { name: 'count_messages', description: '查询某个会话的历史消息数量', schema: z.object({ conversationId: z.string() }) },
  );
}
// chat/chat.service.ts —— agent loop
for (let i = 0; i < maxIterations; i++) {
  const result = await model.invoke(messages);
  messages = [...messages, result];
  if (!result.tool_calls || result.tool_calls.length === 0) { reply = ...; break; }
  const toolMessages = await this.executeTools(result.tool_calls);  // 执行工具
  messages = [...messages, ...toolMessages];
}

📌 9. 对话上下文管理

9.1 原理

多轮对话需要把历史消息持久化(PostgreSQL),并在每轮请求时组装进上下文。当历史过长时需做 Token 估算 / 截断 / 压缩,长时记忆可用摘要或向量记忆(阶段 3 深入)。

9.2 关键代码

// context/context.service.ts
async append(conversationId, role, content) { /* 落库 */ }
async getHistory(conversationId, limit = 20) { /* 按时间正序读取 */ }

estimateTokens(text: string): number {
  const cn = (text.match(/[\u4e00-\u9fa5]/g) ?? []).length;
  return Math.ceil(cn / 1.5 + (text.length - cn) / 4);  // 中文约 1.5 字/token
}

trimToBudget(messages, budgetTokens) { /* 从最新往前保留,直到接近预算 */ }

9.3 验证

curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' -d '{"message":"第一轮"}'
# 记下返回的 conversationId,再发第二轮:
curl -X POST http://localhost:3001/chat -H 'Content-Type: application/json' \
  -d '{"message":"第二轮","conversationId":"<conversationId>"}'
# 数据库 messages 表中可见 user/assistant 多轮记录

📌 10. 流式响应:SSE 与 WebSocket

10.1 原理

model.stream() 返回逐 token 的异步迭代器。Nest 用 @Sse() 装饰器把 Observable 转成 SSE 流;WebSocket 网关则逐 chunk 推送给客户端。

10.2 关键代码

// chat/chat.service.ts —— 流式生成器
async *streamChat(dto: ChatDto): AsyncGenerator<string> {
  const stream = await model.stream(messages);
  for await (const chunk of stream) {
    const text = this.contentToText(chunk.content);
    if (text) { full += text; yield text; }
  }
  await this.context.append(conversationId, 'assistant', full);
}
// chat/chat.controller.ts —— SSE
@Post('stream') @Sse()
stream(@Body() dto: ChatDto): Observable<SseMessage> {
  return new Observable((subscriber) => {
    void (async () => {
      for await (const text of this.chatService.streamChat(dto)) {
        subscriber.next({ data: text });
      }
      subscriber.next({ data: '[DONE]' });
      subscriber.complete();
    })();
  });
}

10.3 验证

curl -N -X POST http://localhost:3001/chat/stream -H 'Content-Type: application/json' \
  -d '{"message":"介绍一下你自己"}'
# 逐字输出 SSE 事件

# WebSocket(wscat)
wscat -c "ws://localhost:3001/chat-ws"
# 发送 {"event":"chat","data":{"message":"你好"}},收到逐 chunk 的 chunk 事件

📌 附录:启动与测试

完整启动说明、.env 配置、Mock/真实 key 切换方式,见 code/02-langchain-module/README.md。

常见坑

现象 原因 解决
bindTools is not a function 自定义模型未实现 bindTools Mock 模型补 bindTools 返回自身
TS2589 类型实例化过深 MCP SDK 1.30 + zod 类型缺陷 @ts-ignore 精确忽略(已在代码中处理)
Node 18 下安装失败 LangChain 1.x 要求 Node 22+ 升级 Node
Mock 模型不调用工具 Mock 不发起 Function Calling 配置真实 key 验证工具闭环

📁 配套代码目录 tutorial-dsv4/code/02-langchain-module

    02-langchain-module/
      .env
      .env.example
      .prettierrc
      README.md
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      src/
        app.module.ts
        main.ts
        chat/
          chat.controller.ts
          chat.gateway.ts
          chat.module.ts
          chat.service.ts
        config/
          configuration.ts
          env.validation.ts
        context/
          context.module.ts
          context.service.ts
        entities/
          message.entity.ts
        llm/
          llm.module.ts
          mock-chat-model.ts
          model-factory.service.ts
          model-router.service.ts
          types.ts
        mcp/
          mcp.controller.ts
          mcp.module.ts
          mcp.service.ts
        skills/
          skills.module.ts
          skills.registry.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 pgvector RAG · 篇首 DeepSeek 版 · tutorial-dsv4

配套代码:code/03-pg-vector-rag/(完整可运行项目) 从基础 CRUD 到 pgvector 向量插件、再到 RAG 全流程,结合大模型做知识增强。


📌 1. PostgreSQL 基础(TypeORM)

1.1 原理

阶段 1 已讲 TypeORM CRUD。本章进阶:复杂查询(QueryBuilder)、事务、索引优化。

// QueryBuilder 复杂查询
await this.docRepo
  .createQueryBuilder('doc')
  .where('doc.status = :status', { status: 'done' })
  .orderBy('doc.createdAt', 'DESC')
  .getMany();

// 事务:保证「删文档 + 删分片」原子性
await this.dataSource.transaction(async (manager) => {
  await manager.delete(Chunk, { documentId: id });
  await manager.delete(DocumentEntity, { id });
});

索引优化要点:高频过滤/排序字段建索引(如 documentId、createdAt),用 @Index() 装饰器或迁移。


📌 2. pgvector 向量插件

2.1 安装启用

# macOS
brew install pgvector
# 若 brew bottle 不含 PG16(新版已不含),需源码编译:
git clone --depth 1 --branch v0.8.6 https://github.com/pgvector/pgvector.git
cd pgvector && make PG_CONFIG=/opt/homebrew/opt/postgresql@16/bin/pg_config
make PG_CONFIG=/opt/homebrew/opt/postgresql@16/bin/pg_config install

# Linux
sudo apt install postgresql-16-pgvector
-- 在数据库中启用(需超级用户)
CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname='vector';  -- 0.8.x

2.2 距离算子

算子 含义 说明
<-> L2 欧氏距离 距离越小越相似
<=> 余弦距离 文本语义检索最常用
<#> 内积(取负) 需向量已归一化

余弦相似度 = 1 - (a <=> b)。

2.3 索引:HNSW / IVFFlat

-- HNSW:查询快,适合高频查询
CREATE INDEX chunks_embedding_hnsw_idx
ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

-- IVFFlat:构建快,适合大数据量批量场景
CREATE INDEX chunks_embedding_ivfflat_idx
ON chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

关键坑:HNSW 索引要求向量列有固定维度(vector(1024)),无维度 vector 会报 column does not have dimensions。TypeORM 里 @Column({ type: 'vector' }) 生成的是无维度列,需用 SQL 固定维度:

ALTER TABLE chunks ALTER COLUMN embedding TYPE vector(1024);

📌 3. 向量数据开发

3.1 文档分片

分片质量直接决定 RAG 效果。本教程实现递归字符切分 + 重叠(documents/splitter.service.ts):

split(text, { chunkSize = 500, chunkOverlap = 50, separators = ['\n\n', '\n', '。', '!', '?', ';', '. ', ' ', ''] })

原理:按分隔符优先级(段落 → 换行 → 句子 → 空格 → 字符)逐级切,直到片段不超过 chunkSize;合并时保留 chunkOverlap 重叠,避免语义被硬切断。

3.2 嵌入模型调用

// embedding/embedding.service.ts —— 统一入口,mock/真实可切换
async embed(texts: string[]): Promise<number[][]> {
  if (this.isMock()) return texts.map((t) => mockEmbedding(t, this.dim()));
  return this.getEmbeddings().embedDocuments(texts);  // 通义千问 text-embedding-v3
}

重要:DeepSeek 官方无 embedding 接口,向量化统一用通义千问 text-embedding-v3(默认 1024 维)。Mock 模式用确定性哈希向量,无 key 离线可跑。

3.3 向量写入与检索

// 写入:embedding 存 `[0.1,0.2,...]` 字符串
await manager.createQueryBuilder().insert().into(Chunk).values(chunkRows).execute();

// 检索:raw SQL 余弦相似度 top-k
const rows = await this.dataSource.query(
  `SELECT id, "documentId", content, 1 - (embedding <=> $1::vector) AS similarity
   FROM chunks WHERE embedding IS NOT NULL
   ORDER BY embedding <=> $1::vector LIMIT $2`,
  [vectorStr, topK],
);

📌 4. RAG 基础实现

4.1 全链路

文档上传 → 解析分片 → 向量化存储 → 查询召回 → 注入 Prompt → 大模型回答

本教程代码分两段:

  • 入库(documents.service.ingest):保存文档 → 分片 → 批量向量化 → 批量写入向量。
  • 问答(rag.service.ask):召回(多路)→ 拼接上下文 → 注入 prompt → 大模型生成。
// rag/rag.service.ts
async ask(dto: AskDto) {
  const chunks = await this.retriever.hybridSearch(dto.question, topK);  // 召回
  const answer = await this.generateAnswer(dto.question, chunks);        // 注入 + 生成
  return { answer, sources: ... };
}

4.2 验证

# 入库
curl -X POST http://localhost:3002/documents/ingest -H 'Content-Type: application/json' \
  -d '{"title":"公司年假政策","content":"根据公司规章制度,员工入职满一年后每年享有 10 天带薪年假……"}'

# RAG 问答
curl -X POST http://localhost:3002/rag/ask -H 'Content-Type: application/json' \
  -d '{"question":"员工每年有多少天带薪年假?","topK":3}'

📌 5. RAG 优化策略

策略 做法 本教程落地
分片优化 调 chunkSize / overlap,按语义边界切 SplitterService 可配参数
多路召回 向量 + 关键词/全文检索合并 RetrieverService.hybridSearch(向量 <=> + ILIKE)
查询改写 MultiQuery / 子问题拆解 文档讲解(可接 LLM 改写)
重排 Rerank 召回后用更精细模型重排 文档讲解(接 rerank 模型或 LLM 打分)
召回率调优 调 ef_search、topK、lists SQL 脚本 + 参数

多路召回实现(retriever.service.ts):向量结果 + ILIKE 关键词结果合并去重,按相似度降序。


📌 6. 大模型数据增强

结合 LLM 做数据清洗、结构化提取、自动打标签,提升入库质量:

  1. 数据清洗:让 LLM 去除文档中的页眉页脚、广告、乱码。
  2. 结构化提取:从非结构化文本抽 JSON(如合同关键条款)。
  3. 自动打标签:为每个分片生成主题标签,增强过滤与召回。

示例 prompt(清洗):

请清理以下文本中的无关内容(页眉、页码、广告),仅保留正文:{chunk}

这些能力在阶段 4 综合项目中会与知识库入库流程结合。


📌 7. 性能优化与压测

7.1 索引调优

-- 查询时调 HNSW 搜索范围(越大越准、越慢)
SET hnsw.ef_search = 100;

-- 查看执行计划确认命中索引
EXPLAIN ANALYZE SELECT id FROM chunks ORDER BY embedding <=> $1::vector(1024) LIMIT 3;

注意:数据量小时 PG 会用顺序扫描(Seq Scan)而非索引扫描,这是正常的——顺序扫描在小数据集更快。数据量上来后自动切换 Index Scan。

7.2 分页与批量写入

  • 分页用 LIMIT/OFFSET 或游标(大结果集用 keyset 分页)。
  • 批量写入用单条 INSERT ... VALUES (...), (...) 而非逐条 insert。

7.3 压测脚本

npm run bench
# 批量写入 N 条向量 → 测试相似度检索延迟

📌 附录:启动与测试

完整启动说明、.env 配置、SQL 脚本见 code/03-pg-vector-rag/README.md。

常见坑

现象 原因 解决
extension "vector" is not available pgvector 未装或装错 PG 版本 源码编译 make PG_CONFIG=... install
column does not have dimensions 无维度 vector 建 HNSW 索引 先 ALTER ... TYPE vector(1024)
permission denied to create extension 非超级用户 用超级用户执行 CREATE EXTENSION
相似度排序"不准确" mock 哈希向量非语义向量 配置真实 embedding key

📁 配套代码目录 tutorial-dsv4/code/03-pg-vector-rag

    03-pg-vector-rag/
      .env
      .env.example
      README.md
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      scripts/
        bench.ts
        init-vector.sql
      src/
        app.module.ts
        main.ts
        config/
          configuration.ts
          env.validation.ts
        database/
          vector.utils.ts
        documents/
          documents.controller.ts
          documents.module.ts
          documents.service.ts
          splitter.service.ts
        embedding/
          embedding.module.ts
          embedding.service.ts
          mock-embedding.ts
        entities/
          chunk.entity.ts
          document.entity.ts
        rag/
          rag.controller.ts
          rag.module.ts
          rag.service.ts
          retriever.service.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 综合实战项目 · 篇首 DeepSeek 版 · tutorial-dsv4

配套代码:code/04-full-project/(完整可上线项目) 整合前三阶段全部能力,落地为一个完整的 AI 知识库问答后端。


📌 1. 架构说明

                      ┌──────────────────────────────┐
                      │      客户端(Web / CLI)       │
                      │   REST / SSE / WebSocket      │
                      └───────────────┬──────────────┘
                                      │
                      ┌───────────────▼──────────────┐
                      │         NestJS 应用层          │
                      │ Guard/Pipe/Interceptor/Filter  │
                      ├───────────────┬──────────────┤
          ┌───────────┤   AuthModule  ├──────────────┐
          │           │  UsersModule   │              │
   ┌──────▼──────┐ ┌───▼────────┐ ┌────▼─────────┐ ┌──▼────────────┐
   │ KnowledgeBase│ │  RagModule │ │   LlmModule  │ │  ChatModule   │
   │ 文档/分片/向量 │ │ 召回/生成   │ │ 工厂/路由     │ │ 对话/SSE/WS   │
   └──────┬──────┘ └───┬────────┘ └────┬─────────┘ └───┬───────────┘
          │            │               │               │
   ┌──────▼────────────▼───────────────▼───────────────▼──────────┐
   │                    SkillsModule(业务查询 Skill)              │
   └──────────────────────────────┬───────────────────────────────┘
                                  │
        ┌─────────────────────────▼──────────────────────────┐
        │     数据层(PostgreSQL 16 + pgvector,HNSW 索引)   │
        │  users · documents · chunks(vector) · conversations│
        │  messages                                           │
        └─────────────────────────┬──────────────────────────┘
                                  │
        ┌─────────────────────────▼──────────────────────────┐
        │   外部大模型(OpenAI 兼容):DeepSeek / 通义 / 字节   │
        │   嵌入模型:通义千问 text-embedding-v3               │
        └────────────────────────────────────────────────────┘

请求流转(RAG 问答为例):客户端 → JWT 守卫鉴权 → RagModule 召回(RetrieverService 用 <=> 余弦检索向量)→ 拼接上下文注入 prompt → ModelFactoryService 取模型 → 流式/非流式生成 → 返回。


📌 2. 功能清单与模块划分

需求功能 落地模块 说明
注册登录 / JWT / 刷新 auth + users Access + Refresh 双令牌,刷新哈希轮换
文档上传 / 解析分片 / 向量化 knowledge-base JSON 文本入库 + multer 文件上传,递归切分 + 向量化
RAG 智能问答 + SSE 流式 rag 多路召回 + 注入 + 生成,/rag/ask/stream 流式
多模型智能路由 llm 简单→qwen-turbo,复杂→deepseek-reasoner
对话历史 + 多轮上下文 chat + conversations/messages 历史持久化,多轮注入
WebSocket 实时对话 chat(chat.gateway) 握手 JWT 鉴权 + 逐 token 推送
工具调用 skills 业务查询 Skill(文档数/消息数统计)
接口文档 + 部署 Swagger + Dockerfile /api-docs + docker-compose

📌 3. 数据表设计

表 关键字段 说明
users id, email(unique), password, roles, refreshTokenHash 用户
documents id, userId, title, content, status 知识库文档
chunks id, documentId, content, embedding vector 分片 + 向量(HNSW 索引)
conversations id, userId, title 会话
messages id, conversationId, role, content 对话历史

📌 4. 本地部署

# 前置:Node 22+、PostgreSQL 16 + pgvector(见 00-environment-setup.md)
cd code/04-full-project
cp .env.example .env
npm install
npm run start:dev

首次启动自动建表(synchronize)。随后固定向量维度并创建索引:

psql -d tutorial -f scripts/init-vector.sql

访问:服务 http://localhost:3000 ,Swagger http://localhost:3000/api-docs 。

默认 LLM_MOCK=true / EMBEDDING_MOCK=true,无 key 即可跑通全部功能。配置真实 key 后关闭 mock。


📌 5. Docker 部署

cd code/04-full-project
docker compose up -d --build

docker-compose.yml 起两个服务:

  • postgres:pgvector/pgvector:pg16 镜像(已内置 pgvector)
  • app:多阶段构建的 NestJS 应用

生产环境务必修改 .env 中的 JWT_SECRET 与数据库密码,并把 synchronize 改为 false 使用迁移。


📌 6. 接口清单

认证(公开)

方法 路径 说明
POST /auth/register 注册
POST /auth/login 登录
POST /auth/refresh 刷新令牌
GET /auth/profile 当前用户(需鉴权)

知识库(需鉴权)

方法 路径 说明
POST /knowledge-base/ingest 文档入库(JSON 文本)
POST /knowledge-base/upload 上传 txt/md 文件入库
GET /knowledge-base 我的文档列表
DELETE /knowledge-base/:id 删除文档

RAG(需鉴权)

方法 路径 说明
POST /rag/ask RAG 问答(非流式)
POST /rag/ask/stream RAG 问答(SSE 流式)

对话(需鉴权)

方法 路径 说明
POST /chat 对话(含工具调用,可选 RAG)
POST /chat/stream SSE 流式对话
WS /chat(socket.io) WebSocket 实时对话

完整 curl 命令见 code/04-full-project/README.md。


📌 7. 常见坑

现象 解决
SSE 输出被包成 data: {"data":...} 用 @SkipTransform 让流式接口跳过统一响应包装
全局拦截器需 Reflector 用 APP_INTERCEPTOR 依赖注入注册,而非手动 new
向量 HNSW 索引报无维度 先 scripts/init-vector.sql 的 ALTER 固定维度
注册重复邮箱 500 唯一约束冲突,应捕获后返回友好错误(可用全局异常过滤器)

📁 配套代码目录 tutorial-dsv4/code/04-full-project

    04-full-project/
      .dockerignore
      .env
      .env.example
      Dockerfile
      README.md
      docker-compose.yml
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      scripts/
        init-vector.sql
      src/
        app.module.ts
        main.ts
        common/
        config/
          configuration.ts
          env.validation.ts
        database/
          data-source.ts
          vector.utils.ts
        entities/
          chunk.entity.ts
          conversation.entity.ts
          document.entity.ts
          message.entity.ts
          user.entity.ts
        modules/

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 教程编排计划 · 篇首 DeepSeek 版 · tutorial-dsv4

面向开发者的系统化学习教程:NestJS 后端 → LangChain 大模型服务 → PostgreSQL 向量检索 + RAG → 综合实战。 全部开源、优先国产大模型(DeepSeek / 通义千问 / 字节 Seed),兼容 OpenAI 协议。 本计划审核确认后,才开始产出教程内容。


📌 0. 已确认的关键决策

决策点 结论
代码验证深度 编译级(TS 严格类型 + 依赖版本锁定)+ 关键路径跑通(本地 PG、向量写入/检索、RAG 召回)
大模型调用 用 Mock LLM 兜底(可离线验证链路),保留真实 key 一键切换
Embedding 模型 通义千问 text-embedding-v3(DashScope 兼容端点)
数据库环境 本地手动安装 PostgreSQL 16 + pgvector(macOS Homebrew 优先 + Linux 通用步骤)
LangChain.js 版本线 1.x 最新稳定线(要求 Node 22+)
包管理器 / Node npm + Node 22 LTS
后端框架 NestJS 11(Express 引擎,TypeScript 5.7 strict)
ORM TypeORM 0.3.x(@nestjs/typeorm 11)

📌 1. 教程整体学习路径与前置环境

1.1 学习路径(四阶段递进)

阶段1 NestJS 工程化         阶段2 LangChain 大模型服务
  ┌─────────────────┐        ┌──────────────────────┐
  │ 模块/DI/分层      │   →    │ 模型工厂/路由/参数/MCP │
  │ TypeORM+PG CRUD  │        │ Skill/上下文/流式     │
  │ JWT/中间件/WS    │        │ SSE/WS 流式对话       │
  └─────────────────┘        └──────────────────────┘
           │                           │
           └──────────┬────────────────┘
                      ▼
       阶段3 PostgreSQL 向量 + RAG(文档分片→向量化→召回→注入)
                      │
                      ▼
       阶段4 综合项目:AI 知识库问答后端(全能力整合 + Docker 部署)

阶段 1 打底(后端骨架 + 鉴权 + 基础设施),阶段 2 补齐 AI 能力(模型管理与流式),阶段 3 补齐知识增强(向量检索 RAG),阶段 4 三合一落地成可上线项目。每阶段独立可运行,前序阶段产出是后续的依赖骨架。

1.2 前置环境要求(00-environment-setup.md 详解)

环境 版本 安装方式(macOS 优先)
Node.js 22 LTS(≥22,LangChain 1.x 硬性要求) nvm / 官网 pkg
npm 随 Node —
PostgreSQL 16.x brew install postgresql@16,brew services start postgresql@16
pgvector 0.8.x brew install pgvector(源码 make && make install 备选)
验证 pgvector — psql 内执行 CREATE EXTENSION IF NOT EXISTS vector; + SELECT extversion FROM pg_extension WHERE extname='vector';
  • Linux(Ubuntu/Debian)备选:sudo apt install postgresql-16 postgresql-16-pgvector。
  • 国产大模型 API Key(真实调用可选,Mock 可离线跑):DeepSeek、阿里云 DashScope(通义千问)、火山方舟(字节 Seed/豆包)。
  • 三个国产端点(均 OpenAI 兼容,教程统一 ChatOpenAI({ baseURL, apiKey }) 接入):
    • DeepSeek:https://api.deepseek.com/v1(deepseek-chat / deepseek-reasoner)
    • 通义千问:https://dashscope.aliyuncs.com/compatible-mode/v1(qwen-turbo/qwen-plus/qwen-max)
    • 字节火山方舟:https://ark.cn-beijing.volces.com/api/v3(doubao-* / deepseek-* 端点)
  • Embedding:通义千问 text-embedding-v3(同一 DashScope 兼容端点 /embeddings)。

📌 2. 四个阶段的章节划分与知识点清单

阶段 1:NestJS 后端工程化全流程(01-nestjs-complete-guide.md)

章 主题 知识点清单
1.1 环境与初始化 @nestjs/cli 安装、nest new、启动命令(dev/start:prod)、目录结构逐文件讲解(main.ts / app.module / module / controller / service / .env / tsconfig)
1.2 核心概念 模块化(@Module)、依赖注入(DI 容器/Provider scope)、Controller/Service/Provider 分层、DTO 与 ValidationPipe
1.3 配置管理 @nestjs/config、.env 多环境(.env.development/.test/.production)、Joi/class-validator 配置校验、ConfigModule.forRoot 用法
1.4 TypeORM + PG 连接配置、实体定义(@Entity/@Column/@PrimaryGeneratedColumn)、Repository CRUD、关联(OneToMany/ManyToOne)、事务(DataSource.transaction)、迁移(typeorm migration:generate/run)
1.5 JWT 鉴权 bcrypt 密码哈希、注册/登录、@nestjs/jwt 签发、Passport JWT Strategy、@UseGuards 守卫、Access + Refresh Token 双令牌与刷新接口、@Roles 权限控制
1.6 中间件体系 中间件/拦截器(Interceptor)/异常过滤器(ExceptionFilter)/管道(Pipe) 四者区别、触发时机与执行顺序、各自实战场景
1.7 文件上传下载 multer 本地存储、ServeStaticModule 静态资源、文件类型(MIME)与大小限制校验、下载接口
1.8 WebSocket @nestjs/websockets + socket.io、@WebSocketGateway、房间管理、消息广播、WS 握手鉴权(token 校验)
1.9 接口文档 @nestjs/swagger + swagger-ui-express、装饰器注解、自动生成 Swagger UI

交付:完整基础后端项目(含上述各独立示例 + 整合版)、启动说明、每接口 curl 测试命令。

阶段 2:LangChain 大模型服务开发(02-langchain-ai-service.md)

按 LangChain.js 1.x 规范编写(.invoke()/.stream()/LCEL .pipe()、ChatPromptTemplate.fromMessages、.bindTools())。

章 主题 知识点清单
2.1 LangChain 基础 1.x 包结构(@langchain/core / @langchain/openai / langchain / @langchain/langgraph / @langchain/classic)、核心概念(Model/Prompt/LCEL/Runnable/Tool)、Nest 模块化封装、模型服务 Provider 初始化
2.2 多模型加载管理 模型工厂(Factory)+ 注册表,同时加载 DeepSeek/通义/字节多模型,统一 ChatOpenAI 接口,运行时热切换
2.3 模型参数详解 temperature/max_tokens/top_p/frequency_penalty/presence_penalty/stop/seed/thinking_mode(推理模型) 全参数作用 + 调优建议
2.4 动态参数配置 请求 DTO 动态覆盖模型参数、按场景预设配置(预设 JSON/代码配置)、参数校验
2.5 模型路由策略 基于任务类型/复杂度/成本的自动路由(简单问题→轻量 qwen-turbo,复杂→deepseek-reasoner/qwen-max)、路由规则可配置
2.6 MCP 协议 MCP 概念(client/server/tool/resource)、@modelcontextprotocol/sdk + @langchain/mcp-adapters 接入第三方 MCP、开发自定义 MCP Server、通过 HTTP(Streamable HTTP)暴露 MCP 接口
2.7 Skill 开发 Skill 概念、自定义 Skill 封装业务逻辑(StructuredTool)、Skill 注册表与挂载、按场景调用
2.8 本地 API 接入 大模型通过工具调用本地 Nest 业务接口/数据库查询(Function Calling),形成工具调用闭环
2.9 对话上下文管理 历史消息持久化到 PostgreSQL、上下文窗口管理、Token 计数/压缩/截断、长时记忆方案(摘要/向量记忆)
2.10 流式响应 SSE 流式输出(@Sse + model.stream())、WebSocket 流式对话

交付:完整 Nest 集成 LangChain 模块(模型工厂、多模型路由、动态参数 DTO、自定义 MCP、自定义 Skill、上下文管理、流式对话),每个功能附调用示例与测试方法。

阶段 3:PostgreSQL 进阶与向量检索 RAG(03-postgresql-vector-rag.md)

章 主题 知识点清单
3.1 PostgreSQL 基础 TypeORM 完整 CRUD、复杂查询(QueryBuilder)、事务、索引优化(B-tree/GIN)
3.2 pgvector 插件 安装启用、vector(n) 字段定义、距离算子(<-> L2 / <=> 余弦 / <#> 内积)、HNSW/IVFFlat 索引创建与参数(m/ef_construction/ef_search/lists)
3.3 向量数据开发 文档分片(递归字符切分/语义切分)、嵌入模型调用(通义 text-embedding-v3)、向量批量写入、相似度检索
3.4 RAG 基础实现 文档上传 → 解析分片 → 向量化存储 → 查询召回 → 注入 Prompt → 大模型回答(全链路)
3.5 RAG 优化 分片策略优化(重叠/大小)、多路召回(向量+关键词/全文)、查询改写(MultiQuery/子问题)、重排(Rerank)、召回率调优
3.6 大模型数据增强 用 LLM 做数据清洗、结构化提取、自动打标签
3.7 性能优化 向量索引调优、分页(游标/offset)、批量写入、查询性能压测方法(EXPLAIN ANALYZE、压测脚本)

交付:TypeORM 向量实体、向量 CRUD 服务、基础 RAG 服务、文档解析分片工具;附 pgvector 安装命令、索引创建 SQL、性能测试脚本。

阶段 4:综合实战项目(04-full-project.md)

功能 说明
用户体系 注册登录、JWT 鉴权、Access/Refresh Token
知识库管理 文档上传(multer)、自动解析分片(pdf/txt/md)、向量化入库
RAG 智能问答 向量召回 + 大模型回答、SSE 流式响应
多模型路由 简单问答→轻量模型,复杂知识库问题→强模型
对话历史 全量持久化、多轮对话上下文(含 Token 压缩)
WebSocket 实时对话 实时消息、流式输出、房间/会话隔离
工具调用 内置业务查询 Skill,可调本地数据库接口
文档+部署 架构说明、接口清单(Swagger)、Docker 部署(Dockerfile + docker-compose:app + postgres/pgvector)

📌 3. 交付文件列表与代码目录结构

3.1 教程根目录结构

tutorial/
├── plan.md                      # 本计划(作为教程总纲归档)
├── README.md                    # 教程首页:导航、技术栈、快速开始
├── 00-environment-setup.md      # 前置环境:Node/PG/pgvector/API Key 全安装步骤
├── 01-nestjs-complete-guide.md  # 阶段1文档
├── 02-langchain-ai-service.md   # 阶段2文档
├── 03-postgresql-vector-rag.md  # 阶段3文档
├── 04-full-project.md           # 阶段4文档(架构+部署+接口清单)
└── code/
    ├── 01-nest-basic/           # 阶段1完整项目
    ├── 02-langchain-module/     # 阶段2完整模块
    ├── 03-pg-vector-rag/        # 阶段3完整项目
    └── 04-full-project/         # 阶段4综合项目

3.2 每阶段代码目录结构(以阶段 1 为例,其余同构)

code/01-nest-basic/
├── package.json
├── tsconfig.json / tsconfig.build.json
├── nest-cli.json
├── .env.example                 # 环境变量模板
├── README.md                    # 启动说明 + curl 测试命令
├── ormconfig.ts / data-source.ts
└── src/
    ├── main.ts                  # 全局管道/过滤器/Swagger 装配
    ├── app.module.ts
    ├── common/                  # 守卫/拦截器/过滤器/装饰器
    ├── config/                  # 多环境配置 + 校验
    ├── modules/
    │   ├── auth/                # 注册/登录/JWT/刷新
    │   ├── users/               # CRUD 示例
    │   ├── upload/              # 文件上传下载
    │   ├── chat/                # WebSocket 网关
    │   └── health/
    ├── entities/                # TypeORM 实体
    └── migrations/

阶段 4 综合项目结构(核心模块):

code/04-full-project/src/
├── main.ts
├── app.module.ts
├── common/                      # 守卫/拦截器/过滤器/管道/装饰器
├── config/                      # 环境配置+校验
├── database/                    # TypeORM + pgvector 实体/迁移
├── modules/
│   ├── auth/                    # JWT 用户体系
│   ├── users/
│   ├── knowledge-base/          # 文档上传/解析/分片/向量化
│   ├── rag/                     # 向量召回 + 注入 + 生成
│   ├── llm/                     # 模型工厂/路由/参数/流式
│   ├── chat/                    # 对话历史 + SSE + WebSocket 网关
│   ├── skills/                  # 业务查询 Skill + 工具注册
│   └── mcp/                     # 自定义 MCP Server
└── scripts/                     # 压测/种子数据脚本

📌 4. 综合项目架构、功能与模块划分

4.1 架构图(文字版)

                        ┌─────────────────────────────┐
                        │       客户端 (Web/CLI)        │
                        │   REST / SSE / WebSocket     │
                        └──────────────┬──────────────┘
                                       │
                        ┌──────────────▼──────────────┐
                        │        NestJS 应用层         │
                        │  Guard/Pipe/Interceptor/Filter│
                        ├──────────────┬──────────────┤
          ┌─────────────┤   AuthModule ├──────────────┐
          │             │   UsersModule│              │
   ┌──────▼─────┐ ┌─────▼─────┐ ┌─────▼─────────┐ ┌───▼──────────┐
   │  Knowledge │ │  RAG 服务  │ │   LLM 模块     │ │ Chat(WS/SSE) │
   │  Base 模块 │ │ 召回+注入  │ │ 工厂/路由/流式  │ │ 会话/历史     │
   └──────┬─────┘ └─────┬─────┘ └─────┬─────────┘ └───┬──────────┘
          │             │             │               │
   ┌──────▼─────────────▼─────────────▼───────────────▼──────────┐
   │                    Skills / MCP 工具层                       │
   │   业务查询 Skill · 自定义 MCP Server · 第三方 MCP 接入       │
   └──────────────────────────────┬──────────────────────────────┘
                                  │
        ┌─────────────────────────▼─────────────────────────┐
        │           数据层(PostgreSQL 16 + pgvector)       │
        │   users · documents · chunks(向量) · conversations │
        │   messages · vector HNSW 索引                      │
        └───────────────────────────────────────────────────┘
                                  │
        ┌─────────────────────────▼─────────────────────────┐
        │          外部大模型(OpenAI 兼容协议)              │
        │  DeepSeek · 通义千问 · 字节Seed · Embedding(v3)    │
        └───────────────────────────────────────────────────┘

4.2 数据表设计(pgvector)

表 关键字段 说明
users id, email, password_hash, roles 用户
documents id, user_id, filename, status, meta 知识库文档
chunks id, document_id, content, embedding vector(n), token_count 分片 + 向量(HNSW 索引)
conversations id, user_id, title 会话
messages id, conversation_id, role, content, token_count 对话历史

4.3 功能 → 模块映射

需求功能 落地模块
注册登录 / JWT / 刷新 auth + users
文档上传/解析分片/向量化 knowledge-base
向量召回 + RAG 回答 + 流式 rag + llm
多模型智能路由 llm(模型工厂 + 路由策略)
对话历史 + 多轮上下文 chat + database
WebSocket 实时对话 chat(WS 网关 + SSE)
业务查询 Skill skills
自定义 MCP + 本地接口工具闭环 mcp + skills

📌 5. 依赖包完整清单与版本号

5.1 通用(全阶段)

依赖 版本 用途
Node.js 22 LTS 运行时(LangChain 1.x 硬性要求)
typescript ^5.7 严格类型
@nestjs/core / common / platform-express ^11.0.1 框架核心
@nestjs/cli ^11.0.0 脚手架
@nestjs/config ^4.0.2 配置管理
@nestjs/typeorm ^11.0.0 ORM 集成
typeorm ^0.3.22 ORM
pg ^8.13 PostgreSQL 驱动(含 pgvector 原生 SQL)
class-validator / class-transformer ^0.14.1 / ^0.5.1 校验
reflect-metadata / rxjs ^0.2.2 / ^7.8.1 元数据/响应式
@nestjs/swagger + swagger-ui-express ^11.0.0 / ^5.0.0 API 文档
@nestjs/websockets + @nestjs/platform-socket.io + socket.io ^11.0.0 / ^4.8 WebSocket
@nestjs/jwt + @nestjs/passport + passport + passport-jwt ^11.0.0 / ^11.0.0 / ^0.7 / ^4.0.1 JWT 鉴权
bcrypt ^5.1.1 密码哈希
@nestjs/serve-static ^5.0.0 静态资源
multer(@types/multer) ^1.4.12 文件上传(platform-express 内置)
joi ^17.13 配置校验

5.2 LangChain 1.x 专用(阶段 2 / 4)

依赖 版本 用途
@langchain/core ^1.1.0 核心抽象
@langchain/openai ^1.0.0 ChatOpenAI / OpenAIEmbeddings(国产模型走 baseURL)
langchain ^1.0.0 顶层包(create_agent 等)
@langchain/langgraph ^1.0.0 Agent 运行时(可选,复杂路由用)
@langchain/community ^1.0.0 文档加载器/切分器等(pdf-parse 等)
@langchain/mcp-adapters ^0.6.0 接入 MCP 工具
@modelcontextprotocol/sdk ^1.x 开发自定义 MCP Server / HTTP 暴露

备注:LangChain 1.x 中旧 langchain/chains、部分 retriever 已迁至 @langchain/classic,教程阶段 2/3 会明确标注「1.x 写法」与「旧版写法」差异,避免读者踩坑。

5.3 文档解析(阶段 3 / 4)

依赖 版本 用途
pdf-parse ^1.1.1 PDF 文本抽取
@langchain/textsplitters(或 RecursiveCharacterTextSplitter) 随 core 分片

5.4 开发/测试

依赖 版本 用途
jest / ts-jest / @types/jest ^29 / ^29 / ^29 测试
supertest / @types/supertest ^7 E2E
@nestjs/testing ^11.0.1 测试工具
ts-node / tsconfig-paths ^10 / ^4 运行 TS
eslint + @typescript-eslint + prettier Nest 默认 规范

📌 6. 学习时长与顺序建议

阶段 建议时长(业余) 建议时长(全职) 前置
阶段 1 1 周 3–4 天 Node/TS 基础
阶段 2 1 周 4–5 天 阶段 1
阶段 3 5–7 天 3–4 天 阶段 1(+可选 2)
阶段 4 1 周 4–5 天 全部前序
合计 约 4 周 约 2 周 —

顺序建议:严格按 1→2→3→4 递进;每阶段「先读文档原理 → 跑通示例代码 → 用 curl 验证 → 再进下一章」。有后端经验者可跳过 1.1–1.3 快速过。阶段 3 的向量部分建议配合 EXPLAIN ANALYZE 实际观察索引命中。


📌 7. 风险点与注意事项

  1. LangChain 1.x 变化大:旧教程大量 .call()/.run()/langchain/chains 写法已废弃,网上旧资料易误导。教程必须统一 1.x 范式并标注差异;写代码时以官方迁移指南为准逐条核对 API。
  2. Node 版本硬约束:1.x 要求 Node 22+,环境搭建章节需前置强调,否则安装/运行报错。
  3. DeepSeek 无 embedding API:向量化已改用通义千问 text-embedding-v3,教程需明确「聊天模型与嵌入模型分开配置」。
  4. 国产模型端点差异:三家 baseURL、模型名、是否支持 reasoning/thinking_mode 各异,模型工厂需做「能力描述」字段(profile)区分,路由与参数配置要按能力判断。
  5. 版本兼容:@langchain/* 各包必须共享同一 @langchain/core 版本(官方明确要求),教程 package.json 用 caret 对齐并锁版本。
  6. TypeORM + pgvector:TypeORM 不原生支持 vector 类型,需用 @Column({ type: 'vector' }) 自定义 + pg 原生 SQL/QueryBuilder 做距离检索,教程要专门讲这个坑。
  7. Mock 与真实 key 切换:Mock LLM 需实现 BaseChatModel/ChatOpenAI 兼容接口,保证「无 key 可跑链路、有 key 一键真调」;真实调用需提示流量费用。
  8. 本地安装 pgvector 失败场景:macOS 若 brew install pgvector 与 postgresql@16 版本不匹配,需给源码编译备选路径。
  9. 安全:JWT 密钥、API Key 一律走 .env,.env.example 不含真实值;Refresh Token 落地要讲清存储与轮换策略。
  10. 可运行性承诺:阶段 1、3 关键路径(PG CRUD、向量写入/检索、RAG 召回)实际跑通;阶段 2、4 的纯 LLM 链路用 Mock 验证 + 真实 key 可选。

📌 8. 执行方式(审核通过后)

  1. 按阶段 1→2→3→4 依次产出:先文档(原理+代码+验证步骤),再落地 code/ 下对应可运行项目。
  2. 每阶段完成即做编译级校验(tsc --noEmit / npm run build 通过、strict: true、禁止 any),阶段 1/3 起本地 PG 跑通关键路径。
  3. 全部接口附 curl 命令;每阶段 README 含启动说明 + 排错。
  4. 产出目录统一 ./tutorial/,最终将本计划归档为 tutorial/plan.md 并生成 tutorial/README.md 导航。

待执行清单(Checklist)

  • [ ] 编写 tutorial/00-environment-setup.md
  • [ ] 阶段 1:文档 + code/01-nest-basic/(编译通过 + PG CRUD/JWT 跑通)
  • [ ] 阶段 2:文档 + code/02-langchain-module/(Mock 链路跑通)
  • [ ] 阶段 3:文档 + code/03-pg-vector-rag/(向量写入/检索/RAG 召回跑通)
  • [ ] 阶段 4:文档 + code/04-full-project/(编译通过 + 关键路径跑通 + Docker 配置)
  • [ ] 归档 tutorial/plan.md + tutorial/README.md

🧭 NestJS 工程化 · 篇首 通义适配版 · tutorial-qw3.8

配套代码:../code/01-nest-basic/(完整可运行项目) 技术栈:NestJS 11(Express 引擎)+ TypeScript 5 + TypeORM 0.3 + PostgreSQL 16 建议学习时长:4~5 天(每天 2~3 小时)

本阶段覆盖后端开发所有常用能力。每个知识点按固定结构展开:

🎯 学习目标 → 📖 原理讲解 → 🔨 最小示例 → 🏗️ 完整实现 → ✅ 验证步骤 → ⚠️ 常见坑点 → 📝 课后练习


📌 第 1 章 环境搭建与项目初始化

🎯 学习目标

  • 能用 NestJS CLI 创建并启动一个项目
  • 能说清项目里每个文件/文件夹的作用
  • 能手写第一个 GET 接口并用 curl 验证

📖 原理讲解

NestJS 是什么? 一个用 TypeScript 构建 Node.js 服务端的框架。它的核心思想是"架构先行":用**模块(Module)+ 控制器(Controller)+ 服务(Service)**的组织方式,让后端代码像搭积木一样分层组装,特别适合团队协作和中大型项目。

一个 HTTP 请求在 Nest 中的旅程:

客户端请求
   │
   ▼
┌─────────────┐    根据 URL 路由     ┌──────────────┐    调用业务方法    ┌─────────────┐
│  Nest 内核   │ ──────────────────► │  Controller  │ ────────────────► │   Service   │
│ (HTTP 服务器) │                     │  (前台接待)  │                   │ (业务办理)  │
└─────────────┘                      └──────────────┘                    └─────────────┘

🔨 最小示例:创建并启动项目

# 1. 全局安装 CLI
npm i -g @nestjs/cli

# 2. 创建项目(选择 npm 作为包管理器)
nest new 01-nest-basic
# ? Please choose a package manager: ❯ npm

# 3. 进入项目并启动
cd 01-nest-basic
npm run start

看到 Nest application successfully started 后,打开新终端验证:

curl http://localhost:3000
# 预期输出:Hello World!

启动命令对照表:

命令 作用 使用场景
npm run start 编译并启动 日常开发
npm run start:dev 监听文件变化自动重启 开发首选
npm run start:prod 运行编译后的 dist 产物 生产环境
npm run build 只编译不启动 CI/部署打包

🏗️ 目录结构详解

01-nest-basic/
├── src/
│   ├── main.ts              # 【入口】创建应用、注册全局能力、监听端口
│   ├── app.module.ts        # 【根模块】整个应用的"总装配车间"
│   ├── app.controller.ts    # 示例控制器:处理 GET /
│   └── app.service.ts       # 示例服务:返回 Hello World
├── nest-cli.json            # CLI 配置(源码目录、编译选项)
├── tsconfig.json            # TypeScript 配置(装饰器支持等)
├── tsconfig.build.json      # 编译时排除测试文件的配置
├── package.json             # 依赖与脚本
└── node_modules/            # 依赖包

main.ts 逐行解读:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // 以根模块为蓝图创建应用实例
  const app = await NestFactory.create(AppModule);
  // 监听 3000 端口,开始接收请求
  await app.listen(3000);
}
void bootstrap();

✅ 验证步骤:手写第一个接口

修改 src/app.controller.ts:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller() // 装饰器:声明这是一个控制器,括号里可填路由前缀
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() // 处理 GET /
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('hello') // 处理 GET /hello
  hello(): { message: string } {
    return { message: '你好,NestJS!' };
  }
}
npm run start:dev   # 保存文件后自动重启
curl http://localhost:3000/hello
# 预期:{"message":"你好,NestJS!"}

⚠️ 常见坑点

  1. nest: command not found:CLI 没装到全局 PATH。用 npx nest new xxx 代替,或检查 npm 全局 bin 目录是否在 PATH 中。
  2. 端口被占用:Error: listen EADDRINUSE: address already in use :::3000。用 lsof -i :3000 找到占用进程,或改用其他端口:await app.listen(3101)。
  3. 装饰器报错 Unable to resolve signature:tsconfig 缺少 experimentalDecorators 和 emitDecoratorMetadata,用 CLI 生成的项目不会有此问题,手动建项目时注意。

📝 课后练习

  1. 新增一个 GET /time 接口,返回当前服务器时间的 JSON。
  2. 把监听端口改成从环境变量 PORT 读取(process.env.PORT ?? 3000)。

📌 第 2 章 核心概念:模块化与依赖注入

🎯 学习目标

  • 理解 Module / Controller / Service / Provider 的分工
  • 能用 CLI 生成模块并理解依赖注入的原理
  • 会写自定义 Provider

📖 原理讲解

用公司来类比:

Nest 概念 公司类比 职责
Module 部门 把相关的人(Provider)和业务窗口(Controller)组织在一起,决定谁能被其他部门借用(exports)
Controller 前台 只负责接待(接收请求、返回响应),不亲自干活
Service 业务专员 真正干活的人:业务逻辑、数据库操作
Provider 员工的统称 任何可以被"注入"(分配)给别人的类,Service 是最常见的 Provider
依赖注入(DI) HR 自动分配人手 你在构造器里声明"我需要一个 UserService",框架自动创建并递给你,不用自己 new

为什么需要依赖注入? 对比一下:

// ❌ 手动 new:耦合死,测试时没法替换成假实现
class UsersController {
  private usersService = new UsersService(new Repository(...));
}

// ✅ 依赖注入:声明需求即可,框架负责创建与生命周期管理
class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

模块的四个配置项:

@Module({
  imports: [],      // 引入其他模块(借用别人的能力)
  controllers: [],  // 本模块的控制器
  providers: [],    // 本模块的服务/提供者
  exports: [],      // 允许被其他模块使用的 providers
})

🔨 最小示例:用 CLI 生成 users 模块

nest g module users
nest g controller users
nest g service users
# 简写:nest g mo users / nest g co users / nest g s users

CLI 会自动把新生成的 Controller/Service 注册进 users.module.ts——这就是"模块化"的便利之处。

🏗️ 完整实现

本项目中的模块组织(见 app.module.ts):

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, ... }),  // 全局配置
    TypeOrmModule.forRootAsync({ ... }),            // 数据库连接
    AuthModule, UsersModule, PostsModule,           // 业务模块
    FilesModule, ChatModule, HealthModule,
  ],
  controllers: [AppController],
  providers: [ /* 全局过滤器/拦截器 */ ],
})
export class AppModule {}

自定义 Provider 的四种写法(了解即可,useClass 之外的三种在写测试/动态实现时很有用):

const providers = [
  UsersService,                                    // 标准写法(useClass 的简写)
  { provide: 'API_VERSION', useValue: 'v1' },      // useValue:注入一个常量
  {                                                // useFactory:动态创建
    provide: 'DB_OPTIONS',
    useFactory: (config: ConfigService) => ({ host: config.get('DB_HOST') }),
    inject: [ConfigService],
  },
  { provide: 'OldService', useExisting: UsersService }, // useExisting:起别名
];

注入常量时用 @Inject:

constructor(@Inject('API_VERSION') private readonly apiVersion: string) {}

✅ 验证步骤

cd code/01-nest-basic && npm run start:dev
# 观察启动日志中的路由映射(Mapper):
# [RouterExplorer] Mapped {/api/users, GET} route
# 说明 users 模块的控制器已被根模块装配成功

⚠️ 常见坑点

  1. Nest can't resolve dependencies of XxxService (?):XxxService 依赖的东西没注册。检查:依赖的 Service 是否在同模块 providers 里?跨模块依赖时,对方模块是否 exports 了它?是否 imports 了对方模块?
  2. 循环依赖:A 模块 import B,B 又 import A → 用 forwardRef(() => BModule) 包裹。更好的做法是重新设计模块边界。
  3. Service 里注入不到 Repository:忘了 TypeOrmModule.forFeature([实体]),见第 4 章。

📝 课后练习

  1. 创建一个 orders 模块(module + controller + service),实现 GET /api/orders 返回一个假订单数组,并在根模块注册。
  2. 用 useValue 注入一个常量 APP_NAME,在某个接口中返回它。

📌 第 3 章 配置管理

🎯 学习目标

  • 理解为什么要用环境变量管理配置
  • 会用 @nestjs/config 加载 .env 并实现多环境配置
  • 会做启动期配置校验(缺配置立即报错)

📖 原理讲解

为什么不把数据库密码写死在代码里?

  1. 代码会提交到 git,密码等于公开;
  2. 开发/测试/生产环境的数据库地址不同,写死就没法切换。

解决方案:配置与代码分离——敏感/易变的配置放 .env 文件(加入 .gitignore,不提交),代码通过 process.env 读取。

.env.example(模板,提交到 git)  →  开发者复制为 .env(真实值,不提交)

🔨 最小示例

npm i @nestjs/config
echo "PORT=3101" > .env
// app.module.ts
ConfigModule.forRoot({ isGlobal: true });  // isGlobal: 任何模块都能注入 ConfigService

// 任意 Service 中
constructor(private readonly configService: ConfigService) {}
const port = this.configService.get('PORT');

🏗️ 完整实现

① 多环境配置工厂(configuration.ts):把所有配置集中成一个有类型的对象,还能按 NODE_ENV 区分环境:

export default (): AppConfig => ({
  env: process.env.NODE_ENV ?? 'development',
  port: parseInt(process.env.PORT ?? '3101', 10),
  db: { host: process.env.DB_HOST ?? '127.0.0.1', /* ... */ },
  jwt: { accessSecret: process.env.JWT_ACCESS_SECRET ?? '...', /* ... */ },
  upload: { dir: process.env.UPLOAD_DIR ?? 'uploads', /* ... */ },
});

② 启动期校验(env.validation.ts):把环境变量当 DTO 校验,缺了关键配置直接拒绝启动(fail fast):

class EnvironmentVariables {
  @IsString() DB_PASSWORD!: string;
  @IsInt() @Min(1) @Max(65535) PORT!: number;
  // ...
}

export function validateEnv(config: Record<string, unknown>) {
  const validated = plainToInstance(EnvironmentVariables, config, {
    enableImplicitConversion: true, // "5432" 自动转数字
  });
  const errors = validateSync(validated, { skipMissingProperties: false });
  if (errors.length > 0) {
    throw new Error(`环境变量校验失败,请检查 .env:\n${错误详情}`);
  }
  return validated;
}

③ 装配(app.module.ts):

ConfigModule.forRoot({
  isGlobal: true,
  load: [configuration],   // 加载配置工厂
  validate: validateEnv,   // 启动期校验
  envFilePath: ['.env'],
});

④ 类型安全取值:ConfigService<AppConfig> 泛型 + infer: true,取值有类型提示:

const secret = this.configService.get('jwt.accessSecret', { infer: true });
// 关键配置用 getOrThrow:缺失直接抛错,而不是拿到 undefined 埋雷
const host = this.configService.getOrThrow<string>('db.host', { infer: true });

✅ 验证步骤

cd code/01-nest-basic
cp .env.example .env
# 故意删掉 .env 里的 DB_PASSWORD 一行,然后启动:
npm run start:dev
# 预期:启动失败,报错 "环境变量校验失败,请检查 .env:DB_PASSWORD: ..."
# 恢复后重新启动即可正常运行

⚠️ 常见坑点

  1. .env 不生效:envFilePath 是相对进程启动目录的,从项目根目录启动即可;或写绝对路径。
  2. 取到的是字符串:env 里一切都是字符串,数字要 parseInt,布尔要自己比较 'true'。
  3. 把 .env 提交到了 git:务必在 .gitignore 中写 .env,仓库里只留 .env.example。

📝 课后练习

  1. 给配置增加 LOG_LEVEL 字段(默认 info),并在启动日志中打印它。
  2. 让 NODE_ENV=production 时关闭 Swagger(提示:在 main.ts 里判断后再 setup)。

📌 第 4 章 TypeORM 接入 PostgreSQL

🎯 学习目标

  • 理解 ORM 的作用,会定义实体并连接 PostgreSQL
  • 掌握 Repository 增删改查、QueryBuilder、关联查询
  • 理解事务的意义并会写事务代码
  • 会用迁移脚本管理表结构

📖 原理讲解

ORM(对象关系映射):把数据库表映射成 TypeScript 类,把 SQL 变成方法调用。

数据库表 users            TS 类 User
┌────┬──────────┐        class User {
│ id │ username │   ←→     id: number;
└────┴──────────┘          username: string;
                         }
一行记录 = 一个对象实例;查表 = repository.find()

本教程的三个实体关系:

User 1 ──── 1 Profile   (一对一:一个用户一份扩展资料)
User 1 ──── N Post      (一对多:一个用户发多篇文章)

🔨 最小示例

npm i @nestjs/typeorm typeorm pg
// 实体:一个类 = 一张表
@Entity('users')
export class User {
  @PrimaryGeneratedColumn()      // 自增主键
  id!: number;

  @Column({ unique: true, length: 50 })  // varchar(50) 唯一列
  username!: string;
}

// app.module.ts 中连接数据库
TypeOrmModule.forRoot({
  type: 'postgres',
  host: '127.0.0.1', port: 5432,
  username: 'tutorial', password: 'tutorial123', database: 'tutorial',
  entities: [User],
  synchronize: true,  // 开发期自动同步表结构(生产禁用!)
});

// 业务模块中注册仓储
TypeOrmModule.forFeature([User])

// Service 中使用
@InjectRepository(User) private readonly userRepo: Repository<User>
await this.userRepo.save(this.userRepo.create({ username: 'alice' }));

🏗️ 完整实现

① 实体定义(user.entity.ts)——常用列选项:

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ name: 'username', type: 'varchar', length: 50, unique: true })
  username!: string;                    // name: 列名;type: 列类型;unique: 唯一索引

  @Column({ name: 'role', type: 'varchar', length: 10, default: 'user' })
  role!: UserRole;                      // default: 默认值

  @Column({ name: 'refresh_token_hash', type: 'varchar', length: 100, nullable: true })
  refreshTokenHash!: string | null;     // nullable: 允许 NULL(TS 类型要对应 | null)

  @CreateDateColumn({ name: 'created_at' })
  createdAt!: Date;                     // 自动写入创建时间

  @OneToOne('Profile', 'user')          // 一对一关联(声明式)
  profile?: Profile;

  @OneToMany('Post', 'author')          // 一对多关联
  posts?: Post[];
}

多对一的一方(post.entity.ts)需要外键列 + @JoinColumn:

@Column({ name: 'author_id', type: 'int' })
authorId!: number;

@ManyToOne('User', 'posts')
@JoinColumn({ name: 'author_id' })   // 指定外键列
author?: User;

② Repository 增删改查(users.service.ts):

// 增
const user = this.userRepository.create({ username, passwordHash });
await this.userRepository.save(user);

// 查:按条件查一条
const found = await this.userRepository.findOne({ where: { username } });
// 查:列表 + 模糊匹配 + 限制条数
const list = await this.userRepository.find({
  where: { username: ILike(`%${keyword}%`) },  // ILIKE 不区分大小写
  take: 20,
});

// 改:先查出来改字段再 save(save 会保留未修改字段)
user.role = 'admin';
await this.userRepository.save(user);

// 删
await this.userRepository.delete(id);

// 原子自增(避免先查后写的并发问题)
await this.postRepository.increment({ id }, 'viewCount', 1);

③ QueryBuilder:复杂查询与分页:

const [list, total] = await this.userRepository
  .createQueryBuilder('u')                    // u 是表别名
  .leftJoinAndSelect('u.profile', 'profile')  // 关联加载扩展资料
  .where('u.username ILIKE :kw', { kw: `%${keyword}%` })  // 参数化防注入
  .orderBy('u.id', 'ASC')
  .skip((page - 1) * pageSize)                // OFFSET
  .take(pageSize)                             // LIMIT
  .getManyAndCount();                         // 同时拿列表和总数

④ 关联查询的两种写法:

// 写法 A:relations 声明式(简单场景推荐)
this.userRepository.findOne({ where: { id }, relations: { profile: true } });

// 写法 B:QueryBuilder leftJoinAndSelect(需要 where/分页时推荐)

⑤ 事务(posts.service.ts)——发帖必须"插入文章 + 作者计数 +1"同生共死:

return this.dataSource.transaction(async (manager) => {
  // ⚠️ 事务内必须用回调参数 manager,不能用 this.xxxRepository!
  const author = await manager.findOne(User, {
    where: { id: dto.authorId },
    lock: { mode: 'pessimistic_write' },  // 悲观锁:防止并发问题
  });
  if (!author) throw new NotFoundException('作者不存在,事务将回滚');

  const saved = await manager.save(manager.create(Post, { ...dto }));
  await manager.increment(User, { id: dto.authorId }, 'postCount', 1);
  return saved;
  // 回调正常结束 → 自动 commit;抛异常 → 自动 rollback
});

⑥ 迁移脚本——生产环境表结构的版本管理:

# data-source.ts 是迁移 CLI 专用的 DataSource(synchronize 必须 false)
npm run migration:run       # 执行未运行的迁移(建表)
npm run migration:revert    # 回滚最近一次
npm run migration:generate -- src/database/migrations/AddXxx  # 对比实体自动生成迁移

迁移脚本长这样(1700000000000-InitSchema.ts):

export class InitSchema1700000000000 implements MigrationInterface {
  async up(qr: QueryRunner)   { /* 升级 SQL:CREATE TABLE ... */ }
  async down(qr: QueryRunner) { /* 回滚 SQL:DROP TABLE ... */ }
}

✅ 验证步骤

cd code/01-nest-basic && cp .env.example .env && npm run start:dev
# synchronize 会自动建表。登录数据库确认:
docker exec -it tutorial-pg psql -U tutorial -d tutorial -c "\dt"
# 预期看到 users / profiles / posts / migrations 四张表

# 接口验证(需先完成第 5 章注册登录拿到 TOKEN):
curl -X POST http://localhost:3101/api/posts \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"测试事务","content":"内容","authorId":1}'
# 然后查 users 表确认 post_count 变成 1:
docker exec -it tutorial-pg psql -U tutorial -d tutorial \
  -c "SELECT username, post_count FROM users;"

# 事务回滚验证:authorId 传不存在的 999 → 返回 404,且 posts 表无新增

⚠️ 常见坑点

  1. No metadata for "User" was found:实体没注册。检查 forFeature([User]) 和 forRoot 的 entities 数组。
  2. 事务里用了 this.repository:操作不在事务内,回滚无效。事务回调里必须用 manager。
  3. 生产环境开着 synchronize:它可能自动删列改表造成数据丢失!生产必须 synchronize: false + 迁移脚本。
  4. findOne 不带 where 报错:TypeORM 0.3 要求 findOne({ where: {...} }),多条匹配会抛错,取最新一条用 find({ order: {id:'DESC'}, take: 1 })。
  5. nullable 列的 TS 类型:列允许 NULL 时,属性类型要写 string | null,否则严格模式下编译/运行都会别扭。

📝 课后练习

  1. 给 Post 实体加一个 tags 字段(varchar),并实现按 tag 搜索文章的接口。
  2. 写一个"转账"事务:users 表加 balance 列,实现 A 给 B 转账,余额不足时整体回滚。

📌 第 5 章 JWT 鉴权体系

🎯 学习目标

  • 理解 Session 与 JWT 的区别
  • 实现注册(密码加盐哈希)与登录
  • 实现 access + refresh 双 Token 机制与刷新轮换
  • 用 Guard 保护接口,用角色控制权限

📖 原理讲解

Session vs JWT:

Session(门禁卡模式) JWT(身份证模式)
凭证存哪 服务器内存/Redis 存会话 客户端自己保存(一段签名的字符串)
服务器压力 要存所有会话,扩容麻烦 无状态,只验签名,天然适合分布式
吊销 删会话即可 需要额外机制(本教程用 refresh token 哈希存库)

JWT 结构:header.payload.signature 三段 Base64。payload 放用户信息(不要放密码等敏感数据,它只是编码不是加密!),signature 用服务器密钥签名——篡改 payload 会导致验签失败。

双 Token 机制:

登录成功
  ├─ accessToken(15 分钟):每次请求携带,短期有效降低泄露风险
  └─ refreshToken(7 天):只用于换新 token,存库哈希,支持轮换
accessToken 过期 → 用 refreshToken 调 /auth/refresh → 拿到新的一对(旧的作废)

完整鉴权链路:

请求带 Authorization: Bearer <accessToken>
   ▼
JwtAuthGuard(守卫:决定能不能进)
   ▼ 调用 JwtStrategy
从 Header 提取 token → 用密钥验签 → 解析 payload → 挂到 request.user
   ▼
RolesGuard(可选:检查角色)
   ▼
Controller 处理(用 @CurrentUser() 取登录用户)

🔨 最小示例

npm i @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
npm i -D @types/passport-jwt @types/bcryptjs
// 签发
const token = await this.jwtService.signAsync({ sub: user.id, username: user.username }, {
  secret: 'my-secret', expiresIn: '15m',
});

// 验证
const payload = await this.jwtService.verifyAsync(token, { secret: 'my-secret' });

🏗️ 完整实现

① 注册:密码绝不存明文(auth.service.ts):

// bcrypt 加盐哈希:同样的密码每次哈希结果都不同(盐随机),无法反推原文
const passwordHash = await bcrypt.hash(password, 10); // cost=10
await this.userRepository.save(this.userRepository.create({ username, passwordHash }));

// 登录时比对
const matched = await bcrypt.compare(password, user.passwordHash);

② JWT 策略(jwt.strategy.ts):

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService<AppConfig>) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 从 Bearer 头提取
      ignoreExpiration: false,                                  // 过期即拒绝
      secretOrKey: configService.get('jwt.accessSecret', { infer: true }) as string,
    });
  }
  // 返回值会挂到 request.user
  validate(payload: JwtPayload): JwtPayload {
    if (payload.type !== 'access') throw new Error('token 类型非法');
    return payload;
  }
}

③ 守卫与装饰器:

// jwt-auth.guard.ts:一行继承,自动接入 JwtStrategy
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// roles.decorator.ts:打标记
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);

// roles.guard.ts:读标记并比对(需配合 Reflector)
const required = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
  context.getHandler(), context.getClass(),
]);

// current-user.decorator.ts:从 request.user 取登录信息
export const CurrentUser = createParamDecorator((_d, ctx) =>
  ctx.switchToHttp().getRequest().user);

④ 使用(auth.controller.ts / users.controller.ts):

@Get('profile')
@UseGuards(JwtAuthGuard)                 // 需要登录
profile(@CurrentUser() user: JwtPayload) { return user; }

@Patch(':id/role')
@UseGuards(JwtAuthGuard, RolesGuard)     // 登录 + 角色检查
@Roles('admin')                          // 仅管理员
updateRole(...) {}

⑤ 刷新与轮换:refresh token 的 sha256 哈希存库,每次刷新后旧 token 立即失效(防重放)。

✅ 验证步骤

# 完整链路见 code/01-nest-basic/README.md,核心命令:
curl -X POST http://localhost:3101/api/auth/register \
  -H 'Content-Type: application/json' -d '{"username":"alice","password":"Passw0rd123"}'

curl -X POST http://localhost:3101/api/auth/login \
  -H 'Content-Type: application/json' -d '{"username":"alice","password":"Passw0rd123"}'
# 拿到 accessToken / refreshToken

export TOKEN="<accessToken>"
curl http://localhost:3101/api/auth/profile -H "Authorization: Bearer $TOKEN"   # 200
curl http://localhost:3101/api/auth/profile                                     # 401
# 用同一个 refreshToken 连续刷新两次 → 第二次 401(轮换生效)

⚠️ 常见坑点

  1. Unknown authentication strategy "jwt":JwtStrategy 没注册到模块 providers。
  2. 401 但 token 看起来没问题:检查签发与验证用的是不是同一个 secret;access/refresh 密钥别混用。
  3. 把密码/手机号放进 JWT payload:payload 只是 Base64 编码,任何人都能解开看,只放 id/角色这类非敏感信息。
  4. RolesGuard 里 request.user 是 undefined:RolesGuard 必须排在 JwtAuthGuard 之后:@UseGuards(JwtAuthGuard, RolesGuard)。

📝 课后练习

  1. 给 refresh token 加"设备指纹"字段,实现按设备登出。
  2. 实现登录失败限流:同一 IP + 账号 60 秒内最多 5 次(提示:内存 Map 或 Redis 计数)。

📌 第 6 章 中间件体系

🎯 学习目标

  • 说清中间件、守卫、拦截器、管道、异常过滤器各自的职责与执行时机
  • 会写请求日志中间件、统一响应拦截器、全局异常过滤器、自定义管道

📖 原理讲解

一次请求的完整生命周期(务必记住这张图):

客户端请求
   │
   ▼
[Middleware 中间件]   最外层:日志、CORS、预处理(Express 风格,next() 传递)
   │
   ▼
[Guard 守卫]          决定"能不能进":鉴权、角色(第 5 章已学)
   │
   ▼
[Interceptor 拦截器·前] 进入处理器之前:记录开始时间等
   │
   ▼
[Pipe 管道]           参数转换与校验:字符串转数字、DTO 校验
   │
   ▼
[Route Handler]       你的 Controller 方法
   │
   ▼
[Interceptor 拦截器·后] 处理结果返回前:包装响应结构、记录耗时
   │
   ▼
客户端响应
   ✗ 任何环节抛异常 → [Exception Filter 异常过滤器] 统一兜底

一句话选型:

需求 用什么
记录日志 / 跨域 / 修改请求头 中间件
判断请求有没有资格执行 守卫
包装响应结构 / 缓存 / 超时 拦截器
校验和转换入参 管道
统一错误响应格式 异常过滤器

🏗️ 完整实现

① 中间件:请求日志(request-logger.middleware.ts):

@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction): void {
    const start = Date.now();
    res.on('finish', () => {
      console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${Date.now() - start}ms`);
    });
    next(); // 必须调用 next(),否则请求卡死
  }
}

// app.module.ts 中注册(模块级)
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(RequestLoggerMiddleware).forRoutes('*');
  }
}

② 拦截器:统一响应结构(transform.interceptor.ts):

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(_ctx: ExecutionContext, next: CallHandler<T>): Observable<ApiResponse<T>> {
    return next.handle().pipe(
      map((data) => ({ code: 0, message: 'success', data })), // 所有返回值包上外壳
    );
  }
}

③ 异常过滤器:统一错误响应(all-exceptions.filter.ts):

@Catch() // 捕获所有异常
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost): void {
    const status = exception instanceof HttpException
      ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    // ... 提取 message(校验错误是数组,要 join)
    response.status(status).json({ code: status, message, data: null });
  }
}

④ 管道:全局校验 + 自定义转换:

// main.ts:全局 ValidationPipe
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,            // 剔除 DTO 未声明的字段
  forbidNonWhitelisted: true, // 出现未声明字段直接 400
  transform: true,            // 自动类型转换(配合 DTO 的 @Type)
}));

// parse-id.pipe.ts:把 :id 转成正整数,否则 400
@Injectable()
export class ParseIdPipe implements PipeTransform<string, number> {
  transform(value: string): number {
    const id = Number(value);
    if (!Number.isInteger(id) || id <= 0) {
      throw new BadRequestException(`非法的 id 参数:${value}`);
    }
    return id;
  }
}
// 使用:findOne(@Param('id', ParseIdPipe) id: number)

⑤ 全局注册(app.module.ts providers):

providers: [
  { provide: APP_FILTER, useClass: AllExceptionsFilter },
  { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
  { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
],

✅ 验证步骤

# 统一响应结构:任何成功接口都被包成 { code, message, data }
curl http://localhost:3101/api/health
# {"code":0,"message":"success","data":{"status":"ok",...}}

# 管道校验:非法 id → 400
curl http://localhost:3101/api/users/abc -H "Authorization: Bearer $TOKEN"

# DTO 校验:注册时密码不合规 → 400 且 message 列出所有错误
curl -X POST http://localhost:3101/api/auth/register \
  -H 'Content-Type: application/json' -d '{"username":"ab","password":"123"}'

# 异常过滤器:访问不存在的路由 → 统一 JSON 错误格式(而非 HTML)
curl http://localhost:3101/api/no-such-route

# 服务端控制台可看到中间件打印的请求日志

⚠️ 常见坑点

  1. 中间件忘了 next():请求永远挂起直到超时。
  2. whitelist 不生效:ValidationPipe 没开 whitelist: true,或 DTO 属性没加任何 class-validator 装饰器(没装饰器的字段不算"已声明")。
  3. 查询参数校验类型错误:查询参数永远是字符串,DTO 里要配 @Type(() => Number) + transform: true。
  4. 拦截器包装了不该包装的响应:文件下载、SSE 流式接口要跳过全局拦截器(阶段 2 会实战)。

📝 课后练习

  1. 写一个"响应耗时"响应头:在拦截器里给响应加 X-Response-Time 头。
  2. 写一个自定义业务异常 BizException(code, message) 和对应过滤器,让业务错误返回 code: 10001 这类自定义码。

📌 第 7 章 文件上传下载

🎯 学习目标

  • 用 multer 实现单文件上传,存储到本地磁盘
  • 用 ServeStaticModule 提供静态访问
  • 实现文件类型白名单、大小限制与流式下载

📖 原理讲解

HTTP 上传文件用 multipart/form-data 格式(把文件切成块编码进请求体)。Nest 底层用 multer 处理解析,FileInterceptor('file') 中的 'file' 就是表单字段名。

两种存储策略:

  • memoryStorage():文件先到内存,由我们校验后再落盘(本教程采用,可以先拦截非法文件);
  • diskStorage():直接写磁盘,性能好但校验滞后。

🏗️ 完整实现

① 上传接口(files.controller.ts):

@Post('upload')
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file', {
  storage: memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024 },  // 5MB 硬限制,超出直接 413/400
}))
upload(@UploadedFile() file: Express.Multer.File) {
  return this.filesService.save(file);
}

② 校验与落盘(files.service.ts):

const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.txt', '.md']);

async save(file: Express.Multer.File): Promise<UploadedFileInfo> {
  // 1. 扩展名白名单
  const ext = extname(file.originalname).toLowerCase();
  if (!ALLOWED_EXTENSIONS.has(ext)) throw new BadRequestException(`不支持的文件类型:${ext}`);
  // 2. 大小双保险
  if (file.size > this.maxSize) throw new BadRequestException('文件超过大小限制');
  // 3. 按日期分目录 + 随机文件名(防重名、防路径穿越)
  const dateDir = new Date().toISOString().slice(0, 7);      // 2026-08
  const filename = `${randomUUID()}${ext}`;
  await mkdir(join(this.uploadDir, dateDir), { recursive: true });
  await writeFile(join(this.uploadDir, dateDir, filename), file.buffer);
  return { filename: `${dateDir}/${filename}`, url: `/api/files/static/${dateDir}/${filename}`, ... };
}

③ 静态资源服务(files.module.ts):

ServeStaticModule.forRootAsync({
  useFactory: (configService: ConfigService<AppConfig>) => [{
    rootPath: join(process.cwd(), configService.get('upload.dir', { infer: true }) ?? 'uploads'),
    serveRoot: '/api/files/static',   // 访问前缀
  }],
  inject: [ConfigService],
});
// 浏览器打开 /api/files/static/2026-08/xxx.png 即可看到图片

④ 流式下载:

@Get('download/*path')
download(@Param('path') path: string): Promise<StreamableFile> {
  return this.filesService.download(p);
}

// service 中:
const fullPath = resolve(this.uploadDir, relativePath);
if (!fullPath.startsWith(this.uploadDir)) throw new BadRequestException('非法路径'); // 防路径穿越
const stream = createReadStream(fullPath);   // 流式读取,大文件不占内存
return new StreamableFile(stream, { disposition: `attachment; filename="..."` });

✅ 验证步骤

echo "hello nestjs" > /tmp/test.txt
curl -X POST http://localhost:3101/api/files/upload \
  -H "Authorization: Bearer $TOKEN" -F "file=@/tmp/test.txt"
# 返回 { filename: "2026-08/xxx.txt", url: "/api/files/static/2026-08/xxx.txt", ... }

curl http://localhost:3101/api/files/static/2026-08/xxx.txt     # 静态访问
curl -OJ http://localhost:3101/api/files/download/2026-08/xxx.txt \
  -H "Authorization: Bearer $TOKEN"                              # 下载

# 非法类型 → 400
echo x > /tmp/a.exe && curl -X POST http://localhost:3101/api/files/upload \
  -H "Authorization: Bearer $TOKEN" -F "file=@/tmp/a.exe"

⚠️ 常见坑点

  1. @UploadedFile() 拿到 undefined:表单字段名和 FileInterceptor('xxx') 的参数不一致;或请求没带 multipart/form-data(curl 用 -F 会自动带)。
  2. 大文件上传报 413:除了 multer limits,反向代理(nginx)也有 client_max_body_size 限制。
  3. 只校验 MIME 不校验扩展名:MIME 可以伪造,生产环境建议扩展名 + 文件头魔数双重校验。
  4. 路径穿越攻击:下载接口必须校验 resolve 后的路径仍在上传目录内(代码中已演示)。

📝 课后练习

  1. 实现多文件上传接口(FilesInterceptor('files', 5),最多 5 个)。
  2. 给上传记录建一张 file_records 表,记录谁在什么时候上传了什么文件。

📌 第 8 章 WebSocket

🎯 学习目标

  • 理解 HTTP 与 WebSocket 的区别
  • 用 @WebSocketGateway 实现消息收发
  • 实现房间管理、广播与握手鉴权

📖 原理讲解

HTTP vs WebSocket:HTTP 像发短信——每次都是客户端主动问,服务器被动答;WebSocket 像打电话——一次握手后建立长连接,双方随时说话。聊天室、实时通知、协同编辑这类场景必须用 WebSocket。

HTTP:    客户端 ──请求──► 服务器     (每次都要重新建立连接)
          客户端 ◄──响应── 服务器

WebSocket:客户端 ◄════════► 服务器    (一次握手,双向实时)

socket.io 的两个关键概念:

  • 命名空间(namespace):同一条连接上的逻辑分区,如 /chat;
  • 房间(room):命名空间内更细的分组,消息可以只发给某个房间。

🏗️ 完整实现(chat.gateway.ts)

① 网关声明与握手鉴权:

@WebSocketGateway({ namespace: '/chat', cors: { origin: '*' } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server!: Server;   // 注入 server 实例,用于广播

  handleConnection(client: Socket): void {
    // 客户端连接时从 handshake.auth 取 token 校验
    const token = client.handshake.auth?.token as string | undefined;
    try {
      const payload = this.jwtService.verify(token, { secret: accessSecret });
      client.data.user = { sub: payload.sub, username: payload.username };
    } catch {
      client.disconnect(true);   // 鉴权失败直接断开
    }
  }
}

② 事件处理与房间:

@SubscribeMessage('join_room')              // 监听客户端 emit 的事件名
joinRoom(@ConnectedSocket() client: Socket, @MessageBody() room: string) {
  void client.join(room);
  client.to(room).emit('system', { message: `${username} 加入了房间` }); // 通知房内其他人
}

@SubscribeMessage('send_message')
sendMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: ChatMessage) {
  // 广播给房间内所有人(含自己);to() 指定房间
  this.server.to(payload.room).emit('new_message', {
    from: (client.data.user as SocketUser).username,
    content: payload.content,
    at: new Date().toISOString(),
  });
}

③ 客户端连接:

const { io } = require('socket.io-client');
const socket = io('http://localhost:3101/chat', {
  auth: { token: '<accessToken>' },   // 握手鉴权
});
socket.on('connected', console.log);
socket.emit('join_room', 'room1');
socket.emit('send_message', { room: 'room1', content: '你好' });
socket.on('new_message', console.log);

✅ 验证步骤

完整脚本见 code/01-nest-basic/README.md 的 ws-test.js:

cd code/01-nest-basic && npm i socket.io-client
export TOKEN="<你的 accessToken>"
node ws-test.js
# 预期输出:已连接 → 已加入房间 room1 → 收到消息(自己发的也会广播回来)
# 不带 token 连接 → 立即被断开

⚠️ 常见坑点

  1. 用 wscat 测 socket.io 失败:socket.io 有自己的握手协议,不是裸 WebSocket,要用 socket.io-client 测试。
  2. handleConnection 里抛异常:会导致连接处理崩溃,应该 client.disconnect() 优雅拒绝。
  3. 跨域连接失败:网关要配 cors: { origin: ... }。
  4. 服务重启所有连接断开:WebSocket 是有状态连接,客户端必须实现断线重连(socket.io-client 默认自带重连)。

📝 课后练习

  1. 实现"在线人数统计":维护每个房间的成员集合,get_online_count 事件返回人数。
  2. 实现私聊:send_private 事件只发给指定 username 的用户(提示:连接时把 client.id 按 username 存进 Map)。

📌 第 9 章 Swagger 接口文档

🎯 学习目标

  • 用 @nestjs/swagger 自动生成可交互的 API 文档
  • 会用装饰器描述接口与 DTO
  • 会在文档页面带 Token 调试受保护接口

📖 原理讲解

手写接口文档又累又容易过时。Swagger 的思路是:文档从代码装饰器自动生成——代码即文档,永远同步。

🏗️ 完整实现

① 初始化(main.ts):

import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

const swaggerConfig = new DocumentBuilder()
  .setTitle('01-nest-basic 教程接口文档')
  .setDescription('阶段 1:NestJS 后端工程化全流程示例接口')
  .setVersion('1.0')
  .addBearerAuth()          // 开启 Bearer Token 认证支持
  .build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, document);   // 挂载到 /api/docs

② 装饰器描述接口(各 controller 已示范):

@ApiTags('认证')                    // 分组标签
@Controller('auth')
export class AuthController {
  @Post('login')
  @ApiOperation({ summary: '登录,返回双 token' })   // 接口说明
  login(@Body() dto: LoginDto) { ... }
}

// DTO 字段描述
export class RegisterDto {
  @ApiProperty({ description: '用户名', example: 'alice' })
  @MinLength(3)
  username!: string;
}

// 受保护接口标记(文档页会出现锁形图标)
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)

✅ 验证步骤

  1. 启动项目,浏览器打开 http://localhost:3101/api/docs;
  2. 先调 POST /api/auth/login 拿到 accessToken;
  3. 点右上角 Authorize → 输入 token → Authorize;
  4. 展开任意带锁接口 → Try it out → Execute,可直接调试。

⚠️ 常见坑点

  1. 文档里看不到某个接口:该 Controller 没注册进任何模块。
  2. DTO 字段不显示:属性必须有至少一个装饰器(class-validator 或 @ApiProperty),否则编译后元数据丢失。
  3. 生产环境暴露文档:上线时应通过环境变量关闭 Swagger(见第 3 章课后练习 2)。

📝 课后练习

  1. 给所有接口补全 @ApiOperation 与 @ApiResponse({ status: 401, description: '未登录' })。
  2. 用 @ApiQuery 给 GET /api/users 的分页参数补充文档说明。

📌 阶段 1 自测清单

学完本阶段后,请逐条自检(都能回答再进入阶段 2):

  1. 能否说清一个请求从进入 Nest 到返回响应,依次经过哪些环节(中间件→守卫→拦截器→管道→处理器→过滤器)?
  2. Module 的 imports / controllers / providers / exports 各自的作用?什么时候需要 exports?
  3. 依赖注入解决了什么问题?Nest can't resolve dependencies 报错如何排查?
  4. 为什么生产环境不能用 synchronize: true?迁移脚本怎么生成和运行?
  5. 为什么密码要 bcrypt 哈希而不是加密存储?双 Token 机制中 refresh token 的作用?
  6. whitelist 与 forbidNonWhitelisted 的区别?查询参数为什么需要 @Type(() => Number)?
  7. 事务回调里为什么必须用 manager 而不是 this.repository?
  8. 文件上传为什么要"扩展名白名单 + 随机文件名 + 路径穿越校验"三件套?
  9. WebSocket 握手鉴权在哪一步做?鉴权失败怎么处理?
  10. 能否不看任何资料,独立写出一个带 DTO 校验的 CRUD 模块(含模块注册、Swagger 描述)?

下一阶段:阶段 2:LangChain 大模型服务开发 —— 在 NestJS 中模块化集成 LangChain,实现多模型管理、智能路由、MCP、Skill 与流式对话。

📁 配套代码目录 tutorial-qw3.8/code/01-nest-basic

    01-nest-basic/
      .env.example
      .gitignore
      README.md
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      src/
        app.controller.ts
        app.module.ts
        main.ts
        auth/
          auth.controller.ts
          auth.module.ts
          auth.service.ts
        chat/
          chat.gateway.ts
          chat.module.ts
        common/
        config/
          configuration.ts
          env.validation.ts
        database/
          data-source.ts
        entities/
          post.entity.ts
          profile.entity.ts
          user.entity.ts
        files/
          files.controller.ts
          files.module.ts
          files.service.ts
        health/
          health.controller.ts
          health.module.ts
        posts/
          posts.controller.ts
          posts.module.ts
          posts.service.ts
        users/
          users.controller.ts
          users.module.ts
          users.service.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 LangChain 服务 · 篇首 通义适配版 · tutorial-qw3.8

配套代码:../code/02-langchain-module/(完整可运行项目) 技术栈:NestJS 11 + LangChain.js 1.x(@langchain/core 1.2 / @langchain/langgraph 1.4)+ PostgreSQL 16 建议学习时长:4~5 天(每天 2~3 小时)

⚠️ 版本提示:LangChain JS 在 1.x 有大量破坏性变更,网上 2024 年前的教程 import 路径大多已失效。本教程全部按 1.x 编写,import 一律以 @langchain/core 为准。


📌 第 1 章 LangChain 基础与 Nest 模块化封装

🎯 学习目标

  • 理解 LangChain 的定位与核心概念
  • 用 5 行代码调通第一次大模型调用
  • 把模型能力封装成 Nest 模块供业务注入

📖 原理讲解

LangChain 是什么? 类比:大模型本身像一个只会"聊天"的天才大脑;LangChain 是给它配的万能遥控器 + 工具箱——统一的模型调用接口、提示词模板、工具调用、记忆管理、Agent 编排,都用标准化的积木提供。

LangChain.js 1.x 包结构:

包 职责
@langchain/core 核心抽象:消息类型、ChatModel 基类、tool()、提示词模板
@langchain/openai OpenAI 及兼容协议集成(国产模型都走它)
@langchain/langgraph Agent 与状态图编排(createReactAgent)
@langchain/mcp-adapters MCP 协议工具适配

核心概念速览:

ChatModel   对话模型实例,invoke(消息数组) → AI 回复
Messages    SystemMessage(设定) / HumanMessage(用户) / AIMessage(助手)
Tool        模型可以调用的函数(zod 描述参数)
Agent       "思考→调工具→观察→再思考"的循环执行体(ReAct)

国产模型怎么接? DeepSeek、通义千问、字节 Seed 都提供 OpenAI 兼容协议端点。所以只需 ChatOpenAI + 改 baseURL + 厂商 Key,一套代码通吃所有模型。

🔨 最小示例:第一次调用大模型

npm i @langchain/core @langchain/openai
import { ChatOpenAI } from '@langchain/openai';

const model = new ChatOpenAI({
  model: 'deepseek-chat',
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: { baseURL: 'https://api.deepseek.com' }, // 关键:指向国产模型端点
});

const res = await model.invoke('用一句话介绍你自己');
console.log(res.content);

🏗️ 完整实现:Nest 模块化封装

把模型能力做成 LlmModule,业务模块注入即用(llm.module.ts):

@Module({
  providers: [ModelFactoryService, ModelRouterService, ParamsService],
  exports: [ModelFactoryService, ModelRouterService, ParamsService],
})
export class LlmModule {}

Node 环境注意点(main.ts):

// Node 18 没有全局 Web Crypto,LangChain 依赖它,需手动补齐
import { webcrypto } from 'crypto';
if (!globalThis.crypto) {
  (globalThis as unknown as { crypto: Crypto }).crypto = webcrypto as unknown as Crypto;
}

✅ 验证步骤

cd code/02-langchain-module && cp .env.example .env  # 填入一个 API Key
npm install && npm run start:dev
curl http://localhost:3102/api/llm/models   # 看到你配置的模型 configured: true

⚠️ 常见坑点

  1. Cannot find module '@langchain/xxx':1.x 拆分了包,tool 在 @langchain/core/tools,消息在 @langchain/core/messages,别用旧教程的 langchain/... 路径。
  2. 401 Invalid API Key:baseURL 与 Key 不是同一家厂商的(拿 DeepSeek 的 Key 请求千问端点)。
  3. Node 18 报 crypto 相关错误:按上面补 globalThis.crypto。

📝 课后练习

  1. 把最小示例改成调用通义千问(换 baseURL 与模型名)。
  2. 在 LlmModule 中新增一个 EchoService,注入到某个 Controller 返回固定文本,体会模块导出/注入。

📌 第 2 章 多模型加载与管理

🎯 学习目标

  • 用"模型注册表"统一管理多个国产模型
  • 实现懒加载、实例缓存与优雅降级

📖 原理讲解

同时接多个模型要解决三个问题:

  1. 配置分散 → 用一张注册表集中描述每个模型(端点、Key 环境变量、能力);
  2. 启动依赖 → 懒加载:用到才创建实例,缺 Key 不影响服务启动;
  3. 重复创建 → 实例缓存:同一模型只 new 一次。

🏗️ 完整实现

① 模型注册表(model-registry.ts)——新增模型只需加一条元数据:

export interface ModelMeta {
  name: string;              // 逻辑名(路由/接口用)
  provider: ModelProvider;   // deepseek / qwen / seed / openai
  modelName: string;         // 发给 API 的真实模型 id
  tier: 'light' | 'strong';  // 成本/能力档位
  contextLength: number;     // 上下文窗口
  supportsTools: boolean;    // 是否支持工具调用
  supportsThinking: boolean; // 是否支持思考模式
  apiKeyEnv: string;         // Key 环境变量名
  baseUrlEnv: string;        // Base URL 环境变量名
  defaultBaseUrl: string;
}

export const MODELS: ModelMeta[] = [
  { name: 'deepseek-chat', provider: 'deepseek', modelName: 'deepseek-chat',
    tier: 'light', supportsTools: true, apiKeyEnv: 'DEEPSEEK_API_KEY', ... },
  { name: 'deepseek-reasoner', tier: 'strong', supportsThinking: true, ... },
  { name: 'qwen-turbo', provider: 'qwen', tier: 'light', ... },
  { name: 'qwen-plus', provider: 'qwen', tier: 'strong', ... },
  // ...
];

② 模型工厂(model-factory.service.ts):

getModel(name: string, overrides?): ChatOpenAI {
  const meta = findModelMeta(name);
  if (!meta) throw new NotFoundException(`未知模型:${name}`);

  const apiKey = process.env[meta.apiKeyEnv];
  if (!apiKey || apiKey === 'sk-please-fill') {
    throw new NotFoundException(`模型 ${name} 未配置 API Key:请设置 ${meta.apiKeyEnv}`);
  }

  if (overrides) return this.createInstance(meta, apiKey, overrides); // 带参数不走缓存
  const cached = this.instances.get(name);
  if (cached) return cached;                                          // 命中缓存
  const instance = this.createInstance(meta, apiKey);
  this.instances.set(name, instance);                                 // 写入缓存
  return instance;
}

热切换的本质:切换模型 = 换一个逻辑名去工厂取实例,实例池不变,无需重启。

✅ 验证步骤

curl http://localhost:3102/api/llm/models
# 返回每个模型的 configured 状态;只配了 DeepSeek 时,qwen/seed 为 false

⚠️ 常见坑点

  1. 把 Key 写死在代码里:一定走环境变量,.env 不提交 git。
  2. 火山方舟需要"推理接入点":doubao 模型的 modelName 要填你在方舟控制台创建的接入点 ID(形如 ep-xxxx),教程注册表中是示例名,实际使用请替换。

📝 课后练习

  1. 在注册表中新增 qwen-max 模型(strong 档)。
  2. 给工厂加一个 releaseModel(name) 方法并写接口调用它,观察下次调用重新创建实例的日志。

📌 第 3 章 模型参数详解

🎯 学习目标

  • 说清每个常用采样参数的作用与调优方向
  • 理解思考模式(thinking mode)的接入差异

📖 原理讲解

模型生成下一个 token 时,会给所有候选词打分再采样,参数就是调节"怎么选"的旋钮:

参数 作用 调优建议
temperature 随机性总开关。0=每次选最高分(严谨如考试),越高越天马行空 代码/事实问答 0~0.3;闲聊 0.7~0.9;创意写作 1+
topP 核采样:只从累计概率前 P 的候选里选 与 temperature 二选一调节,一般 0.8~1
maxTokens 最多生成多少 token 按场景设上限,防止跑飞烧钱
frequencyPenalty 惩罚重复词(按出现次数) 减少车轱辘话:0.2~0.5
presencePenalty 惩罚已出现过的词(不论次数) 鼓励话题发散:0.2~0.5
stop 遇到指定字符串停止生成 结构化输出截断
seed 固定随机种子(部分厂商支持) 结果复现
streaming 是否流式返回 对话 UI 必开
timeout / maxRetries 超时与重试 生产建议 30~60s、重试 2 次

思考模式(thinking mode):DeepSeek deepseek-reasoner 等推理模型会先输出长段推理过程(reasoning_content)再给答案。注意:

  • 推理模型通常不接受 temperature/top_p 参数,传了会 400;
  • 思考内容会消耗大量 token,延迟更高,只给复杂任务用。

✅ 验证步骤(对照实验)

# 同一问题,不同 temperature,观察输出差异
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"message":"给咖啡店起个名字","temperature":0}'
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"message":"给咖啡店起个名字","temperature":1.5}'

⚠️ 常见坑点

  1. 给推理模型传 temperature 报 400:本项目 ParamsService 已按模型能力自动裁剪(见第 4 章)。
  2. maxTokens 设太小回答被截断:回答戛然而止多半是它。
  3. temperature=0 也不完全确定:服务端批处理等因素仍可能带来微小差异,seed 也并非所有厂商支持。

📝 课后练习

  1. 写一个参数实验接口:同一输入用 3 组参数各生成一次,对比返回。
  2. 查 DeepSeek 官方文档,确认 deepseek-reasoner 支持哪些参数,更新注册表元数据。

📌 第 4 章 动态参数配置

🎯 学习目标

  • 用 DTO 接收用户动态参数并做范围校验
  • 实现"请求 > 场景预设 > 默认值"的合并策略

🏗️ 完整实现

① 请求 DTO(chat.dto.ts):

export class ChatDto {
  @IsString() @MinLength(1) @MaxLength(8000) message!: string;
  @IsOptional() @IsString() model?: string;           // 不传则由路由决定
  @IsOptional() @IsEnum(ChatScene) scene?: ChatScene; // chat/translate/code/rag
  @IsOptional() @IsNumber() @Min(0) @Max(2) temperature?: number;
  @IsOptional() @IsNumber() @Min(0.01) @Max(1) topP?: number;
  @IsOptional() @IsInt() @Min(1) @Max(8192) maxTokens?: number;
}

② 场景预设 + 合并(params.service.ts):

const SCENE_PRESETS: Record<ChatScene, ResolvedParams> = {
  chat:      { temperature: 0.8, maxTokens: 1024 },  // 闲聊自然些
  translate: { temperature: 0.2, maxTokens: 2048 },  // 翻译忠实原文
  code:      { temperature: 0.1, maxTokens: 4096 },  // 代码严谨
  rag:       { temperature: 0.3, maxTokens: 2048 },  // 基于资料回答
};

resolve(dto: ChatDto): ResolvedParams {
  const base = dto.scene ? SCENE_PRESETS[dto.scene] : MODEL_DEFAULTS;
  const resolved = {
    temperature: dto.temperature ?? base.temperature,  // 请求优先
    maxTokens: dto.maxTokens ?? base.maxTokens,
    topP: dto.topP ?? base.topP,
  };
  // 按模型能力裁剪:推理模型不接受 temperature/topP
  if (dto.model && findModelMeta(dto.model)?.supportsThinking) {
    resolved.temperature = 1;
    resolved.topP = undefined;
  }
  return resolved;
}

✅ 验证步骤

curl http://localhost:3102/api/llm/presets        # 查看预设表
# 越界参数 → 400 校验错误
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' -d '{"message":"hi","temperature":9}'

⚠️ 常见坑点

  1. 预设与显式参数谁优先没想清楚:坚持"用户显式传参永远最高优先级"。
  2. DTO 数值校验失效:开了 transform: true 后 JSON body 的数字本来就是数字;但查询参数是字符串,注意区分。

📝 课后练习

  1. 新增 summarize 场景预设(低温度、大 maxTokens)。
  2. 给 DTO 增加 frequencyPenalty 字段并打通到模型调用。

📌 第 5 章 模型路由策略

🎯 学习目标

  • 实现基于复杂度评分的模型路由
  • 处理目标模型不可用时的 fallback

📖 原理讲解

类比:简单问题问实习生(快、便宜),复杂问题问专家(贵但靠谱)。路由器的输入是用户消息,输出是"用哪个模型 + 理由"。

本教程的启发式评分(0~100):

基础分 20
+ 长度:超过 100 字后每 50 字 +5(上限 +30)
+ 关键词:命中"分析/证明/方案/对比/架构…"每词 +10(上限 +30)
+ 代码特征:含代码块或多行 +20
score ≥ 60 → 强模型,否则轻量模型

🏗️ 完整实现(model-router.service.ts)

route(message: string, preferredModel?: string): RouteDecision {
  if (preferredModel) return { model: preferredModel, reason: '用户显式指定', score: 0 };

  const score = this.scoreComplexity(message);
  const target = score >= 60 ? strongName : lightName;

  if (this.modelFactory.isAvailable(target)) {
    return { model: target, reason: `复杂度 ${score}…`, score };
  }
  // fallback:目标没配 Key → 找同档位可用模型,再不行任意可用模型
  const fallback = this.findFallback(target);
  if (fallback) return { model: fallback, reason: `${target} 未配置,降级`, score };
  throw new Error('没有任何可用的大模型…');
}

进阶:还可以用一次廉价的 LLM 分类调用判断任务类型("这是闲聊还是复杂分析?"),比关键词规则更准,但多一次网络开销。生产系统常用"规则优先 + LLM 兜底"的混合策略。

✅ 验证步骤(无需 Key)

curl -X POST http://localhost:3102/api/chat/route-demo \
  -H 'Content-Type: application/json' -d '{"message":"你好"}'
# → 轻量模型,complexity ≈ 20

curl -X POST http://localhost:3102/api/chat/route-demo \
  -H 'Content-Type: application/json' \
  -d '{"message":"请详细分析并设计一个高并发秒杀系统的架构方案"}'
# → 强模型,complexity ≥ 60

⚠️ 常见坑点

  1. 路由规则写死在业务代码里:抽成独立 Service + env 可配置(本项目 ROUTER_LIGHT_MODEL / ROUTER_STRONG_MODEL)。
  2. fallback 链缺失:目标模型没配 Key 时直接 500。务必有兜底。

📝 课后练习

  1. 把阈值 60 改成 env 可配置。
  2. 增加"按成本预算路由":DTO 传 budget: 'low'|'high',low 时强制轻量模型。

📌 第 6 章 MCP 协议

🎯 学习目标

  • 理解 MCP 的三个核心概念与三种传输方式
  • 用 MultiServerMCPClient 接入第三方 MCP 工具
  • 开发自定义 MCP Server 并通过 HTTP 暴露

📖 原理讲解

MCP(Model Context Protocol) 类比:大模型工具的 USB 接口标准。以前每个 AI 应用接每个工具都要写一套专用对接代码;有了 MCP,工具方实现一次 MCP Server,任何 MCP 客户端(Agent)即插即用。

MCP Server(工具提供方)              MCP Client(Agent 侧)
┌──────────────────────┐            ┌─────────────────────┐
│ tools:    可调用的函数 │ ◄────────► │ MultiServerMCPClient │
│ resources: 可读取的数据│  协议通信   │  → 转成 LangChain 工具│
│ prompts:  提示词模板   │            │  → 注入 Agent        │
└──────────────────────┘            └─────────────────────┘

三种 transport:

transport 场景
stdio 本地子进程(npx @modelcontextprotocol/server-filesystem)
Streamable HTTP 远程服务,一个端点双向通信(推荐)
SSE 旧版远程方案,逐步被 Streamable HTTP 取代

🏗️ 完整实现

① 接入第三方 MCP(mcp-client.service.ts):

// mcp.example.json:声明要连哪些 MCP server
{ "mcpServers": {
    "filesystem": { "transport": "stdio",
      "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }
} }

// 代码:一行接入,工具自动转成 LangChain 工具
this.client = new MultiServerMCPClient(config);
const tools = await this.client.getTools();   // 注入 Agent 即可用

② 开发自定义 MCP Server(mcp-server/tools.ts)——MCP 协议本质就是响应两类请求:

const server = new Server({ name: 'tutorial-mcp-server', version: '1.0.0' },
                          { capabilities: { tools: {} } });

// tools/list:我有哪些工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{ name: 'safe_calculate', description: '…', inputSchema: {…} }],
}));

// tools/call:执行指定工具(参数必须用 zod 校验,MCP 入参不可信)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  if (name === 'safe_calculate') { /* 校验 + 执行 */ }
});

③ 两种暴露方式:

✅ 验证步骤

# 1. MCP Inspector 图形化调试(推荐)
npx @modelcontextprotocol/inspector
#    Transport 选 Streamable HTTP,URL 填 http://localhost:3102/api/mcp
#    在 Tools 页可看到 get_server_time / safe_calculate 并在线调用

# 2. 让大模型使用 MCP 工具:问它"现在服务器几点了"
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' -d '{"message":"现在服务器时间是几点?"}'
# 观察返回的 toolCalls 中包含 get_server_time

⚠️ 常见坑点

  1. stdio server 的日志打到 stdout:stdout 被协议占用,日志必须 console.error(stderr)。
  2. MCP server 挂了拖垮主流程:加载 MCP 工具要 try/catch 降级(本项目已做)。
  3. Streamable HTTP 会话丢失:客户端后续请求必须带上 initialize 返回的 Mcp-Session-Id 头。

📝 课后练习

  1. 给自定义 MCP Server 增加 random_joke 工具。
  2. 在 mcp.json 中接入 @modelcontextprotocol/server-fetch,让模型能抓取网页内容。

📌 第 7 章 Skill 开发

🎯 学习目标

  • 理解 Skill 与 Tool 的区别
  • 实现 SKILL.md 扫描、注册与渐进式披露

📖 原理讲解

Tool 是一个函数,Skill 是一套带说明书的工作流。

  • Tool:calculate(expr) —— 一次调用一个明确动作;
  • Skill:"周报生成"——包含执行步骤、输出格式、注意事项的完整方法论。

Skill 用 Markdown 文件(SKILL.md)承载,好处:非程序员也能编写维护、随时增删不用改代码。

渐进式披露(省 token 的关键):

系统提示词只注入技能清单(name + description,几十个 token)
        │
        ▼ 模型判断"这个需求匹配 weekly-report 技能"
        │
调用 load_skill("weekly-report") 工具 → 加载完整正文(几百 token,按需才花)

🏗️ 完整实现

① SKILL.md 格式(skills/weekly-report/SKILL.md):

---
name: weekly-report
description: 生成周报。当用户要求写周报、总结本周工作时使用。
version: 1.0.0
---

# 周报生成技能

📌 执行步骤

  1. 确认本周关键事项;2. 按"完成/进行中/风险/计划"组织;…

**② 技能服务**([skills.service.ts](../code/02-langchain-module/src/skills/skills.service.ts)):启动时扫描 `SKILLS_DIR`,解析 frontmatter(js-yaml),缺 description 的技能跳过;`buildPromptSection()` 生成注入系统提示词的清单段落;`getSkillBody(name)` 供 load_skill 工具取正文。

**③ 挂载到 Agent**(chat.service.ts):

```typescript
tools.push(tool(
  async ({ name }) => skillsService.getSkillBody(name),
  { name: 'load_skill', description: '按名称加载技能的完整说明', schema: z.object({ name: z.string() }) },
));
// 系统提示词 += skillsService.buildPromptSection()

✅ 验证步骤

curl http://localhost:3102/api/skills   # 看到 weekly-report / sql-review

# 触发技能:
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"message":"帮我写周报:本周完成了登录模块和支付联调,支付还有 2 个 bug"}'
# 返回的 toolCalls 中可见 load_skill 被调用,回答按技能规定的结构输出

⚠️ 常见坑点

  1. frontmatter 格式错误:--- 必须顶格、成对出现,YAML 缩进错误会导致解析失败(技能被静默跳过,看启动日志)。
  2. description 写得太笼统:模型靠它判断何时加载,要写清"做什么 + 何时用"。
  3. 技能正文太长:正文会整段进上下文,控制在 1~2KB。

📝 课后练习

  1. 新建 code-review 技能:按"可读性/健壮性/性能"三维度审查代码。
  2. 给技能增加 enabled: false frontmatter 开关,加载时过滤。

📌 第 8 章 本地 API 接入(工具调用闭环)

🎯 学习目标

  • 用 tool() + zod 封装本地 Nest 能力
  • 用 createReactAgent 组装 Agent 并观察工具调用轨迹

📖 原理讲解

ReAct 循环(Reason + Act):

用户提问
  ▼
模型思考:"要回答这个问题,我需要先查会话统计"
  ▼ 生成 tool_call: get_chat_stats()
框架执行工具(查数据库)→ 把结果作为 ToolMessage 回填
  ▼
模型基于真实数据组织最终回答

关键点:工具回调里可以直接注入 Nest 的 Service/Repository——大模型就这样接入了你的业务系统。

🏗️ 完整实现

① 封装本地工具(local-api.tools.ts):

const statsTool = tool(
  async () => {   // 回调里直接用注入的 Repository 查库
    const conversations = await this.conversationRepo.count();
    const messages = await this.messageRepo.count();
    return JSON.stringify({ conversations, messages });
  },
  {
    name: 'get_chat_stats',
    description: '查询会话统计。用户询问"有多少会话/消息"时调用。', // 描述决定模型何时调用
    schema: z.object({}),   // zod 描述参数
  },
);

② 组装 Agent 并执行(chat.service.ts):

const agent = createReactAgent({ llm: model, tools });
const result = await agent.invoke({ messages: context });

// 提取工具调用轨迹(可观测性)
const toolCalls = result.messages
  .flatMap((m) => m.tool_calls ?? [])
  .map((tc) => tc.name);

✅ 验证步骤

curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"message":"这个系统里现在有多少个会话和消息?"}'
# 返回中 toolCalls: ["get_chat_stats"],answer 包含真实数字

⚠️ 常见坑点

  1. 工具 description 写得差:模型靠描述决定调不调用,写清"做什么 + 何时用"。
  2. 给不支持工具的模型绑工具:如 deepseek-reasoner,会报错。本项目按 supportsTools 自动跳过。
  3. 工具返回巨大内容:数据库查询结果要先聚合/截断,否则撑爆上下文。

📝 课后练习

  1. 新增工具 search_messages(keyword):按关键词搜索历史消息。
  2. 在工具回调里人为抛错,观察 Agent 的反应(它会把错误信息告诉模型)。

📌 第 9 章 对话上下文管理

🎯 学习目标

  • 把对话历史持久化到 PostgreSQL
  • 实现上下文窗口管理与截断
  • 理解"摘要压缩"长时记忆方案

📖 原理讲解

大模型没有记忆——每次请求都是独立的,所谓"多轮对话"是你把历史消息一起传过去。于是有三个问题:

  1. 历史存哪? → 数据库(刷新不丢、可审计);
  2. 传多少? → 上下文窗口有限且按 token 收费,需要窗口管理:只带最近 N 轮;
  3. 更早的怎么办? → 摘要压缩:把旧对话压成一段摘要当"长期记忆"。
发给模型的上下文 =
  [SystemMessage:系统设定 + 历史摘要(长期记忆)]
+ [最近 6 轮原始消息(短期记忆)]
+ [本轮用户消息]

🏗️ 完整实现

① 实体:conversations(含 summary 字段)+ messages(role/content/model),见 conversation.entity.ts / message.entity.ts。

② 窗口管理(memory.service.ts)——双重截断:

// 轮数上限:最近 6 轮
const recent = await this.messageRepo.find({
  where: { conversationId }, order: { createdAt: 'DESC' }, take: WINDOW_ROUNDS * 2,
});
// 字符预算:从最新往前累加,超 12000 字符就停(粗略 token 估算)
for (let i = recent.length - 1; i >= 0; i--) {
  if (budget - m.content.length < 0 && selected.length > 0) break;
  budget -= m.content.length;
  selected.unshift(m);
}

③ 摘要压缩(summarizer.service.ts):消息超过 20 条时,用轻量模型把较早的消息压成 ≤200 字摘要,滚动合并进 conversation.summary。异步执行,不阻塞对话。

✅ 验证步骤

# 两轮对话验证记忆
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' -d '{"message":"记住:我的幸运数字是 7"}'
# 记下返回的 conversationId
curl -X POST http://localhost:3102/api/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"message":"我的幸运数字是多少?","conversationId":"<上一步的 id>"}'
# 模型应答出 7

# 查看持久化的历史
curl http://localhost:3102/api/chat/conversations/<id>

⚠️ 常见坑点

  1. 无脑全量回传历史:几十轮后必然超窗口/超费用,必须做窗口管理。
  2. 截断截掉了关键信息:所以要有摘要兜底;更讲究的做法是"关键事实抽取"单独存储。
  3. 消息顺序错乱:并发写入时按 createdAt + id 双字段排序更稳。

📝 课后练习

  1. 把窗口轮数改成 env 可配置。
  2. 实现"关键事实抽取":每轮对话后让小模型抽取"用户偏好"存入 conversation 的 JSON 字段。

📌 第 10 章 流式响应

🎯 学习目标

  • 实现 SSE 流式输出(逐 token 推送)
  • 实现 WebSocket 流式对话与中断

📖 原理讲解

类比:非流式像等厨师把整桌菜做完一起上;流式像火锅——食材一盘盘上,体验立刻开始。对话产品必须流式,否则用户干等十几秒。

两种推送通道:

SSE WebSocket
方向 服务器单向推 双向
协议 普通 HTTP 独立长连接协议
中断生成 断开连接 发个事件即可
适用 简单问答流 聊天 UI、需要中断

LangChain 侧:agent.streamEvents({…}, { version: 'v2' }) 产出细粒度事件流,监听 on_chat_model_stream 事件即可拿到每个 token。

🏗️ 完整实现

① 服务端产出 token 流(chat.service.ts):

async *stream(dto: ChatDto): AsyncGenerator<{ type: 'token'|'done'|'meta'; data: string }> {
  // …路由、上下文准备同同步版…
  const stream = await agent.streamEvents({ messages: context }, { version: 'v2' });
  for await (const event of stream) {
    if (event.event === 'on_chat_model_stream') {
      const text = typeof event.data?.chunk?.content === 'string' ? event.data.chunk.content : '';
      if (text) yield { type: 'token', data: text };
    }
  }
  yield { type: 'done', data: conversationId };
}

② SSE 端点(chat.controller.ts):

@Post('chat/stream')
async stream(@Body() dto: ChatDto, @Res() res: Response): Promise<void> {
  // SSE 三件套响应头
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders?.();

  for await (const chunk of this.chatService.stream(dto)) {
    res.write(`data: ${JSON.stringify(chunk)}\n\n`);  // SSE 报文格式
  }
  res.write('data: [DONE]\n\n');   // 结束标记
  res.end();
}

关键坑点:用 @Res() 手动写响应,天然绕过全局"统一响应包装"拦截器。若用全局拦截器包装了 SSE 端点,拦截器会等响应结束才输出——流式效果完全失效。

③ WebSocket 流式 + 中断(chat.gateway.ts):

@SubscribeMessage('chat:send')
async handleChat(client: Socket, payload: WsChatPayload) {
  const flag = { stopped: false };
  this.stopFlags.set(client, flag);
  for await (const chunk of this.chatService.stream(dto)) {
    if (flag.stopped) break;                     // 客户端 emit 'chat:stop' 即中断
    client.emit(`chat:${chunk.type}`, chunk.data);
  }
}

✅ 验证步骤

# SSE:-N 关闭缓冲,观察逐字蹦出
curl -N -X POST http://localhost:3102/api/chat/stream \
  -H 'Content-Type: application/json' -d '{"message":"写一首关于春天的五言绝句"}'

# WebSocket:见 README 的 ws-ai-test.js

⚠️ 常见坑点

  1. curl 看不到流式效果:忘了 -N(禁用缓冲)。
  2. Nginx 代理把 SSE 攒成一坨:加 proxy_buffering off;。
  3. 流式接口的异常没兜底:for await 外要 try/catch,把错误作为一个事件发出去再关闭。
  4. streamEvents 版本参数:1.x 必须显式 { version: 'v2' }。

📝 课后练习

  1. 给 SSE 增加 event: 字段区分 token/meta/done(而不是都塞在 data 里)。
  2. 实现"打字机前端":一个 20 行的 HTML + EventSource 消费 /chat/stream(提示:EventSource 只支持 GET,可另做一个 GET 版端点)。

📌 阶段 2 自测清单

  1. 能否说清 @langchain/core / @langchain/openai / @langchain/langgraph 各自的职责?
  2. 国产模型为什么能用 ChatOpenAI 接入?关键配置是哪一项?
  3. 模型工厂为什么要懒加载 + 缓存?热切换是怎么实现的?
  4. temperature 与 topP 的区别?什么场景用低温度?
  5. 参数合并策略的优先级顺序?为什么要按模型能力裁剪参数?
  6. 复杂度路由的评分维度有哪些?fallback 链为什么必要?
  7. MCP 的三种 transport 分别适用什么场景?tools/list 与 tools/call 各是干什么的?
  8. Skill 与 Tool 的区别?渐进式披露如何省 token?
  9. 大模型为什么"没有记忆"?窗口管理 + 摘要压缩如何配合?
  10. SSE 端点为什么必须绕过全局响应包装拦截器?

下一阶段:阶段 3:PostgreSQL 进阶与向量检索 RAG —— pgvector、向量检索与 RAG 全流程。

📁 配套代码目录 tutorial-qw3.8/code/02-langchain-module

    02-langchain-module/
      .env.example
      .gitignore
      README.md
      mcp.example.json
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      skills/
        sql-review/
          SKILL.md
        weekly-report/
          SKILL.md
      src/
        app.module.ts
        main.ts
        chat/
          chat.controller.ts
          chat.gateway.ts
          chat.module.ts
          chat.service.ts
        config/
          env.validation.ts
        llm/
          llm.module.ts
          model-factory.service.ts
          model-registry.ts
          model-router.service.ts
          params.service.ts
        mcp/
          mcp-client.service.ts
          mcp-server.service.ts
          mcp.controller.ts
        memory/
          conversation.entity.ts
          memory.service.ts
          message.entity.ts
          summarizer.service.ts
        skills/
          skills.service.ts
        tools/
          local-api.tools.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 pgvector RAG · 篇首 通义适配版 · tutorial-qw3.8

配套代码:../code/03-pg-vector-rag/(完整可运行项目) 技术栈:PostgreSQL 16 + pgvector + TypeORM 0.3 + LangChain.js 1.x 建议学习时长:3~4 天(每天 2~3 小时)


📌 第 1 章 PostgreSQL 基础(TypeORM 视角)

🎯 学习目标

  • 巩固 TypeORM 复杂查询、事务与索引优化
  • 会用 EXPLAIN ANALYZE 读执行计划

📖 原理讲解

阶段 1 已掌握 CRUD 与事务,本章补齐三块进阶:

① QueryBuilder 复杂查询:

// 子查询 + 聚合 + 分组:查询发过文章的用户及其文章数
const rows = await this.repo.createQueryBuilder('u')
  .select('u.id', 'id')
  .addSelect('u.username', 'username')
  .addSelect('COUNT(p.id)', 'postCount')
  .leftJoin('u.posts', 'p')
  .groupBy('u.id')
  .having('COUNT(p.id) > :min', { min: 0 })
  .orderBy('postCount', 'DESC')
  .getRawMany();

② 事务隔离级别:PG 默认 Read Committed。需要更强一致性时:

await this.dataSource.transaction('SERIALIZABLE', async (manager) => { /* ... */ });

③ 索引与执行计划:B-Tree 索引像字典的目录页,把 O(n) 全表扫描变成 O(log n) 定位。验证索引是否生效:

EXPLAIN ANALYZE SELECT * FROM document_chunks WHERE source = 'help-center.md';
-- Index Scan using ... → 命中索引;Seq Scan → 全表扫描

✅ 验证步骤

docker exec -it tutorial-pg psql -U tutorial -d tutorial \
  -c "EXPLAIN ANALYZE SELECT * FROM document_chunks WHERE source = 'help-center.md';"

⚠️ 常见坑点

  1. 数据量小时索引"不生效":优化器算出顺序扫描更快会主动放弃索引,属正常现象。
  2. 对列使用函数导致索引失效:WHERE date(created_at) = ... 改为范围查询 created_at >= ... AND created_at < ...。

📝 课后练习

  1. 用 QueryBuilder 写一个"统计每个来源文档的分片数"的查询。
  2. 给 document_chunks.source 建 B-Tree 索引并用 EXPLAIN 验证。

📌 第 2 章 pgvector 向量插件

🎯 学习目标

  • 安装并启用 pgvector
  • 理解三个相似度算子,会定义向量列
  • 理解 HNSW 与 IVFFlat 索引的原理与选型

📖 原理讲解

什么是向量检索? 类比:把每段文字交给 Embedding 模型变成一个坐标点(几百~几千维的数字数组)。语义相近的文字,坐标也相近。"找相关内容"就变成了"找距离最近的点"——这就是向量检索。

"如何退款"   → [0.12, -0.45, 0.78, ...]  ┐ 距离近 ✓
"怎么申请退款" → [0.11, -0.43, 0.80, ...]  ┘
"明天天气"   → [-0.66, 0.31, -0.02, ...]    距离远 ✗

安装(三选一,详见 scripts/init-pgvector.sql):

# 方式 A(推荐):Docker 镜像自带
docker run -d --name tutorial-pg -e POSTGRES_USER=tutorial \
  -e POSTGRES_PASSWORD=tutorial123 -e POSTGRES_DB=tutorial \
  -p 5432:5432 pgvector/pgvector:pg16

# 方式 B:macOS brew
brew install postgresql@16 pgvector

# 方式 C:Linux 源码编译(需 postgresql-server-dev-16、build-essential)
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector && make && sudo make install
CREATE EXTENSION IF NOT EXISTS vector;   -- 每个数据库执行一次

TypeORM 中定义向量列:TypeORM 没有原生 vector 类型,教程统一方案——float8 数组列 + 查询时 ::vector 转换(document.entity.ts):

@Column({ name: 'embedding', type: 'float8', array: true })
embedding!: number[];
-- 检索 SQL 中 cast 成 vector 再算距离
SELECT id, 1 - (embedding::vector <=> $1::vector) AS similarity
FROM document_chunks
ORDER BY embedding::vector <=> $1::vector
LIMIT 5;

三个距离算子:

算子 含义 适用
<-> L2(欧氏)距离 通用
<#> 负内积 向量已归一化时
<=> 余弦距离 文本语义检索首选(只看方向不看长度)

ANN 索引:精确检索是 O(n) 逐个比,百万级数据扛不住。近似最近邻索引用"少量精度换大幅提速":

IVFFlat HNSW
原理 先聚类分桶,查询只扫附近的桶 多层跳表图,逐层逼近目标
建索引 快 慢(3~10 倍)
查询精度 中 高
增量写入 差 好

完整建索引 SQL 与参数说明见 scripts/create-index.sql。

✅ 验证步骤

docker exec -i tutorial-pg psql -U tutorial -d tutorial < scripts/init-pgvector.sql
# 输出 vector 扩展版本号即成功

⚠️ 常见坑点

  1. type "vector" does not exist:忘了 CREATE EXTENSION vector,或建在了别的数据库里(扩展是按库生效的)。
  2. 维度不一致报错 different vector dimensions:查询向量与库中向量维度不同。换 Embedding 模型必须清空重入库。
  3. Linux 编译 pgvector 失败:缺 postgresql-server-dev-16,或 pg_config 不在 PATH。

📝 课后练习

  1. 在 psql 里手工执行一次向量检索 SQL(向量随便写几个 0~1 的小数,维度与库中一致)。
  2. 分别建 HNSW 与 IVFFlat 索引,用 EXPLAIN ANALYZE 观察查询计划差异。

📌 第 3 章 向量数据开发

🎯 学习目标

  • 实现 Embedding 统一服务(在线 + 本地双实现)
  • 掌握文档分片参数选择
  • 实现向量批量写入与 Top-K 检索

🏗️ 完整实现

① Embedding 统一接口(embedding.service.ts):

// env 一键切换:EMBEDDING_MODE=qwen(在线)| local(本地离线)
get mode(): EmbeddingMode { ... }
async embed(text: string): Promise<number[]> { ... }
async embedBatch(texts: string[]): Promise<number[][]> { ... }
  • 在线(qwen.embedding.ts):OpenAIEmbeddings + DashScope 兼容端点,text-embedding-v3(1024 维);
  • 本地(local.embedding.ts):@huggingface/transformers 跑 bge-small-zh(512 维),mean pooling + L2 归一化;动态 import 懒加载,只有本地模式才加载这个大包。国内下载慢设 HF_ENDPOINT=https://hf-mirror.com。

② 分片(chunker.service.ts):

new RecursiveCharacterTextSplitter({
  chunkSize: 500,     // 每片约 500 字:太小缺上下文,太大语义稀释
  chunkOverlap: 80,   // 相邻片重叠 80 字:避免关键句被切断
  separators: ['\n\n', '\n', '。', '!', '?', ';', ' '],  // 中文友好的切分优先级
});

③ 批量写入(vector.service.ts):分批 save(每批 100 条,避开 PG 65535 参数上限),每条记录 embeddingModel 与 embeddingDim 做维度追溯。

✅ 验证步骤

curl http://localhost:3103/api/rag/stats
# { chunkCount, embeddingMode, embeddingModel }

⚠️ 常见坑点

  1. 在线 Embedding 限流:批量入库时并发别太高,DashScope 有 QPS 限制;本项目 embedBatch 已按批串行。
  2. 本地模型首次加载慢:要下载约 100MB,看日志确认进度,别误以为卡死。
  3. chunkSize 拍脑袋:经验值 300~800 字;问答型知识库偏小(300~500),长文档阅读型偏大。

📝 课后练习

  1. 把 CHUNK_SIZE / CHUNK_OVERLAP 做成接口参数,对比不同分片下的召回效果。
  2. 给本地 Embedding 增加模型缓存目录配置(transformers 的 env.cacheDir)。

📌 第 4 章 RAG 基础实现

🎯 学习目标

  • 说清 RAG 的完整管线与每一步的作用
  • 跑通"上传 → 解析分片 → 向量化 → 召回 → 注入 Prompt → 回答"

📖 原理讲解

RAG = 开卷考试:模型先"翻书"(检索知识库)找到相关段落,再基于这些段落组织答案。它解决两个痛点:

  1. 模型不知道你私有数据里的内容(产品手册、公司制度);
  2. 减少幻觉——有据可依,还能标注出处。
用户提问
   │
   ▼ ① 查询向量化(同一个 Embedding 模型!)
   ▼ ② 向量检索 Top-K 分片
   ▼ ③ 把分片塞进 Prompt:【参考资料】+【问题】
   ▼ ④ LLM 依据资料作答(要求标注引用编号)
带引用的答案

🏗️ 完整实现

① 入库管线(ingestion.service.ts):解析(document.parser.ts:pdf-parse / mammoth / 纯文本)→ 分片 → embedBatch → insertChunks。

② 问答管线(rag.service.ts)核心 Prompt:

const prompt = ChatPromptTemplate.fromMessages([
  ['system', `你是知识库问答助手。只依据【参考资料】回答问题:
- 资料中有答案时,给出准确回答,并在句末用 [编号] 标注引用;
- 资料中没有答案时,明确说"知识库中未找到相关内容",不要编造;`],
  ['human', '【参考资料】\n{context}\n\n【问题】\n{question}'],
]);
const chain = prompt.pipe(model);
await chain.invoke({ context, question });

教学降级设计:未配置 LLM_API_KEY 时,/rag/ask 直接返回召回的资料原文,保证没有 Key 也能验证检索管线。

✅ 验证步骤

# 上传示例文档 → 提问 → 看引用
curl -X POST http://localhost:3103/api/rag/ingest -F "file=@sample-docs/help-center.md"
curl -X POST http://localhost:3103/api/rag/ask \
  -H 'Content-Type: application/json' -d '{"question":"退款多久能到账?"}'
# answer 中应出现"3 个工作日",citations 指向 help-center.md 的对应分片

# 知识库外问题 → 不幻觉
curl -X POST http://localhost:3103/api/rag/ask \
  -H 'Content-Type: application/json' -d '{"question":"明天天气怎么样"}'

⚠️ 常见坑点

  1. 入库与查询用了不同的 Embedding 模型:两个向量空间不可比,检索结果完全错乱。必须同一个模型。
  2. 扫描件 PDF 解析为空:pdf-parse 只提取文本层,扫描件需要 OCR(超出本教程范围,文档中说明)。
  3. Prompt 没约束"不知道就说不知道":模型会热情地编造答案。系统提示词必须明确。

📝 课后练习

  1. 给答案增加"置信度":召回最高相似度低于阈值时直接回答未找到,不调 LLM。
  2. 支持 docx 上传并验证(sample 文档转成 docx 试一下)。

📌 第 5 章 RAG 优化策略

🎯 学习目标

  • 掌握父子块、多路召回、查询改写、重排四种优化手段
  • 知道每种手段解决什么问题

📖 原理讲解

基础 RAG 的四个典型病症与药方:

病症 原因 药方
召回的片段"差一点" 分片切断了上下文 父子块:小块检索、大块作答
专有名词/编号搜不到 向量对字面不敏感 多路召回:向量 + 关键词,RRF 融合
用户提问口语化 "这玩意儿咋退款" 查询改写 / HyDE
召回了但不相关排前面 Top-K 只按相似度 重排:LLM 对相关性打分

🏗️ 完整实现

① 父子块(chunker.service.ts splitParentChild):父块 2000 字、子块 200 字。子块带向量用于检索,命中后通过 parentId 取父块给模型。

② 多路召回 + RRF(retriever.service.ts):

// 两路并行
const [vectorHits, keywordHits] = await Promise.all([
  this.vectorRetrieve(query, topK * 2),              // 语义路
  this.vectorService.searchKeyword(query, topK * 2), // 字面路(tsvector)
]);
// RRF 融合:score(d) = Σ 1/(60 + rank),只看排名不看分数,规避量纲问题

③ 查询改写与 HyDE(rewrite.service.ts):改写=把口语问题变成检索友好的关键词句;HyDE=先让 LLM 写一段"假想答案",用它去检索(假想答案与真实文档分布更接近)。

④ LLM 重排(rerank.service.ts):召回 topK×3,逐个让 LLM 打 0~10 分,取前 topK。

✅ 验证步骤

# 口语化提问 + 全优化开启,观察 pipeline 字段记录的每一步
curl -X POST http://localhost:3103/api/rag/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"这玩意儿咋退款啊","rewrite":true,"hybrid":true,"rerank":true}'

⚠️ 常见坑点

  1. 优化全开导致延迟飙升:改写、重排都是额外的 LLM 调用。生产上按场景开关(简单问题走快路径)。
  2. 中文全文检索效果有限:PG 自带 simple 配置按字分词,够用但不精准;生产建议 zhparser/pg_jieba。
  3. 重排把正确答案排没了:LLM 打分为 0 时保留原始相似度兜底,别完全信任。

📝 课后练习

  1. 实现"批量重排":一次 prompt 给 5 个分片打分,对比逐个打分的耗时。
  2. 给多路召回增加第三路:标题精确匹配路。

📌 第 6 章 大模型数据增强

🎯 学习目标

  • 用 LLM 做数据清洗、结构化提取、自动打标签
  • 掌握结构化输出(withStructuredOutput + zod)

🏗️ 完整实现(data-enhance.service.ts)

// zod schema 描述期望输出
const schema = z.object({
  title: z.string().describe('内容标题(10 字以内)'),
  summary: z.string().describe('内容摘要(50 字以内)'),
  keywords: z.array(z.string()).describe('3~5 个关键词'),
  tags: z.array(z.enum(['技术', '产品', '运营', '制度', '教程', '其他'])),
});

// withStructuredOutput:输出被强制约束为 schema 形状,免去手工解析 JSON
const structured = model.withStructuredOutput(schema);
const result = await structured.invoke(`请分析下面的文本并提取信息:\n\n${text}`);

清洗用纯规则(去控制字符、归一空白),不浪费 token。批处理逐条执行、失败跳过不阻塞整批。

✅ 验证步骤

curl -X POST http://localhost:3103/api/enhance/extract \
  -H 'Content-Type: application/json' \
  -d '{"text":"星云商城双十一战报:当日 GMV 突破 5 亿元,同比增长 32%……"}'

⚠️ 常见坑点

  1. 把清洗也交给 LLM:规则能做的别花 token。
  2. 结构化输出偶尔不合 schema:部分模型/端点对 structured output 支持不完整,做好降级解析。

📝 课后练习

  1. 给入库流程加"自动摘要":每个文档入库时生成摘要存入独立字段。
  2. 用 enhanceBatch 处理 5 段文本,观察失败容错。

📌 第 7 章 性能优化

🎯 学习目标

  • 会建 HNSW 索引并调参
  • 会用压测脚本量化检索性能

🏗️ 实操步骤

① 建索引前后对比:

npm run benchmark                      # 无索引基线
docker exec -i tutorial-pg psql -U tutorial -d tutorial < scripts/create-index.sql
npm run benchmark                      # HNSW 索引后对比

② HNSW 调参(create-index.sql):

参数 阶段 默认 调大的效果
m 建索引 16 连接更多→更准,更占内存
ef_construction 建索引 64 建得更细→索引质量高,建得慢
hnsw.ef_search 查询 40 搜得更广→更准,更慢

③ 批量写入与分页:入库分批(每批 100);列表接口用 keyset 分页(WHERE id > :lastId ORDER BY id LIMIT n)替代大 OFFSET。

④ 读执行计划:

SET hnsw.ef_search = 100;
EXPLAIN ANALYZE SELECT id FROM document_chunks
ORDER BY embedding::vector <=> '[...]'::vector LIMIT 5;

⚠️ 常见坑点

  1. 数据量太小看不出索引效果:几千行时 PG 会选择顺序扫描(确实更快),要几万行以上再对比。
  2. IVFFlat 建索引时表是空的:lists 聚类依赖已有数据,空表建 IVFFlat 无意义,需先有数据。
  3. 压测打的是 LLM 接口:压测检索用 /rag/search(不含 LLM),否则瓶颈在模型 API 而非数据库。

📝 课后练习

  1. 写一个脚本批量插入 1 万条随机向量,对比建索引前后的 benchmark 数据。
  2. 用 SET hnsw.ef_search 分别设 20/100/200,观察延迟与召回结果变化。

📌 阶段 3 自测清单

  1. 向量检索为什么能找到"意思相近但用词不同"的内容?
  2. pgvector 三个距离算子的区别?文本检索为什么首选余弦距离?
  3. TypeORM 没有 vector 类型,本教程的方案是什么?代价是什么?
  4. 为什么换 Embedding 模型必须清空向量库重新入库?
  5. chunkSize 与 chunkOverlap 如何影响检索效果?
  6. RAG 完整管线有哪几步?Prompt 中为什么要要求"不知道就说不知道"?
  7. 父子块解决了什么问题?多路召回的 RRF 为什么只看排名?
  8. 查询改写与 HyDE 的思路分别是什么?
  9. HNSW 与 IVFFlat 如何选型?ef_search 调大的代价?
  10. 为什么压测检索不能走带 LLM 的接口?

下一阶段:阶段 4:综合实战项目 —— 把三个阶段的全部能力整合成一个可上线的 AI 知识库问答系统。

📁 配套代码目录 tutorial-qw3.8/code/03-pg-vector-rag

    03-pg-vector-rag/
      .env.example
      .gitignore
      README.md
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      sample-docs/
        help-center.md
      scripts/
        benchmark.ts
        create-index.sql
        init-pgvector.sql
      src/
        app.module.ts
        main.ts
        enhance/
          data-enhance.service.ts
        ingestion/
          chunker.service.ts
          ingestion.service.ts
        rag/
          rag.controller.ts
          rag.service.ts
          rerank.service.ts
          retriever.service.ts
          rewrite.service.ts
        types/
          pdf-parse.d.ts
        vector/
          document.entity.ts
          vector.service.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。

🧭 综合实战项目 · 篇首 通义适配版 · tutorial-qw3.8

配套代码:../code/04-full-project/(完整可运行项目,含 Docker 部署) 建议学习时长:4~6 天(每天 2~3 小时)

本阶段把前三个阶段的全部能力整合成一个可上线的系统。学习目标不是学新知识,而是理解真实项目的模块划分、数据流与部署方式。


📌 1. 系统架构

┌────────────────────────────── 接入层 ──────────────────────────────┐
│  REST (Swagger /api/docs)   SSE 流式   WebSocket 实时对话网关        │
│  全局:ValidationPipe / 统一响应拦截器 / 异常过滤器 / JWT Guard       │
├────────────────────────────── 业务模块层 ──────────────────────────┤
│  AuthModule(注册/登录/刷新)   KnowledgeModule(知识库管理)            │
│  ChatModule(问答会话)         GatewayModule(WS 网关)                │
├────────────────────────────── AI 能力层 ───────────────────────────┤
│  LlmService(模型工厂+路由)    RagService(多路召回+RRF)              │
│  MemoryService(上下文窗口)    SkillsService + BusinessTools         │
│  EmbeddingService(在线千问/本地 bge)                                │
├────────────────────────────── 数据层 ──────────────────────────────┤
│  PostgreSQL 16:users / knowledge_bases / conversations / messages  │
│                 + document_chunks(float8 数组 + pgvector 检索)     │
└────────────────────────────────────────────────────────────────────┘

数据表关系:

users 1 ── N knowledge_bases        (用户的知识库)
users 1 ── N conversations 1 ── N messages   (对话历史)
knowledge_bases 1 ── N document_chunks       (知识库的分片向量)
conversations N ── 1 knowledge_bases(可空,问答范围绑定)

📌 2. 功能清单与模块划分

# 功能 模块 来自阶段
1 注册登录、JWT 双 Token、刷新轮换 src/auth/ 阶段 1
2 知识库 CRUD、文档上传自动入库 src/knowledge/ src/ingestion/ 阶段 3
3 RAG 问答:多路召回 + Prompt 注入 + 引用 src/chat/rag.service.ts 阶段 3
4 多模型路由:简单→轻量,知识库问答→强模型 src/llm/ 阶段 2
5 对话历史持久化 + 多轮上下文窗口 src/chat/memory.service.ts 阶段 2
6 SSE / WebSocket 流式输出 + 中断 src/chat/chat.controller.ts src/gateway/ 阶段 1+2
7 工具调用:业务查询 Tool + SKILL.md 技能 src/tools/ src/skills/ 阶段 2
8 Swagger 文档 + Docker 部署 src/main.ts Dockerfile 阶段 1+4

整合时的关键设计决策(读代码时重点看):

  1. 知识库范围隔离:document_chunks 带 knowledge_base_id,检索 SQL 加 WHERE knowledge_base_id = $2——不同知识库互不串扰;
  2. 会话绑定知识库:conversations.knowledge_base_id 可空。绑定 → 每轮问答自动 RAG(complex=true → 强模型);不绑定 → 纯闲聊(轻量模型);
  3. 工具的 userId 预绑定:业务工具的 userId 在服务端闭包绑定,模型看不到也不需要传——永远不要让模型决定"查谁的数据";
  4. 流式端点绕过全局拦截器:SSE 用 @Res() 手动写响应;
  5. 权限校验:知识库与会话操作都先校验归属(assertOwner / getOwnedConversation),防越权。

📌 3. 核心数据流:一次 RAG 问答的完整旅程

用户 POST /api/chat/completions { message, conversationId }
   │
   ▼ JwtAuthGuard 验签 → 得到 userId
   ▼ 取会话(校验归属,拿到绑定的 knowledgeBaseId)
   │
   ├─ 有知识库 → RagService.retrieve:
   │     查询向量化 → 向量召回 + 关键词召回 → RRF 融合 Top-3
   │     拼成【参考资料】
   │
   ▼ LlmService.route:有参考资料 → 强模型;否则轻量模型
   ▼ MemoryService.buildContext:系统提示 + 技能清单 + 资料 + 最近 6 轮
   ▼ createReactAgent.invoke(工具:业务查询 + load_skill)
   │     模型可能先调工具查真实数据,再组织答案
   ▼ 持久化 user/assistant 两条消息
   │
   返回 { answer, model, routeReason, conversationId, citations }

📌 4. 本地运行全流程演练

完整 curl 命令见 code/04-full-project/README.md,主线是:

注册 → 登录 → 建知识库 → 上传文档 → 建会话(绑定知识库)
→ 同步问答(看引用) → 流式问答 → 工具调用 → 查会话历史

验证要点:

  • 问答返回的 citations 应指向上传文档的分片;
  • routeReason 应显示"复杂问答→强模型"(绑定知识库时)或"简单问答→轻量模型"(纯闲聊);
  • 问"我有哪些知识库"应触发 get_my_knowledge_bases 工具并返回真实数据。

📌 5. Docker 部署教程

① 镜像构建(Dockerfile):多阶段构建——builder 阶段编译 TS,运行阶段只带 dist + 生产依赖,镜像更小。

② 编排(docker-compose.yml):

services:
  postgres:
    image: pgvector/pgvector:pg16      # 自带 pgvector
    volumes:
      - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql:ro  # 首启自动 CREATE EXTENSION
    healthcheck: ...                    # pg_isready 探活
  app:
    build: .
    depends_on:
      postgres:
        condition: service_healthy      # 数据库就绪后才启动应用
    environment:
      DB_HOST: postgres                 # 容器网络内用服务名,不是 127.0.0.1!

③ 部署命令:

cd code/04-full-project
export DEEPSEEK_API_KEY=sk-xxx        # 可选
docker compose up -d --build
docker compose logs -f app            # 观察启动日志
curl http://localhost:3104/api/docs   # 验证

④ 常见部署问题:

  • 应用连不上数据库:检查 DB_HOST 是否为服务名 postgres;
  • 端口冲突:改 compose 的端口映射 "3105:3104";
  • 修改代码后:docker compose up -d --build 重新构建。

📌 6. 接口清单

模块 端点 方法 鉴权
认证 /api/auth/register POST 无
认证 /api/auth/login POST 无
认证 /api/auth/refresh POST 无
认证 /api/auth/profile GET JWT
知识库 /api/knowledge-bases POST / GET JWT
知识库 /api/knowledge-bases/:id/documents POST(上传) JWT + 归属
知识库 /api/knowledge-bases/:id/chunks GET JWT + 归属
知识库 /api/knowledge-bases/:id DELETE JWT + 归属
对话 /api/chat/completions POST JWT
对话 /api/chat/stream(SSE) POST JWT
对话 /api/chat/models GET JWT
对话 /api/chat/conversations POST / GET JWT
对话 /api/chat/conversations/:id GET / DELETE JWT + 归属
WS ws://host/chat 事件:chat:send/token/meta/done/error/stop 握手 token
文档 /api/docs GET 无

📌 7. 上线前检查清单与常见问题

安全检查:

  • [ ] JWT 密钥换成 openssl rand -hex 32 生成的随机值;
  • [ ] .env 不在镜像/git 中(.dockerignore / .gitignore 已配置);
  • [ ] 生产环境关闭 synchronize(NODE_ENV=production 时自动关闭),改用迁移脚本;
  • [ ] Swagger 文档在生产环境按需关闭或加访问限制。

稳定性:

  • [ ] 大模型调用都有 timeout 与降级(本项目:无 Key 时检索降级、路由 fallback);
  • [ ] 文件上传有类型与大小限制;
  • [ ] 向量库记录了 embedding 模型与维度,切换模型时清空重入库。

常见问题:

  1. 两个用户能看到彼此的知识库吗? 不能——所有知识库/会话接口都校验归属。
  2. 不配任何 Key 能跑吗? 能启动,模型列表接口可用;问答需要至少一个厂商 Key(或本地 Embedding 先跑通入库)。
  3. 如何扩展成多租户/SaaS? 在 users 之上加 organization 层,所有实体增加 org_id 并在查询中过滤。

📌 8. 阶段自测清单

  1. 能否画出本系统的架构图并说清每个模块的职责?
  2. 一次绑定知识库的问答,从请求到响应经过哪些服务?
  3. 为什么业务工具的 userId 要在服务端绑定,而不是让模型传参?
  4. 知识库范围隔离是在哪一步实现的?(提示:检索 SQL)
  5. SSE 端点为什么能绕过全局响应包装?
  6. docker-compose 中 app 的 DB_HOST 为什么是 postgres 而不是 127.0.0.1?
  7. 多阶段 Dockerfile 构建的好处是什么?
  8. 生产上线前必须修改哪些配置?
  9. 如果要把"会话摘要长时记忆"加进本项目,应该改哪个服务?(提示:阶段 2 的 SummarizerService)
  10. 给你一个新需求"支持给回答点赞/点踩并用于优化召回",你会加哪些表和接口?

🎉 恭喜完成全部四个阶段! 你现在已经拥有一个完整的 AI 知识库问答后端系统,以及从 NestJS 工程化、LangChain 服务化到向量检索 RAG 的完整知识体系。接下来可以尝试:接入前端聊天 UI、增加文档解析格式、接入 rerank 专用模型、把压测纳入 CI。

📁 配套代码目录 tutorial-qw3.8/code/04-full-project

    04-full-project/
      .dockerignore
      .env.example
      .gitignore
      Dockerfile
      README.md
      docker-compose.yml
      nest-cli.json
      package-lock.json
      package.json
      tsconfig.build.json
      tsconfig.json
      docker/
        init.sql
      skills/
        kb-qa/
          SKILL.md
      src/
        app.module.ts
        main.ts
        auth/
          auth.controller.ts
          auth.module.ts
          auth.service.ts
          current-user.decorator.ts
          jwt-auth.guard.ts
          jwt.strategy.ts
          user.entity.ts
        chat/
          chat.controller.ts
          chat.module.ts
          chat.service.ts
          memory.service.ts
          rag.service.ts
        common/
        config/
          env.validation.ts
        gateway/
          chat.gateway.ts
          gateway.module.ts
        ingestion/
          document.parser.ts
          ingestion.service.ts
        knowledge/
          knowledge-base.entity.ts
          knowledge.controller.ts
          knowledge.module.ts
        llm/
          llm.module.ts
          llm.service.ts
          model-registry.ts
        skills/
          skills.service.ts
        tools/
          business-tools.service.ts
        types/
          pdf-parse.d.ts
        vector/
          document.entity.ts
          embedding.service.ts
          vector.service.ts

完整代码见 Gitee 仓库 codelvbowen/tutorials。