文档代理中心
一个多代理文档助手,允许用户上传PDF并通过智能聊天界面与他们进行交互。作为一个组合项目构建,用于演示多代理编排、MCP服务器实现和全栈TypeScript开发。
注: 这是一个教育和投资组合项目,而不是生产应用程序。
目录
建筑
flowchart TD
A[User Question] --> B[Router Agent]
B -->|retrieve| C[Retriever Agent]
B -->|summarize| D[Summarizer Agent]
B -->|getDocument| E[Document Agent]
B -->|list| F[Document Manager]
C --> G[Similarity Search] --> H[LLM Answer]
D --> I[Full Document Chunks] --> J[LLM Summary]
E --> K[Full Document Content]
F --> L[Database Query]
H --> M[Response]
J --> M
K --> M
L --> M这 路由器代理 使用结构化输出将每个问题分为四条路线之一,并可选择提取文档名称。每个代理都是LangGraph中的专用节点 StateGraph 具有条件边。
服务体系结构
后端聊天管道按照单一职责原则分为三个服务:
| 服务 | 责任 |
|---|---|
OrchestratorService | 组装LangGraph StateGraph 工作流和连接节点和边 |
RouterService | 包含 route 节点(LLM分类)和 routeToAgents 条件边逻辑 |
AgentService | 包含所有代理节点: retrieve, summarize, getDocument, listDocuments |
MCP服务器
该项目包括一个独立的 模型上下文协议(MCP)服务器 公开文档工具(listDocuments, searchDocuments, getDocumentInfo, getDocument)通过流式HTTP传输。外部MCP兼容客户端(如Claude Desktop)可以连接到 http://localhost:3000/mcp 并直接使用这些工具。
架构决策: LangGraph代理调用 DocumentsService 直接而不是通过MCP服务器路由。在同一应用程序中通过HTTP进行自连接会引入时间问题和不必要的开销。MCP服务器作为第三方客户端的外部接口存在。技术栈
| 组件 | 技术 |
|---|---|
| 前端 | React 19、TypeScript、Vite、顺风CSS |
| 后端 | NestJS、TypeScript |
| LLM编排 | LangChain、LangGraph |
| 语言模型 | OpenAI GPT-5/GPT-5-mini |
| 嵌入 | OpenAI文本嵌入3-small |
| 矢量存储 | PostgreSQL+pgvector |
| MCP服务器 | @modelcontextprotocol/sdk(流式HTTP) |
| 追踪 | LangSmith |
| Markdown渲染 | 反应Markdown,备注gfm |
特性
- 多代理路由 --基于LLM的路由器对问题进行分类,并委托给专门的代理
- 文档上传 --通过LangChain自动解析、分块和嵌入PDF上传
- RAG检索 --基于pgvector的文档块语义相似度搜索
- 文件摘要 --通过检索所有块进行完整文档摘要
- MCP服务器 --面向外部客户的标准化工具界面
- LLM追踪 --LangSmith中可见的工作流跟踪和代理调用
- 聊天记忆 --通过LangGraph MemorySaver实现每个会话的会话持久性
- 聊天小部件 --带有文件上传、markdown渲染和加载状态的浮动暗主题小部件
- 错误处理 --前端和后端都有优美的错误消息
- 快速注射保护 --系统提示指示代理仅将文档上下文视为原始数据
示例用法
上传文档并提问:
> [Upload] report.pdf
"report.pdf uploaded successfully."
> "What topics does report.pdf cover?"
"An introductory AI course focused on regression methods, covering
linear regression, logistic regression, sigmoid properties..."
> "Which documents do I have?"
"- report.pdf (uploaded: 26.03.2026, 17:10)"总结一份具体文件:
> "Summarize thesis.pdf"
"The thesis extends the 5Code learning environment from a Java-only
LSP setup to a multilingual platform, adding Kotlin and Python..."获取原始文档内容:
> "Output the content of notes.pdf"
[Full document text returned]设置
先决条件
- Node.js 20+
- 码头工人
- OpenAI API密钥
- pnpm
安装
# Clone
git clone https://github.com/AdamBess/doc-agent-hub.git
cd doc-agent-hub
# Environment
cp .env.example .env
# Add your OPENAI_API_KEY, DB_USER, DB_PASSWORD to .env
# Database
docker compose up -d
# Backend
cd backend
pnpm install
pnpm start:dev
# Frontend (new terminal)
cd frontend
pnpm install
pnpm dev打开 http://localhost:5173.
Docker设置(可选)
运行全栈(DB+后端+前端),无需在本地使用Node.js或pnpm。
cp .env.example .env
# Add your OPENAI_API_KEY, DB_USER, DB_PASSWORD to .env
docker compose up --build打开 http://localhost.
项目结构
doc-agent-hub/
├── backend/
│ └── src/
│ ├── chat/ # LangGraph workflow, agents, state
│ │ ├── orchestrator.service.ts # Workflow assembly (StateGraph wiring)
│ │ ├── router.service.ts # Routing logic (route node + conditional edges)
│ │ ├── agent.service.ts # Agent nodes (retrieve, summarize, getDocument, listDocuments)
│ │ ├── chat.controller.ts # POST /chat endpoint
│ │ └── agent.state.ts # Zod state schema
│ ├── documents/ # Upload pipeline + document queries
│ │ ├── documents.service.ts # PDF parsing, chunking, pgvector
│ │ ├── documents.controller.ts
│ │ └── document.entity.ts
│ ├── mcp/ # MCP server + tools
│ │ ├── mcp.service.ts # Tool registration
│ │ └── mcp.controller.ts # Streamable HTTP transport
│ └── health/ # Health check endpoint
├── frontend/
│ └── src/
│ └── chat/
│ └── ChatWidget.tsx # Floating chat widget
├── docker-compose.yml # PostgreSQL + pgvector
└── .env # Environment variables已知限制
- 重复上传 --多次上传同一文件会创建单独的条目。当按文件名匹配时,
findOneBy返回任意匹配项,但不保证是最新上传的。 - 路由器歧义 --像“这份文件是关于什么的?”这样的问题可能会转到
summarize而不是retrieve这取决于措辞。路由器在明确意图的情况下工作得最好。 - 文档名称匹配 --用户必须引用确切的文件名(包括
.pdf)用于总结和文档检索。 - 完整文档检索 --“获取文档”功能通过使用空查询字符串运行相似性搜索来重建文档内容(
similaritySearch('', 100, { documentId })),最多100块。这是对向量搜索API的误用:结果是根据嵌入距离而不是文档顺序进行排序的,因此重新组合的文本可能会无序。对于大型文档,100块上限也意味着内容会被自动截断。正确的方法是持久化原始文件并直接返回,或者存储块位置元数据并通过按位置排序的普通SQL查询检索块——在这个用例中完全绕过向量存储。实施任何一种解决方案都不在本项目的范围内。 - 无身份验证 --该应用程序没有用户身份验证或文档访问控制。
- 内存聊天历史记录 —
MemorySaver将对话状态存储在内存中;服务器重启时丢失。
我学到了什么
- 多代理路由 --使用LLM结构化输出对用户意图进行分类,并通过LangGraph条件边路由到专用代理
- LangGraph状态管理 --使用Zod定义状态模式,使用reducer进行消息累积,并使用构建工作流
StateGraph - MCP服务器实现 --使用以下命令构建模型上下文协议服务器
registerTool以及流式HTTP传输,了解MCP与直接服务调用相比何时增加价值 - RAG 流程 --使用pgvector相似性搜索进行文档摄取(解析、组块、嵌入、存储)和检索
- NestJS架构 --模块、依赖注入、,
ConfigService,TypeOrmModule.forRootAsync,以及生命周期挂钩(onModuleInit) - 提示工程 --制作系统提示以提高路由准确性,并为文档上下文添加提示注入保护
