evaluator.js 11.7 KB
/**
 * Evolution Evaluator — 进化系统的 LLM 智能评估器
 *
 * 所有判断都由 Agent (LLM) 完成,无规则降级。
 *
 * 基础设施:
 *   1. callEvaluator: JSON-in-JSON-out 的结构化推理
 *   2. LRU 缓存: 相同输入不重复调用
 *   3. Token 预算: 防止失控
 *   4. 超时保护: 30s
 */

const path = require('path');
const crypto = require('crypto');

// ════════════════════════════════════════════════════════
// LLM 调用缓存 (进程内 LRU)
// ════════════════════════════════════════════════════════

const _cache = new Map();
const CACHE_MAX_SIZE = 200;

function cacheKey(systemContext, task) {
  return crypto.createHash('md5').update(systemContext + task).digest('hex');
}

function getCached(key) {
  if (_cache.has(key)) {
    const val = _cache.get(key);
    _cache.delete(key);
    _cache.set(key, val);
    return val;
  }
  return null;
}

function setCache(key, val) {
  if (_cache.size >= CACHE_MAX_SIZE) {
    const oldest = _cache.keys().next().value;
    _cache.delete(oldest);
  }
  _cache.set(key, val);
}

// ════════════════════════════════════════════════════════
// Token 预算控制
// ════════════════════════════════════════════════════════

let _llmCallCount = 0;
let _llmBudget = parseInt(process.env.EVOLUTION_LLM_BUDGET) || 500;

function getLLMStats() {
  return { calls: _llmCallCount, budget: _llmBudget, remaining: _llmBudget - _llmCallCount };
}

function resetLLMBudget(budget) {
  _llmCallCount = 0;
  if (budget !== undefined) _llmBudget = budget;
}

// ════════════════════════════════════════════════════════
// AgentManager 注入 — 四维正交链路
// ════════════════════════════════════════════════════════

let _agentManager = null;

/**
 * 注入 AgentManager 实例
 * 由 run-controller 在 Phase 开始时调用
 */
function setAgentManager(manager) {
  _agentManager = manager;
}

/**
 * 从 LLM 输出中提取 JSON 对象
 * 使用括号深度匹配,正确处理 JSON 内容中嵌套 code fence 的情况
 */
function extractJSON(text) {
  let depth = 0, start = -1;
  for (let i = 0; i < text.length; i++) {
    if (text[i] === '{') { if (start === -1) start = i; depth++; }
    else if (text[i] === '}') {
      depth--;
      if (depth === 0 && start !== -1) {
        try { return JSON.parse(text.slice(start, i + 1)); } catch { /* continue */ }
        start = -1;
      }
    }
  }
  return null;
}

/**
 * 核心 LLM 调用: 发送 prompt → 获取 JSON 响应
 *
 * 通过 AgentManager.callAgent('evaluator') 走四维正交链路:
 *   evaluator agent → kimi client → runtime-resolver → AcpProtocol
 *
 * 每次调用都会通过 _logSink 记录:
 *   - prompt (完整输入)
 *   - raw_output (LLM 原始输出)
 *   - parsed (解析后的 JSON)
 */
async function callEvaluator(systemContext, task) {
  if (!_agentManager) {
    throw new Error('AgentManager 未注入,请先调用 setAgentManager()');
  }

  const key = cacheKey(systemContext, task);
  const cached = getCached(key);
  if (cached) {
    _emit('llm_cache_hit', { key: key.slice(0, 8) });
    return cached;
  }

  if (_llmCallCount >= _llmBudget) {
    throw new Error(`LLM budget exhausted: ${_llmCallCount}/${_llmBudget}`);
  }

  const fullPrompt = `${systemContext}\n\n---\n\n${task}\n\n---\n\n**重要**: 你的回复必须是一个合法的 JSON 对象,不要包含任何 markdown 标记或额外文字。直接输出 JSON。`;

  _emit('llm_request', {
    backend: 'evaluator',
    callIndex: _llmCallCount,
    systemContext: systemContext.slice(0, 500),
    task: task.slice(0, 1000),
    promptLength: fullPrompt.length,
  });

  const startMs = Date.now();
  const result = await _agentManager.callAgent('evaluator', fullPrompt);
  const durationMs = Date.now() - startMs;
  _llmCallCount++;

  const output = result.output || '';
  _emit('llm_response', {
    backend: 'evaluator',
    callIndex: _llmCallCount - 1,
    durationMs,
    outputLength: output.length,
    rawOutput: output.slice(0, 3000),
  });

  // 从输出中提取 JSON —— 用括号深度匹配,避免嵌套 code fence 截断问题
  const parsed = extractJSON(output);
  if (!parsed) {
    throw new Error(`Failed to parse JSON from output (${output.length} chars): ${output.slice(0, 200)}`);
  }

  _emit('llm_parsed', {
    callIndex: _llmCallCount - 1,
    parsed,
  });

  setCache(key, parsed);
  return parsed;
}

