跳至正文
浏览文档
执行引擎

扩展执行引擎

实现引擎接口,把自己的数据与执行环境接入 AVA。

在自己的应用或包中实现引擎,通过 SDK 注册,再按名称选择即可。自定义引擎注册仅适用于 SDK;CLI 只接受内置引擎名。

注册引擎

typescript
import { AVA, registerEngine } from '@antv/ava';
import { SalesEngine } from './sales-engine';
 
registerEngine('sales', SalesEngine);
 
const ava = new AVA({
  llm: { model },
  engine: { type: 'sales' },
});

这里的 model 是已配置的 AI SDK 模型,完整配置见下方示例。引擎实现保留在你的应用中,只需在 source() 前注册,无需修改 AVA 源码。

注册没有返回值,只作用于当前进程已加载的 SDK 模块,不会注册到独立的 CLI 进程。未注册的名称会在 source() 尝试创建引擎时失败。

每次调用 source(),AVA 都会创建新的引擎实例:替换前释放旧引擎,load() 失败时释放失败实例,ava.dispose() 则释放当前实例。请在 load() 中建立连接,以便加载失败后也能清理。

浏览器 SDK 也从 @antv/ava/browser 导出同样的 registerEngine API,但提供的引擎及其依赖必须支持浏览器运行环境。请使用已导出该 API 的版本;注册表尚未对外开放的旧版需要先升级。

引擎接口约定

实现公开导出的 AnalysisEngine 接口。AVA 的生命周期协调器会调用以下成员:

成员输入参数返回值与行为
构造函数llmConfig: LLMConfig,options?: Record<string, unknown>接收模型配置,以及 type 之外的引擎字段。
language只读属性QueryLanguage,供分析提示词使用。
load(config)DataSourceConfigPromise<void>;准备执行所需的数据与连接。
schema()无Promise<Schema>;发现已加载数据源的结构。
profile(schema, options?)必填 Schema,可选 ProfileOptions可选方法,返回 Promise<Profile>,基于传入结构计算统计信息。
getDSL(query, dataModel)必填问题字符串与 DataModel返回 Promise<TranslationResult>,包含 dsl 和可选的 usage。
execute(dsl, options?)必填 DSL 字符串;可选 ExecutionOptionsPromise<ExecutionResult<T>>;负责验证输入并执行结果限制。
dispose()无Promise<void>;释放连接、文件及其他资源。

AVA 管理 schema 与 profile 缓存。load() 准备资源,schema() 按需发现结构;execute() 必须在未提前发现结构时也能使用。Profile 根据传入 schema 计算,查询生成使用传入的数据模型。结构发现失败后,引擎仍应允许重试或执行查询。

迁移旧引擎时,将结构发现从 load() 移到 schema(),让 load() 返回 Promise<void>,补齐必填的 schema 与 data-model 参数,并让 getDSL() 返回 { dsl, usage? }。即使只完成部分加载,dispose() 也应能安全调用。

QueryLanguage 字段类型必填含义
namestring是提示词中使用的语言或方言名称。
fencestring是Markdown 代码块语言,例如 sql 或 python。
instructionsstring否可用变量及结果约定。

DataModel 组合结构元数据、可选统计信息和语义注释。分析、翻译或推荐问题需要它时,AVA 会从缓存的 Schema、Profile、Semantic 组装。引擎直接接收模型,不需要额外的上下文包装对象。

DataModel 字段类型必填含义
modelsModel[]否模型与列,可包含指标和属性。
relationshipsRelationship[]否模型间的连接关系。
viewsView[]否命名查询。
propertiesProperties否数据集注释。

示例:销售指标引擎

假设应用提供两份销售报表:收入合计和分地区收入。这个引擎将问题转换为简短的 JSON 命令,再在内存中计算报表,不执行模型生成的 JavaScript 或 SQL。

