# 03 · 核心模块设计

> 代码级设计，Java 11 + Spring Boot。每个模块给出关键类的完整实现思路。

## 1. 本体服务（ontology/OntologyService）

负责加载本体 + 推理，是系统的语义核心。

```java
@Service
public class OntologyService {
    private final OntModel model;

    public OntologyService(@Value("${ontology.file:classpath:ontology/metric.ttl}") String ontologyFile) {
        // OWL 规则推理模型（支持等价类、传递性、子类）
        this.model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM_RULE_INF);
        this.model.read(ontologyFile);
    }

    /** 等价指标识别：传入"成交额"，返回规范指标（GMV） */
    public String resolveEquivalentMetric(String rawMetric) {
        String sparql = String.format(
            "PREFIX : <http://example.org/metric#> " +
            "PREFIX owl: <http://www.w3.org/2002/07/owl#> " +
            "SELECT ?m WHERE { :%s owl:equivalentClass ?m . ?m a :Metric }", rawMetric);
        try (QueryExecution qe = QueryExecutionFactory.create(sparql, model)) {
            ResultSet rs = qe.execSelect();
            if (rs.hasNext()) {
                String m = rs.next().getResource("m").getLocalName();
                return m.equals(rawMetric) ? rawMetric : m;
            }
        }
        return rawMetric; // 无等价关系，返回原词
    }

    /** 维度层级展开：传入"华东区"，返回子成员列表 */
    public List<String> expandRegion(String region) {
        String sparql = String.format(
            "PREFIX : <http://example.org/metric#> " +
            "SELECT ?r WHERE { ?r :partOf :%s }", region);
        List<String> result = new ArrayList<>();
        try (QueryExecution qe = QueryExecutionFactory.create(sparql, model)) {
            ResultSet rs = qe.execSelect();
            while (rs.hasNext()) result.add(rs.next().getResource("r").getLocalName());
        }
        return result; // 传递性推理自动展开所有层级
    }

    /** 指标推导：传入"客单价"，返回组成指标 */
    public List<String> getComponents(String metric) {
        String sparql = String.format(
            "PREFIX : <http://example.org/metric#> " +
            "SELECT ?c WHERE { :%s :composedOf ?c }", metric);
        // 返回组成指标列表
    }
}
```

## 2. SQL 生成（sql/SqlGenerator）

模板 + DSL，口径固化。LLM 不碰 SQL。

```java
@Component
public class SqlGenerator {
    private final Map<String, MetricDefinition> metrics; // 从 metrics.yaml 加载

    public SqlGenerator(MetricConfigLoader loader) {
        this.metrics = loader.load();
    }

    /** 生成 SQL：根据指标定义 + 维度展开 + 时间范围 */
    public String generateSql(String metric, List<String> regions, TimeRange time) {
        MetricDefinition def = metrics.get(metric);
        if (def == null) throw new MetricNotFoundException(metric);

        String sql;
        if (def.getType() == MetricType.COMPOSITE) {
            // 复合指标：DSL 拼子查询（客单价 = GMV / 订单数）
            sql = buildCompositeSql(def, regions, time);
        } else {
            // 原子/派生指标：模板 + 参数填充
            sql = buildSimpleSql(def, regions, time);
        }
        return sql;
    }

    private String buildSimpleSql(MetricDefinition def, List<String> regions, TimeRange time) {
        // 模板：SELECT {calc} FROM {table} WHERE {filter} AND dt BETWEEN ... {region_filter}
        String regionFilter = regions.isEmpty() ? "" :
            "AND region_id IN (SELECT region_id FROM dim_region WHERE region_name IN (" +
            regions.stream().map(r -> "'" + r + "'").collect(Collectors.joining(",")) + "))";
        return String.format(
            "SELECT %s AS value FROM %s WHERE %s AND dt BETWEEN '%s' AND '%s' %s",
            def.getCalcExpr(), def.getTable(), def.getFilterExpr(),
            time.start(), time.end(), regionFilter);
    }
}
```

