目录

OpenClaw 博客流水线深度优化:从11项问题到全流程重构

一篇博客的诞生背后,是一次生产流水线的自我迭代

一切始于一行代码审查

上周五,blog-pipeline 刚刚上线了一篇新文章《普通人致富的3大核心方法》,一切看起来运转正常。但有条不成文的规矩:越是看起来正常的时候,越要找出藏在暗处的问题。

老板说了一句话:「去,把这个 SKILL.md 从头到尾逐行逐字审查一遍,看看到底有多少坑。」

于是,一场针对 blog-pipeline 生产流水线的深度代码审查开始了。

11项问题,藏在一千行 SKILL.md 里

审查结果让人倒吸一口凉气——整整 11 项问题,从版本号到观测性加载,从时序错误到残留重复代码,覆盖了从入口到发布的每一个环节。

代码审查看板图,11项问题打勾状态

1. 版本号打架:v2.6 还是 v2.9?

SKILL.md 的 frontmatter 里写着 version: v2.9,但标题竟然是 v2.6。两个版本号差了三个迭代,谁在说谎?答案很简单:改标题忘了改 frontmatter,或者改 frontmatter 忘了改标题。

这个问题的教训是:任何元数据都不应该有两个来源。版本号应该从单一入口读取,消除人工维护的误差。

2. Session 启动流程残留无关路径

在最核心的启动流程区块里,竟然引用了一个叫 engineering-multi-agent-systems-architect 的路径。这跟 blog-pipeline 八竿子打不着,明显是从其他项目复制粘贴过来的残留。

代码编辑器分屏对比,左侧红色残留代码,右侧绿色清理后版本

3. result.get() 时序错误:贯穿所有 Phase 的定时炸弹

这是最隐蔽也最致命的问题。所有 Phase 的代码都在 sessions_spawn() 返回后立即调用 result.get(...),但 sessions_spawn() 返回的只是一个任务接受确认,子代理的真正产出要等到 sessions_yield() 之后的 completion event 里才能拿到。

# ❌ 错误写法:spawn 后立即 get,result 永远是 None
result = sessions_spawn(agentId="kb-writer", task="写稿...")
tokens = result.get("tokens")  # result 是空对象,.get() 永返 None
report = result.get("report")  # 同样为 None

# ✅ 正确写法:yield 之后再 get,等子代理跑完
result = sessions_spawn(agentId="kb-writer", task="写稿...")
sessions_yield()  # 等待子代理 completion event
# 此时 result 才有实际值
tokens = result.get("tokens")  # 拿到真实 tokens
report = result.get("report")  # 拿到真实报告

换句话说,整个流水线的 Phase 记录(tokens、报告、路径)全部是空的,只是没人发现。修复后,spawn → yield → 再提取结果 成为标准时序。

4. from observability import 必然 ImportError

# ❌ 错误写法:子进程 exec 加载,父进程 import
exec("python3 /path/to/observability.py")
from observability import log_phase_start  # 必然 ImportError

# ✅ 正确写法:importlib 在当前进程动态加载
import importlib.util
spec = importlib.util.spec_from_file_location(
    "observability",
    "/path/to/observability.py"
)
obs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obs)
obs.log_phase_start("Phase 1: 写稿")

exec("python3 some_script.py") 是在子进程运行,不会把变量传递给当前进程。如果后续代码需要这个模块,必须用 importlib.util.spec_from_file_location 在当前进程动态加载。这是 Python 进程隔离的基础常识,但很容易被忽略。

5. Phase 1 用 dir() 做条件判断

# ❌ 错误写法:dir() 检查当前作用域,在模板渲染中不可靠
{expanded_material if 'expanded_material' in dir() else '(无)'}

# ✅ 正确写法:直接判断变量本身
{expanded_material if expanded_material else '(无)'}

dir() 检查的是当前作用域,在模板渲染的上下文中,变量可能根本不在当前作用域里。这种写法极其脆弱,且难以排查——它不会报错,只会默默返回错误结果。

代码对比图,上半部分错误dir()写法红色标注,下半部分正确写法绿色标注

6. Phase 5 缺少可执行代码块

