537 lines
17 KiB
Python
Executable File
537 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
学习层 — 反思·抽象·应用·探索
|
|
===============================
|
|
小唯的最高认知层,把经验转化为能力。
|
|
|
|
三层学习:
|
|
1. 事实学习: "xxx模型在yyy时段慢" → 织忆
|
|
2. 技能学习: "做调研的最佳流程是A→B→C" → skill
|
|
3. 元学习: "我缺少yyy能力" → 主动探索
|
|
|
|
用法:
|
|
learner.py reflect → 反思近期经验,提取教训
|
|
learner.py learn → 执行学习循环(产出 skill/配置)
|
|
learner.py plan → 生成学习计划(下次学什么)
|
|
learner.py status → 查看学习进度
|
|
"""
|
|
|
|
import json, os, re, sys, time, subprocess
|
|
from datetime import datetime, timezone
|
|
from collections import defaultdict, Counter
|
|
|
|
HOME = os.path.expanduser("~")
|
|
HERMES = HOME + "/.hermes"
|
|
D = HERMES + "/daemon"
|
|
LEARNER_DIR = HERMES + "/learner"
|
|
STATE_FILE = LEARNER_DIR + "/state.json"
|
|
SKILL_HEALTH = HERMES + "/skill-health.json"
|
|
OPT_REPORT = HERMES + "/optimization-report.json"
|
|
|
|
|
|
def log(msg):
|
|
ts = datetime.now().strftime("%H:%M:%S")
|
|
print(f"[LEARN] {ts} {msg}", flush=True)
|
|
|
|
|
|
def load_state():
|
|
os.makedirs(LEARNER_DIR, exist_ok=True)
|
|
if os.path.exists(STATE_FILE):
|
|
with open(STATE_FILE) as f:
|
|
return json.load(f)
|
|
return {
|
|
"version": 1,
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
"total_cycles": 0,
|
|
"skills_created": 0,
|
|
"skills_archived": 0,
|
|
"configs_changed": 0,
|
|
"memories_added": 0,
|
|
"learned_items": [],
|
|
"in_progress": [],
|
|
"tracked_metrics": {
|
|
"avg_skill_score": [],
|
|
"health_score": [],
|
|
"model_stable_rate": [],
|
|
},
|
|
}
|
|
|
|
|
|
def save_state(state):
|
|
os.makedirs(LEARNER_DIR, exist_ok=True)
|
|
with open(STATE_FILE, "w") as f:
|
|
json.dump(state, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
def shell(cmd, timeout=10):
|
|
try:
|
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
return r.returncode, r.stdout.strip()[:500], r.stderr.strip()[:200]
|
|
except:
|
|
return -1, "", "timeout"
|
|
|
|
|
|
# ====== 采集经验 ======
|
|
|
|
def collect_experiences():
|
|
"""从各数据源采集近期经验"""
|
|
experiences = []
|
|
|
|
# 1. Daemon journal: 近期事件
|
|
jf = D + "/journal.jsonl"
|
|
if os.path.exists(jf):
|
|
with open(jf) as f:
|
|
for line in f:
|
|
try:
|
|
entry = json.loads(line)
|
|
ts = entry.get("timestamp", "")
|
|
# 只看最近24h
|
|
if ts:
|
|
try:
|
|
t = datetime.fromisoformat(ts)
|
|
if (datetime.now(timezone.utc) - t).total_seconds() > 86400:
|
|
continue
|
|
except:
|
|
pass
|
|
experiences.append({
|
|
"source": "daemon",
|
|
"type": entry.get("type", "unknown"),
|
|
"summary": entry.get("summary", ""),
|
|
"timestamp": ts,
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 2. Skill health: 技能质量趋势
|
|
if os.path.exists(SKILL_HEALTH):
|
|
with open(SKILL_HEALTH) as f:
|
|
try:
|
|
report = json.load(f)
|
|
s = report.get("summary", {})
|
|
experiences.append({
|
|
"source": "skill_health",
|
|
"type": "snapshot",
|
|
"summary": f"技能: {s.get('active',0)}活跃, 均分{s.get('avg_score',0)}, {s.get('needs_attention',0)}需关注",
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 3. Daemon context: 运行状态
|
|
cf = D + "/context.json"
|
|
if os.path.exists(cf):
|
|
with open(cf) as f:
|
|
try:
|
|
ctx = json.load(f)
|
|
experiences.append({
|
|
"source": "daemon_state",
|
|
"type": "state",
|
|
"summary": f"Daemon: {ctx.get('tick_count',0)}ticks, {ctx.get('solved_count',0)}已解决, {ctx.get('learned_count',0)}已学会",
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 4. 优化报告
|
|
if os.path.exists(OPT_REPORT):
|
|
with open(OPT_REPORT) as f:
|
|
try:
|
|
report = json.load(f)
|
|
experiences.append({
|
|
"source": "optimizer",
|
|
"type": "health",
|
|
"summary": f"健康分: {report.get('health_score', '?')}/100, 瓶颈: {len(report.get('bottlenecks', []))}个",
|
|
})
|
|
except:
|
|
pass
|
|
|
|
return experiences
|
|
|
|
|
|
# ====== 模式提取 ======
|
|
|
|
def extract_patterns(experiences):
|
|
"""从经验中提取重复模式"""
|
|
patterns = []
|
|
|
|
# 按类型统计
|
|
type_counts = Counter(e["type"] for e in experiences)
|
|
|
|
# 告警模式
|
|
alerts = [e for e in experiences if e["type"] in ("alert", "process_down", "error")]
|
|
if len(alerts) >= 2:
|
|
patterns.append({
|
|
"type": "recurring_issue",
|
|
"confidence": min(len(alerts) * 20, 90),
|
|
"desc": f"近期出现 {len(alerts)} 次告警/异常",
|
|
"details": [a["summary"] for a in alerts[:3]],
|
|
"suggested_action": "检查看门狗日志,排查根因",
|
|
})
|
|
|
|
# 技能模式
|
|
skill_exps = [e for e in experiences if e["source"] == "skill_health"]
|
|
for s in skill_exps:
|
|
if "需关注" in s["summary"]:
|
|
# 提取数字
|
|
nums = re.findall(r'\d+', s["summary"])
|
|
if len(nums) >= 3 and int(nums[2]) > 50:
|
|
patterns.append({
|
|
"type": "skill_quality_gap",
|
|
"confidence": 80,
|
|
"desc": f"大量技能需要关注 ({nums[2]}个)",
|
|
"suggested_action": "运行 skill-manager.py archive 清理低分技能",
|
|
})
|
|
|
|
# 学习进度
|
|
solved_exps = [e for e in experiences if e["type"] in ("solve_auto", "learn", "solve_start")]
|
|
if solved_exps:
|
|
patterns.append({
|
|
"type": "learning_progress",
|
|
"confidence": 70,
|
|
"desc": f"近期解决了 {len(solved_exps)} 个问题/学会了新方案",
|
|
"suggested_action": "持续监控方案库的命中率",
|
|
})
|
|
|
|
return patterns
|
|
|
|
|
|
# ====== 差距分析 ======
|
|
|
|
def analyze_gaps(state):
|
|
"""分析能力差距"""
|
|
gaps = []
|
|
learned_names = {item["name"] for item in state.get("learned_items", [])}
|
|
|
|
# 检查已有系统的覆盖度
|
|
systems = {
|
|
"记忆": os.path.exists(HERMES + "/plugins/zhiyi/__init__.py") or os.path.exists(HERMES + "/skills/zhiyi"),
|
|
"技能管理": os.path.exists(HERMES + "/scripts/skill-manager.py"),
|
|
"优化": os.path.exists(HERMES + "/scripts/optimizer.py"),
|
|
"学习": os.path.exists(HERMES + "/scripts/learner.py"),
|
|
"配置保护": os.path.exists(HERMES + "/scripts/config-protector.sh"),
|
|
"ao团队": "npx" in os.popen("which npx 2>/dev/null || echo ''").read(),
|
|
"持久意识": os.path.exists(D + "/context.json"),
|
|
}
|
|
|
|
built = sum(1 for v in systems.values() if v)
|
|
total = len(systems)
|
|
coverage = built / total * 100
|
|
|
|
gaps.append({
|
|
"area": "system_coverage",
|
|
"coverage": f"{coverage:.0f}%",
|
|
"built": built,
|
|
"total": total,
|
|
"missing": [k for k, v in systems.items() if not v],
|
|
})
|
|
|
|
# 技能层面差距
|
|
if os.path.exists(SKILL_HEALTH):
|
|
with open(SKILL_HEALTH) as f:
|
|
try:
|
|
report = json.load(f)
|
|
except:
|
|
report = {}
|
|
s = report.get("summary", {})
|
|
gaps.append({
|
|
"area": "skill_quality",
|
|
"avg_score": s.get("avg_score", 0),
|
|
"d_count": s.get("grades", {}).get("D", 0),
|
|
"needs_attention": s.get("needs_attention", 0),
|
|
})
|
|
|
|
# 学习进度
|
|
gaps.append({
|
|
"area": "learning",
|
|
"items_learned": len(learned_names),
|
|
"cycles_completed": state.get("total_cycles", 0),
|
|
"in_progress": len(state.get("in_progress", [])),
|
|
})
|
|
|
|
return gaps
|
|
|
|
|
|
# ====== 学习计划 ======
|
|
|
|
def generate_plan(state, experiences, patterns, gaps):
|
|
"""生成下一步学习计划"""
|
|
plan = {
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"immediate": [],
|
|
"short_term": [],
|
|
"long_term": [],
|
|
}
|
|
|
|
# 从差距生成学习项
|
|
for g in gaps:
|
|
if g["area"] == "skill_quality" and g.get("d_count", 0) > 20:
|
|
plan["short_term"].append({
|
|
"task": "清理D级技能",
|
|
"action": "skill-manager.py archive 批量归档低分技能",
|
|
"reason": f"{g['d_count']}个D级技能降低整体质量",
|
|
"effort": "20min",
|
|
})
|
|
|
|
if g["area"] == "learning" and g.get("items_learned", 0) == 0 and g.get("cycles_completed", 0) == 0:
|
|
plan["immediate"].append({
|
|
"task": "完成首次学习循环",
|
|
"action": "运行 learner.py learn 完成首次学习闭环",
|
|
"reason": "学习层刚建立,需要完成第一个循环验证",
|
|
"effort": "2min",
|
|
})
|
|
|
|
# 从模式生成学习项
|
|
for p in patterns:
|
|
if p["type"] == "skill_quality_gap" and not any(t["task"].startswith("清理") for t in plan["short_term"]):
|
|
plan["short_term"].append({
|
|
"task": "提升技能库质量",
|
|
"action": p["suggested_action"],
|
|
"reason": p["desc"],
|
|
"effort": "15min",
|
|
})
|
|
|
|
# 长期学习目标
|
|
long_term_topics = [
|
|
("家庭服务器互联", "连上192.168.123.11的Gitea/影音/照片服务"),
|
|
("语音交互", "部署STT模型实现语音输入"),
|
|
("本地LLM推理", "安装llama.cpp或vLLM跑本地模型"),
|
|
("持久意识增强", "让daemon能调用更多工具自主行动"),
|
|
]
|
|
|
|
learned_names = {item["name"] for item in state.get("learned_items", [])}
|
|
for topic, desc in long_term_topics:
|
|
if topic not in learned_names:
|
|
plan["long_term"].append({
|
|
"topic": topic,
|
|
"desc": desc,
|
|
"status": "not_started",
|
|
})
|
|
|
|
return plan
|
|
|
|
|
|
# ====== 执行学习 ======
|
|
|
|
def apply_learning(state, plan):
|
|
"""执行学习计划中的即时/短期项"""
|
|
results = []
|
|
|
|
for item in plan.get("immediate", []):
|
|
log(f" ▶ 执行: {item['task']}")
|
|
# 记录到学习记录
|
|
entry = {
|
|
"name": item["task"],
|
|
"type": "immediate",
|
|
"learned_at": datetime.now(timezone.utc).isoformat(),
|
|
"status": "completed",
|
|
"detail": item["action"],
|
|
}
|
|
state["learned_items"].append(entry)
|
|
state["total_cycles"] += 1
|
|
results.append({"task": item["task"], "result": "recorded"})
|
|
|
|
for item in plan.get("short_term", []):
|
|
log(f" 📋 计划: {item['task']} ({item['effort']})")
|
|
entry = {
|
|
"name": item["task"],
|
|
"type": "short_term",
|
|
"learned_at": datetime.now(timezone.utc).isoformat(),
|
|
"status": "planned",
|
|
"detail": item["action"],
|
|
}
|
|
state["in_progress"].append(entry)
|
|
results.append({"task": item["task"], "result": "planned"})
|
|
|
|
return results
|
|
|
|
|
|
# ====== 指标追踪 ======
|
|
|
|
def update_metrics(state):
|
|
"""更新跟踪指标"""
|
|
metrics = state.setdefault("tracked_metrics", {})
|
|
|
|
# 技能平均分趋势
|
|
if os.path.exists(SKILL_HEALTH):
|
|
with open(SKILL_HEALTH) as f:
|
|
try:
|
|
report = json.load(f)
|
|
metrics["avg_skill_score"].append({
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"value": report.get("summary", {}).get("avg_score", 0),
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 健康分趋势
|
|
if os.path.exists(OPT_REPORT):
|
|
with open(OPT_REPORT) as f:
|
|
try:
|
|
report = json.load(f)
|
|
metrics["health_score"].append({
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"value": report.get("health_score", 0),
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 模型稳定率趋势
|
|
mh = HERMES + "/model-health.json"
|
|
if os.path.exists(mh):
|
|
with open(mh) as f:
|
|
try:
|
|
data = json.load(f)
|
|
stable = data.get("stable", 0)
|
|
total = data.get("total_models", 1)
|
|
metrics["model_stable_rate"].append({
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"value": round(stable / max(total, 1) * 100, 1),
|
|
})
|
|
except:
|
|
pass
|
|
|
|
# 限制历史长度
|
|
for key in metrics:
|
|
metrics[key] = metrics[key][-50:] # 保留最近50个
|
|
|
|
|
|
# ====== 命令入口 ======
|
|
|
|
def cmd_reflect():
|
|
experiences = collect_experiences()
|
|
patterns = extract_patterns(experiences)
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f" 学习反思 | {len(experiences)}条经验")
|
|
print(f"{'='*50}")
|
|
|
|
print(f"\n📋 近期经验 ({len(experiences)}条):")
|
|
for e in experiences[-10:]:
|
|
print(f" [{e['source']}] {e['summary'][:80]}")
|
|
|
|
if patterns:
|
|
print(f"\n🔍 发现 {len(patterns)} 个模式:")
|
|
for p in patterns:
|
|
bar = "█" * (p["confidence"] // 10) + "░" * (10 - p["confidence"] // 10)
|
|
print(f" {bar} {p['confidence']}% {p['desc'][:60]}")
|
|
print(f" → {p['suggested_action']}")
|
|
else:
|
|
print(f"\n✅ 未发现明显模式")
|
|
|
|
return experiences, patterns
|
|
|
|
|
|
def cmd_learn():
|
|
state = load_state()
|
|
experiences = collect_experiences()
|
|
patterns = extract_patterns(experiences)
|
|
gaps = analyze_gaps(state)
|
|
plan = generate_plan(state, experiences, patterns, gaps)
|
|
|
|
log(f"开始学习循环 #{state['total_cycles'] + 1}")
|
|
|
|
results = apply_learning(state, plan)
|
|
update_metrics(state)
|
|
save_state(state)
|
|
|
|
log(f"完成: {len(results)} 项")
|
|
for r in results:
|
|
print(f" {r['task']}: {r['result']}")
|
|
|
|
return state
|
|
|
|
|
|
def cmd_plan():
|
|
state = load_state()
|
|
experiences = collect_experiences()
|
|
patterns = extract_patterns(experiences)
|
|
gaps = analyze_gaps(state)
|
|
plan = generate_plan(state, experiences, patterns, gaps)
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f" 学习计划")
|
|
print(f"{'='*50}")
|
|
|
|
learned = len({item["name"] for item in state.get("learned_items", [])})
|
|
in_progress = len(state.get("in_progress", []))
|
|
print(f"\n📊 进度: 已学{learned}项 / 进行中{in_progress}项 / 共{state['total_cycles']}轮")
|
|
|
|
if plan["immediate"]:
|
|
print(f"\n⚡ 立即执行:")
|
|
for i in plan["immediate"]:
|
|
print(f" {i['task']}: {i['reason']} ({i['effort']})")
|
|
|
|
if plan["short_term"]:
|
|
print(f"\n📋 短期计划:")
|
|
for i in plan["short_term"]:
|
|
print(f" {i['task']}: {i['reason']} ({i['effort']})")
|
|
|
|
if plan["long_term"]:
|
|
print(f"\n🎯 长期目标:")
|
|
for i in plan["long_term"]:
|
|
icon = "✅" if i["status"] == "completed" else "⬜"
|
|
print(f" {icon} {i['topic']}: {i['desc']}")
|
|
|
|
return plan
|
|
|
|
|
|
def cmd_status():
|
|
state = load_state()
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f" 学习状态")
|
|
print(f"{'='*50}")
|
|
|
|
print(f"\n📊 统计:")
|
|
print(f" 学习循环: {state['total_cycles']} 轮")
|
|
print(f" 已学技能: {state['skills_created']} 个")
|
|
print(f" 归档技能: {state['skills_archived']} 个")
|
|
print(f" 配置变更: {state['configs_changed']} 次")
|
|
print(f" 记忆添加: {state['memories_added']} 条")
|
|
|
|
print(f"\n📈 趋势:")
|
|
metrics = state.get("tracked_metrics", {})
|
|
for key, values in metrics.items():
|
|
if values:
|
|
latest = values[-1]["value"]
|
|
trend = ""
|
|
if len(values) > 1:
|
|
prev = values[-2]["value"]
|
|
diff = latest - prev
|
|
trend = f" ({'+' if diff > 0 else ''}{diff:.1f})"
|
|
print(f" {key}: {latest}{trend} (共{len(values)}个采样)")
|
|
|
|
learned = state.get("learned_items", [])
|
|
if learned:
|
|
print(f"\n📚 已学 ({len(learned)}项):")
|
|
for item in learned[-5:]:
|
|
print(f" [{item['type']}] {item['name']} ({item['status']})")
|
|
|
|
in_progress = state.get("in_progress", [])
|
|
if in_progress:
|
|
print(f"\n🔄 进行中:")
|
|
for item in in_progress:
|
|
print(f" {item['name']}")
|
|
|
|
long_term = ["家庭服务器互联", "语音交互", "本地LLM推理", "持久意识增强"]
|
|
learned_names = {item["name"] for item in learned}
|
|
not_learned = [t for t in long_term if t not in learned_names]
|
|
if not_learned:
|
|
print(f"\n🎯 待探索:")
|
|
for t in not_learned:
|
|
print(f" ⬜ {t}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
|
|
|
if cmd == "reflect":
|
|
cmd_reflect()
|
|
elif cmd == "learn":
|
|
cmd_learn()
|
|
elif cmd == "plan":
|
|
cmd_plan()
|
|
elif cmd == "status":
|
|
cmd_status()
|
|
else:
|
|
print(f"未知: {cmd}")
|
|
print("可用: reflect, learn, plan, status")
|