三个优化让知识库流水线更聪明

2026-04-16 · 基于今日实测数据,从浪费、丢数据、不一致三个真实痛点出发
86%
LLM 调用浪费率
+1B
opus 偷懒只改 1 字节
3 级
port-monitor 全失败
~17min
优化后总耗时

这文档讲什么?

今天我们把整个知识库流水线大改了一遍,跑通了。但跑的过程中暴露了 3 个值得修的问题。这文档把这 3 个问题用大白话讲清楚,让任何人(不需要懂代码)都能看懂为什么要改、改了能省什么。

把整个知识库系统当成一家书店:

  • extract.sh = 收购员,每天从一堆旧报纸里把有用的内容剪下来
  • merge.sh = 编辑员,把剪下来的内容整理进对应的书
  • summarize_skill.py = 目录维护员,把每本书的简介更新到目录
  • GLOBAL_INDEX.md = 书店门口那张目录表,客人(以后的 Claude)进店第一眼看的就是它

今天的 3 个问题,分别对应:目录维护员重复劳动、编辑员和目录维护员各干一套打架、收购员遇到难处理的报纸直接扔了。

三个优化一句话总结

  • 1. 增量模式 — 只改动了 13 本书,目录别每次重写 98 本(省 86% 的力气)
  • 2. 统一目录 — 让编辑员别再自己改目录了,交给目录维护员一个人管(消除打架)
  • 3. 失败重试 — 收购员遇到难报纸,先放抽屉里,明天再试一次(不丢数据)

1summarize_skill.py 增量模式

每次更新目录,系统都要重写所有 98 本书的简介,但实际只有 13 本书改了内容。

问题:浪费 86% 的力气

真实案例 — 今天发生了什么

下午 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 调用。

改进方案:加一个 --changed-from 参数

