03 · 核心模块设计

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

1. 本体服务(ontology/OntologyService)

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

@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。

@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 是否满足,双保险。

@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 兼容),两个职责:槽位提取 + 归因分析。

@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)

串起完整链路。

@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. 异常体系

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

7. 关键设计要点