Files
ai-agent-book/chapter9/ai-style-skill/skill_manager.py
T
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

143 lines
5.8 KiB
Python
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.
"""Skill 的增量维护:按开放式规则 id 合并、prune 与 SKILL.md 生成。
防膨胀原则:提炼模型看到当前规则,语义相同时复用稳定 id;本模块按 id 合并来源,
而不是再用预置 detector 指纹把新发现筛掉。
长期未被新证据确认、或被评估证据推翻的规则归档到 skill/archive/,不再进入
SKILL.md。所有合并/激活/归档决定都发生在这里,不交给生成候选的模型。
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple
ROOT = Path(__file__).resolve().parent
SKILL_DIR = ROOT / "skill"
def rule_signature(rule: Dict[str, Any]) -> str:
"""模型在已有规则语义相同时复用 id;该稳定 id 就是合并键。"""
return str(rule.get("id", "")).strip().lower()
def merge_rules(
existing: List[Dict[str, Any]], candidates: List[Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
"""把候选规则合并进现有规则集,返回 (规则集, 合并报告)。"""
rules = [dict(rule) for rule in existing]
report: Dict[str, Any] = {"added": [], "merged": [], "conflicts": []}
for cand in candidates:
sig = rule_signature(cand)
match = next(
(rule for rule in rules if rule_signature(rule) == sig), None
)
if match is None:
rules.append(cand)
report["added"].append(cand["id"])
continue
# 去重合并:并集来源与作用域,保留首次通过 schema 检查的定义与范例。
match["source_ids"] = sorted(set(match.get("source_ids", [])) | set(cand.get("source_ids", [])))
match["scope"] = sorted(set(match.get("scope", [])) | set(cand.get("scope", [])))
report["merged"].append(cand["id"])
return rules, report
def prune_rules(
rules: List[Dict[str, Any]],
*,
current_batch: int,
idle_batches: int = 2,
contradicted_ids: Set[str] | None = None,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""把长期未被新证据确认、或被证据推翻的规则归档,返回 (存活, 归档)。"""
contradicted = contradicted_ids or set()
active, archived = [], []
for rule in rules:
last = rule.get("last_confirmed_batch", current_batch)
reason = None
if rule["id"] in contradicted:
reason = "被评估证据推翻(误伤率过高或与金标集冲突)"
elif current_batch - last > idle_batches:
reason = f"连续超过 {idle_batches} 批反馈未再被触发"
if reason:
archived.append({**rule, "status": "archived", "archive_reason": reason})
else:
active.append(rule)
return active, archived
def _describe_detector(detector: Dict[str, Any]) -> str:
if detector.get("type") != "llm":
return "无效检测器(规则不会激活)"
return "LLM judge 语义判定(上线前须通过独立人工金标集校准)"
def render_skill_md(rules: List[Dict[str, Any]]) -> str:
"""按 house 风格渲染 SKILL.md:何时加载 + 每条规则的定义/坏例/好例/作用域。"""
lines = [
"---",
"name: ai-style",
"description: 中文文案去「AI 味」检查清单,由用户纠正反馈持续提炼而来",
"---",
"",
"# 去 AI 味写作 Skill",
"",
"## 何时加载",
"",
"当任务是用中文撰写或改写面向读者的文案(产品发布稿、公众号文章、邮件、README 等),",
"或用户反馈文字「AI 味太重」「不像人写的」时,加载本 Skill。",
"",
"## 使用方式",
"",
"起草或改写时逐条对照下面的规则自查。每条规则都给出定义、可检查的检测方法、",
"坏例与好例;规则只在声明的作用域内生效,作用域之外的文体不要套用。",
"",
f"## 规则清单(共 {len(rules)} 条)",
"",
]
for i, rule in enumerate(rules, 1):
lines += [
f"### 规则 {i}{rule['name']}`{rule['id']}`",
"",
f"- **定义**{rule['definition']}",
f"- **检测方法**{_describe_detector(rule['detector'])}",
f"- **坏例**{rule.get('bad_example', '')}",
f"- **好例**{rule.get('good_example', '')}",
f"- **改写建议**{rule.get('rewrite_hint', '按定义改写。')}",
f"- **作用域**{'、'.join(rule.get('scope', [])) or '通用'}",
f"- **来源反馈**{', '.join(rule.get('source_ids', [])) or '无'}",
"",
]
return "\n".join(lines)
def write_skill(rules: List[Dict[str, Any]], skill_dir: Path | None = None) -> Path:
out_dir = skill_dir or SKILL_DIR
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / "SKILL.md"
path.write_text(render_skill_md(rules), encoding="utf-8")
# 同步保存机器可读的规则集,供 evaluate/judge 直接加载。
(out_dir / "rules.json").write_text(
json.dumps(rules, ensure_ascii=False, indent=2), encoding="utf-8"
)
return path
def write_archive(archived: List[Dict[str, Any]], skill_dir: Path | None = None) -> Path | None:
if not archived:
return None
out_dir = (skill_dir or SKILL_DIR) / "archive"
out_dir.mkdir(parents=True, exist_ok=True)
lines = ["# 已归档规则", ""]
for rule in archived:
lines += [
f"## {rule['name']}`{rule['id']}`",
f"- 归档原因:{rule.get('archive_reason', '未说明')}",
f"- 原定义:{rule['definition']}",
f"- 来源反馈:{', '.join(rule.get('source_ids', [])) or '无'}",
"",
]
path = out_dir / "ARCHIVED.md"
path.write_text("\n".join(lines), encoding="utf-8")
return path