PMFlow/docs/superpowers/specs/2026-04-15-external-query-g...

10 KiB
Raw Permalink Blame History

External Query Gateway Design

Goal

为 non-plan query.answer 的外部实时查询建立一条独立、受控、可审计的执行主链:Query Route Gateway 只负责判到 route_mode=external,后续统一进入 External Query Gateway,由大模型协助产出结构化查询计划,再交给受控 adapter 拉取外部证据,最后由 verifier 校验来源合同与 freshness 合同后才能出站。

Why Now

当前外部查询已经能在路由层识别 freshness/public 问题,但执行层还没有真正独立出来。

现在的实际链路是:

  1. Query Route Gateway 通过 query_profile.needs_freshness + referenced_objects 判定 route_mode=external
  2. AdaptiveRAGAnswerRetrievalMixin._maybe_fetch_live_external_freshness_context() 直接调用 _retrieve_live_external_context()
  3. _retrieve_live_external_context() 再走 _search_live_external_results(),本质是 DuckDuckGo HTML 搜索 + 抓网页正文 + chunk 打分
  4. 若没有可靠 url/http(s) 证据,就返回 External Freshness Guard

这条链的问题不是“少了天气 provider”,而是“没有通用外部执行层”。

另外,当前来源过滤还有一处明确 bug:

  • _filter_sources_for_route() 会在 external 过滤结果为空时 return filtered or list(sources or [])
  • 这意味着外部回答如果没筛出 url/system,会把原始 feishu/doc/pmflow sources 兜回去
  • 所以你截图里的 外部实时查询 下出现 来源摘要:Feishu history 不是展示偶发,而是现有来源合同的 fail-open 缺口

Confirmed Baseline

  • 不做天气专用 provider,不按单个场景堆专用接入。
  • 外部查询的第一实现是通用 External Query Gateway,MCP 作为这层的第一个 adapter。
  • Query Route Gateway 继续保留;external 判定后不再直接调用 DuckDuckGo HTML 抓取。
  • 大模型只负责把问题整理成 ExternalQueryPlan,不直接自由回答外部事实。
  • 外部回答必须只引用 adapter 返回的外部证据;内部历史、项目图谱、Feishu history、PMFlow records 不得再混进外部来源摘要。
  • 现有 DuckDuckGo HTML 抓取降级为 bounded fallback,不再是主执行层。

Scope

In Scope

  • route_mode=external 的 non-plan query.answer
  • 结构化 ExternalQueryPlan
  • External Query Gateway 与 adapter 合同
  • MCP 作为第一外部 adapter
  • 外部来源合同、freshness 合同、answer verifier
  • 现有 DuckDuckGo HTML 检索的降级收口

Out Of Scope

  • internal DSL / semantic facts 主链
  • requirement-plan / followup / draft confirm
  • 针对天气、股价、体育等逐类 provider 拆分
  • 自由 browser agent、多步网页操作 agent
  • 自动把外部事实反写入内部库

Current State

1. 路由层已经能判 external

backend/app/services/adaptive_rag_service/mixin_query_route_gateway.py

  • _EXTERNAL_ROUTE_OBJECTS 已包含 public_model / public_company / weather / market_data / sports_event / news_topic
  • _run_external_route_probe() 在 needs_freshness 或命中 external object 时会先尝试 live external retrieval

2. 执行层仍然是 HTML 搜索抓取

backend/app/services/adaptive_rag_service/mixin_conversation_utils_a_p1.py

  • _search_live_external_results() 直接请求 https://html.duckduckgo.com/html/?q=...
  • _retrieve_live_external_context() 基于搜索结果抓取网页正文、切块、打分后返回 RetrievedItem

3. freshness guard 只看是否拿到外部 grounding

backend/app/services/adaptive_rag_service/mixin_deep_guard_rules.py

  • _has_external_grounding_sources() 目前只把 url/http(s) 视为外部 grounding
  • 没拿到这类证据,就返回“当前未接入实时外部数据...”

4. 来源过滤当前是 fail-open

backend/app/services/adaptive_rag_service/mixin_query_route_gateway.py

  • _EXTERNAL_ALLOWED_SOURCES = {"url", "system"}
  • 但 _filter_sources_for_route() 在过滤后为空时会退回原始 sources
  • 这会让 Feishu history、doc、pmflow 在 external 回答里重新出现

Design Decision

外部实时查询主链改成:

  1. Query Route Gateway
  2. External Query Planner
  3. External Query Gateway
  4. External Provider Registry
  5. MCPExternalAdapter
  6. bounded fallback live search
  7. External Answer Verifier
  8. reply_contract renderer

这里真正让大模型参与的是 External Query Planner,不是让模型自由联网,也不是让模型自由写查询脚本。

ExternalQueryPlan Contract

ExternalQueryPlan 建议统一为单一结构化对象:

字段 说明
plan_version 版本号
query_kind weather / news / company / model / market / generic_web / unknown
answer_contract direct_fact / summary_with_sources / compare_with_sources / clarify
freshness_required 是否要求最新信息
topic 用户真正要查的主题
subject_entities[] 人、公司、产品、地点等显式对象
location_hint 如“广州”
time_range_hint 如“未来5天”“截至今天”
preferred_domains[] 官方站点或高可信站点偏好
search_queries[] adapter 可直接执行的查询短语
result_limit 证据数量上限
requires_multi_source 是否要求多来源交叉
clarify_on_ambiguity 歧义时是否先澄清

