593 lines
20 KiB
Python
Executable File
593 lines
20 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):
|
||
"""从经验中提取重复模式
|
||
|
||
门禁(书中第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")
|