805 lines
29 KiB
Python
Executable File
805 lines
29 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个
|
||
|
||
|
||
# ====== 失败回归闭环(2026-08-12 新增,借鉴《AI Agent 的自我进化》PEV/离线演化)======
|
||
|
||
def collect_failures(days=3):
|
||
"""收集近期失败痕迹(Traces)——Harness is the Dataset
|
||
数据源: ① cron 输出目录里 Status: script failed ② daemon journal 的 alert/error
|
||
③ skill-health 低分技能 ④ stock_backtest 数据文件过期
|
||
"""
|
||
failures = []
|
||
cutoff = time.time() - days * 86400
|
||
|
||
# 1. cron 输出目录
|
||
cron_out = HERMES + "/cron/output"
|
||
if os.path.isdir(cron_out):
|
||
for jid_dir in os.listdir(cron_out):
|
||
jdir = os.path.join(cron_out, jid_dir)
|
||
if not os.path.isdir(jdir):
|
||
continue
|
||
for fname in sorted(os.listdir(jdir)):
|
||
if not fname.endswith(".md"):
|
||
continue
|
||
fpath = os.path.join(jdir, fname)
|
||
mtime = os.path.getmtime(fpath)
|
||
if mtime < cutoff:
|
||
continue
|
||
try:
|
||
with open(fpath, encoding="utf-8", errors="ignore") as f:
|
||
content = f.read()
|
||
except:
|
||
continue
|
||
if "script failed" in content or "Status: **error**" in content or "Traceback" in content:
|
||
failures.append({
|
||
"source": "cron",
|
||
"job": jid_dir,
|
||
"file": fname,
|
||
"time": datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M"),
|
||
"content": content[:1500],
|
||
})
|
||
|
||
# 2. daemon journal
|
||
jf = D + "/journal.jsonl"
|
||
if os.path.exists(jf):
|
||
try:
|
||
with open(jf) as f:
|
||
for line in f:
|
||
try:
|
||
entry = json.loads(line)
|
||
except:
|
||
continue
|
||
ts = entry.get("timestamp", "")
|
||
etype = entry.get("type", "")
|
||
if etype not in ("alert", "process_down", "error", "failure"):
|
||
continue
|
||
if ts:
|
||
try:
|
||
t = datetime.fromisoformat(ts)
|
||
if (datetime.now(timezone.utc) - t).total_seconds() > days * 86400:
|
||
continue
|
||
except:
|
||
pass
|
||
failures.append({
|
||
"source": "daemon",
|
||
"type": etype,
|
||
"time": ts,
|
||
"content": str(entry.get("summary", ""))[:500],
|
||
})
|
||
except Exception as e:
|
||
log(f"⚠️ daemon journal 读取失败: {e}")
|
||
|
||
return failures
|
||
|
||
|
||
def classify_failure(f):
|
||
"""确定性失败分类(对应文章:先代码后模型——分类用规则不用 LLM)
|
||
返回: (类别, 子类, 可信度)
|
||
"""
|
||
content = f.get("content", "")
|
||
job = f.get("job", "")
|
||
src = f.get("source", "")
|
||
|
||
# 网络/连通性类
|
||
if any(k in content for k in ["Connection refused", "connection refused", "网络", "离线",
|
||
"挂载失败", "不在局域网", "timeout", "timed out", "超时",
|
||
"unexpected disconnect", "Name or service not known"]):
|
||
return ("network", "connectivity", 0.9)
|
||
# 权限/凭证类
|
||
if any(k in content for k in ["Permission denied", "密码", "auth", "Invalid token",
|
||
"app_secret", "401", "403"]):
|
||
return ("config", "auth", 0.85)
|
||
# 数据/文件类
|
||
if any(k in content for k in ["No such file", "not found", "MISSING", "FileNotFound",
|
||
"数据文件", "过期", "STALE"]):
|
||
return ("data", "missing_file", 0.85)
|
||
# 脚本代码类
|
||
if any(k in content for k in ["Traceback", "TypeError", "KeyError", "IndexError",
|
||
"NameError", "SyntaxError", "AttributeError", "exit code 1"]):
|
||
return ("code", "script_error", 0.9)
|
||
# 服务/进程类
|
||
if any(k in content for k in ["service", "进程", "crash", "restart", "failed",
|
||
"systemd", "Process"]):
|
||
return ("service", "process_down", 0.7)
|
||
# 模型/API类
|
||
if any(k in content for k in ["DEGRADED", "model", "模型", "stream", "openai_error"]):
|
||
return ("model", "api_error", 0.7)
|
||
return ("unknown", "unclassified", 0.3)
|
||
|
||
|
||
def suggest_fix(cls, sub, f):
|
||
"""根据失败分类给出修复建议(确定性规则)"""
|
||
content = f.get("content", "")
|
||
job = f.get("job", "")
|
||
if cls == "network":
|
||
return "网络/连通性问题——先确认目标是否在线(不在局域网是常态,双备份已静默跳过);重试或换通道"
|
||
if cls == "config":
|
||
return "配置/凭证问题——检查 .env / token / 密钥是否轮换,对照 hermes-debug skill 排查"
|
||
if cls == "data":
|
||
return "数据/文件问题——检查数据文件是否生成、是否按各自周期更新(周更文件勿按日更检查)"
|
||
if cls == "code":
|
||
return f"脚本代码错误——看 {f.get('file','?')} 的 Traceback 定位根因,修复后立即回归验证(能力5补足①)"
|
||
if cls == "service":
|
||
return "服务/进程问题——systemctl 状态 + journalctl 拉根因,别乱 kill(牧尘 07-25 教训)"
|
||
if cls == "model":
|
||
return "模型/API 问题——检查 model-health.json 稳定模型,DEGRADED 重启 new-api"
|
||
return f"未分类失败({job})——人工查看 {f.get('file','?')} 或 journalctl 日志"
|
||
|
||
|
||
def cmd_regress():
|
||
"""失败回归闭环:收集失败 → 分类 → 根因 → 修复建议 → 回归验证清单"""
|
||
state = load_state()
|
||
print(f"\n{'='*56}")
|
||
print(f" 失败回归闭环 | Harness is the Dataset")
|
||
print(f"{'='*56}")
|
||
|
||
failures = collect_failures(days=3)
|
||
print(f"\n📥 近期失败痕迹: {len(failures)} 条")
|
||
|
||
if not failures:
|
||
print("✅ 无失败痕迹——系统健康")
|
||
return
|
||
|
||
# 按类别汇总
|
||
by_cls = defaultdict(list)
|
||
for f in failures:
|
||
cls, sub, conf = classify_failure(f)
|
||
by_cls[cls].append({**f, "cls": cls, "sub": sub, "conf": conf})
|
||
|
||
print(f"\n📊 失败分类(确定性规则,无 LLM 成本):")
|
||
for cls, items in sorted(by_cls.items(), key=lambda x: -len(x[1])):
|
||
print(f" {cls}: {len(items)} 条")
|
||
|
||
# 输出明细 + 建议
|
||
regression_items = []
|
||
for cls, items in sorted(by_cls.items(), key=lambda x: -len(x[1])):
|
||
for it in items[:3]:
|
||
fix = suggest_fix(cls, it["sub"], it)
|
||
print(f"\n 🔴 [{it['source']}] {it.get('job', it.get('type','?'))} ({it['time']})")
|
||
print(f" {it['content'][:120].replace(chr(10),' ')}")
|
||
print(f" 💡 {fix}")
|
||
regression_items.append({
|
||
"job": it.get("job", it.get("type", "?")),
|
||
"cls": cls,
|
||
"sub": it["sub"],
|
||
"time": it["time"],
|
||
"suggestion": fix,
|
||
"status": "open",
|
||
})
|
||
|
||
# 回归清单落地为文件,供后续验证追踪
|
||
REGRESSION_FILE = LEARNER_DIR + "/regression.json"
|
||
os.makedirs(LEARNER_DIR, exist_ok=True)
|
||
with open(REGRESSION_FILE, "w") as f:
|
||
json.dump({"generated_at": datetime.now(timezone.utc).isoformat(),
|
||
"items": regression_items}, f, indent=2, ensure_ascii=False)
|
||
|
||
# 写入 state
|
||
state.setdefault("regression", []).extend(regression_items)
|
||
state["regression"] = state["regression"][-50:]
|
||
save_state(state)
|
||
|
||
print(f"\n📝 回归清单已保存: {REGRESSION_FILE}")
|
||
print(" 下一步: 逐项修复 → 运行 verify 确认关闭 → 更新到 skill/SOUL/AGENTS")
|
||
return regression_items
|
||
|
||
|
||
def cmd_verify():
|
||
"""回归验证:确认历史失败是否已恢复(确定性检查——重跑/检查输出)"""
|
||
state = load_state()
|
||
REGRESSION_FILE = LEARNER_DIR + "/regression.json"
|
||
if not os.path.exists(REGRESSION_FILE):
|
||
print("📭 无回归清单——先运行 learner.py regress 收集失败")
|
||
return
|
||
with open(REGRESSION_FILE) as f:
|
||
data = json.load(f)
|
||
items = data.get("items", [])
|
||
print(f"\n{'='*56}")
|
||
print(f" 回归验证 | 确认修复是否生效")
|
||
print(f"{'='*56}")
|
||
print(f"\n📋 待验证: {len(items)} 项")
|
||
open_items = [i for i in items if i.get("status") == "open"]
|
||
if not open_items:
|
||
print("✅ 全部已关闭——无待验证项")
|
||
return
|
||
print(f"\n🔄 未关闭 {len(open_items)} 项——需逐项检查(cron 输出/服务状态/数据新鲜度)")
|
||
for it in open_items:
|
||
print(f" - [{it['cls']}] {it['job']}: {it['suggestion'][:60]}")
|
||
print("\n 验证方式: 查看该 job 最新 cron 输出是否 Status: ok,或运行对应脚本确认")
|
||
return open_items
|
||
|
||
|
||
# ====== 命令入口 ======
|
||
|
||
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()
|
||
elif cmd == "regress":
|
||
cmd_regress()
|
||
elif cmd == "verify":
|
||
cmd_verify()
|
||
else:
|
||
print(f"未知: {cmd}")
|
||
print("可用: reflect, learn, plan, status, regress, verify")
|