V2|混合检索与 Schema Linking
实现状态:已完成。本页记录真实代码入口、调用关系、实测结果和面试复述骨架。
这一期学会什么:表字段召回、概念链接和 Join 正确性是三种不同问题;它们需要分层评测,不能把所有错误归给大模型。
本期链路
text
Intent → Domain → Hybrid Table Search → Rerank → Column Search → Schema/Value Linking → Join Graph → SQL真实代码入口
| 模块 | 文件 | 核心类/函数 |
|---|---|---|
| 意图解析 | server/domain/intent.py | QueryIntent、TimeRange、MetricMention |
| 域路由 | server/domain/router.py | DomainRouter.route(question, now) |
| 检索仓储 | server/search/repository.py | SearchRepository(Protocol)、Candidate、InMemorySearchRepository |
| OpenSearch BM25 | server/search/opensearch.py | OpenSearchRetriever.search_schema() |
| Milvus Dense | server/search/milvus.py | MilvusRetriever.search_schema() |
| 混合检索 | server/search/hybrid.py | HybridSearchRepository、_merge_and_fuse() |
| RRF 融合 | server/search/fusion.py | RRFFusion.fuse(bm25, dense, top_k) |
| Reranker | server/search/reranker.py | BGEReranker、FakeReranker、_ensure_required_fields() |
| 两级召回 | server/search/retrieval.py | TwoLevelRetriever.retrieve()、SchemaContext |
| Schema Linking | server/linking/schema.py | SchemaLinker.link()、SchemaLink、LinkType |
| Value Linking | server/linking/value.py | ValueLinker.link_values()、TypedValue、_ALIAS_DICT |
| Join Graph | server/linking/join_graph.py | JoinGraph、TableRelationship(ORM)、JoinPath |
| 检索评测 | server/evaluation/retrieval.py | RetrievalEvaluator.run()、RetrievalReport.summary() |
| 检索工作台 API | server/api/retrieval_workbench.py | POST /api/v1/admin/retrieval/inspect |
| 数据库迁移 | migrations/versions/a1b2c3d4e5f6_v2_s05_表关系定义.py | table_relationship 表 |
调用顺序(完整请求链路)
text
用户问题
↓
DomainRouter.route(question, now=date.today())
→ QueryIntent(含 primary_domain、time_range、filters、metric_mentions)
↓
TwoLevelRetriever.retrieve(question, intent, datasource_id)
├── Level 1: SearchRepository.search_schema(object_types=["table"])
│ ├── OpenSearchRetriever → BM25 top_k
│ └── MilvusRetriever → Dense top_k
│ → HybridSearchRepository._merge_and_fuse()
│ → RRFFusion.fuse(bm25, dense) [去重 + RRF 分]
│ → Reranker.rerank(question, candidates, top_n) [精排]
│ → table_candidates
│
├── Level 2: SearchRepository.search_schema(object_types=["column"], filter by tables)
│ → 同上流程
│ → _budget_trim() [token 预算裁剪,保护主外键]
│ → column_candidates
│
→ SchemaContext(tables + columns + estimated_tokens)
↓
SchemaLinker.link(question, intent, schema_context)
→ SchemaLinkResult(links: metric→指标ID, filter→列, dimension→表)
↓
ValueLinker.link_values(intent, schema_context, repository)
→ ValueLinkResult(typed_values: column_id + value + evidence)
↓
JoinGraph.complete_schema(selected_tables)
→ JoinGraphResult(complete_tables 含桥接表,paths,pre_aggregation_required)
↓
SchemaContext.to_prompt_schema() → SQL Generator Context测试入口
bash
# 意图解析 + 域路由(V2-S01 全部验收场景)
pytest tests/test_v2_intent_and_routing.py -v
# 检索、融合、降级、Reranker、两级召回、Token 预算(V2-S02/S03)
pytest tests/test_v2_search_and_fusion.py -v
# Schema Linking、Value Linking 消歧、Join Graph(V2-S04/S05)
pytest tests/test_v2_linking.py -v
# 全部测试(含 V0/V1 回归)
pytest tests/ -q实测结果(2026-09-10,本地 Python 3.12.9,无外部服务):
271 passed in 5.72s所有 V2 新增测试 88 个全部通过,V0/V1 共 183 个历史测试无回退。
三个典型案例
案例一:检索成功——"华东地区今年销售额同比"
text
输入:华东地区今年销售额同比增长多少
DomainRouter.route() 输出:
primary_domain = "sales"
domain_candidates = {"sales": 1.0, "product": 0.5, "store": 0.5}
time_range = TimeRange(start=2026-01-01, end=2026-12-31, label="今年",
comparison_start=2025-01-01, comparison_end=2025-12-31)
filters = [FilterCondition(concept="区域", value="华东")]
metric_mentions = [MetricMention(text="销售额", resolved_metric_id="net_sales")]
intent_type = COMPARISON
RRF 融合:
BM25 命中 fact_order_item(rank=1)、dim_region(rank=2)
Dense 命中 fact_order_item(rank=1)、fact_order(rank=2)
RRF 结果:fact_order_item(0.0328)、fact_order(0.0161)、dim_region(0.0161)
SchemaLinker 输出:
"销售额" → metric:net_sales(evidence=resolved_by_domain_router, confidence=0.95)
"区域" → dim_region.region_name(evidence=column_name_match, confidence=0.85)
ValueLinker 输出:
"华东" → TypedValue(column=dim_region.region_name, value="East China", evidence=alias_dict)
JoinGraph.complete_schema(["fact_order_item", "dim_region"]) 补齐:
complete_tables = ["fact_order_item", "dim_region", "dim_city", "dim_store", "fact_order"]案例二:误召回——"营收怎么样"
text
输入:今年营收怎么样
DomainRouter 输出:
"营收" 是歧义词(_AMBIGUOUS_METRIC_TEXTS 中)
unresolved = ["营收"]
requires_clarification = True
clarification_prompt = '"营收"可能对应多种指标,您希望查询哪个?例如:销售额、毛利率、客户数?'
失败原因:用户没有指定具体指标,系统正确拒绝了静默猜测。
修复方法:用户明确"净销售额"或"收入(财务域)"后,系统可继续处理。案例三:扇出失败——多事实表直接 JOIN
text
输入(假设):订单金额减去退款金额的净收入
JoinGraph 检测:
fact_order 和 fact_refund 都以 "fact_" 开头
check_fan_out() 输出:
"多事实表 ['fact_order', 'fact_refund'] 直接 JOIN 可能导致金额翻倍,
需要预聚合或使用 CTE 分别聚合后再关联。"
requires_clarification = True
失败原因:两张事实表的粒度不同(订单维度 vs 退款申请维度),直接 JOIN 会产生重复计数。
正确做法:各自用 SUM() 聚合后再按 order_id 关联,或使用 CTE。设计取舍记录
| 决策 | 选择 | 理由 |
|---|---|---|
| Join Graph 存储 | PostgreSQL + 内存图 | 不引入 Neo4j;关系数量(<1000条)不需要图数据库 |
| 时间解析 | 规则匹配(不用 LLM) | 时间表达式需要确定性可重放;规则可测试;V3 可替换 |
| 域路由 | 关键词得分(不用 LLM) | V2 够用;V3/V4 可升级为向量分类;节省 LLM token |
| Value Linking 别名字典 | 静态 Python 字典 | V3 迁移到 business_terms 表;V2 优先跑通链路 |
| BM25+Dense 降级 | 单路超时→另一路降级 | 保证服务可用性;宁可召回率下降,也不要返回错误 |
| Token 预算裁剪 | 保护主外键,裁剪低分普通列 | JOIN 键缺失会导致 SQL 直接出错;普通列缺失只影响精度 |
| Reranker | FakeReranker 离线测试,BGEReranker 生产 | 避免 CI 依赖 GPU/ML 框架 |
评测指标(实测门槛)
目前测试基于内存 Fake 仓储,无真实 OpenSearch/Milvus 数据,以下指标为待测状态。
| 指标 | 目标 | 当前状态 |
|---|---|---|
| Domain Accuracy | ≥98% | 待接真实索引后实测 |
| Table Recall@10 | ≥97% | 待测(列召回按最终上下文 macro 口径) |
| Column Recall | ≥95% | 待测 |
| Value Linking Acc | - | 待测 |
| Join Path Acc | - | 待测 |
评测命令(接真实数据后运行):
python
from server.evaluation.retrieval import RetrievalEvaluator, compare_v1_v2
evaluator = RetrievalEvaluator(datasource_id=1, k=10)
report = await evaluator.run(questions, retriever, intent_parser, config="hybrid+rerank")
print(report.summary())
print(report.meets_v2_targets())面试复述骨架
"检索决定候选,链接决定概念对应哪个对象,Join Graph 决定合法关系路径。我们用分层指标定位失败,避免把所有错误归给大模型。"
具体做法:
域路由先于 Schema 检索:不让全局搜索浪费在无关域的表,同时注入
now保证时间可重放。Hybrid Search + RRF + Reranker:BM25 擅长精确编码词(
SKU000392),Dense 擅长语义改写(苹果手机),两路融合后 Reranker 精排,主外键强制保留防止 SQL 缺少 JOIN 键。Value Linking 的层次:精确样例值 > 别名字典 > BM25/fuzzy > 向量回退,找不到证据绝不捏造,而是请求澄清。
Join Graph 内存实现:关系定义存 PostgreSQL,运行时加载为邻接表,BFS 找最短路径补齐桥接表;多事实直接 JOIN 触发扇出警告,不因"外键存在"就自动放行。
分层评测:召回率、链接精度、JOIN 路径准确率独立计算;最终 SQL 错误可以追溯到具体的失败层(是召回缺失还是链接错误还是 SQL 生成问题)。