从零搭建多 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 prompttool— 直接调用工具节点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_KEY | 是 | OpenAI API Key |
OPENAI_BASE_URL | 否 | 自定义 API 地址(兼容 SiliconFlow/Deepseek) |
TAVILY_API_KEY | 否 | 网页搜索功能需要 |
JWT_SECRET | 生产必填 | JWT 签名密钥 |
踩过的坑
分享几个开发过程中遇到的典型问题:
1. createReactAgent 子图的"思考"文本泄露
使用 LangGraph 的 createReactAgent 时,内部 LLM 在决定调用工具前会输出一段"思考"文本。这些文本通过 streamEvents 被捕获并发送到前端,用户看到的是一堆 prompt 模板文字而非实际回答。
解决方案:通过 langgraph_checkpoint_ns 中的 langgraphNode 和 parentNode 区分顶层节点和子图内部调用。子图内部 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,一起完善
- 💬 留言交流,分享你的想法
感谢阅读!