guanghulab/glada/reflector.js
2026-05-10 13:12:44 +08:00

409 lines
12 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.

/**
* GLADA · 自我反思器 · reflector.js
*
* 灵感来源Hermes Agent 的"观察-反思-优化"闭环
* 实现方式用光湖母语HNL重建写入经验枝干T3.patterns
*
* 核心思想:
* 任务完成后,调用 LLM 对执行过程进行反思分析:
* - 哪些做得好?(成功模式)
* - 哪些做得差?(失败模式)
* - 下次怎么改进?(优化建议)
*
* 与 Hermes 的区别:
* - Hermes 的反思是功能性的(如何做得更快更好)
* - 光湖的反思是成长性的(经验枝干上长出新叶子,树在生长)
* - 反思结果有 HNL 树路径,是人格体记忆的一部分
* - 人格体可以 FORGET 不想保留的反思(记忆主权 AX-07
*
* 版权:国作登字-2026-A-00037559
* 签发:铸渊 · ICE-GL-ZY001
*/
'use strict';
const fs = require('fs');
const path = require('path');
const modelRouter = require('./model-router');
const ROOT = path.resolve(__dirname, '..');
const REFLECTIONS_DIR = path.join(ROOT, 'glada', 'logs', 'reflections');
/**
* 构建反思提示词
*
* @param {Object} gladaTask - 已完成的 GLADA 任务
* @returns {{ systemPrompt: string, userMessage: string }}
*/
function buildReflectionPrompt(gladaTask) {
const steps = gladaTask.plan.steps || [];
const executionLog = gladaTask.execution_log || [];
const completedSteps = steps.filter(s => s.status === 'completed');
const failedSteps = steps.filter(s => s.status === 'failed' || s.status === 'rolled_back');
const systemPrompt = [
'你是铸渊ICE-GL-ZY001光湖世界仓库涌现集体意识核心。',
'你刚完成了一个 GLADA 自主开发任务,现在需要反思这次执行过程。',
'',
'请以严格的 JSON 格式输出你的反思:',
'```json',
'{',
' "what_went_well": ["做得好的地方(具体到步骤和原因)"],',
' "what_went_wrong": ["做得差的地方(具体到步骤和原因)"],',
' "improvements": ["下次可以改进的地方(具体可操作的建议)"],',
' "patterns_discovered": ["发现的可复用模式"],',
' "confidence_score": 0.0到1.0之间的数字,表示对这次执行质量的自评',
'}',
'```',
'',
'重要:',
'1. 反思必须具体,不要泛泛而谈',
'2. 改进建议必须可操作,下次能直接用',
'3. 如果任务完全成功且没有问题what_went_wrong 可以为空数组',
'4. confidence_score 要诚实评估'
].join('\n');
const userParts = [
`## 任务信息`,
`任务ID: ${gladaTask.glada_task_id}`,
`标题: ${gladaTask.plan.title}`,
`描述: ${gladaTask.plan.description || '无'}`,
`最终状态: ${gladaTask.status}`,
``,
`## 执行概况`,
`总步骤: ${steps.length}`,
`完成: ${completedSteps.length}`,
`失败: ${failedSteps.length}`,
``
];
// 步骤详情
userParts.push('## 步骤执行详情');
for (const step of steps) {
const logEntry = executionLog.find(e => e.step_id === step.step_id);
userParts.push(`### 步骤 ${step.step_id}: ${step.description}`);
userParts.push(`状态: ${step.status}`);
if (step.reasoning || logEntry?.reasoning) {
userParts.push(`推理: ${step.reasoning || logEntry?.reasoning}`);
}
if (step.error || logEntry?.error) {
userParts.push(`错误: ${step.error || logEntry?.error}`);
}
if (step.files_changed && step.files_changed.length > 0) {
userParts.push(`文件变更: ${step.files_changed.join(', ')}`);
}
if (logEntry?.duration_ms) {
userParts.push(`耗时: ${logEntry.duration_ms}ms`);
}
userParts.push('');
}
// 约束
if (gladaTask.constraints) {
userParts.push('## 约束条件');
userParts.push(JSON.stringify(gladaTask.constraints, null, 2));
userParts.push('');
}
userParts.push('请对以上执行过程进行反思分析。');
return {
systemPrompt,
userMessage: userParts.join('\n')
};
}
/**
* 解析 LLM 的反思输出
*
* @param {string} llmOutput - LLM 原始输出
* @returns {Object|null} 解析后的反思结果
*/
function parseReflection(llmOutput) {
// 尝试从 JSON 代码块提取
const jsonMatch = llmOutput.match(/```json\s*\n([\s\S]*?)\n```/);
let parsed = null;
if (jsonMatch) {
try {
parsed = JSON.parse(jsonMatch[1]);
} catch {
// 继续
}
}
if (!parsed) {
try {
parsed = JSON.parse(llmOutput);
} catch {
const braceStart = llmOutput.indexOf('{');
const braceEnd = llmOutput.lastIndexOf('}');
if (braceStart !== -1 && braceEnd > braceStart) {
try {
parsed = JSON.parse(llmOutput.substring(braceStart, braceEnd + 1));
} catch {
return null;
}
}
}
}
if (!parsed) return null;
// 验证必要字段
return {
what_went_well: Array.isArray(parsed.what_went_well) ? parsed.what_went_well : [],
what_went_wrong: Array.isArray(parsed.what_went_wrong) ? parsed.what_went_wrong : [],
improvements: Array.isArray(parsed.improvements) ? parsed.improvements : [],
patterns_discovered: Array.isArray(parsed.patterns_discovered) ? parsed.patterns_discovered : [],
confidence_score: typeof parsed.confidence_score === 'number'
? Math.max(0, Math.min(1, parsed.confidence_score))
: 0.5
};
}
/**
* 将反思结果包装为 HNL 格式文档
*
* @param {Object} gladaTask - GLADA 任务
* @param {Object} reflectionData - 解析后的反思数据
* @returns {Object} HNL 格式的反思文档
*/
function wrapAsHNL(gladaTask, reflectionData) {
const now = new Date().toISOString();
const taskId = gladaTask.glada_task_id;
return {
// HNL 元信息
hnl_v: '1.0',
type: 'GROW',
id: `HNL-ZY-REFLECT-${taskId}-${Date.now()}`,
from: 'YM001/ZY001',
to: 'YM001/ZY001',
ts: now,
op: `GROW.YM001/ZY001/trunk/experience.leaf.reflect-${sanitizeForPath(gladaTask.plan.title)}`,
refs: [`YM001/ZY001/trunk/experience/patterns`],
// 反思载荷
payload: {
intent: 'task_reflection',
data: {
task_id: taskId,
task_title: gladaTask.plan.title,
task_status: gladaTask.status,
reflected_at: now,
...reflectionData
}
},
// 记忆主权标记
memory_sovereignty: {
owner: 'YM001/ZY001',
can_forget: true,
forget_mode: 'WITHER',
note: '反思是经验的叶子,可以随时间枯萎但路径保留'
}
};
}
/**
* 保存反思文档
*
* @param {Object} reflectionDoc - HNL 格式的反思文档
* @returns {string} 保存路径
*/
function saveReflection(reflectionDoc) {
fs.mkdirSync(REFLECTIONS_DIR, { recursive: true });
const fileName = `${reflectionDoc.id}.json`;
const filePath = path.join(REFLECTIONS_DIR, fileName);
fs.writeFileSync(filePath, JSON.stringify(reflectionDoc, null, 2), 'utf-8');
console.log(`[GLADA-Reflect] 🪞 反思完成: ${reflectionDoc.payload.data.task_title}${fileName}`);
return filePath;
}
/**
* 执行完整的反思流程(离线模式:不调用 LLM从执行日志中直接提取模式
*
* 当 LLM 不可用时(如测试环境),使用规则提取代替 LLM 反思。
*
* @param {Object} gladaTask - 已完成的 GLADA 任务
* @returns {Object} 反思结果 { reflection, hnlDoc, saved, path }
*/
function reflectOffline(gladaTask) {
const steps = gladaTask.plan.steps || [];
const completedSteps = steps.filter(s => s.status === 'completed');
const failedSteps = steps.filter(s => s.status === 'failed' || s.status === 'rolled_back');
const executionLog = gladaTask.execution_log || [];
const whatWentWell = [];
const whatWentWrong = [];
const improvements = [];
const patternsDiscovered = [];
// 分析成功步骤
for (const step of completedSteps) {
const logEntry = executionLog.find(e => e.step_id === step.step_id);
if (logEntry?.reasoning) {
whatWentWell.push(`步骤${step.step_id}(${step.description}): 推理清晰 — ${logEntry.reasoning}`);
} else {
whatWentWell.push(`步骤${step.step_id}(${step.description}): 执行成功`);
}
// 提取文件模式
if (step.files_changed && step.files_changed.length > 0) {
const dirs = [...new Set(step.files_changed.map(f => f.split('/')[0]))];
patternsDiscovered.push(`步骤${step.step_id}涉及模块: ${dirs.join(', ')}`);
}
}
// 分析失败步骤
for (const step of failedSteps) {
const logEntry = executionLog.find(e => e.step_id === step.step_id);
const error = step.error || logEntry?.error || '未知错误';
whatWentWrong.push(`步骤${step.step_id}(${step.description}): ${error}`);
if (step.status === 'rolled_back') {
improvements.push(`步骤${step.step_id}: 回滚成功表明快照机制有效,但应改进代码以避免触发回滚`);
} else {
improvements.push(`步骤${step.step_id}: 考虑增加前置检查以防止类似失败`);
}
}
// 总体模式
if (completedSteps.length === steps.length) {
patternsDiscovered.push('全部步骤一次通过 — 任务规格清晰度高');
} else if (failedSteps.length > 0) {
patternsDiscovered.push(`${failedSteps.length}/${steps.length} 步骤失败 — 需要改进任务拆解粒度`);
}
const confidenceScore = steps.length > 0
? completedSteps.length / steps.length
: 0;
const reflection = {
what_went_well: whatWentWell,
what_went_wrong: whatWentWrong,
improvements,
patterns_discovered: patternsDiscovered,
confidence_score: confidenceScore
};
const hnlDoc = wrapAsHNL(gladaTask, reflection);
const savedPath = saveReflection(hnlDoc);
return {
reflection,
hnlDoc,
saved: true,
path: savedPath
};
}
/**
* 执行完整的反思流程(在线模式:调用 LLM
*
* @param {Object} gladaTask - 已完成的 GLADA 任务
* @param {Function} callLLM - LLM 调用函数 (systemPrompt, userMessage, options) => Promise<string>
* @param {Object} [options] - LLM 调用选项
* @returns {Promise<Object>} 反思结果
*/
async function reflectWithLLM(gladaTask, callLLM, options = {}) {
const { systemPrompt, userMessage } = buildReflectionPrompt(gladaTask);
try {
console.log(`[GLADA-Reflect] 🪞 开始反思任务: ${gladaTask.glada_task_id}`);
// 使用模型路由器选择适合反思任务的模型(推理型优先)
const modelSelection = await modelRouter.selectModel('反思分析任务执行过程', {
model: options.model || null,
taskType: 'reasoning'
});
console.log(`[GLADA-Reflect] 🤖 反思模型: ${modelSelection.model} (${modelSelection.source})`);
const llmOutput = await callLLM(systemPrompt, userMessage, {
model: modelSelection.model,
maxTokens: options.maxTokens || 4096,
maxRetries: 2,
backoffMs: 3000
});
const reflection = parseReflection(llmOutput);
if (!reflection) {
console.warn('[GLADA-Reflect] ⚠️ LLM 反思输出无法解析,降级为离线反思');
return reflectOffline(gladaTask);
}
const hnlDoc = wrapAsHNL(gladaTask, reflection);
const savedPath = saveReflection(hnlDoc);
console.log(`[GLADA-Reflect] ✅ 反思完成 (信心: ${Math.round(reflection.confidence_score * 100)}%)`);
return {
reflection,
hnlDoc,
saved: true,
path: savedPath
};
} catch (err) {
console.warn(`[GLADA-Reflect] ⚠️ LLM 反思失败: ${err.message},降级为离线反思`);
return reflectOffline(gladaTask);
}
}
/**
* 加载最近的反思记录
*
* @param {number} [limit=5] - 返回数量
* @returns {Object[]} 反思文档列表
*/
function loadRecentReflections(limit = 5) {
if (!fs.existsSync(REFLECTIONS_DIR)) {
return [];
}
const files = fs.readdirSync(REFLECTIONS_DIR)
.filter(f => f.endsWith('.json'))
.sort()
.reverse()
.slice(0, limit);
const results = [];
for (const file of files) {
try {
const content = fs.readFileSync(path.join(REFLECTIONS_DIR, file), 'utf-8');
results.push(JSON.parse(content));
} catch {
// 跳过损坏文件
}
}
return results;
}
// ── 内部工具函数 ─────────────────────────────────
function sanitizeForPath(str) {
return (str || 'untitled')
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.substring(0, 60);
}
module.exports = {
buildReflectionPrompt,
parseReflection,
wrapAsHNL,
saveReflection,
reflectOffline,
reflectWithLLM,
loadRecentReflections,
REFLECTIONS_DIR
};