从零搭建多 Agent 智能编排平台:NestJS + LangGraph 全栈实战

60 阅读8分钟

从零搭建多 Agent 智能编排平台:NestJS + LangGraph 全栈实战

本文分享一个开源项目 Nest-Agent——基于 NestJS + LangGraph 构建的多 Agent 编排平台,支持 Supervisor 自动路由、DAG 自定义工作流、RAG 知识库检索,遵循 AG-UI 标准协议实现 token 级流式输出。如果对你有帮助,欢迎 GitHub Star 支持 ⭐


为什么做这个项目?

当前 AI Agent 框架层出不穷,Python 生态的 LangChain、CrewAI 已经很成熟。但在 Node.js / TypeScript 生态,企业级的 Agent 编排后端方案相对匮乏。作为一个 NestJS 爱好者,我希望用最熟悉的技术栈打造一个:

  • 生产级的多 Agent 协同平台,不是 demo
  • 支持 Supervisor 自动路由DAG 自定义工作流 两种编排模式
  • 遵循 AG-UI 标准协议,前后端解耦,可对接 CopilotKit 等主流前端 SDK
  • 内置 RAG 知识库多 LLM 供应商多租户隔离,开箱即用

于是有了 Nest-Agent


技术栈一览

层次技术
后端框架NestJS 11
Agent 编排LangChain + LangGraph
数据库MySQL 8.0 (TypeORM)
缓存Redis 7 (ioredis)
向量库Milvus 2.3
认证Passport JWT
前端React 18 + shadcn/ui + Vite
包管理pnpm workspace (monorepo)
部署Docker Compose 一键启动

核心功能展示

1. AI 对话 — 流式输出 + 工具调用可视化

对话是整个平台的核心。输入一个问题,Supervisor 自动判断是否需要搜索、检索知识库还是直接回答,整个过程实时流式展示。

对话首页

流式输出

亮点:

  • Token 级别的流式输出,逐字显示 AI 回复
  • 实时展示 Agent 执行步骤(researcher → 搜索 → 生成回答)
  • 工具调用全程可视化——工具名、参数、返回结果一目了然
  • Markdown 富文本渲染(代码块、表格、列表、链接等)
  • 对话标题自动生成,不再是千篇一律的 "New Conversation"

对话详情

2. 工作流编排 — 可视化 DAG 工作流

除了 Supervisor 自动模式,你还可以自定义 DAG 工作流,精确控制 Agent 的执行流程。

工作流列表

创建工作流

支持的节点类型:

  • agent — 可调用工具的 AI Agent,支持自定义 system prompt
  • tool — 直接调用工具节点
  • condition — 条件分支,根据关键词路由到不同分支
  • start / end — 起止标记

3. RAG 知识库 — 语义检索

上传文档到知识库,Agent 对话时自动检索相关知识片段,实现基于私有数据的问答。

知识库列表

语义检索

RAG Pipeline:

文档 → 分块(1000/200) → BAAI/bge-m3 Embedding → Milvus 向量存储
查询 → Embedding → Milvus ANN 检索 → Top-K 结果

4. 认证系统 — 多租户隔离

登录 注册

JWT 认证 + TenantGuard,所有数据按 tenantId 隔离,天然支持多团队使用。


架构设计

整体分层

┌──────────────────────────────────────────────────────────────┐
│                     React + shadcn/ui                         │
│                (SSE 客户端 + AG-UI 事件解析)                   │
└────────────────────────┬─────────────────────────────────────┘
                         │ HTTP POST + SSE 流
┌────────────────────────▼─────────────────────────────────────┐
│                     NestJS 应用层                              │
│  ┌──────┐ ┌──────┐ ┌───────┐ ┌──────┐                       │
│  │ Auth │ │ Chat │ │ Agent │ │ RAG  │  Controller 层          │
│  └──┬───┘ └──┬───┘ └───┬───┘ └──┬───┘                       │
│     │        │         │        │                             │
│  ┌──▼───┐ ┌──▼───┐ ┌───▼───┐ ┌──▼───┐                       │
│  │ Auth │ │ Chat │ │Agent  │ │ RAG  │  Service 层            │
│  └──────┘ └──────┘ └───┬───┘ └──────┘                       │
│                        │                                      │
│           ┌────────────┼────────────┐                        │
│     ┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐                   │
│     │ Supervisor │ │   DAG   │ │  Tool   │  核心编排层        │
│     │  Factory   │ │ Engine  │ │Registry │                   │
│     └───────────┘ └─────────┘ └─────────┘                   │
│                                                               │
│     ┌─────────┐ ┌────────┐ ┌────────┐                       │
│     │   LLM   │ │ Milvus │ │ Redis  │  基础设施层            │
│     │ Service  │ │Service │ │Service │                       │
│     └─────────┘ └────────┘ └────────┘                       │
└──────────────────────────────────────────────────────────────┘
         │             │           │
    ┌────▼────┐  ┌─────▼────┐ ┌───▼──┐
    │OpenAI/  │  │ Milvus   │ │Redis │  存储层
    │Anthropic│  │ 2.3      │ │  7   │
    │/Qwen    │  └──────────┘ └──────┘
    └─────────┘

