Skip to main content
Glama
README.md
# visitproject

一个**无头的 agentic 网络搜索服务**,通过 [MCP(Model Context Protocol)](https://modelcontextprotocol.io) 把深度 web 搜索能力暴露给 AI agent。

普通的 web search 工具只是把链接丢给 agent。visitproject 多做了一系列让搜索结果真正可用的事:

1. **聚合召回**——基于 SearXNG 元搜索,一次查遍 Google / Bing / DuckDuckGo 等 70+ 引擎
2. **去重 + 重排**——用 embedding 算相似度,过滤掉与问题无关的、去掉重复的,按相关性排序;embedding 不可用时自动降级到 URL 规范化 + 标题 Jaccard 去重
3. **正文提取**——deep 模式用无头浏览器抓取 + Readability 提取正文,再让 LLM 抽取关键事实
4. **真实的 relevance score**——quick 模式用 embedding 相似度,deep 模式用 LLM picker 打分(不是编造的递减序列)
5. **成本护栏**——deep 模式有 LLM 调用次数、抓取 URL 数的硬上限,防止研究循环跑飞烧钱
6. **结果缓存**——LRU + TTL,重复相似 query 省下 50%+ 成本
7. **多引擎 fallback**——SearXNG 不可用时自动退到 DuckDuckGo / Tavily
8. **流式中间结果**——deep 模式每轮通过 MCP progress notification 推送当前进度

最终 agent 拿到的是**干净的结构化结果**(标题 + URL + 正文 + 分数),而不是一堆要自己点开的链接。

## 架构

```
┌─────────────────────────────────────────────────────┐
│  MCP 客户端(MadCop / ZCode / Claude Desktop / ...) │
└───────────────────────┬─────────────────────────────┘
                        │ stdio (MCP 协议)
                        ▼
┌─────────────────────────────────────────────────────┐
│  visitproject (Node 进程)                            │
│                                                      │
│  search 工具 ──┬─ quick: 召回 → embedding 重排       │
│                └─ deep : agentic 多轮研究循环        │
│                          │                           │
│                          ▼                           │
│  scrape 工具 ── Playwright + Readability 正文提取    │
└───────────────────────┬─────────────────────────────┘
                        │ HTTP
                        ▼
┌─────────────────────────────────────────────────────┐
│  SearXNG (Docker 容器) —— 元搜索引擎                 │
└─────────────────────────────────────────────────────┘
```

## 两个工具

| 工具 | 作用 | 适用场景 |
|---|---|---|
| `search` | 主搜索。`depth=quick` 单次召回+重排(快);`depth=deep` agentic 多轮研究+抓正文(质量高,默认) | agent 需要联网查最新信息、调研主题 |
| `scrape` | 抓取指定 URL 的正文 | agent 已知具体网址,想直接读内容 |

`search` 支持 `source` 参数:`web`(通用)/ `academic`(arxiv/scholar/pubmed)/ `discussions`(reddit)。

## 快速开始

### 1. 启动 SearXNG

需要 Docker(macOS 推荐 [Colima](https://github.com/abiosoft/colima)):

```bash
docker-compose up -d
# 验证:应返回 JSON 结果
curl 'http://localhost:8080/search?format=json&q=hello+world' | head -c 200
```

### 2. 安装依赖并构建

```bash
npm install
npm run build

# 安装 Playwright 浏览器(deep 模式抓正文用)
npx playwright install chromium
```

### 3. 配置

```bash
cp .env.example .env
# 编辑 .env,填入 LLM_API_KEY 等
```

visitproject 需要两类模型:
- **Chat 模型**:deep 模式的 researcher 循环、结果筛选、事实抽取
- **Embedding 模型**:quick 模式的去重与重排

两者都支持 OpenAI 兼容协议(OpenAI / DeepSeek / 智谱 / NVIDIA NIM / 通义等),也支持本地 Ollama。详见 `.env.example`。

### 4. 本地测试

```bash
npm start
# server 启动后会通过 stdio 等待 MCP 客户端连接
```

## 接入 MadCop

visitproject 主要场景是给 [MadCop](https://github.com/linmy666) 的 agent 提供网络搜索。在 MadCop 的 MCP 配置(`~/.madcop/mcp_servers.json` 或设置界面)中添加:

```json
{
  "name": "visitproject",
  "command": "node",
  "args": ["/absolute/path/to/visitproject/dist/index.js"],
  "enabled": true,
  "env": {
    "SEARXNG_URL": "http://localhost:8080",
    "LLM_PROVIDER": "openai",
    "LLM_BASE_URL": "https://api.openai.com/v1",
    "LLM_API_KEY": "sk-...",
    "LLM_MODEL": "gpt-4o-mini",
    "EMBEDDING_MODEL": "text-embedding-3-small"
  }
}
```

挂载后,MadCop 的所有 agent(standard 模式 / deep 模式的 researcher 等)即可调用 `search` 和 `scrape` 工具。

## 接入其他 MCP 客户端

任何支持 stdio MCP 的客户端都能用,配置方式类似。例如 Claude Desktop 在 `claude_desktop_config.json` 中:

```json
{
  "mcpServers": {
    "visitproject": {
      "command": "node",
      "args": ["/absolute/path/to/visitproject/dist/index.js"],
      "env": { "SEARXNG_URL": "http://localhost:8080", "...": "..." }
    }
  }
}
```

## 深度 vs 快速

| | quick | deep |
|---|---|---|
| 速度 | 快(1 次 SearXNG + 1 次 embedding) | 慢(多轮 LLM 调用 + 抓取) |
| 质量 | 摘要 + 向量重排,够用 | 抓全文 + LLM 抽事实,高 |
| LLM 依赖 | 仅 embedding | embedding + chat(多轮) |
| 适合 | 简单事实、快速核实 | 深度调研、复杂问题 |

默认 `deep`,可在 `.env` 用 `VISIT_DEPTH=quick` 改默认值,或每次调用时用 `depth` 参数覆盖。

## 项目结构

```
visitproject/
├── src/
│   ├── index.ts              # MCP server 入口(stdio 传输)
│   ├── server.ts             # 注册 search / scrape 工具
│   ├── config.ts             # 环境变量配置
│   ├── types.ts              # 共享类型
│   ├── core/
│   │   ├── searxng.ts        # SearXNG HTTP 客户端
│   │   ├── scraper.ts        # Playwright + Readability 正文提取
│   │   ├── pipeline.ts       # 搜索管线:召回→去重→重排 / 召回→选→抓→抽
│   │   └── researcher.ts     # agentic 多轮研究循环
│   ├── models/
│   │   ├── base/             # LLM / Embedding 抽象
│   │   ├── providers/        # OpenAI / Ollama 实现
│   │   └── factory.ts        # 按 provider 构造实例
│   ├── prompts/researcher.ts # researcher / picker / extractor 提示词
│   └── utils/                # 相似度计算 / 文本分块
├── searxng/settings.yml      # SearXNG 配置
├── docker-compose.yml        # SearXNG 容器
└── .env.example              # 配置模板
```

## 开发

```bash
npm run dev      # watch 模式编译
npm run typecheck
npm test         # 跑测试(29 个,覆盖 similarity/chunker/dedup/cache)
```

## 全部配置项

| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `SEARXNG_URL` | http://localhost:8080 | SearXNG 地址(必需) |
| `LLM_PROVIDER` | openai | openai / ollama |
| `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL` | — | chat 模型 |
| `EMBEDDING_*` | 同 LLM | embedding 模型 |
| `VISIT_DEPTH` | deep | 默认搜索深度 |
| `VISIT_MAX_RESULTS` | 12 | 返回结果上限 |
| `VISIT_MAX_ITERATIONS` | 6 | deep 模式最大轮数 |
| `VISIT_MAX_LLM_CALLS` | 20 | **成本护栏**:单请求 LLM 调用上限 |
| `VISIT_MAX_SCRAPE_URLS` | 8 | **成本护栏**:单请求抓取上限 |
| `VISIT_CACHE_TTL` | 3600000 | 缓存 TTL(毫秒),0=禁用 |
| `VISIT_CACHE_SIZE` | 64 | 缓存条数 |
| `VISIT_FALLBACK` | duckduckgo | SearXNG 挂时的后备引擎 |
| `TAVILY_API_KEY` | — | Tavily fallback 用(可选) |
| `VISIT_SCRAPE_CONCURRENCY` | 3 | 抓取并发上限 |
| `VISIT_INDEX_PATH` | ./data/index.json | **本地索引文件**(自建垂直索引) |
| `VISIT_SOURCES_PATH` | ./data/sources.json | **数据源配置**(要爬取哪些站) |
| `VISIT_INDEX_TOP_K` | 5 | 本地索引召回数 |
| `VISIT_INDEX_MIN_HITS` | 3 | 本地命中多少条才跳过 SearXNG |
| `HTTPS_PROXY` / `HTTP_PROXY` | — | 代理(解决国内网络) |

## 垂直自建索引(v0.4.0)

除了借 SearXNG 搜全网,visitproject 还能**自建垂直索引**——你指定几个网站,它爬取建本地索引,搜索时**先查本地索引,命中够多就不再依赖 Google/Bing**。数据你掌控,可定制,零外部依赖。

### 建索引

```bash
# 1. 编辑数据源配置(编辑 data/sources.json,加你想索引的网站)
# 2. 爬取 + 建索引
npm run index
# 或指定自定义配置
node dist/cli.js index --sources ./my-sources.json
```

数据源配置 `data/sources.json`:
```json
[
  {
    "name": "nodejs-docs",
    "url": "https://nodejs.org/api/",
    "maxDepth": 1,
    "maxPages": 50
  }
]
```

### 工作原理

```
search(query)
  ├─ 1. 查本地 MiniSearch 索引(自建,CJK 分词,毫秒级)
  │     命中 ≥ indexMinHits → 直接返回,跳过 SearXNG
  ├─ 2. 命中不足 → searchWithFallback(SearXNG → DDG → Tavily)
  └─ 合并去重
```

爬虫:BFS 同域遍历,礼貌限速(每页 500ms 间隔),限并发(3),用 Playwright + Readability 提正文。CJK tokenizer 让中文 query 能单字召回。

## License

MIT