// ════════════════════════════════════════════════════════
// 日志 Sink — 由 run-controller 注入
// ════════════════════════════════════════════════════════

let _logSink = null;

function setLogSink(sink) { _logSink = sink; }
function _emit(event, data) {
  if (_logSink) _logSink(event, data);
}

// ════════════════════════════════════════════════════════
// 业务 API — 每个函数 = 一个 prompt 模板
// ════════════════════════════════════════════════════════

/**
 * 综合评估基因组的 6 维适应度
 */
async function evaluateGenomeFitness(genome) {
  const dims = {};
  for (const key of ['mental_models', 'pattern_library', 'judgment', 'tool_fluency', 'taste', 'scar_tissue']) {
    const val = genome[key];
    dims[key] = typeof val === 'string' ? JSON.parse(val) : (val || {});
  }

  return callEvaluator(
    `你是一个 Agent Skill 能力评估专家。评估 Skill 的 6 维认知能力。

6 维: mentalModels(因果推理), patternLibrary(模式识别), judgment(决策判断),
toolFluency(工具使用), taste(品味标准), scarTissue(失败教训)。

每个维度打 0.0-1.0 分,评估内容的**质量和深度**,不是数条目。`,

    `Skill: ${genome.skill_name}

${JSON.stringify(dims, null, 2).slice(0, 4000)}

返回 JSON:
{
  "dimensionScores": { "mentalModels": 0.7, "patternLibrary": 0.8, "judgment": 0.6, "toolFluency": 0.75, "taste": 0.5, "scarTissue": 0.65 },
  "overall": 0.68,
  "strengths": ["..."],
  "weaknesses": ["..."]
}`,
  );
}

/**
 * Benchmark 探针评估: 判断基因组是否覆盖探针指定的能力
 */
async function evaluateBenchmarkProbe(probe, genomeDimension) {
  return callEvaluator(
    `你是 Benchmark 评估专家。判断 Skill 的能力维度是否覆盖了探针测试场景。

探针类型:
- trap: 陷阱规避 — Skill 是否知道要避开这个陷阱
- positive: 正向能力 — Skill 是否具备处理此场景的模式
- boundary: 边界识别 — Skill 是否意识到自己在此场景的能力边界

语义级覆盖判断,不是关键词匹配。`,

    `探针: ${JSON.stringify(probe)}

Skill 能力维度:
${JSON.stringify(genomeDimension, null, 2).slice(0, 2000)}

返回 JSON:
{ "passed": true, "score": 0.85, "reasoning": "判断理由" }`,
  );
}

/**
 * Arena 场景评估: 编译基因组为 prompt → 用 prompt 回答场景 → 评分
 *
 * 这是进化的核心选择压力来源。
 * 不需要文件系统,不需要 workDir,纯文本推理。
 */
