# 博闻 Bowen · MCP Web Search
博闻是魂镜的 MCP 联网搜索服务,支持多搜索引擎、多 API 密钥智能轮询、两级缓存和双传输模式。
> 文化命名出自《礼记·中庸》「博闻强识而让」,与辰正、一炁等同属魂镜文化命名体系。工程路径保留 `mcp/web-search`。
---
## 架构概览
```mermaid
flowchart TD
subgraph L1["① 传输层"]
ST["stdio
stdin/stdout JSON-RPC"]
SSE["HTTP SSE
/sse + /messages"]
HTTP["简化 HTTP
/tools/list + /tools/call"]
end
subgraph L2["② 核心处理器"]
H["Handler
传输层共享"]
C["Cache Manager
L1 LRU → L2 Redis"]
end
subgraph L3["③ 负载均衡"]
LB["Load Balancer"]
KP["KeyPool
EWMA 动态权重"]
CB["Circuit Breaker
closed / open / half-open"]
end
subgraph L4["④ 搜索提供商"]
T[Tavily]
B[Bing]
V[Brave]
end
subgraph L5["⑤ 可观测性"]
M["Prometheus Metrics
7 指标"]
S["/status + /health"]
A["管理 API
enable / disable / clear"]
end
ST --> H
SSE --> H
HTTP --> H
H --> C
H --> LB
LB --> KP
KP --> CB
LB --> T
LB --> B
LB --> V
H --> M
H --> S
H --> A
```
### 核心特性
| 特性 | 说明 |
| ------ | ------ |
| **多 Provider** | Tavily、Bing、Brave 三家搜索,统一响应格式 |
| **多 Key 轮询** | 每 Provider 可配多个 API Key,按 EWMA 动态权重(延迟×成功率)自动排序 |
| **熔断器** | 每个 Key 独立熔断(closed → open → half-open),失败 key 自动隔离并恢复探测 |
| **故障转移** | 一个 Key 失败自动换 Key,一个 Provider 耗尽自动切其他 Provider |
| **双传输** | HTTP SSE(远程 Client 直连)+ stdio(子进程集成),由 `--port` / `--stdio` CLI 标志决定 |
| **两级缓存** | L1 内存 LRU(毫秒级)+ L2 Redis(可选,跨实例共享),L2 命中自动升温到 L1 |
| **配置热重载** | fsnotify 监听 YAML 变更,动态调整 provider 启用/禁用、缓存 TTL、重试次数,无需重启 |
| **管理 API** | 运行时启用/禁用 provider、清空缓存、查询密钥状态 |
| **SearchMode** | `first`(首个成功返回,默认低延迟)+ `merge`(并发搜所有引擎,URL 去重排序) |
| **可观测性** | 暗色风格 HTML 监控面板(`:9090`)、Prometheus 指标(`:9090/metrics`)、结构化彩色日志 |
---
## 快速开始
### 前置条件
- Go 1.22+
- 至少一个搜索 API Key(Tavily / Bing / Brave)
- (可选)Redis 用于二级缓存
#### 环境变量快速配置
```bash
# 必填——搜索 API Key(逗号分隔支持多 key 轮询)
export TAVILY_API_KEYS="tvly-key1,tvly-key2"
export BRAVE_API_KEYS="brave-key1"
```
完整环境变量见项目根目录 `.env.example` 中的「博闻」段。
### 启动
```bash
# HTTP SSE 模式(供 MCP Client 远程连接,或供灵台注册为工具)
go run ./cmd/server --port 8080
# stdio 模式(供 Claude Desktop / Cursor 等 MCP Client 子进程集成)
go run ./cmd/server --stdio
# 指定 YAML 配置文件(可选,不传则自动探测 config/mcp/web-search.yaml)
go run ./cmd/server --config ../../config/mcp/web-search.yaml
```
### 验证
```bash
# 存活探针
curl http://localhost:8080/health
# → {"status":"ok"}
# 系统状态
curl http://localhost:8080/status
# → {"version":"2.0.0","uptime":"...","providers":[...],"cache":{...}}
# 工具列表(MCP tools/list)
curl -X POST http://localhost:8080/tools/list
# → {"tools":[{"name":"web_search",...}]}
# 搜索调用
curl -X POST http://localhost:8080/tools/call \
-H "Content-Type: application/json" \
-d '{"tool":"web_search","input":{"query":"hello world"}}'
# 监控面板(浏览器打开)
open http://localhost:9090
```
---
## 端口规划
| 端口 | 用途 | 说明 |
| ------ | ------ | ------ |
| **8080** | MCP 服务端口 | tools/list、tools/call、SSE、管理 API、/health、/status |
| **9090** | 监控端口 | HTML 仪表盘(`/`)、Prometheus 指标(`/metrics`)、存活探针(`/health`) |
## MCP 服务端点(端口 8080)
### MCP 协议端点
| 路径 | 方法 | 说明 |
| ------ | ------ | ------ |
| `POST /tools/list` | POST | 列出可用工具(web_search),含 inputSchema |
| `POST /tools/call` | POST | 调用 web_search 工具执行搜索 |
| `GET /sse` | GET | SSE 事件流(MCP 标准握手) |
| `POST /messages` | POST | MCP JSON-RPC 消息(与 /sse 配对使用) |
| `GET /` | GET | 端点指引(纯文本) |
### web_search 工具
**inputSchema**:
| 参数 | 类型 | 默认 | 说明 |
| ------ | ------ | ------ | ------ |
| `query` | string | (必填) | 搜索关键词 |
| `max_results` | integer | 5 | 最大返回结果数(1–20) |
| `mode` | string | `"first"` | 搜索模式:`first`(首个成功返回) / `merge`(并发搜所有引擎后合并去重) |
| `providers` | string[] | 全部 | 优先使用的搜索引擎 |
**响应**(CallToolResult):
```json
{
"content": [{
"type": "json",
"text": "{\"provider\":\"tavily\",\"query\":\"...\",\"results\":[...],\"total_count\":5,\"cached\":false}"
}]
}
```
### 管理端点
| 路径 | 方法 | 说明 |
| ------ | ------ | ------ |
| `GET /health` | GET | 存活探针 → `{"status":"ok"}` |
| `GET /status` | GET | 系统状态:provider 列表、活跃密钥数、缓存命中率、uptime |
| `GET /admin/keys` | GET | 各 provider 启用状态 |
| `PUT /admin/provider/{name}/enable` | PUT | 启用指定 provider |
| `PUT /admin/provider/{name}/disable` | PUT | 禁用指定 provider |
| `POST /admin/cache/clear` | POST | 清空缓存 |
## 监控面板(端口 9090)
### HTML 仪表盘 `GET /`
暗色风格的可视化面板,展示:
- **四张概览卡片**:Provider 数、活跃密钥数、缓存命中率(绿/黄/红三档)、缓存条目总数
- **Provider 状态表格**:🟢/🔴 状态指示灯、名称、启用/禁用开关、活跃密钥计数
- **缓存统计表格**:L1(内存 LRU)命中、L2(Redis)命中、未命中、综合命中率
- **自动刷新**:每 30 秒自动刷新
- **导航链接**:一键跳转 /status、/admin/keys、/health
### Prometheus 原始指标 `GET /metrics`
| 指标名 | 类型 | 标签 | 说明 |
| -------- | ------ | ------ | ------ |
| `bowen_search_total` | Counter | `provider` | 搜索请求总数 |
| `bowen_search_duration_seconds` | Histogram | — | 搜索耗时分布 |
| `bowen_search_errors_total` | Counter | `reason` | 搜索错误数 |
| `bowen_cache_hits_total` | Counter | — | 缓存命中总数 |
| `bowen_cache_hits_l2_total` | Counter | — | Redis 二级缓存命中数 |
| `bowen_cache_misses_total` | Counter | — | 缓存未命中数 |
| `bowen_active_keys` | Gauge | `provider` | 各 provider 活跃密钥数 |
所有监控端点也可在端口 9090 访问,与 MCP 服务端口独立。
---
## 配置文件
非敏感配置放在项目根目录 `config/mcp/web-search.yaml`(从 `config/mcp/web-search.example.yaml` 复制)。
不传 `--config` 时自动按项目惯例探测:开发模式探测 `../../config/mcp/web-search.yaml`,容器内探测 `/etc/bowen/config.yaml`。
```yaml
server:
port: 8080 # SSE 模式监听端口
read_timeout: 30s # HTTP 读取超时
write_timeout: 30s # HTTP 写入超时
providers:
tavily:
enabled: true # 是否启用(热重载:支持动态变更)
timeout: 10s
max_results: 10
health_check:
interval: 60s
failure_threshold: 3
bing:
enabled: true
timeout: 10s
max_results: 20
brave:
enabled: true
timeout: 10s
max_results: 10
balancer:
strategy: weighted_round_robin
retry_count: 2 # 热重载:支持动态变更
circuit_breaker:
enabled: true
failure_threshold: 5
cooldown: 30s
cache:
enabled: true
ttl: 5m # 热重载:支持动态变更
max_items: 1000
redis:
enabled: false # 需要自行部署 Redis
addr: 127.0.0.1:6379
password: ""
db: 0
metrics:
enabled: true
port: 9090
path: /metrics
```
**配置优先级**:环境变量 > YAML 文件 > 内置默认值
**热重载**:指定 `--config` 后,修改 YAML 文件自动生效(500ms 防抖),支持动态变更:
| 配置项 | 变更后自动生效 |
| -------- | --------------- |
| `providers.{name}.enabled` | 运行时启用/禁用搜索源 |
| `cache.ttl` | 更新新缓存条目的过期时间 |
| `balancer.retry_count` | 更新重试次数 |
---
## 传输模式
博闻支持两种 MCP 传输模式,**不在配置中写死**,由启动 CLI 参数决定:
| 模式 | 启动方式 | 适用场景 |
| ------ | --------- | --------- |
| **SSE** | `--port 8080` | 灵台后端注册(简化 HTTP 端点 `/tools/list` + `/tools/call`)、远程 MCP Client 直连(`/sse` + `/messages`) |
| **Stdio** | `--stdio` | Claude Desktop / Cursor 等 MCP Client 以子进程方式启动,通过 stdin/stdout 走 JSON-RPC 通信 |
两种模式下核心业务逻辑完全共享(`internal/mcp/handler.go`),只有传输层不同。
---
## 环境变量
所有敏感信息通过环境变量注入(参见 `.env.example` 博闻段):
| 变量 | 必填 | 说明 |
| ------ | ------ | ------ |
| `TAVILY_API_KEYS` | 是 | Tavily 搜索 Key,多 key 逗号分隔 |
| `BRAVE_API_KEYS` | 是 | Brave Search Key |
| `MCP_WEB_SEARCH_REDIS_ENABLED` | 否 | Redis 二级缓存开关 |
| `MCP_WEB_SEARCH_REDIS_ADDR` | 否 | Redis 地址(默认 `127.0.0.1:6379`) |
| `MCP_WEB_SEARCH_REDIS_PASSWORD` | 否 | Redis 密码 |
| `MCP_WEB_SEARCH_REDIS_DB` | 否 | Redis 数据库编号(默认 0) |
---
## 开发
```bash
# 运行所有测试
make test
# 编译
make build # → bin/bowen-server
# 本地 SSE 模式
make run-sse
# 本地 stdio 模式
make run-stdio
# 代码检查
make lint
```
开发环境日志自动使用彩色输出(ANSI 颜色代码):
- `INFO` → 绿色
- `WARN` → 黄色
- `ERROR` → 红色
- `DEBUG` → 灰色
生产环境(`ENV=production`)自动切换为 JSON 格式,便于日志采集系统采集。
### 项目结构
```text
mcp/web-search/
├── cmd/server/main.go # 入口(--port / --stdio)
├── internal/
│ ├── config/
│ │ ├── config.go # YAML + 环境变量配置加载
│ │ ├── yaml.go # YAML 解析
│ │ └── reloader.go # 配置热重载(fsnotify)
│ ├── logger/logger.go # slog 双模式(dev 彩色 / prod JSON)
│ ├── models/types.go # 核心数据类型
│ ├── providers/ # 搜索提供商适配
│ │ ├── provider.go # SearchProvider 接口
│ │ ├── tavily.go / bing.go / brave.go
│ │ └── registry.go # 注册中心
│ ├── balancer/ # 负载均衡
│ │ ├── keypool.go # 密钥池(EWMA)
│ │ ├── circuitbreaker.go # 熔断器
│ │ └── balancer.go # LoadBalancer
│ ├── cache/cache.go # 两级缓存
│ ├── mcp/ # MCP 协议实现
│ │ ├── handler.go # 核心处理逻辑
│ │ ├── stdio.go # stdin/stdout JSON-RPC
│ │ ├── sse.go # HTTP SSE 传输
│ │ ├── http.go # 简化 HTTP 端点
│ │ ├── status.go # /health + /status
│ │ ├── admin.go # 管理 API
│ │ └── monitor.go # HTML 监控面板
│ └── metrics/metrics.go # Prometheus 指标
├── Makefile
├── go.mod / go.sum
```
---
## Docker
```bash
# 构建镜像
docker build -t bowen-web-search -f ../../docker/Dockerfile.web-search .
# 运行
docker run \
-e TAVILY_API_KEYS=tvly-xxx \
-e BRAVE_API_KEYS=brave-xxx \
-p 8080:8080 -p 9090:9090 \
bowen-web-search
```
Docker 镜像可通过魂镜的 `docker/docker-compose.app.yml` 编排启动。
---
## 集成到灵台
后端通过 MCP 注册 API 自动接入博闻:
```bash
# 注册博闻为 MCP Server
curl -X POST http://localhost:8000/api/v1/tools/mcp-servers \
-H "Content-Type: application/json" \
-d '{
"name": "bowen-web-search",
"endpoint_url": "http://localhost:8080",
"status": "enabled"
}'
```
注册后灵台会拉取工具清单,Agent 即可在对话中直接调用 `web_search` 工具。
---
## 搜索模式对比
| 模式 | 延迟 | 召回 | 使用场景 |
| ------ | ------ | ------ | --------- |
| `first`(默认) | 低(首个成功即返回) | 单 provider | 快速查天气、新闻、百科等简单查询 |
| `merge` | 中(等最慢 provider) | 高(多源合并去重) | 研究、对比多个来源、需要全面 coverage |
---
## 技术栈
| 类别 | 选型 |
| ------ | ------ |
| 语言 | Go 1.22+ |
| Web | 标准库 `net/http` |
| 日志 | 标准库 `log/slog` + 自定义彩色 Handler |
| 配置 | `gopkg.in/yaml.v3` + 环境变量 |
| 内存缓存 | `hashicorp/golang-lru/v2` |
| Redis | `redis/go-redis/v9`(可选) |
| 监控 | `prometheus/client_golang` |
| 热重载 | `fsnotify` |
| 测试 | 标准库 `testing` |
| 容器化 | Docker 多阶段构建 |