{"repo": "biggress/deepseekx", "files": {"docs/superpowers/specs/2026-05-23-search-orchestrator-design.md": {"e4978801c3566cde912cbba8b0dd249f3b996e20": {"t": 1786068572.6417894, "n": 7297, "h": "5740e03cc4143244", "c": "# DeepSeekX 联网搜索闭环设计\n\n> 日期:2026-05-23\n> 状态:Draft\n> 主题:方案 B - 两阶段调用 + 应用侧搜索编排\n\n---\n\n## 1. 背景\n\n当前 DeepSeekX 在 GUI 主链路中通过 `send_message_via_api` 调用 DeepSeek Chat Completions API,并依赖:\n\n- system prompt 中的“你拥有联网搜索能力”提示\n- 请求体中的 `web_search: {\"enable\": true}`\n\n来让模型自动完成实时信息检索。\n\n该方案存在明显不稳定性:\n\n- 模型有时直接返回最终答案\n- 模型有时直接回答自己无法联网\n- 模型有时只输出类似 `SOXL stock price today` 的中间协议文本,然后流程结束\n\n根因是:当前应用没有把“搜索”实现为本地可控的工具闭环,而只是依赖模型和上游 API 自行决定是否完成搜索。\n\n---\n\n## 2. 目标\n\n本方案的目标是将“联网搜索”改为应用侧可控流程,确保实时问题能够稳定走完:\n\n1. 判断是否需要搜索\n2. 生成搜索词\n3. 执行搜索\n4. 将搜索结果回填给模型\n5. 生成最终答案\n\n同时满足以下要求:\n\n- 默认对所有用户零配置可用\n- 提供一个内置免费搜索源\n- 在免费源失效时自动回退到第二层免费方案\n- 支持第三方搜索提供商,供用户在设置中切换\n- 不再将 `` 等中间协议暴露为最终回答\n\n---\n\n## 3. 非目标\n\n本方案第一版不解决以下问题:\n\n- 多轮自动搜索和复杂 agent 循环\n- 搜索结果可信度评分系统\n- 网页正文深度抓取与全文阅读\n- 第三方 provider 的全量接入\n- 搜索结果来源卡片、富文本来源引用等高级 UI\n\n第一版只保证:一次判定、一次搜索、一次综合、一次收敛。\n\n---\n\n## 4. 方案概述\n\n采用两阶段模型调用和应用侧搜索编排:\n\n### 阶段一:Planner\n\n模型只负责判断:\n\n- 是否需要联网搜索\n- 如果需要,应该搜索什么关键词\n\nPlanner 只输出结构化结果,不直接产出最终答案。\n\n### 阶段二:Search Orchestrator\n\n应用收到搜索请求后自行执行搜索,优先走默认免费源:\n\n1. `PublicSearchProvider`\n2. `WebScrapeProvider` 兜底\n\n### 阶段三:Synthesizer\n\n应用将裁剪后的搜索结果回填给模型,由模型生成最终中文答复。\n\n---\n\n## 5. 高层架构\n\n```text\nUser Question\n |\n v\nsend_message_via_api\n |\n +--> Planner Completion\n | |\n | +--> action = answer ------> Final Response\n | |\n | +--> action = search\n | |\n | v\n | Search Router\n | |\n | +----------+-----------+\n | | |\n | v v\n | PublicSearchProvider WebScrapeProvider\n | | |\n | +----------+-----------+\n | |\n | v\n +------------> Synthesis Completion\n |\n v\n Final Response\n |\n v\n Session Persistence\n```\n\n---\n\n## 6. 核心数据流\n\n### 6.1 用户输入\n\n示例:\n\n```text\n帮我查询下 SOXL 的今天的价格\n```\n\n### 6.2 Planner 输出\n\nPlanner 强制输出 JSON,仅允许两种形态。\n\n直接回答:\n\n```json\n{\"action\":\"answer\",\"content\":\"...\"}\n```\n\n请求搜索:\n\n```json\n{\"action\":\"search\",\"query\":\"SOXL stock price today\",\"reason\":\"需要实时价格\"}\n```\n\n### 6.3 搜索执行\n\n应用执行搜索并得到统一结构:\n\n```json\n{\n \"provider\": \"builtin_free\",\n \"query\": \"SOXL stock price today\",\n \"results\": [\n {\n \"title\": \"...\",\n \"url\": \"...\",\n \"snippet\": \"...\",\n \"source\": \"...\",\n \"score\": 0.92\n }\n ],\n \"status\": \"success\"\n}\n```\n\n### 6.4 综合回答\n\n应用将问题与搜索结果摘要一起发给 Synthesizer:\n\n```text\n[User Question]\n帮我查询下 SOXL 的今天的价格\n\n[Search Results]\n1. Title: ...\n URL: ...\n Snippet: ...\n2. Title: ...\n URL: ...\n Snippet: ...\n```\n\nSynthesizer 必须输出最终可读中文答复,禁止再次输出 ``。\n\n---\n\n## 7. 搜索提供商设计\n\n### 7.1 默认提供商:Built-in Free\n\n默认 provider 为 `builtin_free`,对所有用户零配置可用。\n\n它不是单一搜索实现,而是一个带回退策略的 provider:\n\n1. 优先:`PublicSearchProvider`\n2. 回退:`WebScrapeProvider`\n\n### 7.2 PublicSearchProvider\n\n职责:\n\n- 请求预置公共 `searxng` 风格实例\n- 获取通用网页搜索结果\n- 统一转成 `SearchResult[]`\n\n要求:\n\n- 短超时,避免界面长时间等待\n- 支持多个公共实例轮询或备用列表\n- 对失败、空结果、低质量结果给出明确状态\n\n### 7.3 WebScrapeProvider\n\n职责:\n\n- 当公共实例失败时抓取公开搜索结果页\n- 提取标题、链接、摘要\n- 作为兜底免费能力\n\n限制:\n\n- 解析逻辑脆弱\n- 不保证长期稳定\n- 第一版只做轻量结果抽取,不做复杂反爬对抗\n\n### 7.4 第三方 Provider\n\n支持用户选择第三方提供商,例如:\n\n- Tavily\n- Exa\n- SerpAPI\n- Firecrawl\n- Perplexity\n\n这些 provider 通过统一接口适配,在未配置 API Key 时不可激活,并自动回退为 `builtin_free`。\n\n---\n\n## 8. 配置设计\n\n### 8.1 应用级配置\n\n联网搜索默认作为应用级能力配置,放在现有设置体系中,而不是项目表中。\n\n建议新增字段:\n\n- `search_enabled`\n- `search_provider`\n- `search_timeout_ms`\n- `search_max_results`\n- `search_api_keys`\n\n其中:\n\n- `search_provider` 默认值为 `builtin_free`\n- `search_timeout_ms` 建议默认 8000-12000ms\n- `search_max_results` 建议默认 3-5\n\n### 8.2 项目级配置\n\n项目只保留轻量控制项:\n\n- `search_mode = inherit | disabled | force_enabled`\n\n含义:\n\n- `inherit`:使用全局搜索设置\n- `disabled`:该项目禁用联网搜索\n- `force_enabled`:即使全局关闭,该项目仍启用联网搜索\n\n---\n\n## 9. 会话与持久化设计\n\n### 9.1 用户可见消息\n\n最终 assistant 消息只保存最终答复文本。\n\n禁止将以下内容写为最终消息:\n\n- `...`\n- planner 原始 JSON\n- 原始网页抓取文本\n\n### 9.2 中间过程记录\n\n搜索步骤记录为 turn 内工具调用摘要,映射到现有 Turn 模型:\n\n- `tool_name = \"web_search\"`\n- `tool_input = query`\n- `tool_result.summary = provider + 结果数 + 错误摘要`\n\n这样可以复用现有 Turn UI 进行展示。\n\n### 9.3 调试信息\n\n默认不把完整搜索结果写入 session 主消息,避免 session 文件膨胀。\n\n如需调试,可保留轻量摘要:\n\n- 搜索 provider\n- 查询词\n- 结果数量\n- 失败原因\n\n---\n\n## 10. 提示词设计\n\n### 10.1 Planner Prompt\n\nPlanner 只做搜索决策,不做最终回答。\n\n要求:\n\n- 强制 JSON 输出\n- 不允许输出 `` 标签\n- 不允许输出自然语言前奏\n- 只允许 `answer` 或 `search`\n\n### 10.2 Synthesizer Prompt\n\nSynthesizer 根据搜索结果生成最终用户可见回答。\n\n要求:\n\n- 使用简体中文\n- 直接回答用户问题\n- 如果结果中有时效信息,优先标注“截至某时点”\n- 不允许再次发起搜索\n- 不允许输出 `` 或中间协议\n\n---\n\n## 11. 失败状态设计\n\n系统必须在单次请求中收敛,不允许悬挂或无限等待。\n\n定义以下失败状态:\n\n- `planner_failed`\n- `search_timeout`\n- `search_no_results`\n- `search_provider_unavailable`\n- `synthesis_failed`\n\n用户可见失败文案原则:\n\n- 简洁\n- 可理解\n- 可重试\n- 不暴露内部协议\n\n示例:\n\n- `这次联网搜索超时了,请稍后重试。`\n- `没有找到足够可靠的实时结果。`\n- `当前搜索服务暂时不可用,已回退为普通回答。`\n\n---\n\n## 12. 模块拆分\n\n建议按以下方式拆分:\n\n### Rust 后端\n\n- `src-tauri/src/search/mod.rs`\n - 搜索模块入口\n - 定义 trait 和通用类型\n- `src-tauri/src/search/router.rs`\n - provider 选择与回退逻辑\n- `src-tauri/src/search/settings.rs`\n - 搜索配置读取与默认值\n- `src-tauri/src/search/providers/builtin_free.rs`\n - 免费双回退 provider\n- `src-tauri/src/search/providers/public_instance.rs`\n - 公共实例 provider\n- `src-tauri/src/search/providers/web_scrape.rs`\n - 网页抓取 provider\n- `src-tauri/src/search/providers/third_party.rs`\n - 第三方 provider 适配\n- `src-tauri/src/api.rs`\n - 增加 `planner_completion()` 和 `synthesis_completion()`\n- `src-tauri/src/lib.rs`\n - 将 `send_message_via_api()` 改造为 orchestrator\n- `src-tauri/src/session.rs`\n - 搜索步骤写入 turn 结构\n\n### 前端\n\n- `src/components/SettingsDialog.tsx`\n - 新增搜索 provider 设置项\n- `src/components/ProjectSettings.tsx`\n - 新增 `search_mode`\n- `src/api.ts`\n - 新增搜索设置读写接口\n- `src/types.ts`\n - 新增 provider、settings、mode 类型\n\n---\n\n## 13. 实施顺序\n\n### Phase 1:后端最小闭环\n\n- 增加 `SearchPlan`、`SearchResult`、`SearchProvider`\n- 实现 `planner_completion()`\n- 实现 `synthesis_completion()`\n- 在 `send_message_via_api()` 中串起两阶段调用\n\n### Phase 2:默认免费搜索源\n\n- 实现 `PublicSearchProvider`\n- 实现 `WebScrapeProvider`\n- 封装 `builtin_free`\n\n### Phase 3:设置系统\n\n- 应用级搜索设置\n- 项目级 `search_mode`\n- 默认值与配置回退规则\n\n### Phase 4:会话和 UI\n\n- 搜索步骤写入 session\n- Turn 视图展示 `web_search`\n- 设置页增加 provider 选择\n\n### Phase 5:第三方 Provider\n\n- 先接入 Tavily\n- 再按统一接口逐步补其他 provider\n\n---\n\n## 14. 测试策略\n\n### 14.1 单元测试\n\n- Planner JSON 解析\n- `关键词` 兼容解析\n- provider 回退逻辑\n- 搜索结果裁剪和摘要格式\n\n### 14.2 集成测试\n\n- Planner 返回 `answer`\n- Planner 返回 `search`\n- 公共实例失败时自动走抓取兜底\n- Synthesizer 失败时返回确定性错误消息\n\n### 14.3 回归测试\n\n- `帮我查询下 SOXL 的今天的价格`\n- `今天上海天气怎么样`\n- `OpenAI 今天有什么新闻`\n- `帮我解释 Go 的 interface`\n\n回归验证重点:\n\n- 实时问题能触发搜索\n- 非实时问题不误触发搜索\n- 最终消息不再出现 ``\n- session 中存在 `web_search` 工具调用摘要\n\n---\n\n## 15. 风险与取舍\n\n### 风险\n\n- 公共搜索实例稳定性不可控\n- 网页抓取受目标页面结构变化影响\n- 两阶段调用增加时延和 token 成本\n- 第三方 provider 接入后测试矩阵增大\n\n### 取舍\n\n- 选择两阶段调用,是以额外成本换确定性\n- 选择免费双回退,是以更高实现复杂度换零配置体验\n- 第三方 provider 延后接入,是为了优先修复现有“卡死在 ``”的问题\n\n---\n\n## 16. 验收标准\n\n- 输入“帮我查询下 SOXL 的今天的价格”时,不再出现 `...` 作为最终回复\n- 默认安装后无需额外配置即可触发联网搜索\n- 公共实例失败时自动走抓取兜底\n- 第三方 provider 配置后可替换默认 provider\n- Turn 视图可显示一次 `web_search` 调用及摘要\n- 普通问题不强制进入两阶段调用\n- 任一失败场景都能在一次请求内返回确定性结果\n\n---\n\n## 17. 结论\n\n本方案通过“Planner -> Search Router -> Synthesizer”的应用侧闭环,将联网搜索从不稳定的提示词能力,升级为 DeepSeekX 可控、可回退、可扩展的核心能力。\n\n该方案优先解决当前用户最明显的问题:\n\n- 不再卡在 `...`\n- 实时问题具备更高成功率\n- 默认对所有用户零配置可用\n\n在此基础上,再逐步开放第三方搜索 provider,实现“默认免费可用 + 高级用户可扩展”的整体体验。\n"}}}}