两种编排模式对比

Supervisor 模式(默认)

适合通用对话场景。LLM 充当"主管",根据用户意图动态路由到合适的 Agent:

用户: "搜索一下 NestJS 11 新功能"
  → Supervisor 判断: 需要搜索,路由到 researcher
  → researcher 调用 web_search 工具
  → researcher 根据搜索结果生成回答
  → Supervisor 判断: 任务完成,路由到 __end__

核心实现:Supervisor 使用 LLM + withStructuredOutput(zod schema) 做结构化输出,返回 { next: "researcher" | "responder" | "__end__" }

DAG 模式(自定义工作流)

适合复杂业务流程。用户预定义有向无环图,精确控制执行链路:

Start → 研究员Agent(调用搜索) → 条件判断 → 写手Agent(生成报告) → End
                                    ↘ 工具节点(直接检索) ↗

核心实现:将 JSON 格式的 nodes[] + edges[] 编译为 LangGraph 的 StateGraph,通过 Command({ goto, update }) 控制状态转移。


关键技术实现细节

1. AG-UI 流式事件协议

这是项目中最复杂也最有价值的部分。系统遵循 AG-UI 标准协议,通过 SSE 推送细粒度事件:

event: RUN_STARTED
data: {"type":"RUN_STARTED","threadId":"xxx","runId":"xxx"}

event: STEP_STARTED
data: {"type":"STEP_STARTED","stepName":"researcher"}

event: TOOL_CALL_START
data: {"type":"TOOL_CALL_START","toolCallId":"xxx","toolCallName":"web_search"}

event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"xxx","delta":"逐字输出..."}

event: RUN_FINISHED
data: {"type":"RUN_FINISHED","threadId":"xxx","runId":"xxx"}

三段式文本消息TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT(增量) → TEXT_MESSAGE_END

四段式工具调用TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END → TOOL_CALL_RESULT

遵循标准协议意味着可以直接对接 CopilotKit 等前端 SDK,无需自定义解析。

2. LangGraph streamEvents → AG-UI 的精细转换

AgentService.processStreamEvents() 方法实现了 LangGraph 底层事件到 AG-UI 协议的映射,需要处理几个棘手的问题:

问题 1:Supervisor 路由节点的输出不应暴露给用户

Supervisor 的结构化输出({ next: "researcher" })是内部路由决策,不应该推送给前端。通过节点名过滤解决。

问题 2:嵌套子 Agent 的"思考"文本

createReactAgent 内部的 LLM 在调用工具前会输出一些"思考"文本(通常是复述 prompt),这些不应显示给用户。解决方案是通过 langgraph_checkpoint_ns 区分嵌套层级:

const isNestedAgent = parentNode && langgraphNode !== parentNode;
if (isNestedAgent && !nodeToolsDone.get(nodeKey)) {
  // 暂存到 pendingTextPerNode,工具完成后才释放
}

问题 3:XML 工具调用格式兼容

某些模型(如 qwen)会以 <tool_call> XML 标签输出工具调用。代码使用正则检测并过滤,避免 XML 标签泄露到前端。

3. 智能记忆管理

对话记忆采用 滑动窗口 + LLM 自动摘要 的混合策略:

消息数 ≤ 10        → 全量返回,无压缩
10 < 消息数 ≤ 20   → 滑动窗口,取最近 10 条
消息数 > 20        → LLM 对早期消息生成摘要,摘要 + 最近 10 条

摘要存储在 Conversation.summary 字段,支持增量摘要(新对话内容追加到现有摘要上)。会话消息缓存在 Redis,TTL 3600 秒,减少数据库查询。

4. 多 LLM 供应商抽象

统一的模型工厂,切换供应商只需一个参数:

// 用户请求时指定
{ llmOptions: { provider: "openai", model: "gpt-4o" } }
{ llmOptions: { provider: "dashscope", model: "qwen-max" } }
{ llmOptions: { provider: "anthropic", model: "claude-sonnet-4-20250514" } }

DashScope(通义千问)通过 OpenAI 兼容协议接入,自定义 baseURL 即可。同样的方式可以接入 SiliconFlow、Deepseek、vLLM 等任何兼容 OpenAI API 的服务。

5. 动态工具 + 多租户隔离

