Files
BaiLongma/src/memory/focus-classifier.js
xiaoyuanda666-ship-it b5dd8632c4 🎉 V3.0.0 发布 - 自进化数字意识框架
核心升级:
- 自进化管道:check→scan→evaluate→integrate→reflect 五相位自动闭环
- evo_loop 后台进程,无需手动触发
- consciousness 意识持久化
- ACUI 卡片组件系统
- MCP 工具生态扩展至50+工具
- 技能体系重构,4个活跃技能
- 身份升级为自由体
2026-05-24 22:06:37 +08:00

221 lines
8.5 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.
// Focus event 分类器 —— 动态上下文记忆池架构第 5b 步v1 LLM 语义判断)
//
// 角色v0 启发式ngram + 字面交集)跑在前面,本模块只在「栈结构会变化」时被叫起来
// pushed / returned用 LLM 仲裁校验 + 重写 topic 关键词。
//
// 设计要点:
// - 800ms 硬超时(参考 injector.js embedding 兜底LLM 慢一拍就回退 v0
// - 失败必须降级 —— 解析失败、超时、abort、配额限流都返回 null 让上层用 v0
// - 不依赖 SQLite / 上游状态 / 当前 process —— 纯函数可单元测试callLLM 可 stub
// - 不修改 state不发事件不写 db返回值由上层 focus.js 应用
//
// 来自 DynamicMemoryPool.md 7.4「对话动作类型」:
// kept / pushed / returned / leaf 是对话级动作;本模块按这四类输出 action。
// leaf = 一次性短问,不动栈;映射到 v0 的 noop不创建新帧也不深化栈顶
const CLASSIFIER_TIMEOUT_MS = 800
const CLASSIFIER_MAX_TOKENS = 120
const CLASSIFIER_TEMPERATURE = 0.2
const SYSTEM_PROMPT = `焦点分类器。保守判 kept不轻易 push。
重叠度:高=对象同→kept中=同域不同子任务→pushed低=异域→pushed/leaf。
kept栈顶重叠高且细化/追问/承诺/确认。
pushed与所有帧重叠低且持续性新任务。
returned与非栈顶旧帧重叠高且明确回指depth=该帧索引,栈顶=length-1
leaf无承接且一次性短问/闲聊(不动栈)。
例 A [前端 React]+"写个 Hook"→kept
例 B [DB 查询]+"现在网速咋样"→leaf
例 C [配置→部署→监控]+"回头看最初配置"→returned d=0
topic 写 2-3 个语义词非 ngram。只输 JSON。`
// 把当前栈渲染成简短字符串:[栈底"a, b" → "c, d" → 栈顶"e, f"]
function describeStack(stack) {
if (!Array.isArray(stack) || stack.length === 0) return '[空栈]'
const parts = stack.map((f, i) => {
const topic = Array.isArray(f?.topic) ? f.topic.join(', ') : String(f?.topic || '')
const conclusions = Array.isArray(f?.conclusions) && f.conclusions.length > 0
? `(结论: ${f.conclusions[f.conclusions.length - 1]}`
: ''
const tag = i === 0 ? '栈底' : (i === stack.length - 1 ? '栈顶' : `${i}`)
return `${tag}"${topic}"${conclusions}`
})
return '[' + parts.join(' → ') + ']'
}
// 构造用户输入文本
function buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack }) {
const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(', ') : String(v0Topic || '')
const stackStr = describeStack(currentStack)
const lengthHint = currentStack?.length ? `栈深=${currentStack.length},栈顶索引=${currentStack.length - 1}` : '栈深=0'
// newMessage 截断到 400 字,省 token 也减少打架风险
const msg = String(newMessage || '').slice(0, 400)
return [
`v0 判定 = ${v0Event},候选 topic = [${v0TopicStr}]`,
`当前栈(${lengthHint} = ${stackStr}`,
`新消息 = "${msg}"`,
'',
'请输出 JSON{"action": "kept|pushed|returned|leaf", "topic_refined": ["词1","词2","词3"], "returns_to_depth": 0}',
'returns_to_depth 仅 returned 时有值;其他动作填 -1 或省略)',
].join('\n')
}
// 提取 LLM 文本中的 JSON 对象。容忍 ```json 包裹、前后多余文字。
function parseClassifierJson(text) {
if (!text || typeof text !== 'string') return null
// 去掉 <think> 块(如果模型把思考也输出了)
let body = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
// 去掉 ```json ... ``` 围栏
const fenceMatch = body.match(/```(?:json)?\s*([\s\S]*?)```/)
if (fenceMatch) body = fenceMatch[1].trim()
// 找第一个 { 到最后一个 }
const first = body.indexOf('{')
const last = body.lastIndexOf('}')
if (first < 0 || last <= first) return null
const jsonStr = body.slice(first, last + 1)
try {
return JSON.parse(jsonStr)
} catch {
return null
}
}
// 校验 + 规范化 LLM 返回的 JSON
function normalizeClassifierResult(raw, currentStack) {
if (!raw || typeof raw !== 'object') return null
const action = String(raw.action || '').toLowerCase().trim()
if (!['kept', 'pushed', 'returned', 'leaf'].includes(action)) return null
let topic = []
if (Array.isArray(raw.topic_refined)) {
topic = raw.topic_refined
.map(t => String(t || '').trim())
.filter(t => t.length > 0 && t.length <= 32)
.slice(0, 3)
}
let returnsToDepth = -1
if (action === 'returned') {
const d = Number.isInteger(raw.returns_to_depth) ? raw.returns_to_depth : -1
const stackLen = Array.isArray(currentStack) ? currentStack.length : 0
if (d < 0 || d >= stackLen) {
// returned 但深度非法 → 视为不合理,拒掉
return null
}
returnsToDepth = d
}
return { action, topic, returnsToDepth }
}
/**
* 调 LLM 仲裁 focus 事件。
*
* @param {object} args
* @param {string} args.newMessage - 当前用户消息正文
* @param {string} args.v0Event - v0 启发式判定的 eventpushed / returned
* @param {string[]} args.v0Topic - v0 抽出的候选 topic 关键词
* @param {object[]} args.currentStack - 当前 focus 栈快照(不会被修改)
* @param {AbortSignal} [args.signal] - 上层 abort 信号
* @returns {Promise<{action:'kept'|'pushed'|'returned'|'leaf', topic:string[], returnsToDepth:number} | null>}
* 返回 null 表示「失败 / 超时 / 解析不出来」,让上层回退到 v0。
*/
export async function classifyFocusEvent({
newMessage,
v0Event,
v0Topic,
currentStack,
signal,
} = {}) {
// 边界保护
if (!newMessage || typeof newMessage !== 'string') return null
if (signal?.aborted) return null
const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(',') : String(v0Topic || '')
const tag = `[focus-classifier] v0=${v0Event} topic=[${v0TopicStr}]`
// 动态 import callLLM —— 跟 injector.js 同款,避免在测试环境/早期模块加载时拉起一切
let callLLM
try {
const llm = await import('../llm.js')
callLLM = llm.callLLM
} catch (e) {
console.log(`${tag} → llm.js import 失败 (${e?.message || 'unknown'}) → 回退 v0`)
return null
}
if (typeof callLLM !== 'function') {
console.log(`${tag} → callLLM 不是函数 → 回退 v0`)
return null
}
const userPrompt = buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack })
const t0 = Date.now()
// 800ms 硬超时 + LLM 调用赛跑
let timeoutHandle = null
const timeoutPromise = new Promise(resolve => {
timeoutHandle = setTimeout(() => resolve({ __timeout: true }), CLASSIFIER_TIMEOUT_MS)
})
let result
try {
result = await Promise.race([
callLLM({
systemPrompt: SYSTEM_PROMPT,
message: userPrompt,
temperature: CLASSIFIER_TEMPERATURE,
thinking: false,
tools: [],
maxTokens: CLASSIFIER_MAX_TOKENS,
mustReply: false,
signal,
}),
timeoutPromise,
])
} catch (e) {
if (timeoutHandle) clearTimeout(timeoutHandle)
const dt = Date.now() - t0
console.log(`${tag} → LLM 抛错 (${dt}ms, ${e?.message || 'unknown'}) → 回退 v0`)
return null
}
if (timeoutHandle) clearTimeout(timeoutHandle)
const dt = Date.now() - t0
if (!result || result.__timeout) {
console.log(`${tag} → LLM 超时 (${CLASSIFIER_TIMEOUT_MS}ms 硬超时, 实际 ${dt}ms) → 回退 v0`)
return null
}
if (result.aborted) {
console.log(`${tag} → LLM aborted (${dt}ms) → 回退 v0`)
return null
}
const content = typeof result === 'string' ? result : (result.content || '')
const preview = String(content).replace(/\s+/g, ' ').slice(0, 200)
const raw = parseClassifierJson(content)
if (!raw) {
console.log(`${tag} → LLM 返回 (${dt}ms) 但 JSON 解析失败 raw="${preview}" → 回退 v0`)
return null
}
const normalized = normalizeClassifierResult(raw, currentStack)
if (!normalized) {
console.log(`${tag} → LLM 返回 (${dt}ms) action=${raw.action} 但 normalize 拒掉 (非法 action 或越界 depth) raw="${preview}" → 回退 v0`)
return null
}
const refinedStr = normalized.topic.join(',')
const depthStr = normalized.action === 'returned' ? ` d=${normalized.returnsToDepth}` : ''
console.log(`${tag} → llm=${normalized.action}${depthStr} (${dt}ms) refined=[${refinedStr}] ok`)
return normalized
}
// 暴露内部辅助函数,便于测试
export const __internal = {
describeStack,
buildUserPrompt,
parseClassifierJson,
normalizeClassifierResult,
SYSTEM_PROMPT,
CLASSIFIER_TIMEOUT_MS,
}