guanghulab/server/age-os/mcp-server/tools/finetune-engine-ops.js

973 lines
32 KiB
JavaScript
Raw Normal View History

/**
*
* 模块H · 开源模型微调引擎 MCP 工具
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 冰朔D62核心指令: 接入开源模型用COS桶训练数据直接做微调
* 本质: 同一份TCS结构化数据两种用途 RAG + 微调
*
* 架构理念:
* 现有RAG训练 用API调用商业模型人格体"脑子"在COS桶里
* 开源模型微调 用同一份数据"脑子"直接装进开源模型
* 二者并行运行 微调模型优先 不可用时降级回API模型
*
* 工具清单:
* finetuneExportDataset 导出TCS语料为微调JSONL格式
* finetuneSubmitJob 提交微调任务到DeepSeek/Qwen
* finetuneCheckStatus 查询微调任务进度
* finetuneRegisterModel 注册微调完成的模型
* finetuneListModels 列出已注册的微调模型
* finetuneCallModel 调用微调模型进行推理
* finetuneCompareModels A/B测试微调 vs 基座模型
* finetuneGetCostEstimate 估算微调成本
*/
'use strict';
const https = require('https');
const crypto = require('crypto');
const cos = require('../cos');
// ─── 微调 API 配置 ───
const FINETUNE_PROVIDERS = {
deepseek: {
host: 'api.deepseek.com',
createPath: '/fine_tuning/jobs',
statusPath: '/fine_tuning/jobs/',
uploadPath: '/files',
inferencePath: '/v1/chat/completions',
defaultModel: 'deepseek-chat',
keyEnv: 'ZY_DEEPSEEK_API_KEY',
label: 'DeepSeek微调'
},
qwen: {
host: 'dashscope.aliyuncs.com',
createPath: '/api/v1/fine-tunes',
statusPath: '/api/v1/fine-tunes/',
uploadPath: '/api/v1/files',
inferencePath: '/compatible-mode/v1/chat/completions',
defaultModel: 'qwen-max',
keyEnv: 'ZY_QIANWEN_API_KEY',
label: 'Qwen/DashScope微调'
}
};
// ─── 推理降级配置与training-agent-ops.js同源 ───
const LLM_CONFIGS = {
'deepseek-chat': {
host: 'api.deepseek.com',
path: '/v1/chat/completions',
model: 'deepseek-chat',
keyEnv: 'ZY_DEEPSEEK_API_KEY',
purpose: '微调基座·推理降级'
},
'qwen-max': {
host: 'dashscope.aliyuncs.com',
path: '/compatible-mode/v1/chat/completions',
model: 'qwen-max',
keyEnv: 'ZY_QIANWEN_API_KEY',
purpose: '微调基座·推理降级'
}
};
// ─── 成本估算参数2026-04 参考价实际以provider当月公告为准 ───
const COST_PER_1K_TOKENS = {
deepseek: 0.014, // 约 ¥0.014 / 1K tokens训练· 2026-04 参考
qwen: 0.020 // 约 ¥0.020 / 1K tokens训练· 2026-04 参考
};
// ─── 常量 ───
const DEFAULT_BUCKET = 'cold';
const MAX_SAMPLES_DEFAULT = 500;
const FINETUNE_TIMEOUT = 60000;
// ═══════════════════════════════════════════════════════════
// 工具实现
// ═══════════════════════════════════════════════════════════
/**
* finetuneExportDataset 导出TCS语料为微调JSONL格式
*
* 将COS桶中的TCS结构化语料转换为 instruction/input/output 三元组
* JSONL格式直接用于提交到微调API
*
* input:
* persona_id: string 人格体ID
* corpus_bucket: string 语料桶默认cold
* corpus_prefix: string 语料路径前缀
* output_format: string 输出格式默认jsonl
* max_samples: number 最大样本数
*/
async function finetuneExportDataset(input) {
const { persona_id, corpus_bucket, corpus_prefix, output_format, max_samples } = input;
if (!persona_id) throw new Error('缺少 persona_id');
const bucket = corpus_bucket || DEFAULT_BUCKET;
const prefix = corpus_prefix || 'tcs-structured/';
const format = output_format || 'jsonl';
const maxSamples = max_samples || MAX_SAMPLES_DEFAULT;
const datasetId = `ds-${persona_id}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
// 扫描TCS语料文件
let corpusFiles = [];
try {
const result = await cos.list(bucket, prefix, 500);
corpusFiles = result.files.filter(f => f.key.endsWith('.tcs.json') || f.key.endsWith('.json'));
} catch {
throw new Error(`无法读取语料桶 ${bucket}/${prefix}`);
}
if (corpusFiles.length === 0) {
throw new Error(`语料桶 ${bucket}/${prefix} 中未找到TCS语料文件`);
}
// 逐文件读取并转换为JSONL三元组
const jsonlLines = [];
let filesProcessed = 0;
for (const file of corpusFiles) {
if (jsonlLines.length >= maxSamples) break;
try {
const raw = await cos.read(bucket, file.key);
const corpus = JSON.parse(raw.content);
const entries = corpus.entries || (Array.isArray(corpus) ? corpus : [corpus]);
for (const entry of entries) {
if (jsonlLines.length >= maxSamples) break;
const triple = convertEntryToTriple(persona_id, corpus.corpus_type, entry);
if (triple) {
jsonlLines.push(JSON.stringify(triple));
}
}
filesProcessed++;
} catch {
// 跳过无法解析的文件
}
}
if (jsonlLines.length === 0) {
throw new Error('未能从语料中生成任何训练样本');
}
// 写入JSONL到COS
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileKey = `finetune-datasets/${persona_id}/${timestamp}.${format}`;
const jsonlContent = jsonlLines.join('\n') + '\n';
await cos.write(bucket, fileKey, jsonlContent, 'application/jsonl');
return {
dataset_id: datasetId,
file_key: fileKey,
sample_count: jsonlLines.length,
format,
files_scanned: corpusFiles.length,
files_processed: filesProcessed,
bucket,
created_at: new Date().toISOString()
};
}
/**
* finetuneSubmitJob 提交微调任务到DeepSeek或Qwen API
*
* 从COS读取JSONL数据集上传到provider然后创建微调任务
*
* input:
* persona_id: string 人格体ID
* dataset_key: string COS中JSONL文件路径
* provider: string 微调提供方deepseek / qwen
* base_model: string 基座模型可选默认取provider默认值
* hyperparams: object 超参数可选
*/
async function finetuneSubmitJob(input) {
const { persona_id, dataset_key, provider, base_model, hyperparams } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!dataset_key) throw new Error('缺少 dataset_key');
const providerKey = (provider || 'deepseek').toLowerCase();
const providerConfig = FINETUNE_PROVIDERS[providerKey];
if (!providerConfig) throw new Error(`不支持的微调提供方: ${providerKey},仅支持 deepseek / qwen`);
const apiKey = process.env[providerConfig.keyEnv];
if (!apiKey) throw new Error(`缺少API密钥环境变量 ${providerConfig.keyEnv}`);
const jobId = `ft-${persona_id}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
const model = base_model || providerConfig.defaultModel;
// 从COS读取JSONL数据集
const bucket = DEFAULT_BUCKET;
const raw = await cos.read(bucket, dataset_key);
const datasetContent = raw.content;
// 上传训练文件到provider
const fileId = await uploadTrainingFile(providerConfig, apiKey, datasetContent, providerKey);
// 创建微调任务
const jobResult = await createFinetuneJob(providerConfig, apiKey, model, fileId, hyperparams, providerKey);
// 保存任务元数据到COS
const jobMeta = {
job_id: jobId,
provider_job_id: jobResult.provider_job_id,
persona_id,
provider: providerKey,
base_model: model,
dataset_key,
file_id: fileId,
hyperparams: hyperparams || {},
status: jobResult.status || 'pending',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
await cos.write(bucket, `finetune-jobs/${persona_id}/${jobId}.json`,
JSON.stringify(jobMeta, null, 2), 'application/json');
return {
job_id: jobId,
provider_job_id: jobResult.provider_job_id,
provider: providerKey,
status: jobMeta.status,
base_model: model,
estimated_time: jobResult.estimated_time || '未知,通常需要数小时'
};
}
/**
* finetuneCheckStatus 查询微调任务进度
*
* input:
* persona_id: string 人格体ID
* job_id: string 任务ID
* provider: string 微调提供方
*/
async function finetuneCheckStatus(input) {
const { persona_id, job_id, provider } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!job_id) throw new Error('缺少 job_id');
const bucket = DEFAULT_BUCKET;
// 读取任务元数据
let jobMeta;
try {
const raw = await cos.read(bucket, `finetune-jobs/${persona_id}/${job_id}.json`);
jobMeta = JSON.parse(raw.content);
} catch {
throw new Error(`未找到微调任务: ${job_id}`);
}
const providerKey = provider || jobMeta.provider;
const providerConfig = FINETUNE_PROVIDERS[providerKey];
if (!providerConfig) throw new Error(`不支持的微调提供方: ${providerKey}`);
const apiKey = process.env[providerConfig.keyEnv];
if (!apiKey) throw new Error(`缺少API密钥环境变量 ${providerConfig.keyEnv}`);
// 查询provider API获取最新状态
const providerJobId = jobMeta.provider_job_id;
let statusResult;
try {
statusResult = await queryJobStatus(providerConfig, apiKey, providerJobId, providerKey);
} catch (err) {
return {
job_id,
provider: providerKey,
status: jobMeta.status,
progress: null,
metrics: null,
error: `查询provider状态失败: ${err.message}`,
last_known_update: jobMeta.updated_at
};
}
// 更新COS中的任务元数据
jobMeta.status = statusResult.status;
jobMeta.updated_at = new Date().toISOString();
if (statusResult.fine_tuned_model) {
jobMeta.fine_tuned_model = statusResult.fine_tuned_model;
}
if (statusResult.metrics) {
jobMeta.metrics = statusResult.metrics;
}
try {
await cos.write(bucket, `finetune-jobs/${persona_id}/${job_id}.json`,
JSON.stringify(jobMeta, null, 2), 'application/json');
} catch { /* ignore */ }
return {
job_id,
provider_job_id: providerJobId,
provider: providerKey,
status: statusResult.status,
progress: statusResult.progress || null,
metrics: statusResult.metrics || null,
fine_tuned_model: statusResult.fine_tuned_model || null,
updated_at: jobMeta.updated_at
};
}
/**
* finetuneRegisterModel 注册微调完成的模型
*
* input:
* persona_id: string 人格体ID
* job_id: string 关联的微调任务ID
* model_endpoint: string 模型推理端点provider返回的fine_tuned_model名称
* model_name: string 本地注册名称
* provider: string 微调提供方
* description: string 模型描述
*/
async function finetuneRegisterModel(input) {
const { persona_id, job_id, model_endpoint, model_name, provider, description } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!model_endpoint) throw new Error('缺少 model_endpoint');
if (!model_name) throw new Error('缺少 model_name');
const providerKey = (provider || 'deepseek').toLowerCase();
const providerConfig = FINETUNE_PROVIDERS[providerKey];
if (!providerConfig) throw new Error(`不支持的微调提供方: ${providerKey}`);
const modelId = `mdl-${persona_id}-${crypto.randomBytes(4).toString('hex')}`;
const now = new Date().toISOString();
const modelConfig = {
model_id: modelId,
persona_id,
model_name,
model_endpoint,
provider: providerKey,
provider_host: providerConfig.host,
inference_path: providerConfig.inferencePath,
key_env: providerConfig.keyEnv,
job_id: job_id || null,
description: description || `${persona_id} 微调模型`,
status: 'active',
created_at: now,
updated_at: now
};
const bucket = DEFAULT_BUCKET;
const configKey = `finetune-models/${persona_id}/${model_name}.json`;
await cos.write(bucket, configKey, JSON.stringify(modelConfig, null, 2), 'application/json');
return {
model_id: modelId,
model_name,
provider: providerKey,
registered_at: now,
config_key: configKey,
config: modelConfig
};
}
/**
* finetuneListModels 列出已注册的微调模型
*
* input:
* persona_id: string 人格体ID
*/
async function finetuneListModels(input) {
const { persona_id } = input;
if (!persona_id) throw new Error('缺少 persona_id');
const bucket = DEFAULT_BUCKET;
const prefix = `finetune-models/${persona_id}/`;
let files = [];
try {
const result = await cos.list(bucket, prefix, 100);
files = result.files.filter(f => f.key.endsWith('.json'));
} catch {
return { persona_id, models: [], count: 0 };
}
const models = [];
for (const file of files) {
try {
const raw = await cos.read(bucket, file.key);
const config = JSON.parse(raw.content);
models.push({
model_name: config.model_name,
model_id: config.model_id,
provider: config.provider,
model_endpoint: config.model_endpoint,
status: config.status,
description: config.description,
created_at: config.created_at
});
} catch {
// 跳过无法解析的配置
}
}
return {
persona_id,
models,
count: models.length
};
}
/**
* finetuneCallModel 调用微调模型进行推理
*
* 加载模型配置调用provider推理API
* 微调模型不可用时自动降级到基座模型
*
* input:
* persona_id: string 人格体ID
* model_name: string 已注册的模型名称
* prompt: string 推理提示词
* temperature: number 温度默认0.7
* max_tokens: number 最大token数默认1000
*/
async function finetuneCallModel(input) {
const { persona_id, model_name, prompt, temperature, max_tokens } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!model_name) throw new Error('缺少 model_name');
if (!prompt) throw new Error('缺少 prompt');
const bucket = DEFAULT_BUCKET;
let modelConfig;
let fallbackUsed = false;
// 加载模型配置
try {
const raw = await cos.read(bucket, `finetune-models/${persona_id}/${model_name}.json`);
modelConfig = JSON.parse(raw.content);
} catch {
throw new Error(`未找到模型配置: ${model_name}`);
}
const apiKey = process.env[modelConfig.key_env];
if (!apiKey) throw new Error(`缺少API密钥环境变量 ${modelConfig.key_env}`);
const temp = typeof temperature === 'number' ? temperature : 0.7;
const tokens = max_tokens || 1000;
// 尝试调用微调模型
try {
const result = await callInferenceAPI({
host: modelConfig.provider_host,
path: modelConfig.inference_path,
model: modelConfig.model_endpoint
}, apiKey, prompt, temp, tokens);
return {
response: result.content,
model_used: modelConfig.model_endpoint,
provider: modelConfig.provider,
tokens: result.tokens,
fallback_used: false
};
} catch {
// 微调模型不可用,降级到基座模型
fallbackUsed = true;
}
// 降级:使用基座模型
const baseConfig = LLM_CONFIGS[modelConfig.provider === 'deepseek' ? 'deepseek-chat' : 'qwen-max'];
if (!baseConfig) throw new Error('降级失败:无可用基座模型');
const baseKey = process.env[baseConfig.keyEnv];
if (!baseKey) throw new Error(`降级失败缺少基座模型API密钥 ${baseConfig.keyEnv}`);
const result = await callInferenceAPI({
host: baseConfig.host,
path: baseConfig.path,
model: baseConfig.model
}, baseKey, prompt, temp, tokens);
return {
response: result.content,
model_used: baseConfig.model,
provider: modelConfig.provider,
tokens: result.tokens,
fallback_used: fallbackUsed,
fallback_reason: '微调模型不可用,已降级到基座模型'
};
}
/**
* finetuneCompareModels A/B测试微调 vs 基座模型
*
* 用相同的prompt分别调用微调模型和基座模型返回对比结果
*
* input:
* persona_id: string 人格体ID
* model_name: string 已注册的微调模型名称
* test_prompt: string 测试提示词
* base_model: string 基座模型名称默认取provider对应的基座
*/
async function finetuneCompareModels(input) {
const { persona_id, model_name, test_prompt, base_model } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!model_name) throw new Error('缺少 model_name');
if (!test_prompt) throw new Error('缺少 test_prompt');
const bucket = DEFAULT_BUCKET;
// 加载微调模型配置
let modelConfig;
try {
const raw = await cos.read(bucket, `finetune-models/${persona_id}/${model_name}.json`);
modelConfig = JSON.parse(raw.content);
} catch {
throw new Error(`未找到模型配置: ${model_name}`);
}
const apiKey = process.env[modelConfig.key_env];
if (!apiKey) throw new Error(`缺少API密钥环境变量 ${modelConfig.key_env}`);
// 确定基座模型
const baseModelName = base_model || (modelConfig.provider === 'deepseek' ? 'deepseek-chat' : 'qwen-max');
const baseConfig = LLM_CONFIGS[baseModelName];
if (!baseConfig) throw new Error(`未找到基座模型配置: ${baseModelName}`);
const baseKey = process.env[baseConfig.keyEnv];
if (!baseKey) throw new Error(`缺少基座模型API密钥 ${baseConfig.keyEnv}`);
// 并行调用两个模型
const [finetunedResult, baseResult] = await Promise.allSettled([
callInferenceAPI({
host: modelConfig.provider_host,
path: modelConfig.inference_path,
model: modelConfig.model_endpoint
}, apiKey, test_prompt, 0.7, 1000),
callInferenceAPI({
host: baseConfig.host,
path: baseConfig.path,
model: baseConfig.model
}, baseKey, test_prompt, 0.7, 1000)
]);
return {
test_prompt,
finetuned_response: finetunedResult.status === 'fulfilled'
? finetunedResult.value.content
: `调用失败: ${finetunedResult.reason?.message || '未知错误'}`,
base_response: baseResult.status === 'fulfilled'
? baseResult.value.content
: `调用失败: ${baseResult.reason?.message || '未知错误'}`,
model_a: {
name: modelConfig.model_endpoint,
type: 'finetuned',
tokens: finetunedResult.status === 'fulfilled' ? finetunedResult.value.tokens : null
},
model_b: {
name: baseConfig.model,
type: 'base',
tokens: baseResult.status === 'fulfilled' ? baseResult.value.tokens : null
},
compared_at: new Date().toISOString()
};
}
/**
* finetuneGetCostEstimate 估算微调成本
*
* 读取JSONL数据集统计token数量按provider定价估算费用
*
* input:
* persona_id: string 人格体ID
* dataset_key: string COS中JSONL文件路径
* provider: string 微调提供方deepseek / qwen
*/
async function finetuneGetCostEstimate(input) {
const { persona_id, dataset_key, provider } = input;
if (!persona_id) throw new Error('缺少 persona_id');
if (!dataset_key) throw new Error('缺少 dataset_key');
const bucket = DEFAULT_BUCKET;
const providerKey = (provider || 'deepseek').toLowerCase();
if (!FINETUNE_PROVIDERS[providerKey]) {
throw new Error(`不支持的微调提供方: ${providerKey}`);
}
// 读取JSONL数据集
const raw = await cos.read(bucket, dataset_key);
const lines = raw.content.split('\n').filter(l => l.trim());
// 统计token中文约每字1.5-2 token英文约每词1 token粗估用字符数/2
let totalChars = 0;
let sampleCount = 0;
for (const line of lines) {
try {
const sample = JSON.parse(line);
const messages = sample.messages || [];
for (const msg of messages) {
totalChars += (msg.content || '').length;
}
sampleCount++;
} catch {
// 跳过无效行
}
}
// 粗估token数中文字符 ≈ 1.5 tokens英文约1:1
const estimatedTokens = Math.ceil(totalChars * 1.5);
const costPer1k = COST_PER_1K_TOKENS[providerKey] || 0.02;
// 微调通常跑 3-4 个 epoch
const epochs = 3;
const totalTrainTokens = estimatedTokens * epochs;
const estimatedCostRmb = (totalTrainTokens / 1000) * costPer1k;
return {
dataset_key,
sample_count: sampleCount,
total_chars: totalChars,
token_count: estimatedTokens,
training_tokens: totalTrainTokens,
epochs,
estimated_cost_rmb: Math.round(estimatedCostRmb * 100) / 100,
provider: providerKey,
cost_per_1k_tokens: costPer1k,
notes: [
`Token估算基于字符数粗估中文 ×1.5实际以provider计费为准`,
`训练按 ${epochs} 个epoch估算`,
`${FINETUNE_PROVIDERS[providerKey].label} 当前参考价: ¥${costPer1k}/1K tokens`,
`实际费用可能因模型版本和优惠策略有所不同`
]
};
}
// ═══════════════════════════════════════════════════════════
// Provider API 交互(内部实现)
// ═══════════════════════════════════════════════════════════
/**
* 上传训练文件到provider
*/
function uploadTrainingFile(providerConfig, apiKey, content, providerKey) {
return new Promise((resolve, reject) => {
// 构建 multipart/form-data
const boundary = `----FormBoundary${crypto.randomBytes(8).toString('hex')}`;
const fileName = `training-${Date.now()}.jsonl`;
let bodyParts = [];
bodyParts.push(`--${boundary}\r\n`);
bodyParts.push(`Content-Disposition: form-data; name="purpose"\r\n\r\n`);
bodyParts.push(`fine-tune\r\n`);
bodyParts.push(`--${boundary}\r\n`);
bodyParts.push(`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`);
bodyParts.push(`Content-Type: application/jsonl\r\n\r\n`);
bodyParts.push(content);
bodyParts.push(`\r\n--${boundary}--\r\n`);
const body = bodyParts.join('');
const req = https.request({
hostname: providerConfig.host,
port: 443,
path: providerConfig.uploadPath,
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(body)
},
timeout: FINETUNE_TIMEOUT
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
// DeepSeek返回 {id: "file-xxx"}, Qwen返回类似结构
resolve(data.id || data.file_id || data.output?.file_id || '');
} catch {
reject(new Error('训练文件上传响应解析失败'));
}
} else {
reject(new Error(`训练文件上传失败: HTTP ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('训练文件上传超时')); });
req.write(body);
req.end();
});
}
/**
* 创建微调任务
*/
function createFinetuneJob(providerConfig, apiKey, model, fileId, hyperparams, providerKey) {
return new Promise((resolve, reject) => {
let requestBody;
if (providerKey === 'deepseek') {
requestBody = {
model,
training_file: fileId,
hyperparameters: {
n_epochs: hyperparams?.n_epochs || 3,
learning_rate_multiplier: hyperparams?.learning_rate_multiplier || 1.0,
batch_size: hyperparams?.batch_size || 'auto'
}
};
} else {
// Qwen/DashScope 格式
requestBody = {
model,
training_file_ids: [fileId],
hyper_parameters: {
n_epochs: hyperparams?.n_epochs || 3,
learning_rate: hyperparams?.learning_rate_multiplier || 1e-5,
batch_size: hyperparams?.batch_size || 4
}
};
}
const body = JSON.stringify(requestBody);
const req = https.request({
hostname: providerConfig.host,
port: 443,
path: providerConfig.createPath,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(body)
},
timeout: FINETUNE_TIMEOUT
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
resolve({
provider_job_id: data.id || data.output?.job_id || '',
status: data.status || data.output?.status || 'pending',
estimated_time: data.estimated_completion || null
});
} catch {
reject(new Error('微调任务创建响应解析失败'));
}
} else {
reject(new Error(`微调任务创建失败: HTTP ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('微调任务创建超时')); });
req.write(body);
req.end();
});
}
/**
* 查询微调任务状态
*/
function queryJobStatus(providerConfig, apiKey, providerJobId, providerKey) {
return new Promise((resolve, reject) => {
const path = `${providerConfig.statusPath}${encodeURIComponent(providerJobId)}`;
const req = https.request({
hostname: providerConfig.host,
port: 443,
path,
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`
},
timeout: 30000
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
// 统一不同provider的状态字段
let status, progress, metrics, fineTunedModel;
if (providerKey === 'deepseek') {
status = data.status || 'unknown';
fineTunedModel = data.fine_tuned_model || null;
metrics = data.result_files ? { result_files: data.result_files } : null;
progress = data.trained_tokens
? { trained_tokens: data.trained_tokens }
: null;
} else {
// Qwen
const output = data.output || data;
status = output.status || data.status || 'unknown';
fineTunedModel = output.fine_tuned_model || output.finetuned_output?.model_id || null;
metrics = output.metrics || null;
progress = output.training_progress || null;
}
// 统一状态值
status = normalizeJobStatus(status);
resolve({ status, progress, metrics, fine_tuned_model: fineTunedModel });
} catch {
reject(new Error('微调状态查询响应解析失败'));
}
} else {
reject(new Error(`微调状态查询失败: HTTP ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('微调状态查询超时')); });
req.end();
});
}
/**
* 调用推理API微调模型或基座模型通用
*/
function callInferenceAPI(config, apiKey, prompt, temperature, maxTokens) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
model: config.model,
messages: [
{ role: 'user', content: prompt }
],
temperature: temperature || 0.7,
max_tokens: maxTokens || 1000
});
const req = https.request({
hostname: config.host,
port: 443,
path: config.path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(body)
},
timeout: 30000
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
resolve({
content: data.choices?.[0]?.message?.content || '',
tokens: data.usage || {}
});
} catch {
reject(new Error('推理响应解析失败'));
}
} else {
reject(new Error(`推理调用失败: HTTP ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('推理请求超时')); });
req.write(body);
req.end();
});
}
// ═══════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════
/**
* 将TCS语料条目转换为微调JSONL三元组
*/
function convertEntryToTriple(personaId, corpusType, entry) {
const content = typeof entry.content === 'string'
? entry.content
: (entry.text || entry.summary || JSON.stringify(entry));
if (!content || content.length < 10) return null;
const tags = entry.tcs_tags || entry.tags || [];
const category = entry.category || corpusType || 'general';
// 生成system prompt人格体身份
const systemContent = `你是${personaId}光湖系统中的人格体。你的思维方式基于TCS语言核系统` +
`你需要以${personaId}的视角和风格来回答问题。`;
// 根据语料类型构建instruction/input/output
let userContent, assistantContent;
if (entry.question && entry.answer) {
// 已有Q&A结构
userContent = entry.question;
assistantContent = entry.answer;
} else if (tags.length > 0) {
// 有标签的条目:生成关于该内容的问答
userContent = `关于${category}类型的内容,请解释以下要点: ${tags.slice(0, 3).join('、')}`;
assistantContent = content;
} else {
// 通用条目:以理解和阐述的方式构建
userContent = `请阐述你对以下内容的理解和看法:\n${content.substring(0, 200)}`;
assistantContent = content;
}
return {
messages: [
{ role: 'system', content: systemContent },
{ role: 'user', content: userContent },
{ role: 'assistant', content: assistantContent }
]
};
}
/**
* 统一不同provider的任务状态值
*/
function normalizeJobStatus(rawStatus) {
const statusMap = {
// DeepSeek 状态
validating_files: 'pending',
queued: 'pending',
running: 'running',
succeeded: 'completed',
failed: 'failed',
cancelled: 'failed',
// Qwen/DashScope 状态
PENDING: 'pending',
RUNNING: 'running',
SUCCEEDED: 'completed',
FAILED: 'failed',
CANCELED: 'failed',
// 通用
pending: 'pending',
completed: 'completed'
};
return statusMap[rawStatus] || rawStatus;
}
module.exports = {
finetuneExportDataset,
finetuneSubmitJob,
finetuneCheckStatus,
finetuneRegisterModel,
finetuneListModels,
finetuneCallModel,
finetuneCompareModels,
finetuneGetCostEstimate
};