示例复用已有的内存 JSON 数据源结构。远程引擎可以将连接设置放在引擎选项中,由 load() 解释受支持的 DataSourceConfig 结构。

实现引擎

在 你的应用 中创建 sales-engine.ts。示例使用 ai 调用模型,只依赖 @antv/ava 的公开导出。

typescript
import { generateText, type LanguageModel } from 'ai';
import { executionResult } from '@antv/ava';
import type {
  AnalysisEngine,
  TranslationResult,
  DataModel,
  DataSourceConfig,
  ExecutionOptions,
  ExecutionResult,
  LLMConfig,
  Schema,
} from '@antv/ava';
 
type Sale = { region: string; revenue: number };
 
export class SalesEngine implements AnalysisEngine {
  readonly language = {
    name: 'Sales metrics JSON',
    fence: 'json',
    instructions:
      'Return exactly {"metric":"total_revenue"} or {"metric":"revenue_by_region"}. No other keys or operations are supported.',
  };
  private sales: Sale[] | null = null;
 
  private readonly model: LanguageModel;
 
  constructor(private readonly llm: LLMConfig) {
    if (typeof llm.model === 'string') {
      throw new Error('SalesEngine expects a configured AI SDK model object.');
    }
    this.model = llm.model;
  }
 
  async load(config: DataSourceConfig): Promise<void> {
    await this.dispose();
    if (config.type !== 'json' || !Array.isArray(config.options.data)) {
      throw new Error('SalesEngine requires an inline JSON array.');
    }
    this.sales = config.options.data.map((row) => {
      if (
        !row ||
        typeof row !== 'object' ||
        typeof row.region !== 'string' ||
        typeof row.revenue !== 'number' ||
        !Number.isFinite(row.revenue)
      ) {
        throw new Error('Each sale requires a region string and finite revenue number.');
      }
      return { region: row.region, revenue: row.revenue };
    });
  }
 
  async schema(): Promise<Schema> {
    if (!this.sales) throw new Error('Load a source first.');
    return {
      models: [
        {
          name: 'sales',
          tableReference: { table: 'sales' },
          columns: [
            { name: 'region', type: 'string' },
            { name: 'revenue', type: 'number' },
          ],
        },
      ],
    };
  }
 
  async getDSL(query: string, _dataModel: DataModel): Promise<TranslationResult> {
    if (!this.sales) throw new Error('Load a source first.');
    const { text, usage } = await generateText({
      model: this.model,
      maxRetries: this.llm.maxRetries ?? 3,
      system: `Convert a question into a sales metric. ${this.language.instructions}
Return raw JSON without Markdown. If neither metric answers the question, return {"metric":"unsupported"}.`,
      prompt: query,
    });
    return { dsl: text.trim(), usage };
  }
 
  async execute<T = Record<string, unknown>>(dsl: string, options?: ExecutionOptions): Promise<ExecutionResult<T>> {
    if (!this.sales) throw new Error('Load a source first.');
    const command = JSON.parse(dsl);
    if (
      !command ||
      Array.isArray(command) ||
      Object.keys(command).length !== 1 ||
      !['total_revenue', 'revenue_by_region'].includes(command.metric)
    ) {
      throw new Error('Expected one supported metric and no additional keys.');
    }
 
    let rows: Record<string, unknown>[];
    if (command.metric === 'total_revenue') {
      rows = [{ total_revenue: this.sales.reduce((sum, row) => sum + row.revenue, 0) }];
    } else {
      const totals = new Map<string, number>();
      for (const row of this.sales) {
        totals.set(row.region, (totals.get(row.region) ?? 0) + row.revenue);
      }
      rows = Array.from(totals, ([region, total_revenue]) => ({ region, total_revenue }));
    }
    if (rows.some((row) => !Number.isFinite(row.total_revenue))) {
      throw new Error('Revenue total exceeds the numeric range.');
    }
    const schema =
      command.metric === 'total_revenue'
        ? [{ name: 'total_revenue', type: 'number' }]
        : [
            { name: 'region', type: 'string' },
            { name: 'total_revenue', type: 'number' },
          ];
    return executionResult(rows as T[], schema, options);
  }
 
