xiaowei-system/scripts/learner.py

593 lines
20 KiB
Python
Executable File
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.

#!/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):
"""从经验中提取重复模式
门禁书中第8章方法论: 只有支持度≥2 且无冲突的模式才升级为知识。
单例观察support=1不入 patterns避免单次噪声误写知识库。
"""
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",
"support_count": len(alerts),
"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",
"support_count": len(skill_exps),
"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",
"support_count": len(solved_exps),
"confidence": 70,
"desc": f"近期解决了 {len(solved_exps)} 个问题/学会了新方案",
"suggested_action": "持续监控方案库的命中率",
})
# 通用归纳门禁: 同类型 experience 支持度 ≥2 才保留书中第8章: 支持≥2且无冲突才升级
# 单例观察(如一次性错误)不入 patterns
gated = []
for p in patterns:
support = p.get("support_count", type_counts.get(p.get("type", ""), 1))
p["support_count"] = support
if support >= 2:
gated.append(p)
else:
log(f" ⏳ 门禁拦截: {p['type']} 支持度={support} <2不升级为知识")
patterns = gated
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)
# 达标模式 → 织忆书中第8章: 支持度≥2 的模式才升级为知识并持久化)
zhiyi_written = 0
for p in patterns:
if p.get("support_count", 0) >= 2:
ok = write_pattern_to_zhiyi(p)
if ok:
zhiyi_written += 1
if zhiyi_written:
log(f"📝 已将 {zhiyi_written} 个达标模式写入织忆")
else:
log("无新增达标模式写入织忆")
update_metrics(state)
save_state(state)
log(f"完成: {len(results)}")
for r in results:
print(f" {r['task']}: {r['result']}")
return state
def write_pattern_to_zhiyi(pattern):
"""把达标模式写入织忆 commit API"""
try:
import urllib.request, json as _json
content = f"[learner模式] {pattern.get('desc', '')} | 建议: {pattern.get('suggested_action', '')} | 支持度: {pattern.get('support_count', 0)}"
body = _json.dumps({
"agent_id": "learner",
"content": content,
"category": "distilled",
"source": "learner",
}).encode()
req = urllib.request.Request(
"http://localhost:7821/api/v1/commit",
data=body,
headers={"Content-Type": "application/json", "X-API-Key": "zhiyi-dev-key-2026"},
)
with urllib.request.urlopen(req, timeout=10) as r:
resp = _json.loads(r.read())
return bool(resp.get("episode_id") or resp.get("memory_id") or resp.get("id") or resp.get("status") == "ok")
except Exception as e:
log(f"⚠️ 织忆写入失败: {e}")
return False
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")