## 3. 口径校验（sql/CaliberValidator）

从指标定义读口径，校验 SQL 是否满足，双保险。

```java
@Component
public class CaliberValidator {
    /** 校验生成的 SQL 是否包含指标口径的必需过滤条件 */
    public void validate(String metric, String sql, MetricDefinition def) {
        for (String clause : def.getRequiredFilters()) {
            // requiredFilters 例：["status='paid'", "refunded=false"]
            if (!sql.contains(clause)) {
                throw new CaliberViolationException(
                    String.format("指标 %s 的口径校验失败：SQL 缺少过滤条件 %s", metric, clause));
            }
        }
    }
}
```

## 4. LLM 服务（llm/LlmService）

直调 DeepSeek（OpenAI 兼容），两个职责：槽位提取 + 归因分析。

```java
@Service
public class LlmService {
    private final String apiKey;
    private final String baseUrl; // https://api.deepseek.com

    /** 槽位提取：结构化输出（JSON） */
    public QueryIntent parseIntent(String question) {
        String prompt = """
            你是指标查询的意图识别器。提取槽位，输出 JSON：
            {"intent":"metric_query","metric":"<指标原话>","region":"<地区原话>",
             "time":"<时间原话>","compare":"<对比方式>"}
            不要做语义对齐，只提取原话。
            问题：%s
            """.formatted(question);
        String json = call(prompt, true); // response_format=json_object
        return objectMapper.readValue(json, QueryIntent.class);
    }

    /** 归因分析：基于真实查询结果 */
    public String analyze(String question, String metric, Object result) {
        String prompt = """
            你是数据分析师。基于结果做归因分析，只解释数据反映的事实，不编造。
            问题：%s
            指标：%s
            结果：%s
            """.formatted(question, metric, result);
        return call(prompt, false);
    }
}
```

## 5. 编排服务（service/MetricQueryService）

串起完整链路。

```java
@Service
public class MetricQueryService {
    private final LlmService llmService;
    private final OntologyService ontologyService;
    private final SqlGenerator sqlGenerator;
    private final CaliberValidator caliberValidator;
    private final MaxComputeQueryService queryService;

    public QueryResponse query(String question) {
        // ① LLM 槽位提取
        QueryIntent intent = llmService.parseIntent(question);
        // ② 本体推理：等价识别（成交额 → GMV）
        String metric = ontologyService.resolveEquivalentMetric(intent.getMetric());
        // ③ 本体推理：维度展开（华东区 → 7省）
        List<String> regions = ontologyService.expandRegion(intent.getRegion());
        // ④ 时间解析
        TimeRange time = TimeParser.parse(intent.getTime());
        // ⑤ SQL 生成 + 口径校验
        MetricDefinition def = metricConfig.get(metric);
        String sql = sqlGenerator.generateSql(metric, regions, time);
        caliberValidator.validate(metric, sql, def);
        // ⑥ 查询执行
        List<Map<String, Object>> result = queryService.query(sql);
        // ⑦ LLM 归因
        String analysis = llmService.analyze(question, metric, result);
        return new QueryResponse(metric, result, analysis);
    }
}
```

## 6. 异常体系

```java
public class MetricNotFoundException extends RuntimeException {}      // 指标未定义
public class CaliberViolationException extends RuntimeException {}    // 口径校验失败
public class IntentParseException extends RuntimeException {}         // 槽位提取失败
```

## 7. 关键设计要点

- **本体服务是独立的语义层**：不依赖 SQL 生成和 LLM，只暴露推理接口
- **指标定义是配置驱动**：`MetricDefinition`（calcExpr/filterExpr/table）从 YAML 加载，SQL 生成只读配置
- **口径双保险**：模板固化（生成时）+ 公理校验（生成后）
- **LLM 职责最小化**：只做槽位提取和归因，语义对齐全走本体
