10 KiB
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 问题,但执行层还没有真正独立出来。
现在的实际链路是:
Query Route Gateway通过query_profile.needs_freshness + referenced_objects判定route_mode=externalAdaptiveRAGAnswerRetrievalMixin._maybe_fetch_live_external_freshness_context()直接调用_retrieve_live_external_context()_retrieve_live_external_context()再走_search_live_external_results(),本质是 DuckDuckGo HTML 搜索 + 抓网页正文 + chunk 打分- 若没有可靠
url/http(s)证据,就返回 External Freshness Guard
这条链的问题不是“少了天气 provider”,而是“没有通用外部执行层”。
另外,当前来源过滤还有一处明确 bug:
_filter_sources_for_route()会在 external 过滤结果为空时return filtered or list(sources or [])- 这意味着外部回答如果没筛出
url/system,会把原始feishu/doc/pmflowsources 兜回去 - 所以你截图里的
外部实时查询下出现来源摘要: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-planquery.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
外部实时查询主链改成:
Query Route GatewayExternal Query PlannerExternal Query GatewayExternal Provider RegistryMCPExternalAdapterbounded fallback live searchExternal Answer Verifierreply_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
这条链路里大模型只做三件事:
- 从自然语言问题里提炼
topic / entities / time_range / location / answer_contract - 归一化成受控
ExternalQueryPlan - 在证据已拿到后做 grounded answer composition
大模型不能做的事:
- 不能直接决定“我觉得这个网站可信就算”
- 不能在没证据时直接口头给外部结论
- 不能绕过 plan 直接调用任意工具
- 不能把内部历史当外部来源
External Provider Registry
建议新增统一 registry:
MCPExternalAdapterLiveSearchFallbackAdapter
选择规则:
- 优先走
MCPExternalAdapter MCP不可用、超时或返回空证据时,再走 bounded fallback- 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 问答统一走:
Query Route Gateway产出route_mode=externalExternal Query Planner生成ExternalQueryPlanExternal Query Gateway校验 plan schema、补充默认 limit/timeoutExternal Provider Registry选择 adapterMCPExternalAdapter执行并返回ExternalEvidenceBundle- 若证据不足,再走
LiveSearchFallbackAdapter External Answer Verifier校验来源与 freshness 合同reply_contract渲染外部实时查询:
Source Contract
外部回答必须遵守下面四条:
sources只允许 external adapter 返回的url/http(s)证据进入来源摘要- external 过滤必须 fail-closed,过滤空了就返回空,不得把原始
feishu/doc/pmflowsources 放回去 - 外部回答正文不得引用内部历史作为“来源依据”
来源摘要与 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__.pybackend/app/services/external_query_service/schemas.pybackend/app/services/external_query_service/gateway.pybackend/app/services/external_query_service/planner.pybackend/app/services/external_query_service/registry.pybackend/app/services/external_query_service/adapters/mcp.pybackend/app/services/external_query_service/adapters/live_search.py
运行时接入
backend/app/config/ai_prompts.pybackend/app/config/ai_prompts_catalog/backend/app/services/adaptive_rag_service/mixin_answer_retrieval.pybackend/app/services/adaptive_rag_service/mixin_query_route_gateway.pybackend/app/services/adaptive_rag_service/mixin_answer_payload.pybackend/app/services/adaptive_rag_service/mixin_deep_guard_rules.pybackend/app/services/adaptive_rag_service/mixin_conversation_utils_a_p1.py
回归测试
backend/tests/test_external_query_gateway.pybackend/tests/test_external_query_planner.pybackend/tests/test_external_query_source_contract.pybackend/tests/test_adaptive_rag_external_freshness_contract.py
文档基线
AGENTS.mddocs/architecture/RUNTIME_GUARDRAILS.mddocs/architecture/ARCHITECTURE_INVENTORY.mddocs/superpowers/specs/2026-04-15-external-query-gateway-design.md