工具系统采用注册表模式。web_search 是全局静态工具,而 rag_retrieval 因为需要 tenantId 上下文,采用工厂函数动态创建

// 每次请求动态创建,确保 tenantId 隔离
const ragTool = createRagRetrievalTool(ragService, tenantId);

这样不同租户只能检索到自己的知识库数据。


项目结构

nest-agent/
├── src/                       # 后端(NestJS)
│   ├── auth/                  # JWT 认证 + 多租户守卫
│   ├── chat/                  # 对话管理 + SSE 流式接口 + 记忆策略
│   ├── agent/                 # 核心:Supervisor 路由 + DAG 引擎
│   │   ├── agent.service.ts   # 编排入口 + AG-UI 事件转换
│   │   ├── supervisor.factory.ts  # Supervisor 有向图构建
│   │   └── dag-engine.ts      # DAG 编译执行引擎
│   ├── rag/                   # RAG 知识库(Milvus 向量检索)
│   ├── llm/                   # 多 LLM 供应商抽象
│   ├── tools/                 # 工具注册中心
│   ├── redis/                 # Redis 缓存
│   ├── entities/              # TypeORM 实体
│   └── common/                # AG-UI 协议定义、配置、异常过滤器
├── web/                       # 前端(React + shadcn/ui + Vite)
│   └── src/
│       ├── pages/             # 对话、工作流、知识库、登录
│       ├── components/        # 布局 + shadcn/ui 组件
│       └── lib/               # API 封装、认证上下文
├── Dockerfile                 # 多阶段构建
├── docker-compose.yml         # MySQL + Redis + Milvus 一键启动
└── pnpm-workspace.yaml        # monorepo 配置

快速启动

Docker 一键部署

git clone https://github.com/peng-yin/nest-agent.git
cd nest-agent
cp .env.example .env
# 编辑 .env,填入 OPENAI_API_KEY 等配置
docker-compose up -d
# 访问 http://localhost:3000

本地开发

# 1. 启动基础设施
docker-compose up -d mysql redis etcd minio milvus-standalone

# 2. 安装依赖
pnpm install

# 3. 启动后端(热重载)
pnpm dev            # http://localhost:3000

# 4. 启动前端(另开终端)
pnpm dev:web        # http://localhost:5173

环境变量

变量必填说明
OPENAI_API_KEYOpenAI API Key
OPENAI_BASE_URL自定义 API 地址(兼容 SiliconFlow/Deepseek)
TAVILY_API_KEY网页搜索功能需要
JWT_SECRET生产必填JWT 签名密钥

踩过的坑

分享几个开发过程中遇到的典型问题:

1. createReactAgent 子图的"思考"文本泄露

使用 LangGraph 的 createReactAgent 时,内部 LLM 在决定调用工具前会输出一段"思考"文本。这些文本通过 streamEvents 被捕获并发送到前端,用户看到的是一堆 prompt 模板文字而非实际回答。

解决方案:通过 langgraph_checkpoint_ns 中的 langgraphNodeparentNode 区分顶层节点和子图内部调用。子图内部 LLM 的 langgraphNode"agent",而 parentNode"researcher",两者不同;普通顶层节点两者相同。利用 langgraphNode !== parentNode 识别嵌套调用,将工具调用前的文本暂存丢弃。

2. StateGraph 节点的 checkpointNs 理解偏差

最初以为只有 createReactAgent 构建的子图才有 checkpointNs,但实际上 StateGraph 中所有节点都有。导致 responder(直接回答)的正常文本也被错误暂存。debug 日志大法好。

3. 阿里云 DashScope 的 XML 工具调用

通过 OpenAI 兼容 API 调用 qwen 模型时,部分场景下工具调用不走标准的 tool_calls 字段,而是在文本中输出 <tool_call> XML 标签。需要正则检测并过滤,否则前端会显示一堆 XML。


后续计划

  • 支持更多工具(代码执行、文件上传、图片生成等)
  • 工作流可视化编辑器(拖拽式 DAG 编辑)
  • Agent 执行过程的可视化 Trace
  • 支持更多向量数据库(Pinecone、Qdrant)
  • 支持文件上传(PDF、Word 等文档直接入库)

写在最后

这个项目从架构设计到编码实现,全部由一个人完成。涵盖了 Agent 编排、流式通信、RAG 检索、多租户隔离等多个技术领域,希望能为 Node.js/TypeScript 生态的 AI Agent 开发提供一个可参考的实践案例。

代码完全开源,如果这个项目对你有帮助,或者你对 NestJS + AI Agent 感兴趣,欢迎:

  • Star 这个项目:GitHub - nest-agent
  • 🐛 提 Issue 或 PR,一起完善
  • 💬 留言交流,分享你的想法

感谢阅读!