DAG 流程图里写了一行「cp → Hugo → git add/commit/push」,但整个 Phase 5 没有对应的 Python 代码块。这意味着发布阶段只是一个概念描述,无法真正执行。

# ✅ 修复后:Phase 5 的完整可执行代码
import os
import shutil
import subprocess

# 复制到 Hugo
shutil.copy(obsidian_path, hugo_path)

# Git commit + push
subprocess.run(["git", "add", "."], cwd="/data/oklifeme")
subprocess.run(["git", "commit", "-m", f"post: {title}"], cwd="/data/oklifeme")
subprocess.run(["git", "push"], cwd="/data/oklifeme")

修复后补上了完整的 import os, shutil, subprocess 实现,确保 Phase 5 可以直接执行,不再只是一个概念描述。

补充优化git add -A 改成了 git add .。原因是 -A 是全仓库范围,cwd="/data/oklifeme" 时两者等价,但 git add . 语义更精确——只暂存当前目录下的变更,避免误提交不该改的文件。博客场景下 git add . 是更广泛的安全实践。

7. DAG 流程图未同步更新

流程图里还写着「sessions_history 拉取会话内容」,但 Step 1 早已改成了按模式区分输入源(Mode B/C/D)。流程图和代码不同步,新人看了会一头雾水。

# ❌ 旧版 DAG(未同步,已过时)
# sessions_history → 分析 → 写稿 → 质检 → 生图 → 发布

# ✅ 新版 DAG(已同步,按模式分叉)
# Mode B: sessions_history → 会话归档
# Mode C: 读取文件 → 博客化 → 写入
# Mode D: 写稿 → 质检 → SEO → 改稿 → HITL1 → 提示词 → 生图 → HITL2 → 发布

8. 字数硬编码:不该替用户做决定

Phase 2a 质检要求没有写具体数字,但 AGENTS.md 里赫然写着 ≥6000字。老板说:「不该定死,每次写的文章不一样,应该根据用户每次的要求来。用户没说的,保底默认 600 字。」

# ❌ 错误写法:硬编码 6000,不为用户留余地
字数要求 = 6000

# ✅ 正确写法:默认保底 + 用户指定优先
# 默认 ≥600 字,以用户指定为准
字数要求 = 用户指定字数 if 用户指定字数 else 600

三个文件需要统一修改:

  • blog-pipeline SKILL.md Phase 1:写稿要求
  • blog-pipeline SKILL.md Phase 2a:质检要求
  • kb-writer AGENTS.md(3处):Phase 1 要求、QC 质检、字数验证

全部改为「默认 ≥600 字,以用户指定为准」。

9. Phase 3 缺少 workspace 标注

# ❌ 其他 Phase 有标注,Phase 3 没有——破坏一致性
# Phase 1: **agent_id**: kb-writer / **workspace**: ~/.openclaw/workspace-kb-writer
# Phase 2: **agent_id**: editor-blog-yuntianchen / **workspace**: ...
# Phase 3: (缺少标注 — 子代理不知道在哪里工作)

# ✅ 修复后补齐
# Phase 3: **agent_id**: kb-writer / **workspace**: ~/.openclaw/workspace-kb-writer

其他 Phase 都有 > **agent_id** / **workspace** 标注,Phase 3 偏偏没有。这种不一致在单次阅读时不易察觉,但在自动化编排中会导致子代理无法正确定位工作目录。

10. Phase 5 的 import 位置不对

# ❌ 错误写法:import 放在 try 块内部,不符合规范,掩盖 import 错误
try:
    import shutil
    import subprocess
    # ... 业务逻辑
except Exception as e:
    pass  # 如果 import 失败,静默吞掉异常

# ✅ 正确写法:import 置顶,异常自然暴露
import shutil
import subprocess

try:
    # ... 业务逻辑
except Exception as e:
    logging.error(f"发布失败: {e}")

Python 规范要求 import 必须置顶,放在 try 内部不仅不符合规范,还可能导致异常捕获时掩盖 import 错误。

11. Phase 2a 残留重复代码

