622 lines
19 KiB
JavaScript
622 lines
19 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 模块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
|
||
};
|