内容
# 价格监控 AI 代理
由 Eric Cheng 构建的全栈 **AI 代理** 项目,首先是为了演示 **AI 代理和 AI 集成** 工程,并以生产级 **全栈产品** 开发为支持。
该应用程序从任意 URL 监控产品价格,并通过电子邮件发送趋势摘要,但产品是载体。重点是它背后的 AI 和平台工程:
- 一个通过自定义 **MCP 服务器** (输入参数验证的工具 - 模型无直接 SQL 访问权限)驱动应用程序的**对话式 AI 代理**,
- 使用 **pgvector** 矢量存储的 **RAG 语义搜索** ,通过**本地托管的 MiniLM 嵌入模型** 在离线运行,
- **结构化 AI 提取** (Vercel AI SDK + Zod)用作难以访问页面的智能回退,而不是每个请求的默认设置,
- 所有这些都在一个带有 Dockerized、CI/CD 部署的后台工作器的 **事件驱动** Next.js 16 / React 19 / Redis + BullMQ / PostgreSQL 平台上。
**在线演示**
https://price-monitor.onlineeric.net/
## 系统架构

整个系统一瞥:一个 AI 聊天代理和仪表板(Next.js),一个自定义 MCP 服务器拥有语义搜索和本地嵌入模型,一个事件驱动的后台工作器,以及一个 PostgreSQL + pgvector / Redis 数据层。实线是同步请求/响应调用;虚线是异步 BullMQ 作业。
## 本项目演示内容
这是一个作品集项目:价格监控产品是真实且端到端的,但它的存在是为了展示两件事,优先级如下。
### 1. AI 代理和 AI 集成
- **对话式 AI 代理** — 通过自定义模型上下文协议(MCP)服务器进行多步骤工具调用、流式 UI、多轮历史记录和可点击的产品卡片,
- **RAG 语义搜索** — 通过 pgvector 矢量存储查找具有 *含义* 的产品(不是关键词重叠),使用单个 **本地托管的 `all-MiniLM-L6-v2` 模型** ,
- **输入参数验证的 MCP 工具** — 关键词 + 语义搜索、价格历史、趋势摘要、添加产品。模型无 SQL 访问权限;每个操作都受验证模式的约束。
- **结构化 AI 提取** — Vercel AI SDK `generateObject()` 配合严格的 Zod 模式,用于难以访问的页面,而不是每个请求的默认设置。
- **提供商抽象** — OpenAI、Anthropic 和 Google 可通过一个环境变量切换,用于提取和聊天。
- **生产防护** — 每轮步骤预算、超时、结构化错误分类和领域受限的系统提示。
### 2. 全栈产品工程
- **事件驱动架构** — Redis 上的 BullMQ 将 Web 应用程序与后台工作器解耦;作业涵盖价格检查、元数据丰富、矢量重建、摘要和调度器管理的可重复工作。
- **现代全栈** — Next.js 16 应用路由器、React 19、TypeScript、TanStack Query/Table、Shadcn UI。
- **输入参数的数据层** — Drizzle ORM 连接到 PostgreSQL 18 + pgvector,一个共享的模式包跨越 Web、后台工作器和 MCP 服务器,具有版本控制的自动应用迁移。
- **后台工作器** — 2 级提取(Cheerio → Playwright + 隐身),一个工作器管理的调度器(无外部 cron),以及通过 FlowProducer 的摘要分发。
- **生产运维** — 一个网络上的三个独立 Dockerized 服务、健康端点、GitHub Actions CI/CD 到 GHCR,以及自托管的 Coolify 部署。
## 配备 MCP 的 AI 聊天代理
一个专用的 `/dashboard/chat` 页面允许用户用自然语言与价格监控器对话。聊天页面从 `/api/chat` 流式传输响应,它调用一个自定义模型上下文协议(MCP)服务器来读写与仪表板相同的数据库。AI 代理无 SQL 访问权限,只有以下输入参数验证的工具。
```text
浏览器(聊天 UI,流式传输)
|
v
Next.js /api/chat (Vercel AI SDK streamText,多步骤工具调用)
|
v
MCP 客户端 ---> apps/mcp-server (输入参数验证的工具,Zod 验证)
(HTTP / stdio) | + 本地嵌入模型
v
PostgreSQL(+ pgvector) + BullMQ 队列
```
- **自定义 MCP 服务器(`apps/mcp-server/`)**:独立的 Node 进程使用 `@modelcontextprotocol/sdk` ,通过 `MCP_TRANSPORT` 选择两种传输方式——无状态的 Streamable HTTP(Web → mcp-server 在 Docker 开发和生产环境中)和 stdio(由 IDE 运行时启动,用于本地工具检查)。工具:`search_products` (关键词),`semantic_search_products` (基于含义,pgvector RAG),`get_product_history` , `get_price_summary` , `add_product` (它会排队一个 `update-product-info` BullMQ 作业,以便聊天添加的产品开始丰富和可搜索),以及 `ping` 。它还提供 `GET /health` , `GET /mcp/health` 和仅限内部的 `POST /internal/reindex` (不是 MCP 工具)。所有工具输入都是 Zod 模式;失败被封装成一个结构化的 `{ error: { code, message } }` 形状。
- **流式聊天 API:** `apps/web/src/app/api/chat/route.ts` 在 Node 运行时运行,将实时 MCP 工具桥接到 AI SDK `tool()` 实例,强制执行 5 步工具预算和 60 秒每轮超时,并返回具有结构化错误分类的 AI SDK v6 UI 消息协议(`validation_error` 、 `provider_config_missing` 、 `mcp_unreachable` 、 `step_budget_exceeded` 、 `turn_timeout` 、 `empty_response` 、 `provider_error` )。
- **提供商抽象:** 与驱动工作器提取回退的同一个 `AI_PROVIDER` 环境变量(`openai` | `anthropic` | `google` )选择聊天模型。
- **回复中的可点击产品:** 当回复检索产品(关键词或语义)时,它们会呈现为一个去重的可点击卡片列表(≤5 + “+N 更多匹配”),并且提到的产品名称成为内联的 `#product-<id>` 链接。两者都打开同一个可重用的 **ProductDetailDialog** ,用于仪表板 — 图片、价格 + 趋势、完整元数据、规格、来源链接和“检查价格现在” / “更新产品信息”操作 — 通过 `ChatProductDialogProvider` ,所有操作都不离开聊天。无法解析的内联提及会降级为纯文本。
- **多轮 UI:** Zustand 支持的内存聊天状态,通过 `streamdown` 进行清理的 Markdown 渲染,以及内联工具调用指示器,显示运行的工具、参数和结果,以实现演示透明度。
- **领域防护:** 系统将代理限制在产品 / 价格 / 监控主题上,因此它礼貌地拒绝非主题请求。
- **传输选择:** Web MCP 客户端(`apps/web/src/lib/mcp/client.ts` )在设置 `MCP_HTTP_URL` 时选择 `StreamableHTTPClientTransport` (生产和 Docker 开发),否则回退到通过 stdio 启动服务器以进行 IDE 工作流。
- **本地开发:** 同一个 MCP 服务器在 VSCode/Cursor 中注册,因此可以通过 `npx @modelcontextprotocol/inspector` 或直接从 IDE 驱动工具。
## 语义产品搜索(RAG 连接 pgvector)
一旦产品具有丰富的元数据,聊天代理就可以通过 *含义* 而不是字面关键词重叠来搜索它们 - “为视频编辑寻找一款适合的游戏显示器” 即使这些词在产品名称中没有出现,也会显示正确的结果。这是存储库中端到端构建的检索增强生成流水线。
```text
产品元数据 --> 综合文档 --> 准确的 token 块 --> MiniLM 矢量
(007 个字段,按优先级排序) (~200-token 片段,重叠) 产品嵌入(矢量(384))
|
聊天查询 --> 嵌入查询(相同模型) --> pgvector 余弦 KNN(HNSW) --> 每个产品的最佳片段
```
- **单一嵌入权限:** `mcp-server` 是唯一加载本地 `Xenova/all-MiniLM-L6-v2` 模型(384-dim,int8 量化,通过 `@huggingface/transformers` )的进程。它拥有查询时嵌入(搜索)和写入时嵌入(重建/回填),因此模型的 ~300 MB RAM 成本仅需支付一次 — 将生产环境中的 droplet 保持在预算内。工作器永远不会嵌入;它会排队。
- **索引构建:** 每个产品的 007 个元数据被组装成一个按优先级排序的综合文档(名称 → 品牌 → 类别 → 国家 → 描述 → 规格),分成有界的 ~200-token 片段(使用 `@langchain/textsplitters` )以重叠存储,因此长描述和大型规格列表不会被静默截断,并存储每个(产品,片段)在 `product_embeddings` 中一行。
- **矢量存储:** PostgreSQL 18 + pgvector,一个 `vector(384)` 列,具有 HNSW 余弦索引,通过 `cosineDistance` 与 Drizzle 原生查询。检索将每个产品折叠为其单个最佳匹配片段,因此一个长产品只出现一次,而不是每个片段一次。
- **相关性,而不是最近:** 一个可配置的余弦距离阈值(和 top-N,默认为 5)意味着一个无关的查询返回一个空集合,而不是最近但不相关的产品。
- **新鲜度:** 矢量仅在元数据被(重新)提取时重建(删除并替换,原子操作)。在 `update-product-info` 写入后,工作器会排队一个可重试的 `reindex-product-embeddings` 作业(5 次尝试,回退)以发布到 mcp-server 的内部 `/internal/reindex` 端点 — 嵌入侧的瞬时闪烁自行愈合,而不会影响元数据/价格写入。一个一次性 `backfill:embeddings` 脚本以幂等方式索引现有的目录。
- **提供商缝隙:** `EMBEDDING_PROVIDER` 是一个抽象(本地默认);切换提供商是一个有意的维度变化迁移,而不是运行时切换。
## AI 提取流水线
该存储库在有明显价值的地方使用 AI:作为难以访问页面的回退,而不是每个请求的默认设置。
- **快速通道:** `fetch()` + Cheerio 廉价快速地处理简单的产品页面(仅限价格)。
- **渲染回退:** Playwright 加载 JavaScript-heavy 页面,等待 DOM 稳定性,并使用浏览器端选择器重试提取。
- **AI 回退:** Vercel AI SDK `generateObject()` 提取 `title` 、 `price` 、 `currency` 和 `imageUrl` 到一个严格的 Zod 模式中。
- **丰富元数据提取:** `update-product-info` 操作始终使用 AI 层,并使用与 OpenAI 严格模式兼容的模式提取 `description` 、 `category` 、 `brand` 、 `countryOfOrigin` 和最多 100 个键值规格 — 每次成功运行时覆盖整个集合。
- **价格 / 元数据解耦:** 例行价格检查停留在廉价通道,从不运行元数据提取;昂贵的 AI 元数据刷新仅在添加时运行,按需或在显式批处理中。
- **提供商灵活性:** OpenAI、Anthropic 和 Google 提供商可通过环境变量切换。
- **操作细节:** 工作器重用一个单一的浏览器实例,并为部署检查公开一个健康服务器。
## 产品功能
- 添加一个产品 URL 并立即排队一个完整的元数据 + 价格提取作业,以便产品开始丰富
- 捕获丰富的产品元数据 — 描述、类别、品牌、原产国和一个键值规格列表(最多 100 个) — 通过按需的“更新产品信息”操作,与廉价的仅限价格检查分开
- 从卡片视图、表格视图和聊天中打开一个可重用的产品详情对话框(图片、来源链接、价格 + 趋势、完整元数据和规格)
- 使用创建、编辑、删除、活动/非活动状态和手动重新检查 / 更新信息操作管理产品
- 在卡片或表格视图中浏览受监控的产品,并查看最近的价格历史
- 使用全局产品搜索快速定位和编辑产品
- 与一个可以进行关键词 **和** 语义(基于含义)产品搜索、总结价格趋势、检索历史和通过 MCP 工具调用添加新产品的 AI 代理聊天
- 从聊天代理显示的产品直接打开一个产品,无需离开对话 — 作为可点击的卡片(去重,顶部 5 + “+N 更多”)和内联 `#product-<id>` 提及 — 无需离开对话
- 从 UI 配置每日或每周的摘要邮件调度
- 手动触发一个“检查所有产品并发送摘要”运行,选择仅限价格或完整信息 + 价格刷新
- 跟踪历史价格并比较当前价格与上次检查和 7/30/90/180 天的平均值
## 系统工作原理
```text
Next.js 仪表板 + API 路由 -----------------> mcp-server (嵌入权限)
| - 本地 MiniLM 模型
v - pgvector 上的语义搜索
Redis 上的 BullMQ 队列 <---- 重建索引(HTTP) ----- - 重建 product_embeddings
|
v
节点工作器
- HTML 获取 + Cheerio
- Playwright + 隐身
- AI 结构化提取(价格 + 丰富元数据)回退
|
v
PostgreSQL 18 + pgvector (价格历史、元数据、嵌入向量)
|
v
发送摘要邮件
```
### 端到端流程
1. 用户在 Web 应用程序中添加一个产品 URL(或通过聊天代理的 `add_product` 工具)。
2. Next.js API 存储产品并排队一个 `update-product-info` 作业(完整的元数据 + 价格),因此产品开始丰富而不是仅限价格。
3. 工作器按层尝试提取:
- 层 1:直接 HTTP 获取 + Cheerio 选择器(仅限价格的快速通道)
- 层 2:Playwright 渲染页面 + 选择器提取
- 层 3:AI 提取,具有类型化 Zod 验证 — 价格、货币、图像,以及(对于信息刷新)丰富的元数据集合
4. 最新结果存储在 PostgreSQL 中作为新的价格记录;信息刷新也覆盖产品的元数据。
5. 在元数据写入后,工作器尽力排队一个可重试的 `reindex-product-embeddings` 作业;mcp-server 重建该产品的向量索引,以便语义搜索保持最新。
6. 例行仅限价格的 `check-price` 作业仅刷新价格,从不触及元数据或向量。
7. 调度或手动的摘要作业分发刷新(仅限价格或信息 + 价格)所有活跃的产品,计算趋势并发送摘要邮件。
## 技术栈
| 领域 | 技术 |
| --- | --- |
| Web 应用程序 | Next.js 16 应用路由器、React 19、TypeScript |
| UI | Tailwind CSS v4、Shadcn UI、Radix 原始元素、Sonner、Lucide |
| 表单和验证 | React Hook Form、Zod |
| 数据访问 | Drizzle ORM、PostgreSQL 18、pgvector |
| 队列和后台作业 | BullMQ、Redis 8 |
| 提取 | Cheerio、Playwright、Playwright Extra、`puppeteer-extra-plugin-stealth` |
| AI | Vercel AI SDK、OpenAI、Anthropic、Google、 `@modelcontextprotocol/sdk` 、 `streamdown` |
| 嵌入 / RAG | 本地 `all-MiniLM-L6-v2` 通过 `@huggingface/transformers` 、 `@langchain/textsplitters` 、pgvector(HNSW 余弦) |
| 邮件 | Resend、React Email |
| 测试 | Vitest、Testing Library、jsdom |
| DevOps | Docker Compose、GitHub Actions、GHCR、Coolify |
## 值得注意的工程细节
- **工作器管理的调度:** 一个工作器实例拥有 BullMQ 可重复作业,避免外部 cron 依赖。
- **摘要编排:** 一个 `send-digest-flow` FlowProducer 分发子作业(仅限价格或信息 + 价格)以获取每个活跃的产品,然后在所有作业完成后发送邮件。
- **单一嵌入权限:** 只有 mcp-server 加载嵌入模型,因此其 RAM 成本仅需支付一次;工作器和回填脚本排队/POST 索引工作,而不是自己嵌入。
- **自愈索引:** `reindex-product-embeddings` 是一个可重试作业(回退)与元数据写入解耦,因此简短的 mcp-server 中断永远不会失败 `update-product-info` 运行,并且索引会自行恢复。
- **版本化、自动应用的迁移:** 模式更改作为提交的 Drizzle 迁移(规范路径);`drizzle-kit push` 仅用于快速本地原型设计。
### 本地开发
### 先决条件
- Node.js 20+
- pnpm
- Docker
### 快速开始
```bash
pnpm install
cp .env.example .env
# 配置至少一个 AI 提供商密钥;添加 Resend 设置以发送摘要邮件
pnpm docker:up
pnpm --filter @price-monitor/db migrate # 应用提交的迁移(见下文)
pnpm worker:up
pnpm --filter @price-monitor/web dev
```
打开 `http://localhost:3000/dashboard`.
### 数据库迁移
模式更改作为 **版本提交的Drizzle 迁移** (规范路径);`drizzle-kit push` 仅用于快速本地原型设计。
```bash
pnpm --filter @price-monitor/db generate # 作者:diff schema.ts → drizzle/NNNN_*.sql(提交它)
pnpm --filter @price-monitor/db migrate # 应用待处理迁移(幂等,带日志)
```
### 开发工作器
如果您希望在本地观察模式下运行工作器,而不是 Docker:
```bash
pnpm --filter @price-monitor/worker exec playwright install chromium
pnpm worker:dev # 从仓库根目录
```
等效:
cd apps/worker && pnpm dev
```
这两个命令都通过 `scripts/dev-worker.sh` 进行路由:暂时停止 Docker 工作器,使用 `tsx watch` 运行工作器,然后在退出(Ctrl+C)时恢复 Docker 工作器。
### 重新构建 Docker 工作器镜像
在更改工作器源代码或依赖项后,重新构建并重启 Docker 工作器:
```bash
pnpm worker:up # 重新构建镜像(--build)并启动容器
pnpm worker:down # 停止容器而不重新构建
```
### 代码检查和测试
代码检查和测试在仓库根目录下对所有工作区运行:
```bash
pnpm lint # Biome 对 web、worker、mcp-server、packages、scripts 进行代码检查
pnpm lint:fix # 应用 Biome 的安全 + 不安全修复(始终检查差异)
pnpm test # vitest 在每个具有 `test` 脚本的工作区中运行
```
测试与它们所覆盖的代码一起存放(例如 `src/lib/format.ts` → `src/lib/format.test.ts`)。Web 工作区还为仪表盘页面和 API 路由在 `src/test/` 下有集成测试。
测试套件**与 CI 绑定 - 它旨在推送代码前在本地运行。每当您更改应用程序代码时,还要添加或更新一个同位置的 `*.test.ts`(或 `*.test.tsx`)并运行 `pnpm test` 以确认没有回归。
## 部署
部署通过 GitHub Actions 和 Coolify 自动进行。
- 对 `dev` 分支的推送会构建 `web:dev` 和 `worker:dev` 镜像用于 CI 验证。
- 对 `main` 分支的推送会构建 `web:latest` 和 `worker:latest`,将它们推送到 GHCR,并触发 Coolify webhook。
- 生产环境运行三个独立的容器 - `web`、`worker` 和仅限内部的 `mcp-server` - 在同一个 Docker 网络上,加上 PostgreSQL(带有 pgvector)和 Redis。
- `mcp-server` 镜像在构建时会在编译时嵌入 `all-MiniLM-L6-v2` 嵌入权重,因此它在运行时加载模型 - 24 美元/月的数字 droplet 上无需下载模型,单个模型加载可保持在主机的 RAM 安全范围内。
- 只有一个生产工作器应ENABLE_SCHEDULER=true` 以避免重复发送定时邮件。同一工作器还设置了 `RUN_MIGRATIONS=true`,因此它在启动时自动应用待处理的数据库迁移,然后再使用作业(参见 [数据库迁移](docs/migrations.md))。
## 仓库结构
```text
apps/
web/ Next.js 仪表盘、聊天页面、REST + 流式 API 路由
worker/ BullMQ 工作器、提取管道、调度器、电子邮件 + 索引作业
m/ 自定义 MCP 类型聊天工具 + 单个(语义搜索、重新索引)
packages/
db/ 共享 Drizzle 模式、客户端迁移
reporting/ 共享电子邮件渲染/发送(React Email 摘要),由 web + worker 使用
docs/ 部署和环境说明
specs/ 按功能划分 / 计划 / 任务scripts/ 本地实用脚本
```
## 技术评审入口
- `apps/mcp-server/src/index.ts`
cp-server/src/tools/semantic-search-products.ts`
apps/mcp-server/src/embeddings/ `apps/web/src/app/api/chat/route.ts`
- `/src/lib/mcp/client.ts`
- `apps/web/src/lib/ai/chat-tools.ts`
- `apps/web/src/lib/chat/product-cards.ts`
- `apps/web/src/app/(main)/dashboard/chat`
- `apps/worker/src/services/scraper.ts`
-/worker/src/services/playwright`
- `apps/worker/src/services/aiExtractor.ts`
- `apps/worker/src/jobs/updateProductInfo.ts`
- `apps/worker/src/jobs/reindexEmbeddings.ts`
- `apps/worker/src/jobs/sendDigest.ts`
- `apps/worker/src/scheduler.ts`
- `apps/web/src/app/api/products/route.ts`
- `apps/web/src/app/(main)/dashboard/products`
- `packages/db/src/schema.ts`
- `.github/workflows/build-and-push.yml`
## 额外文档
- [生产环境参考](docs/production-env.md)
- [数据库迁移](docs/migrations.md)
- [Docker 故障排除](docs/troubleshooting-docker.md)
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器