  async dispose(): Promise<void> {
    this.sales = null;
  }
}

即使通过 query() 直接调用,execute() 也会验证命令。公开导出的 executionResult(rows, schema, options?) 辅助函数负责应用 AVA 的行数与字节限制,详见下方参数。

这个引擎的命令集合固定,因此 getDSL() 接收但不使用数据模型。它未实现 profile(),analyze()、suggest()、query() 仍可使用,但 ava.profile() 会报告不支持统计。示例将所有输入行保留在内存中;结果限制约束的是返回数据,并不限制输入规模或聚合计算量。

注册并使用

应用需要安装 @antv/ava、ai 和 @ai-sdk/openai。配置 OPENAI_API_KEY、OPENAI_MODEL 后,使用项目的 TypeScript 执行器运行。应用启动时注册一次类,再按名称选择;AVA 会将配置好的模型传给每个新实例:

typescript
import { createOpenAI } from '@ai-sdk/openai';
import { AVA, registerEngine } from '@antv/ava';
import { SalesEngine } from './sales-engine';
 
registerEngine('sales', SalesEngine);
 
const model = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
}).chat(process.env.OPENAI_MODEL || 'gpt-4o-mini');
 
const ava = new AVA({
  llm: { model },
  engine: { type: 'sales' },
});
 
try {
  await ava.source({
    type: 'json',
    options: {
      data: [
        { region: 'East', revenue: 120 },
        { region: 'West', revenue: 90 },
        { region: 'East', revenue: 80 },
      ],
    },
  });
 
  const answer = await ava.analyze('What is total revenue by region?', {
    strategy: { type: 'direct', maxRetries: 0 },
    includeSummary: false,
  });
  console.log(answer.sql); // Expected command: {"metric":"revenue_by_region"}
  console.table(answer.data);
 
  // Execute a known command directly, without calling the model.
  const total = await ava.query('{"metric":"total_revenue"}');
  console.table(total.data); // [{ total_revenue: 290 }]
} finally {
  await ava.dispose();
}

示例未做统计计算,并关闭分析重试与总结。answer.sql 保存 JSON 命令,answer.data 包含计算结果,answer.text 为空。已经知道要执行的命令时,直接使用 query() 即可。

扩展 API 参考

registerEngine

registerEngine(type: string, engineClass: EngineClass): void

两个 SDK 入口均导出此方法。请在加载数据源之前,注册应用中实现的引擎。

参数类型必填含义
typestring是引擎名称。重复注册会替换后续加载所用的构造函数。
engineClassnew (llmConfig: LLMConfig, options?: Record<string, unknown>) => AnalysisEngine是引擎类。AVA 将模型配置与其余引擎选项传给每个新实例。

注册仅影响当前加载的 SDK 模块。

executionResult 辅助函数

executionResult<T>(rows: T[], schema: QueryColumn[], options?: ExecutionOptions, upstreamTruncatedBy?: 'maxRows' | 'maxResultBytes'): ExecutionResult<T>

Node.js SDK 为自定义引擎提供的同步辅助函数,从 @antv/ava 导入。

参数类型必填含义与默认值
rowsT[]是引擎生成的结果行。
schemaQueryColumn[]是输出列,每列包含 name: string 和可选的 type: string。
optionsExecutionOptions否{},使用执行限制。
upstreamTruncatedBy'maxRows' 或 'maxResultBytes'否保留上游执行器已报告的截断原因;本地限制进一步截断时则以本地原因为准。

返回受限后的 data、schema,以及完整结果的 rowCount 或部分结果的截断信息。单个结果字段超过 1 MiB 时抛错。它只约束已经生成的结果行,不限制查询执行过程或输入的内存使用。