示例:用户问“帮我看看未来 5 天的广州天气如何”。

{
  "plan_version": "v1",
  "query_kind": "weather",
  "answer_contract": "summary_with_sources",
  "freshness_required": true,
  "topic": "广州未来5天天气",
  "subject_entities": ["广州"],
  "location_hint": "广州",
  "time_range_hint": "未来5天",
  "preferred_domains": [],
  "search_queries": [
    "广州 未来5天天气",
    "广州 天气 5日预报"
  ],
  "result_limit": 3,
  "requires_multi_source": false,
  "clarify_on_ambiguity": false
}

Large-Model Assist Boundary

这条链路里大模型只做三件事:

  1. 从自然语言问题里提炼 topic / entities / time_range / location / answer_contract
  2. 归一化成受控 ExternalQueryPlan
  3. 在证据已拿到后做 grounded answer composition

大模型不能做的事:

  • 不能直接决定“我觉得这个网站可信就算”
  • 不能在没证据时直接口头给外部结论
  • 不能绕过 plan 直接调用任意工具
  • 不能把内部历史当外部来源

External Provider Registry

建议新增统一 registry:

  • MCPExternalAdapter
  • LiveSearchFallbackAdapter

选择规则:

  1. 优先走 MCPExternalAdapter
  2. MCP 不可用、超时或返回空证据时,再走 bounded fallback
  3. fallback 只负责补救,不负责承担主链语义

这里不做 WeatherProvider / StockProvider / NewsProvider 这种逐类 provider 拆分;query_kind 只是 plan 信号,不是 provider 数量膨胀入口。

MCPExternalAdapter Contract

输入:ExternalQueryPlan

输出:ExternalEvidenceBundle

字段 说明
source_type 固定外部类型,如 url / mcp_search
source_label 给用户展示的来源名
title 外部结果标题
target_url 原始 URL
snippet 可引用摘要
retrieved_at 抓取时间
freshness_score 时效性评分
authority_score 来源可信度评分
grounded_fields 当前证据支持了哪些字段
raw_metadata adapter 私有元信息

上层只认 ExternalEvidenceBundle,不认 MCP 的私有响应格式。

Mandatory Execution Flow

external 问答统一走:

  1. Query Route Gateway 产出 route_mode=external
  2. External Query Planner 生成 ExternalQueryPlan
  3. External Query Gateway 校验 plan schema、补充默认 limit/timeout
  4. External Provider Registry 选择 adapter
  5. MCPExternalAdapter 执行并返回 ExternalEvidenceBundle
  6. 若证据不足,再走 LiveSearchFallbackAdapter
  7. External Answer Verifier 校验来源与 freshness 合同
  8. reply_contract 渲染 外部实时查询:

Source Contract

外部回答必须遵守下面四条:

  1. sources 只允许 external adapter 返回的 url/http(s) 证据进入来源摘要
  2. external 过滤必须 fail-closed,过滤空了就返回空,不得把原始 feishu/doc/pmflow sources 放回去
  3. 外部回答正文不得引用内部历史作为“来源依据”
  4. 来源摘要 与 answer grounding 必须来自同一次 external execution

这意味着本轮必须把 _filter_sources_for_route() 从 fail-open 改成 fail-closed。

Failure Semantics

  • planner 结构化失败:显式 clarify_or_fail
  • MCP 超时或不可用:进入 bounded fallback
  • fallback 仍无外部 grounding:返回 freshness guard
  • verifier 发现来源合同违背:拒绝出站
  • 只有拿到外部 evidence bundle,才允许产出 grounded external answer

Rollout

Phase A

先确认并写入基线文档,不动运行时代码。

Phase B

落地 ExternalQueryPlan + External Query Gateway + MCPExternalAdapter,把 DuckDuckGo HTML 抓取降为 fallback。

Phase C

补 external answer verifier + fail-closed source contract,止住 Feishu history 串源。

Planned File Changes

新增模块

  • backend/app/services/external_query_service/__init__.py
  • backend/app/services/external_query_service/schemas.py
  • backend/app/services/external_query_service/gateway.py
  • backend/app/services/external_query_service/planner.py
  • backend/app/services/external_query_service/registry.py
  • backend/app/services/external_query_service/adapters/mcp.py
  • backend/app/services/external_query_service/adapters/live_search.py

运行时接入

  • backend/app/config/ai_prompts.py
  • backend/app/config/ai_prompts_catalog/
  • backend/app/services/adaptive_rag_service/mixin_answer_retrieval.py
  • backend/app/services/adaptive_rag_service/mixin_query_route_gateway.py
  • backend/app/services/adaptive_rag_service/mixin_answer_payload.py
  • backend/app/services/adaptive_rag_service/mixin_deep_guard_rules.py
  • backend/app/services/adaptive_rag_service/mixin_conversation_utils_a_p1.py

回归测试

  • backend/tests/test_external_query_gateway.py
  • backend/tests/test_external_query_planner.py
  • backend/tests/test_external_query_source_contract.py
  • backend/tests/test_adaptive_rag_external_freshness_contract.py

文档基线

  • AGENTS.md
  • docs/architecture/RUNTIME_GUARDRAILS.md
  • docs/architecture/ARCHITECTURE_INVENTORY.md
  • docs/superpowers/specs/2026-04-15-external-query-gateway-design.md