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

622 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ═══════════════════════════════════════════════════════════
* 模块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
};