# Semantic Facts Read Model Design ## Goal 为内部查询建立一层独立的“语义事实读模型”,把项目、信号、文档、配置等原始对象中的非结构化语义抽成可检索、可计数、可解释的事实条目。后续内部查询在 `Query DSL Plan` 之后优先查这层读模型,再决定是否回退到受控文本检索。 ## Why This Is Needed 方案 1 能止住“问 existence 却答 total count”这类合同漂移,但它解决不了“数据库根本没有结构化字段”的问题。 像下面这些问法,本质上都需要一个语义层: - 有没有 A 股项目 - 我们系统里有没有开票系统 - 哪些项目属于视频方向 - 这个项目是不是偏运营工具 如果底层只有 `name/title/description/status`,那执行器再受控,也只能做文本猜测。要真正提准,必须提前把“项目主题、行业标签、别名、用途、对象关系”这类语义抽出来,沉淀成 facts。 ## Confirmed Baseline - 方案 2 是内部查询的第二道必经步骤。 - 当语义事实数据可用时,内部查询必须优先走 `semantic_facts`。 - `semantic_facts` 是只读辅助模型,不是业务写入真源。 - facts 的生成必须异步完成,禁止在用户主链里同步抽取长耗时语义。 - facts 必须带 `confidence`、`evidence`、`extract_version` 和租户边界,避免“抽出来了但说不清依据”。 ## Design Decision 这里不做 `project_semantic_facts` 这种单对象专用表,而是直接设计成跨对象通用表 `semantic_facts`。 原因很简单: - 用户真实问法不只围绕项目 - `signal / doc / config / run` 也会进入内部问答 - 一开始就用通用读模型,后续不会因为对象扩大再重做一轮 schema ## Data Model ### Table 1: `semantic_facts` 建议字段如下: | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `tenant_id` | 多租户边界 | | `entity_type` | `project/signal/doc/config/run/...` | | `entity_id` | 对应对象主键 | | `fact_namespace` | 事实命名空间,如 `domain/theme/capability/alias/status` | | `fact_key` | 事实键,如 `market_theme`、`industry`、`aliases` | | `fact_value_text` | 原始值 | | `fact_value_norm` | 归一化值,便于精确匹配 | | `value_type` | `keyword/text/number/date/enum/json` | | `confidence` | 抽取置信度 | | `evidence_source_type` | 证据来源对象类型 | | `evidence_source_id` | 证据来源对象 id | | `evidence_excerpt` | 短证据摘要 | | `extract_version` | 抽取器版本 | | `freshness_at` | 事实对应的源数据时间 | | `is_active` | 是否有效 | | `created_at` | 创建时间 | | `updated_at` | 更新时间 | ### Table 2: `semantic_fact_extraction_runs` 建议补一张轻量 run/audit 表: | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `tenant_id` | 多租户边界 | | `entity_type` | 被抽取对象类型 | | `entity_id` | 被抽取对象 id | | `trigger_type` | `create/update/backfill/manual_retry` | | `status` | `queued/running/completed/failed` | | `input_fingerprint` | 输入快照指纹,用于幂等 | | `facts_written` | 本次写入事实数 | | `failure_reason` | 失败原因 | | `extract_version` | 抽取器版本 | | `started_at` | 开始时间 | | `finished_at` | 结束时间 | ## Fact Examples 示例一:项目描述里出现“面向证券投研团队,聚焦 A 股公告解析”。 可能抽出: - `fact_namespace=theme` - `fact_key=market_theme` - `fact_value_text=A股` - `fact_value_norm=a_share` 示例二:项目标题叫“开票助手”,描述里出现“面向财务运营,自动生成增值税发票草稿”。 可能抽出: - `fact_key=business_domain` - `fact_value_norm=invoicing` - `fact_key=aliases` - `fact_value_text=开票系统` ## Async Extraction Flow 语义事实抽取链建议统一为: 1. 项目、信号、文档、功能目录、配置快照等源对象完成事务提交。 2. 提交后只入队一个 `semantic fact extraction` 事件,不在主事务里直接跑 LLM。 3. worker 读取事件,加载最小源上下文与证据片段。 4. LLM 只负责结构化抽取事实,不负责最终回答。 5. 服务端做 normalize、去重、merge、失效旧 facts。 6. 抽取结果写入 `semantic_facts`,并把 run 状态写入 `semantic_fact_extraction_runs`。 7. 后续内部查询命中该对象时,优先从 `semantic_facts` 读取。 ## Query Read Path 方案 2 与方案 1 的衔接方式是固定的: 1. `Query Route Gateway` 决定本轮走内部查询。 2. `Operation Contract` 先锁定回答合同与对象 mentions。 3. `Target Resolver` 在 project / feature / config / doc 等候选间做受控仲裁。 4. `Query DSL Plan` 再基于已解析目标产出 `query_type/entity_type/filters/answer_contract`。 5. `Bounded Executor` 先查 `semantic_facts`。 6. 若 facts 命中且置信度满足阈值,直接形成 existence/count/list/status 的证据基础。 7. 若 facts 不足,再回退到受控文本检索、图谱检索或配置快照读取。 8. `Answer Verifier` 再检查回答与证据是否一致。 这意味着后续内部查询的“真正默认路径”会变成: `Operation Contract -> Target Resolver -> DSL Plan -> facts-first -> bounded fallback -> verifier` ## Confidence And Evidence Rules - `confidence` 只用于排序和阈值判断,不允许把低置信猜测包装成确定性事实 - facts 用于回答时,必须能回指 `evidence_source_type/evidence_source_id/evidence_excerpt` - 同一个 `entity_type/entity_id/fact_key/fact_value_norm` 要支持幂等 upsert - 源对象删除、重写或语义失效后,facts 必须支持失效标记,不允许永久悬挂 ## Guardrails - `semantic_facts` 只用于内部只读查询和解释,不作为写操作真源 - 不允许让 LLM 在 facts 缺失时自由脑补 - 不允许把 facts 命中为空解释成“对象不存在”,除非计划明确允许且 fallback 也已执行 - 不允许绕过 `Query DSL Plan` 直接裸查 facts 表 ## Backfill Strategy 首轮建议: 1. 对现有 `projects` 做一次租户内 backfill 2. 对与项目强绑定的 `signals / docs / feature catalog` 做增量补抽 3. 在运行稳定后,再扩到 `config / run / integration snapshot` ## 计划改动文件清单(基线确认后) ### 方案 2 数据模型与迁移 - `backend/alembic/versions/_add_semantic_facts.py` - `backend/app/models/semantic_fact.py` - `backend/app/models/semantic_fact_extraction_run.py` - `backend/app/models/__init__.py` ### 方案 2 服务与异步链路 - `backend/app/services/semantic_fact_service/service.py` - `backend/app/services/semantic_fact_service/mixin_extract.py` - `backend/app/services/semantic_fact_service/mixin_query.py` - `backend/app/services/project_knowledge_graph_service/mixin_event_queue.py` - `backend/app/services/project_knowledge_graph_service/service.py` - `backend/app/services/adaptive_rag_service/mixin_answer_retrieval.py` - `backend/app/services/adaptive_rag_service/mixin_retrieval_router_db.py` - `backend/app/services/adaptive_rag_service/mixin_fast_answer_project_main.py` ### 方案 2 回归测试 - `backend/tests/test_project_knowledge_graph_event_queue_contract.py` - `backend/tests/test_adaptive_rag_mainline_contract.py` - `backend/tests/test_ai_pmo_ingress_mainline.py` - `backend/tests/test_runtime_structured_semantics_contract.py` - `backend/tests/test_semantic_fact_service.py` ### 本轮文档基线 - `AGENTS.md` - `CLAUDE.md` - `docs/architecture/RUNTIME_GUARDRAILS.md` - `docs/architecture/DATAFLOW.md` - `docs/architecture/ARCHITECTURE_INVENTORY.md` - `docs/superpowers/specs/2026-04-15-internal-query-dsl-plan-design.md` - `docs/superpowers/specs/2026-04-15-semantic-facts-read-model-design.md`