# 博闻 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 多阶段构建 |