208 lines
6.0 KiB
JavaScript
208 lines
6.0 KiB
JavaScript
// exe-engine/src/executor/multi-model.js
|
||
// EXE-Engine · 多模型执行器
|
||
// 支持 fastest / consensus / fallback 多策略执行
|
||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-003
|
||
// 版权:国作登字-2026-A-00037559
|
||
|
||
'use strict';
|
||
|
||
const { randomUUID } = require('crypto');
|
||
|
||
// consensus 策略:输出长度差异在此比例内视为一致
|
||
const CONSENSUS_LENGTH_THRESHOLD = 0.2;
|
||
|
||
/**
|
||
* 多模型执行器
|
||
*
|
||
* 本体论锚定:多模型 = 同时用多瓶墨水写字。
|
||
* fastest 赛跑取最快,consensus 共识取多数,fallback 逐瓶尝试。
|
||
*/
|
||
class MultiModelExecutor {
|
||
/**
|
||
* @param {object} deps
|
||
* @param {Map<string, BaseAdapter>} deps.adapters 适配器 Map
|
||
* @param {LoadBalancer} deps.loadBalancer 负载均衡器
|
||
*/
|
||
constructor(deps = {}) {
|
||
this._adapters = deps.adapters || new Map();
|
||
this._loadBalancer = deps.loadBalancer || null;
|
||
}
|
||
|
||
/**
|
||
* 执行多模型策略
|
||
* @param {object} request EXE 标准请求
|
||
* @param {string} [strategy='fastest'] 执行策略
|
||
* @returns {Promise<object>} EXE 标准响应 + _strategy + _modelsUsed
|
||
*/
|
||
async execute(request, strategy = 'fastest') {
|
||
const requestId = `exe-mm-${randomUUID().slice(0, 8)}`;
|
||
const startTime = Date.now();
|
||
|
||
const availableAdapters = this._getHealthyAdapters();
|
||
if (availableAdapters.length === 0) {
|
||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||
'NO_AVAILABLE_MODEL', '没有可用的模型适配器');
|
||
}
|
||
|
||
try {
|
||
switch (strategy) {
|
||
case 'fastest':
|
||
return await this._executeFastest(request, availableAdapters, requestId, startTime);
|
||
case 'consensus':
|
||
return await this._executeConsensus(request, availableAdapters, requestId, startTime);
|
||
case 'fallback':
|
||
return await this._executeFallback(request, availableAdapters, requestId, startTime);
|
||
default:
|
||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||
'INVALID_STRATEGY', `未知策略: ${strategy}`);
|
||
}
|
||
} catch (err) {
|
||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||
'EXECUTION_FAILED', err.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取可用模型名列表
|
||
* @returns {string[]}
|
||
*/
|
||
getAvailableModels() {
|
||
const models = [];
|
||
for (const [name, adapter] of this._adapters) {
|
||
if (!adapter.isInCooldown()) {
|
||
models.push(name);
|
||
}
|
||
}
|
||
return models;
|
||
}
|
||
|
||
// ── 策略实现 ──
|
||
|
||
/**
|
||
* fastest 策略:赛跑,返回最快结果
|
||
*/
|
||
async _executeFastest(request, adapters, requestId, startTime) {
|
||
const promises = adapters.map(adapter =>
|
||
adapter.execute(request).then(result => ({ adapter, result }))
|
||
);
|
||
|
||
const { adapter, result } = await Promise.any(promises);
|
||
|
||
return {
|
||
requestId,
|
||
model: result.model || adapter.name,
|
||
output: result.output,
|
||
usage: result.usage,
|
||
latency: Date.now() - startTime,
|
||
status: 'success',
|
||
_strategy: 'fastest',
|
||
_modelsUsed: [adapter.name]
|
||
};
|
||
}
|
||
|
||
/**
|
||
* consensus 策略:全部执行,多数同意则返回
|
||
* 简化判断:输出长度差异在 ±20% 内视为一致
|
||
*/
|
||
async _executeConsensus(request, adapters, requestId, startTime) {
|
||
const results = await Promise.allSettled(
|
||
adapters.map(adapter =>
|
||
adapter.execute(request).then(result => ({ adapter, result }))
|
||
)
|
||
);
|
||
|
||
const successful = results
|
||
.filter(r => r.status === 'fulfilled')
|
||
.map(r => r.value);
|
||
|
||
if (successful.length === 0) {
|
||
return this._buildErrorResponse(requestId, startTime, 'consensus',
|
||
'ALL_MODELS_FAILED', '所有模型执行失败');
|
||
}
|
||
|
||
// 用第一个成功结果作为基准
|
||
const baseline = successful[0];
|
||
const baseLen = (baseline.result.output || '').length || 1;
|
||
let agreements = 0;
|
||
|
||
for (const s of successful) {
|
||
const len = (s.result.output || '').length;
|
||
if (Math.abs(len - baseLen) / baseLen <= CONSENSUS_LENGTH_THRESHOLD) {
|
||
agreements++;
|
||
}
|
||
}
|
||
|
||
const consensus = agreements > successful.length / 2;
|
||
const chosen = baseline;
|
||
|
||
return {
|
||
requestId,
|
||
model: chosen.result.model || chosen.adapter.name,
|
||
output: chosen.result.output,
|
||
usage: chosen.result.usage,
|
||
latency: Date.now() - startTime,
|
||
status: 'success',
|
||
_strategy: 'consensus',
|
||
_consensus: consensus,
|
||
_modelsUsed: successful.map(s => s.adapter.name)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* fallback 策略:按优先级逐个尝试,返回第一个成功
|
||
*/
|
||
async _executeFallback(request, adapters, requestId, startTime) {
|
||
const errors = [];
|
||
|
||
for (const adapter of adapters) {
|
||
try {
|
||
const result = await adapter.execute(request);
|
||
return {
|
||
requestId,
|
||
model: result.model || adapter.name,
|
||
output: result.output,
|
||
usage: result.usage,
|
||
latency: Date.now() - startTime,
|
||
status: 'success',
|
||
_strategy: 'fallback',
|
||
_modelsUsed: [adapter.name],
|
||
_attemptsCount: errors.length + 1
|
||
};
|
||
} catch (err) {
|
||
errors.push({ model: adapter.name, error: err.message });
|
||
}
|
||
}
|
||
|
||
return this._buildErrorResponse(requestId, startTime, 'fallback',
|
||
'ALL_MODELS_FAILED', `所有模型执行失败: ${errors.map(e => e.model).join(', ')}`);
|
||
}
|
||
|
||
// ── 辅助方法 ──
|
||
|
||
_getHealthyAdapters() {
|
||
const healthy = [];
|
||
for (const [, adapter] of this._adapters) {
|
||
if (!adapter.isInCooldown()) {
|
||
healthy.push(adapter);
|
||
}
|
||
}
|
||
return healthy;
|
||
}
|
||
|
||
_buildErrorResponse(requestId, startTime, strategy, code, message) {
|
||
return {
|
||
requestId,
|
||
model: null,
|
||
output: null,
|
||
usage: { inputTokens: 0, outputTokens: 0, cost: 0, unit: 'CNY' },
|
||
latency: Date.now() - startTime,
|
||
status: 'error',
|
||
error: { code, message },
|
||
_strategy: strategy,
|
||
_modelsUsed: []
|
||
};
|
||
}
|
||
}
|
||
|
||
module.exports = MultiModelExecutor;
|