409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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
|
|||
|
|
};
|