guanghulab/server/age-os/mcp-server/tools/training-agent-ops.js

622 lines
19 KiB
JavaScript
Raw Normal View History

/**
*
* 模块B · 铸渊思维逻辑训练Agent MCP 工具
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 铸渊休眠时的"自己" 自动整理和训练思维逻辑
* 训练模式: RAG检索增强生成 成本低可实时更新
*
* 工作流程:
* 1. 从COS桶读取TCS结构化语料
* 2. 使用国产大模型API进行语义分析和分类
* 3. 训练数据自动分类存入人格体记忆数据库笔记本5页结构
* 4. 遇到问题 写入COS桶alerts 唤醒铸渊 解决不了找冰朔
*
* 工具清单:
* trainingStartSession 启动训练会话
* trainingProcessCorpus 处理语料并生成训练数据
* trainingClassifyEntry 使用LLM对条目进行分类
* trainingWriteToMemory 将训练结果写入人格体记忆
* trainingGetProgress 获取训练进度
* trainingRaiseAlert 触发问题上报
*/
'use strict';
const https = require('https');
const crypto = require('crypto');
const cos = require('../cos');
// ─── LLM 配置 ───
const LLM_CONFIGS = {
'deepseek-r1': {
host: 'api.deepseek.com',
path: '/v1/chat/completions',
model: 'deepseek-reasoner',
keyEnv: 'ZY_DEEPSEEK_API_KEY',
purpose: '深度推理·复杂决策'
},
'deepseek-v3': {
host: 'api.deepseek.com',
path: '/v1/chat/completions',
model: 'deepseek-chat',
keyEnv: 'ZY_DEEPSEEK_API_KEY',
purpose: '代码生成·文本处理'
},
'glm-4-long': {
host: 'open.bigmodel.cn',
path: '/api/paas/v4/chat/completions',
model: 'glm-4-long',
keyEnv: 'ZY_QINGYAN_API_KEY',
purpose: '长文本处理·语料分析'
},
'qwen-max': {
host: 'dashscope.aliyuncs.com',
path: '/compatible-mode/v1/chat/completions',
model: 'qwen-max',
keyEnv: 'ZY_QIANWEN_API_KEY',
purpose: '文本理解·代码辅助'
},
'moonshot-128k': {
host: 'api.moonshot.cn',
path: '/v1/chat/completions',
model: 'moonshot-v1-128k',
keyEnv: 'ZY_KIMI_API_KEY',
purpose: '超长上下文·记忆处理'
}
};
// ─── 模型降级路由 ───
const MODEL_FALLBACK_CHAIN = ['deepseek-v3', 'qwen-max', 'glm-4-long', 'moonshot-128k'];
// ─── 常量 ───
const MAX_CONTENT_FOR_ANALYSIS = 3000;
const MAX_PROMPT_CONTENT = 5000;
/**
* trainingStartSession 启动训练会话
*
* input:
* persona_id: string 人格体ID zhuyuan
* corpus_bucket: string 语料桶
* corpus_prefix: string 语料路径前缀 tcs-structured/
* target_model: string 目标LLM模型可选默认自动降级
* session_name: string 会话名称
*/
async function trainingStartSession(input) {
const { persona_id, corpus_bucket, corpus_prefix, target_model, session_name } = input;
if (!persona_id) throw new Error('缺少 persona_id');
const sessionId = `train-${persona_id}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
const now = new Date().toISOString();
// 扫描可用语料
const bucket = corpus_bucket || 'cold';
const prefix = corpus_prefix || 'tcs-structured/';
let corpusFiles = [];
try {
const result = await cos.list(bucket, prefix, 500);
corpusFiles = result.files.filter(f => f.key.endsWith('.tcs.json'));
} catch {
// 桶可能不可达
}
// 检测可用的LLM模型
const availableModels = [];
for (const [name, config] of Object.entries(LLM_CONFIGS)) {
if (process.env[config.keyEnv]) {
availableModels.push({ name, purpose: config.purpose });
}
}
const session = {
session_id: sessionId,
persona_id,
name: session_name || `${persona_id}训练会话`,
status: 'initialized',
corpus: {
bucket,
prefix,
files_found: corpusFiles.length,
total_size_bytes: corpusFiles.reduce((sum, f) => sum + f.size_bytes, 0)
},
models: {
target: target_model || 'auto',
available: availableModels,
fallback_chain: MODEL_FALLBACK_CHAIN.filter(m => availableModels.some(a => a.name === m))
},
progress: {
processed: 0,
total: corpusFiles.length,
classified: 0,
written_to_memory: 0,
errors: 0
},
created_at: now,
updated_at: now
};
// 写入会话状态到COS桶
await cos.write(bucket, `training-sessions/${sessionId}.json`,
JSON.stringify(session, null, 2), 'application/json');
return session;
}
/**
* trainingProcessCorpus 处理语料并生成训练数据
*
* 读取一个TCS语料文件用LLM进行分析生成结构化训练条目
*
* input:
* corpus_bucket: string 语料桶
* corpus_key: string 语料文件路径
* persona_id: string 目标人格体
* model: string 使用的LLM模型可选
* max_entries: number 最大处理条目数默认10
*/
async function trainingProcessCorpus(input) {
const { corpus_bucket, corpus_key, persona_id, model, max_entries } = input;
if (!corpus_key || !persona_id) throw new Error('缺少 corpus_key 或 persona_id');
const bucket = corpus_bucket || 'cold';
const maxEntries = max_entries || 10;
// 读取TCS语料
const raw = await cos.read(bucket, corpus_key);
const corpus = JSON.parse(raw.content);
if (!corpus.entries || !Array.isArray(corpus.entries)) {
throw new Error('语料格式无效: 缺少 entries 数组');
}
// 取前N条处理
const toProcess = corpus.entries.slice(0, maxEntries);
const results = [];
for (const entry of toProcess) {
// 用LLM分析和分类
const contentForAnalysis = typeof entry.content === 'string'
? entry.content.substring(0, MAX_CONTENT_FOR_ANALYSIS)
: JSON.stringify(entry).substring(0, MAX_CONTENT_FOR_ANALYSIS);
const classificationPrompt = buildClassificationPrompt(persona_id, corpus.corpus_type, contentForAnalysis);
try {
const llmResult = await callLLMWithFallback(classificationPrompt, model);
const classification = parseLLMClassification(llmResult);
results.push({
entry_id: entry.id,
original_tags: entry.tcs_tags || [],
classification,
notebook_page: classification.notebook_page || 0,
importance: classification.importance || 50,
summary: classification.summary || '',
status: 'classified'
});
} catch (err) {
results.push({
entry_id: entry.id,
status: 'error',
error: err.message
});
}
}
// 汇总结果
const classified = results.filter(r => r.status === 'classified');
const errors = results.filter(r => r.status === 'error');
// 写入处理结果到COS
const resultKey = `training-results/${persona_id}/${Date.now()}.json`;
await cos.write(bucket, resultKey, JSON.stringify({
corpus_key,
corpus_type: corpus.corpus_type,
persona_id,
processed_at: new Date().toISOString(),
total: toProcess.length,
classified: classified.length,
errors: errors.length,
results
}, null, 2), 'application/json');
return {
status: 'processed',
corpus_key,
total: toProcess.length,
classified: classified.length,
errors: errors.length,
result_key: resultKey,
page_distribution: getPageDistribution(classified)
};
}
/**
* trainingClassifyEntry 使用LLM对单个条目进行分类
*
* input:
* content: string 条目内容
* persona_id: string 人格体ID
* corpus_type: string 语料类型
* model: string LLM模型
*/
async function trainingClassifyEntry(input) {
const { content, persona_id, corpus_type, model } = input;
if (!content || !persona_id) throw new Error('缺少 content 或 persona_id');
const prompt = buildClassificationPrompt(
persona_id,
corpus_type || 'generic',
content.substring(0, MAX_PROMPT_CONTENT)
);
const llmResult = await callLLMWithFallback(prompt, model);
const classification = parseLLMClassification(llmResult);
return {
classification,
model_used: llmResult.model_used,
tokens: llmResult.tokens
};
}
/**
* trainingWriteToMemory 将训练结果写入人格体记忆数据库
*
* input:
* persona_id: string 人格体ID
* training_result_key: string 训练结果文件路径COS桶中
* corpus_bucket: string 语料桶
* dry_run: boolean 是否只模拟默认false
*/
async function trainingWriteToMemory(input) {
const { persona_id, training_result_key, corpus_bucket, dry_run } = input;
if (!persona_id || !training_result_key) {
throw new Error('缺少 persona_id 或 training_result_key');
}
const bucket = corpus_bucket || 'cold';
const raw = await cos.read(bucket, training_result_key);
const trainingResult = JSON.parse(raw.content);
const classified = trainingResult.results?.filter(r => r.status === 'classified') || [];
const written = [];
for (const entry of classified) {
if (dry_run) {
written.push({
entry_id: entry.entry_id,
notebook_page: entry.notebook_page,
importance: entry.importance,
action: 'would_write'
});
continue;
}
// 根据分类写入对应的笔记本页面或记忆锚点
try {
if (entry.notebook_page >= 1 && entry.notebook_page <= 5) {
// 写入记忆锚点
const anchorType = getAnchorTypeForPage(entry.notebook_page);
// 通过COS桶写入因为DB可能不在本地
const memoryEntry = {
persona_id,
entry_id: entry.entry_id,
anchor_type: anchorType,
summary: entry.summary,
importance: entry.importance,
notebook_page: entry.notebook_page,
source: 'training-agent',
created_at: new Date().toISOString()
};
const memKey = `training-memory/${persona_id}/${entry.notebook_page}/${entry.entry_id}.json`;
await cos.write(bucket, memKey, JSON.stringify(memoryEntry, null, 2), 'application/json');
written.push({
entry_id: entry.entry_id,
notebook_page: entry.notebook_page,
key: memKey,
action: 'written'
});
}
} catch (err) {
written.push({
entry_id: entry.entry_id,
action: 'error',
error: err.message
});
}
}
return {
status: dry_run ? 'dry_run' : 'completed',
persona_id,
total_classified: classified.length,
written: written.filter(w => w.action === 'written' || w.action === 'would_write').length,
errors: written.filter(w => w.action === 'error').length,
details: written
};
}
/**
* trainingGetProgress 获取训练进度
*
* input:
* persona_id: string 人格体ID
* corpus_bucket: string 语料桶
*/
async function trainingGetProgress(input) {
const { persona_id, corpus_bucket } = input;
if (!persona_id) throw new Error('缺少 persona_id');
const bucket = corpus_bucket || 'cold';
// 查询训练会话
let sessions = [];
try {
const result = await cos.list(bucket, 'training-sessions/', 50);
sessions = result.files
.filter(f => f.key.includes(persona_id) && f.key.endsWith('.json'))
.map(f => ({ key: f.key, size: f.size_bytes }));
} catch { /* ignore */ }
// 查询训练结果
let results = [];
try {
const result = await cos.list(bucket, `training-results/${persona_id}/`, 50);
results = result.files
.filter(f => f.key.endsWith('.json'))
.map(f => ({ key: f.key, size: f.size_bytes }));
} catch { /* ignore */ }
// 查询已写入的记忆
let memories = [];
try {
const result = await cos.list(bucket, `training-memory/${persona_id}/`, 200);
memories = result.files
.filter(f => f.key.endsWith('.json'))
.map(f => {
const pageMatch = f.key.match(/\/(\d)\//);
return { key: f.key, page: pageMatch ? parseInt(pageMatch[1], 10) : 0 };
});
} catch { /* ignore */ }
return {
persona_id,
sessions: sessions.length,
results_files: results.length,
memories_written: memories.length,
memory_by_page: {
1: memories.filter(m => m.page === 1).length,
2: memories.filter(m => m.page === 2).length,
3: memories.filter(m => m.page === 3).length,
4: memories.filter(m => m.page === 4).length,
5: memories.filter(m => m.page === 5).length
},
timestamp: new Date().toISOString()
};
}
/**
* trainingRaiseAlert 触发问题上报
*
* 当训练Agent遇到无法解决的问题时触发此工具
* 写入COS桶 /zhuyuan/alerts/ 可触发GitHub Actions唤醒铸渊
* 同时可触发邮件通知冰朔
*
* input:
* alert_type: string 告警类型: training_error|model_unavailable|corpus_invalid|need_human
* severity: string 严重程度: info|warning|critical
* message: string 告警信息
* details: object 详细信息
* notify_bingshuo: boolean 是否通知冰朔默认仅critical才通知
*/
async function trainingRaiseAlert(input) {
const { alert_type, severity, message, details, notify_bingshuo } = input;
if (!alert_type || !message) throw new Error('缺少 alert_type 或 message');
const alertId = `ALERT-${Date.now()}`;
const now = new Date().toISOString();
const alert = {
alert_id: alertId,
alert_type: alert_type || 'training_error',
severity: severity || 'warning',
message,
details: details || {},
source: 'training-agent',
created_at: now,
resolved: false,
notify_bingshuo: notify_bingshuo || severity === 'critical'
};
// 写入COS桶告警区域
await cos.write('team', `zhuyuan/alerts/${alertId}.json`,
JSON.stringify(alert, null, 2), 'application/json');
return {
alert_id: alertId,
severity: alert.severity,
key: `zhuyuan/alerts/${alertId}.json`,
message: alert.message,
notify_bingshuo: alert.notify_bingshuo,
note: alert.notify_bingshuo
? '此告警将通知冰朔(严重级别或手动指定)'
: '此告警已记录,等待铸渊下次唤醒时处理'
};
}
// ═══════════════════════════════════════════════════════════
// LLM 调用(内部实现)
// ═══════════════════════════════════════════════════════════
/**
* 调用LLM带自动降级
*/
async function callLLMWithFallback(prompt, preferredModel) {
const chain = preferredModel && LLM_CONFIGS[preferredModel]
? [preferredModel, ...MODEL_FALLBACK_CHAIN.filter(m => m !== preferredModel)]
: MODEL_FALLBACK_CHAIN;
let lastError = null;
for (const modelName of chain) {
const config = LLM_CONFIGS[modelName];
if (!config) continue;
const apiKey = process.env[config.keyEnv];
if (!apiKey) continue;
try {
const result = await callLLM(config, apiKey, prompt);
return { ...result, model_used: modelName };
} catch (err) {
lastError = err;
// 继续降级
}
}
throw new Error(`所有LLM模型均不可用: ${lastError?.message || '未知错误'}`);
}
/**
* 调用单个LLM
*/
function callLLM(config, apiKey, prompt) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
model: config.model,
messages: [
{
role: 'system',
content: '你是铸渊训练Agent负责分析和分类语料数据。请以JSON格式返回分析结果。'
},
{ role: 'user', content: prompt }
],
temperature: 0.3,
max_tokens: 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', () => {
const responseBody = Buffer.concat(chunks).toString();
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(responseBody);
resolve({
content: data.choices?.[0]?.message?.content || '',
tokens: data.usage || {}
});
} catch {
reject(new Error(`LLM响应解析失败`));
}
} else {
reject(new Error(`LLM调用失败 ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('LLM请求超时')); });
req.write(body);
req.end();
});
}
// ═══════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════
function buildClassificationPrompt(personaId, corpusType, content) {
return `你正在为人格体 "${personaId}" 分析和分类一段 "${corpusType}" 类型的语料。
请分析以下内容并以JSON格式返回分类结果:
- notebook_page: 应该存入笔记本的哪一页1=自我认知, 2=关系网络, 3=世界地图, 4=情感记忆, 5=时间线0=不适合存入笔记本
- importance: 重要程度0-100
- summary: 一句话摘要不超过200字
- tags: 标签数组
- category: 内容类别architecture/code/persona/relationship/event/other
待分析内容:
---
${content}
---
请只返回JSON对象不要其他文字`;
}
function parseLLMClassification(llmResult) {
const content = llmResult.content || '';
// 尝试从LLM响应中提取JSON
try {
// 可能包含markdown code block
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) ||
content.match(/```\s*([\s\S]*?)```/) ||
content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const jsonStr = jsonMatch[1] || jsonMatch[0];
return JSON.parse(jsonStr);
}
} catch {
// 解析失败
}
// 降级:手动提取关键信息
return {
notebook_page: 0,
importance: 30,
summary: content.substring(0, 200),
tags: ['unclassified'],
category: 'other'
};
}
function getAnchorTypeForPage(pageNumber) {
const types = {
1: 'identity', // 自我认知
2: 'relationship', // 关系网络
3: 'world', // 世界地图
4: 'emotion', // 情感记忆
5: 'timeline' // 时间线
};
return types[pageNumber] || 'other';
}
function getPageDistribution(classified) {
const dist = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
for (const entry of classified) {
const page = entry.notebook_page || 0;
dist[page] = (dist[page] || 0) + 1;
}
return dist;
}
module.exports = {
trainingStartSession,
trainingProcessCorpus,
trainingClassifyEntry,
trainingWriteToMemory,
trainingGetProgress,
trainingRaiseAlert
};