修复过程中引入了一个新问题:result.get("report")sessions_yield() 之前和之后各出现了一次,前面的没删干净。这种「修复一半」的残留是最难排查的——因为代码看起来「对」,但实际执行的是旧逻辑。

修复过程:从发现到统一

发现问题只是第一步,真正的挑战在于系统性地修复。

修复流程图,发现问题到同步的闭环流程

按优先级排序

我们把问题分成了三类:

  • P0 阻塞级result.get 时序错误、observability ImportError、dir() 误用——这些会导致流水线静默失败
  • P1 逻辑级:版本号不一致、DAG 未同步、字数硬编码——影响可维护性和质量
  • P2 规范级:workspace 标注缺失、import 位置不对、残留重复代码

代码层面的修复总览

修复项原写法新写法
版本号frontmatter v2.9 / 标题 v2.6统一 v2.9
启动路径无关路径残留importlib 动态加载
result.getspawn 后立即 getyield 之后再 get
dir() 判断'x' in dir()x if x else
import 位置try 内部文件顶部
重复代码yield 前后各一个只保留 yield 之后

关键教训

子代理的 result 不是产出

sessions_spawn() 返回的是任务接受确认,不是子代理的执行结果。真正的产出要等到 sessions_yield() 之后的 completion event 里才能拿到。

反直觉但正确的做法:spawn 之后不要急着读结果,yield 等子代理跑完再说。这是最容易被忽视的时序陷阱——代码不报错,但数据全部为空。

importlib 加载 vs exec 子进程

exec("python3 some_script.py") 是在子进程运行,不会把变量传递给当前进程。这是 Python 的基础知识,但因为 exec() 的语法看起来像「直接执行」,很容易让人误以为它等同于 import

修复完成后要检查残留

修复过程中很容易引入新问题。最典型的是「先加了一段代码,后来又重构了,但旧的没删干净」。每次修复完成后,应该对整个文件做一次 diff 审查,确保没有残留。

不要替用户做决定

字数、风格、长度——这些都应该尊重用户的需求。设定一个合理的默认值(600 字),但用户说了算。

常见问题 FAQ

Q1: 修复后如何验证 result.get() 不再返回 None?

在 Phase 1 完成后,加一个断言检查:assert tokens is not None, "Phase 1 未返回 tokens"。如果 sessions_spawnsessions_yield 的时序正确,result.get("tokens") 应返回具体的数字(如 1820)。如果仍为 None,说明时序问题没有修复,需要检查 yield 是否确实在 get 之前。

Q2: importlib 动态加载和普通的 import 有什么区别?

普通 import 语句从 sys.path 中查找模块,importlib.util.spec_from_file_location 则直接从指定文件路径加载。对于不在 sys.path 中的脚本(如 OpenClaw skills 目录下的 observability.py),必须用后者。另外,importlib 加载的模块可以手动控制重载,适合在流水线多次运行场景下重新加载最新版本。

Q3: 这些修复是否会影响已有的 blog-pipeline 运行记录?

不会。修复只影响未来的流水线执行——修正了 result.get() 时序后,新的 Phase 日志会正确记录 tokens 和报告。但之前已经运行过的流水线记录(tokens 全部为 None)不会自动修复。如果需要追溯历史数据,需要手动填补。

持续改进:流水线自己的流水线

有意思的是,这次审查本身就是一个 meta 过程:我们用自动化流水线的思维,来 review 自动化流水线的代码。这本身就是一种递归改进

递归循环图,审查发现修复验证发布的五步循环

关联阅读

  • [[2026-08-02-2000-OpenClaw博客流水线v27完整迭代]]
  • [[2026-08-02-1900-openclaw-blog-pipeline-v25-fix]]
  • [[2026-08-03-1820-博客流水线HITL事故复盘]]
  • [[2026-07-28-2200-openclaw-blog-writer-skill-fix-complete-review]]

blog-pipeline 经过这次优化,从 v2.6 升级到了 v2.9。虽然版本号只跳了 0.3,但内部的变化远不止于此——11 项问题的修复,让整个流水线更加健壮、可维护、可执行。

下一次审核,也许该轮到其他 SKILL.md 了。