今天我们把整个知识库流水线大改了一遍,跑通了。但跑的过程中暴露了 3 个值得修的问题。这文档把这 3 个问题用大白话讲清楚,让任何人(不需要懂代码)都能看懂为什么要改、改了能省什么。
把整个知识库系统当成一家书店:
今天的 3 个问题,分别对应:目录维护员重复劳动、编辑员和目录维护员各干一套打架、收购员遇到难处理的报纸直接扔了。
每次更新目录,系统都要重写所有 98 本书的简介,但实际只有 13 本书改了内容。
下午 21:00 左右,我跑了一次目录全量更新:
$ python3 scripts/summarize_skill.py --all --parallel 3 # 结果:5.5 分钟,跑了 98 次 LLM 调用
但回头看 changelog,这次实际只有 13 本书有内容变更(其他 85 本完全没动)。也就是说我让 AI 重新分析了 85 本根本没变的书,得出了完全一样的结论。
就像你每次只改了一篇日记的标题,却要把一整本日记重新誊抄一遍。明明可以只擦掉那一行重写,你偏要从第 1 页开始抄到第 200 页。
现在 summarize_skill.py 只有两种模式:
# 模式 1:跑单个 skill
python3 scripts/summarize_skill.py cpmiao-main
# 模式 2:跑全部 98 个 skill
python3 scripts/summarize_skill.py --all --parallel 3
没有"只跑变更过的"这个模式,所以每次 merge 完只能用 --all,导致 85 次浪费的 LLM 调用。
# 新增第三种模式
python3 scripts/summarize_skill.py \
--changed-from changelog.md \
--update-in-place GLOBAL_INDEX.md
# 系统行为:
# 1. 解析 changelog.md,提取出受影响的 skill 名(比如 13 个)
# 2. 只对这 13 个 skill 跑 LLM
# 3. 读现有 GLOBAL_INDEX.md,把这 13 行覆盖,其他 85 行保留
# 4. 写回 GLOBAL_INDEX.md
def changed_skills_from_changelog(path):
"""从 changelog 解析出受影响的 skill 名"""
affected = set()
pattern = re.compile(r'^### .+ \[(?:ADD|UPDATE|DELETE|NEW_SKILL)\] (\S+) →', re.M)
for m in pattern.finditer(open(path).read()):
affected.add(m.group(1))
return affected
# changelog 解析示例:
# ### 2026-04-16T20:34 [UPDATE] cpmiao-main → § 用户注册流程
# ### 2026-04-16T20:35 [ADD] knotchat-paypal → § 退款接口
# 解析结果:{cpmiao-main, knotchat-paypal} 这 2 个 skill
| 维度 | 当前 --all | 新增 --changed-from | 节省 |
|---|---|---|---|
| LLM 调用次数 | 98 | 13 | 87% |
| 耗时(并发 3) | 5.5 分钟 | ~1 分钟 | 82% |
| API 成本 | 100% | ~13% | 87% |
| 输出准确度 | 100% | 100%(同) | — |
有人可能问:既然只重算 13 行,是不是输出就只是这 13 行?不是。输出依然是完整的 98 行目录表,只不过其中 13 行是新算的、85 行是从老目录复制过来的。这样调用方可以直接覆盖 GLOBAL_INDEX.md,不用关心增量逻辑。
| 情况 | 怎么办 |
|---|---|
| 新增 skill(NEW_SKILL) | changelog 里有,但 INDEX 里还没有 → 必须新算 |
| 删除 skill(DELETE) | changelog 里标 DELETE → 直接从 INDEX 删行,不调 LLM |
| changelog 解析失败 | fallback 回 --all 模式,绝不静默跳过(避免数据陈旧) |
| changelog 为空 | 直接 exit 0,不动 INDEX |
现在系统里有两套更新目录的逻辑,它们写出来的目录格式不一样,长期会打架。
今天下午 20:48,merge.sh 跑完了,Step 4 用 claude-p opus 去更新 GLOBAL_INDEX.md(目录表)。
结果:INDEX 文件大小只变了 +1 字节(从 26532 → 26533)。
但 changelog 里有 29 条新知识,涉及 13 个 skill。意味着 opus 拿着 29 条新知识,看了一眼老目录,然后说"嗯,基本不用改",就改了 1 个字节交差了。
这是 opus 的"偷懒模式":Edit 工具在面对庞大现有内容时,倾向最小改动,容易跳过实质更新。
相当于你雇了个聪明但懒的实习生改简历。你给他 29 个新项目说"加进去",他扫了一眼说"嗯,你简历看起来还行",然后改了一个标点符号交回来。
现在系统里有两套更新目录的逻辑:
| 路径 | 调用时机 | 规则 | 输出风格 |
|---|---|---|---|
| 路径 A merge.sh Step 4 |
每次 merge 完自动跑 | opus 自由 Edit,无固定 schema | 不可预测,经常偷懒 |
| 路径 B summarize_skill.py |
手动触发(或 cron) | 4 步脚本 + 1 次 LLM,有 Pydantic schema 校验 | v2 规范 7 列详细表 |
问题来了:每次 cron 跑完 merge.sh,目录被路径 A 改成"小变化版";然后我手动跑路径 B,又改成"v2 详细版"。两个版本反复覆盖,目录里最终长什么样取决于谁最后跑。
把 merge.sh 里 L450-577 整个 Step 4(用 opus Edit 改 INDEX 的逻辑)删掉,换成:
# merge.sh 末尾新增 Step 5
echo "[$(date +%H:%M:%S)] === Step 5: 增量更新 INDEX § 1 ===" >> "$LOG"
python3 "$BASE/scripts/summarize_skill.py" \
--changed-from "$CHANGELOG" \
--update-in-place "$BASE/GLOBAL_INDEX.md" 2>>"$LOG"
if [[ $? -eq 0 ]]; then
echo "[$(date +%H:%M:%S)] INDEX § 1 已更新" >> "$LOG"
else
echo "[$(date +%H:%M:%S)] WARN: INDEX § 1 更新失败,保留旧版" >> "$LOG"
fi
不再有第二条手动路径,只有一个统一入口。
summarize_skill --update-in-place 写 GLOBAL_INDEX.md 之前必须:
GLOBAL_INDEX.md.before-summarize.bak这样最坏情况是"INDEX 没更新",而不是"INDEX 被写坏了"。
某个 skill 三级退化全失败时,内容会随 changelog 清空而永久丢失。
今天下午 20:37,merge.sh Step 2 处理 port-monitor 时:
[20:37:12] 合并 port-monitor... [20:37:45] WARN: claude-p opus 失败 rc=1 [20:38:21] WARN: cursor-agent claude-4.6-opus 失败 rc=3 [20:38:55] ERROR: OpenRouter 失败 (Remote end closed connection) [20:38:55] ERROR: port-monitor 失败(认证/限流)
三级退化全挂了,内容丢了吗? 差点丢了。
只是恰好今天 Step 3(创建新 skill)又把 port-monitor 内容补回来了 — 这是巧合。如果是 UPDATE 而不是 ADD,那就是真的丢了。
因为接下来 Step 6 会清空 changelog.md(merge 完成的标志),clearing 之后那段 port-monitor 的内容就在地球上消失了。
邮局收到一个包裹但派送失败了三次。当前系统的做法是:派送员回来报告"派送失败",然后把包裹扔掉,因为今天的工单已经处理完了。第二天根本没人知道有个包裹丢了。
正确做法应该是:派送失败的包裹放回仓库,标个"待重派",明天的派送员看到先处理这些。
merge.sh Step 2 失败分支(简化版):
# 当前
if [[ failed ]]; then
echo "[$(date)] ERROR: $skill 失败" >> "$LOG"
# 啥都不做,直接放弃
fi
# Step 6 后续
> "$CHANGELOG" # 清空 changelog,失败 skill 的内容也一起没了
~/Desktop/skill-knowledge-base/tmp/retry_queue/
├── port-monitor_2026-04-16T20-37.changelog
├── cpmiao-main_2026-04-16T20-39.changelog
└── _index.json
# {
# "port-monitor": {
# "retry_count": 1,
# "last_attempt": "2026-04-16T20:37",
# "original_ts": "2026-04-16T20:37"
# }
# }
if [[ failed ]]; then
# 从 changelog 切出该 skill 的段落
skill_changelog=$(python3 -c "
import re
content = open('$CHANGELOG').read()
matches = re.findall(
r'(### [^\n]+ → § ' + '$skill' + r'.+?)(?=\n### |\Z)',
content, re.S
)
print('\n'.join(matches))
")
if [[ -n "$skill_changelog" ]]; then
retry_file="$BASE/tmp/retry_queue/${skill}_$(date +%Y-%m-%dT%H-%M).changelog"
echo "$skill_changelog" > "$retry_file"
echo "[$(date)] $skill 内容已暂存 retry_queue" >> "$LOG"
fi
echo "[$(date)] ERROR: $skill 失败(已存 retry_queue)" >> "$LOG"
fi
# Step 0(在 Step 1 之前)
if ls "$BASE/tmp/retry_queue/"*.changelog 2>/dev/null; then
echo "[$(date)] === Step 0: 处理 retry_queue ===" >> "$LOG"
cat "$BASE/tmp/retry_queue/"*.changelog >> "$CHANGELOG"
mv "$BASE/tmp/retry_queue" "$BASE/tmp/retry_queue.processing"
fi
# 末尾(commit 成功后)
if [[ -d "$BASE/tmp/retry_queue.processing" ]]; then
for f in "$BASE/tmp/retry_queue.processing/"*.changelog; do
skill=$(basename "$f" | cut -d_ -f1)
if grep -q "$skill 完成" "$LOG"; then
rm -f "$f" # 这次成功了,从队列移除
else
mv "$f" "$BASE/tmp/retry_queue/" # 仍失败,放回队列
fi
done
rm -rf "$BASE/tmp/retry_queue.processing"
fi
| 场景 | 处理 |
|---|---|
| 同一 skill 连续失败 N 次 | _index.json 记 retry_count,>5 触发告警(不再 retry,可能是 OpenRouter 长期挂) |
| 同 skill 多次 retry 内容累积 | 文件名带时间戳,按时间顺序合并,语义上等价于"完整 changelog 重放" |
| retry 文件解析坏了 | 移到 tmp/retry_queue.broken/,人工排查,不阻塞主流程 |
| 多个 skill 同时失败 | 每个独立文件,互不影响,下次 cron 一起处理 |
| Cron 跑很多次都没新数据 | Step 0 仍会处理 retry_queue(主路径有空才能处理积压) |
三项独立都有价值,但整合后会形成一个完整的、自愈的、可观测的流水线。
| 维度 | 今天版本 | P0 后 | 变化 |
|---|---|---|---|
| 总耗时 | ~22 min | ~17 min | -23% |
| INDEX 准确性 | opus 偷懒 | 脚本确定性 | — |
| 失败数据丢失 | 有风险 | 零丢失 | — |
| LLM 调用 | ~150 次 | ~50 次 | -67% |
| Token 成本 | 100% | ~50% | -50% |
| 架构一致性 | 两套 INDEX 逻辑 | 单一入口 | — |
总工作量约 2 小时,可以拆 3 步独立实施,每步 commit 可单独 revert。
三项中 #3 风险最低(只加不改),#1 风险中(改独立脚本),#2 风险最高(改主流程)。按风险从低到高,每步独立验证,出问题影响最小。
如果想节省时间,#1 和 #3 完全独立,可以并发实施。#2 必须等 #1 完成后做(因为 Step 5 需要调用 #1 的新功能)。
| 不做哪一项 | 结果 |
|---|---|
| 不做 #1 | 每次 merge 都浪费 5 分钟和 85 次 LLM 调用 |
| 不做 #2 | INDEX § 1 长期不准,opus 偷懒持续 |
| 不做 #3 | 下次 OpenRouter 抽风时数据真丢失,不可恢复 |
如果 P0 三项都做完了,P1 还有这些可以考虑: