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

275 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 天的广州天气如何”。
```json
{
"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`