# 新增第三种模式
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 调用次数981387%
耗时(并发 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

2merge.sh 删 Step 4,改调 summarize_skill

现在系统里有两套更新目录的逻辑,它们写出来的目录格式不一样,长期会打架。

问题:两个人在改同一本目录,规则还不一样

真实案例 — opus 偷懒事件

今天下午 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 详细版"。两个版本反复覆盖,目录里最终长什么样取决于谁最后跑。

当前流程图

extract.sh
merge.sh Step 1-3
Step 4: opus Edit INDEX
(偷懒/格式不稳)
commit
手动触发
summarize_skill --all
(覆盖了上面的成果)

改进方案:删 Step 4,改成 Step 5 调用 summarize_skill

把 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

改进后流程图

extract.sh
merge.sh Step 1-3
Step 5: summarize_skill --changed
(确定性脚本,不偷懒)
commit

不再有第二条手动路径,只有一个统一入口。

为什么这个方案靠谱?

风险与防御

这是动主路径的改动,需要保险

summarize_skill --update-in-place 写 GLOBAL_INDEX.md 之前必须:

  1. 备份原文件到 GLOBAL_INDEX.md.before-summarize.bak
  2. 定位 § 1 段落起止 — 找不到就报错不写,保留原文
  3. 写入后 sanity check:总行数变化 < 5%(否则回滚备份)

这样最坏情况是"INDEX 没更新",而不是"INDEX 被写坏了"。

收益

2 → 1
两套规则减为一套
2min → 1min
Step 4 耗时减半
0 偷懒
脚本不会跳过更新

3失败 skill 自动重试队列

某个 skill 三级退化全失败时,内容会随 changelog 清空而永久丢失。

问题:数据可能悄悄丢失

真实案例 — port-monitor 三级全挂

今天下午 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 的内容也一起没了

改进方案:三件套

(1) retry queue 目录结构

~/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"
      #   }
      # }

(2) merge.sh Step 2 失败时:切片暂存

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

(3) merge.sh 入口加 Step 0:消化 retry queue

# 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

完整流程示意

cron 触发
Step 0: 看 retry_queue
有 → 合并到 changelog
Step 1-3 正常 merge
Step 6: 失败的回 queue
成功的从 queue 移除

边界 case

场景处理
同一 skill 连续失败 N 次 _index.json 记 retry_count,>5 触发告警(不再 retry,可能是 OpenRouter 长期挂)
同 skill 多次 retry 内容累积 文件名带时间戳,按时间顺序合并,语义上等价于"完整 changelog 重放"
retry 文件解析坏了 移到 tmp/retry_queue.broken/,人工排查,不阻塞主流程
多个 skill 同时失败 每个独立文件,互不影响,下次 cron 一起处理
Cron 跑很多次都没新数据 Step 0 仍会处理 retry_queue(主路径有空才能处理积压)

收益

0 丢失
失败数据进队列
可观测
retry_queue/ 目录可见
+30 行
实施成本极低

三项一起做的最终架构

三项独立都有价值,但整合后会形成一个完整的、自愈的、可观测的流水线。

对比:今天 vs P0 实施后

今天的流水线

[cron 触发]
extract.sh: S0 + opus 直萃 ~5 min
merge.sh:
Step 1-3: opus 三级退化 ~10 min
Step 4: opus Edit INDEX(偷懒,只改 1 字节) ~2 min
→ 失败的 skill 数据丢失
手动触发(选择性):
summarize_skill --all 全量重跑 ~5.5 min(85 次浪费)
→ 覆盖 Step 4 的成果(打架)
[完成] 总耗时 ~22 min,有数据丢失风险

P0 实施后的流水线

[cron 触发]
extract.sh: S0 + opus 直萃 ~5 min
merge.sh:
Step 0: 消化 retry_queue(如有) ~1 min ← 优化 3
Step 1-3: opus 三级退化 ~10 min
Step 5: summarize_skill --changed-from ~1 min ← 优化 1+2
Step 6: commit + 整理 retry_queue ~10s
[完成] 总耗时 ~17 min,数据零丢失

整体收益对比

维度今天版本P0 后变化
总耗时~22 min~17 min-23%
INDEX 准确性opus 偷懒脚本确定性
失败数据丢失有风险零丢失
LLM 调用~150 次~50 次-67%
Token 成本100%~50%-50%
架构一致性两套 INDEX 逻辑单一入口

实施建议

总工作量约 2 小时,可以拆 3 步独立实施,每步 commit 可单独 revert。

1
先做 #3 retry queue(最小风险)
耗时 ~30 分钟 · 只加新功能,不改主路径,出问题不影响现有 merge
  • 改 merge.sh 失败分支:加切片暂存逻辑
  • 改 merge.sh 入口:加 Step 0 消化 queue
  • 改 merge.sh 末尾:整理 queue(成功移除/失败放回)
  • 测试:手动 mock 一次失败,看能否进 retry_queue
2
再做 #1 增量模式(改 summarize_skill.py)
耗时 ~45 分钟 · 改的是独立脚本,不影响主路径
  • 新增 changed_skills_from_changelog() 函数
  • 新增 --changed-from 和 --update-in-place 参数
  • 解析现有 INDEX § 1 → dict → 覆盖 → 渲染
  • 加 sanity check(行数变化 < 5%)和备份
  • 测试:用 mock changelog 跑一次,对比输出
3
最后做 #2 merge.sh Step 4 替换(动主路径)
耗时 ~45 分钟 · 需要端到端测试,确认 INDEX 输出无问题再替换
  • 备份现有 merge.sh Step 4 整段(约 130 行)
  • 新增 Step 5 调用 summarize_skill --changed-from
  • 删除原 Step 4(用注释保留一段时间观察)
  • 跑一次完整 cron,看 INDEX § 1 输出质量
  • 跑通 1 周后正式删除注释

执行方式

为什么按这个顺序?

三项中 #3 风险最低(只加不改),#1 风险中(改独立脚本),#2 风险最高(改主流程)。按风险从低到高,每步独立验证,出问题影响最小。

如果想节省时间,#1 和 #3 完全独立,可以并发实施。#2 必须等 #1 完成后做(因为 Step 5 需要调用 #1 的新功能)。

不做的代价

不做哪一项结果
不做 #1每次 merge 都浪费 5 分钟和 85 次 LLM 调用
不做 #2INDEX § 1 长期不准,opus 偷懒持续
不做 #3下次 OpenRouter 抽风时数据真丢失,不可恢复

做完之后的下一步(P1 项)

如果 P0 三项都做完了,P1 还有这些可以考虑:

本文档基于 2026-04-16 实测数据生成 · 数据来源:今日完整 compact 上下文