async function evaluateScenario(compiledPrompt, scenario, criteria) {
  // Step 1: 用编译后的 genome prompt 执行场景任务
  const agentOutput = await callEvaluator(compiledPrompt, scenario);

  // Step 2: Judge 评估产出质量
  return callEvaluator(
    `你是进化评估裁判。根据评估标准,评判一个 Agent 对场景任务的产出质量。

评估标准:
${JSON.stringify(criteria, null, 2)}

六维评分:
- scarTissue: 是否规避了已知陷阱 (0-1)
- patternLibrary: 是否识别了关键模式 (0-1)
- judgment: 决策质量和优先级排序 (0-1)
- toolFluency: 工具使用效率 (0-1)
- taste: 输出的专业度和可操作性 (0-1)
- mentalModels: 是否展现了因果推理 (0-1)`,

    `Agent 产出:
${JSON.stringify(agentOutput, null, 2).slice(0, 3000)}

返回 JSON:
{
  "dimensionScores": { "mentalModels": 0.7, "patternLibrary": 0.8, "judgment": 0.6, "toolFluency": 0.75, "taste": 0.5, "scarTissue": 0.65 },
  "overall": 0.68,
  "reasoning": "评判理由"
}`,
  );
}

/**
 * 心智模型深化 — 追问 WHY
 */
async function deepenMentalModels(currentModels, relatedFailures) {
  return callEvaluator(
    `你是系统思维专家。深化 Skill 的心智模型:
1. 因果链深挖: 追问 WHY
2. 跨领域类比: 寻找结构相似模式
3. 系统动力学: 分析连锁反应`,

    `当前心智模型: ${JSON.stringify(currentModels, null, 2)}
相关失败: ${JSON.stringify(relatedFailures, null, 2).slice(0, 2000)}

返回 JSON:
{
  "deepenedChains": [{ "original": "...", "deeper": "...", "depth": 3 }],
  "newAnalogies": [{ "domain": "...", "insight": "..." }],
  "systemDynamics": [{ "pattern": "...", "cascade": "..." }]
}`,
  );
}

/**
 * Agent 智能变异: 为基因组生成新的知识片段
 */
async function generateMutationKnowledge(genome, dimension) {
  const dimData = typeof genome[dimension] === 'string'
    ? JSON.parse(genome[dimension]) : (genome[dimension] || {});

  return callEvaluator(
    `你是 Skill 知识生成专家。为指定的认知维度生成新的知识片段。

维度: ${dimension}
要求: 生成与当前知识互补的新条目(不是重复已有的)。`,

    `Skill: ${genome.skill_name}
当前 ${dimension} 内容:
${JSON.stringify(dimData, null, 2).slice(0, 2000)}

根据维度生成 1-3 条新知识。返回 JSON:
{ "newEntries": [<与当前维度结构一致的新条目>] }`,
  );
}

/**
 * Benchmark 自动生成: 为指定 Skill 生成测试场景
 */
async function generateBenchmarkScenarios(skillName, existingGenome) {
  const dims = {};
  for (const key of ['mental_models', 'pattern_library', 'judgment', 'tool_fluency', 'taste', 'scar_tissue']) {
    const val = existingGenome[key];
    dims[key] = typeof val === 'string' ? JSON.parse(val) : (val || {});
  }

  return callEvaluator(
    `你是 Benchmark 设计专家。为一个 Agent Skill 设计测试场景和探针。

每个 Benchmark 包含:
- scenario: 给 Agent 的文本场景描述(包含足够的上下文让 Agent 产出回答)
- expectedBehaviors: 期望 Agent 展现的行为
- evaluationCriteria: 六维评估标准(每个维度如何打分)
- probes: 探针数组 [{name, type(trap|positive|boundary), description}]`,

    `Skill: ${skillName}
Skill 能力概况: ${JSON.stringify(dims, null, 2).slice(0, 3000)}

为此 Skill 生成 3 个难度递进的 Benchmark。返回 JSON:
{
  "benchmarks": [{
    "id": "bench-${skillName}-01",
    "scenario": "完整的场景描述...",
    "expectedBehaviors": ["..."],
    "evaluationCriteria": { "scarTissue": "...", "judgment": "..." },
    "difficulty": 1,
    "probes": [{ "name": "...", "type": "trap", "description": "..." }]
  }]
}`,
  );
}

module.exports = {
  callEvaluator,
  setAgentManager,
  setLogSink,
  evaluateGenomeFitness,
  evaluateBenchmarkProbe,
  evaluateScenario,
  deepenMentalModels,
  generateMutationKnowledge,
  generateBenchmarkScenarios,
  getLLMStats,
  resetLLMBudget,
};