龙虾调教(四):项目管理工程实践优化

这篇文章是一次架构升级的尝试。记录的是每个改动是什么、为什么这样做,以及权衡过什么。如果你在跟进 OpenClaw Training 系列,可以把这篇看作前几篇的工程实现续集。


升级改了什么

前三篇文章分别解决了配置一个有记忆的助手建立 Workflow 系统项目管理集成。那时候系统已经「能用」了。但「能用」和「投入生产」是两回事。

这次的改动涉及七个区域:

  1. AGENTS.md — 记忆架构正式化,补充知识库 CRUD 校验规则
  2. HEARTBEAT.md — 项目监控模块重写,接入 Linear Auto 标签 + flowchain 脚本
  3. WORKFLOW.md — 新增两个工作流(邮件监控、项目接入)
  4. TOOLS.md — 记录网络请求约束、固定 Obsidian vault 路径
  5. MEMORY.md — 层级表格整理,日记写入规则显式化
  6. flowchain/ — Python 脚本层抽象为执行器,也可以使用 Node.js 的模块化脚本 *.mjs
  7. status/ — 运行时状态目录独立出来,与记忆层彻底分离

主线是一致的:从「OpenClaw 自由发挥」变成「OpenClaw 按规格执行」


记忆架构问题

旧方式

早期的配置有一个简单的记忆模型:MEMORY.md 装一切。这在规模小的时候可以运转,但当文件变大,信噪比就开始下降——三个月前踩过的坑和今天正在做的事情竞争同样的检索优先级,互相干扰。

扁平记忆的根本问题:所有信息处于同一检索层级。 没有「这是当下状态」和「这是长期知识」的区分。每次加载上下文,OpenClaw 不得不把整个文件塞进去,其中大量内容对当前任务毫无关系——既浪费 token,也稀释了真正有用的信息的权重。

三层架构

新架构按信息的时效性和复用性分层:

对话内容
    │ 实时写入

日记层(memory/YYYY-MM-DD.md)     ← 时间戳事件流,当天细节
    │ 每晚 23:45 提炼

知识库(memory/knowledge/)        ← 可复用的教训 / 决策 / 人物档案
    │ 每周 GC 归档

冷存档(memory/.archive/)         ← 过期数据,不主动加载

第一层·日记:只追加,带时间戳,原始事件流。不加工,直接写。文件按日期命名:memory/2026-03-23.md

第二层·知识库:三个子目录——lessons/(踩坑经验)、decisions/(重要决策)、people/(人物档案)。每个文件有结构化 frontmatter。进这一层的内容是提炼后的:「六个月后我还需要知道这件事吗?」

第三层·冷存档:每周 GC 把过期日记移到 .archive/。保持检索空间干净,避免旧上下文污染当前会话。

分层的代价

扁平结构的优点恰恰是它的简单:一个文件,随手写,不需要判断「这条信息该放哪一层」。

分层之后,每次写入都多了一个决策:这是日记还是知识库条目?知识库里应该放 lessons/ 还是 decisions/?提炼的时机对不对?判断错了,信息要么放错层、要么根本没被提炼进去,实际上比扁平结构更难找到。

还有维护成本:每晚提炼、每周 GC、CRUD 校验规则——这些机制本身需要持续运转,任何一个环节失效,分层结构就会开始腐烂,而且腐烂得比扁平结构更隐蔽。

所以分层适合的前提是:你愿意为这套结构持续付出维护成本,并且信息量已经大到扁平结构的噪音问题开始真正影响你的工作。 如果你的记忆文件还很小,扁平的 MEMORY.md 就够了。

具体写什么

日记层是「发生了什么」的原始记录:

### 14:29 — 启动「一键去背景」功能

- GitHub issue 已建:#1
- Linear issue 已建:与 GitHub 双向关联
- Issue 已分配给 GitHub Copilot Agent 处理
- 待办:监控 Copilot Agent 完成进度

知识库条目带结构化 frontmatter,便于检索和生命周期管理:

---
title: "AI Agent 任务委派模式"
date: 2026-03-22
category: lessons
priority: 🟡
status: active
last_verified: 2026-03-22
tags: [ai-agent, delegation, copilot]
---

优先级标记:🔴 核心知识永不归档,🟡 一般重要,⚪ 低优先级。超过 30 天未验证的条目会加 ⚠️ stale 标记——提醒你这条知识可能已经过期,需要重新核实。

CRUD 校验规则

记忆管理最难的不是「写」——是防止「写烂」。写久了,知识库里会开始出现矛盾条目、重复条目、已经过时但没有标注的旧内容。

为此引入了写入前强制校验:

新知识到来

├─ Step 1:读取目标文件现有内容(不存在则新建)
├─ Step 2:与新知识对比
│  ├─ 已有内容完全覆盖 → NOOP(不写)
│  ├─ 新知识是对旧内容的更新 → 旧版加 ~~Superseded~~ 标记
│  ├─ 新知识与旧内容矛盾 → 两版保留,加 ⚠️ CONFLICT 标记
│  └─ 全新知识 → 追加新段落
└─ Step 3:更新 frontmatter 中的 last_verified 日期

为什么必须先读再写: 不做这个校验,知识库会随着时间静默腐烂。不是立刻坏掉,而是慢慢降级成另一个噪音文件——只是比 MEMORY.md 坏得慢一些。


Heartbeat 问题

旧方式:自由发挥的轮询

旧版 heartbeat 是对话式的:OpenClaw 检查 GitHub、检查邮件,然后「决定」推送什么。逻辑活在上下文里,不在文件里。每个 session 的行为都会稍微漂移一点,新起一个 session,行为可能就变了。

新方式:flowchain 作为执行器

新的 heartbeat 是一份分步规格,每一步职责明确:

Step A → Linear Auto 任务检查(超时检测、验收重试计数、并发控制门)
Step B → flowchain Python 脚本(PR 扫描、CI 状态、Linear 状态同步)
Step C → NOW.md 覆写(当前状态快照)

Step A 是纯 OpenClaw 驱动的逻辑层:用 Linear CLI 查询所有标了 Auto 标签且处于 In Progress 状态的 issue,检查它们是否超时(超过 3 天没有关联 PR)、是否需要触发验收兜底、是否可以从 Backlog 自动移入 Todo。这部分逻辑轻量,判断规则写在 HEARTBEAT.md 里,OpenClaw 直接读文件执行。

Step B 是需要跨平台数据聚合的重活,全部交给 Python 脚本:

python3 flowchain/projects.py heartbeat

脚本内部做的事情:从 PROJECTS.json 加载所有被监控的仓库 → 批量调用 GitHub API 拉取每个 repo 的 open PR 和 merged PR → 对每个 merged PR 提取分支名里的 issue ID → 调用 Linear API 将对应 issue 推进到 Done → 对比 heartbeat-state.json 里的 seen_prs 去重 → 输出结构化 JSON。

脚本返回的 JSON 格式固定:

{
  "ci_failures":        [{"repo": "user/MyApp", "pr_number": 5, "pr_title": "fix: crash on launch"}],
  "pr_open_stale":      [{"repo": "user/BackClaw", "pr_number": 12, "hours_open": 25}],
  "pr_merged_to_done":  [{"repo": "user/BackClaw", "pr_number": 8, "identifier": "PROJ-45"}],
  "stale_in_progress":  [{"identifier": "PROJ-30", "days_in_progress": 4}],
  "validation_retries": [{"identifier": "PROJ-20", "retries": 3}]
}

OpenClaw 读取这份 JSON,按字段存在与否决定推送什么内容——有 ci_failures 就立即推 CI 告警,有 pr_merged_to_done 就推完成通知,全为空则静默。整个监控层没有任何自由发挥:结构化数据进,格式化通知出。

Step C 是每次 heartbeat 必做的收尾:覆写 NOW.md,把当前焦点、最近事件、待处理事项更新一遍。这个文件是 OpenClaw 的「当下状态快照」,session 启动时用它快速恢复上下文,不需要重新读大量历史日记。


为什么用脚本而不是 prompt 来做 Step B?

Prompt 是无状态的近似。脚本是确定性的。当你需要「检查这个 PR 是否已开了超过 24 小时还没有合并」,你需要的是算术,不是推断。更重要的是,PR 去重依赖 seen_prs 状态持久化——这件事 prompt 根本做不到,因为上下文会被压缩、会在 session 结束后消失。


验收 Gate

这次改动里设计感最强的一个:对所有 AI 执行的任务强制设置验收关卡。

它解决的问题:AI 编码 Agent(Codex、Claude Code、GitHub Copilot Agent)完成工作后会标注「完成」。但 AI 定义的「完成」和产品定义的「完成」是两回事。以前没有任何机制在中间拦截。

AI Agent 报告完成

└─ Phase 4.5 验收 Gate
   ├─ Step 1:从 Linear issue 读取验收标准
   ├─ Step 2:运行代码质量检查(语法 + 单测)
   ├─ Step 3:对照验收标准逐项核查
   ├─ Step 4:将验收报告写入 Linear comment
   └─ Step 5:通过 → 标记 Done / 不通过 → 自动修复循环(最多 2 次)
              └─ 第 3 次失败 → 上报人工介入

设计模式:你定义验收标准,AI 在关闭前验证。 这道关卡防止的是「issue 被标 Done 只是因为没有任何东西去检查它」这种失败模式。

重试计数持久化在 status/heartbeat-state.json

{
  "validation_retries": {
    "PROJ-74": 1,
    "PROJ-83": 0
  }
}

超过 3 次则上报。计数不会自动重置——只有你确认处理后才清除。确保反复失败的任务会得到人的关注,而不是被静默跳过。

验收标准写在 Linear issue description 的固定区块里:

## 验收标准
- [ ] 网络超时时用户可见错误提示
- [ ] 错误信息包含重试建议
- [ ] 现有登录流程无回归

建 issue 时如果没有写验收标准,OpenClaw 会根据描述自行推断补写,然后推给你确认。没有验收标准的 issue 不允许进入自动化开发流程——这是一条硬规则,不是建议。


两个新 Workflow

邮件监控(Workflow 04)

邮件监控要解决的问题:如何从嘈杂的收件箱里得到信号,而不需要逐封阅读?

方案:一个规则文件(status/MAILLIST.json),把「什么重要、什么不重要」的个人判断编码进去:

{
  "last_summary_date": "2026-03-23",
  "last_urgent_ids": ["msg_abc123", "msg_def456"],
  "last_run": "2026-03-23T18:25:00+08:00",
  "config": {
    "immediate": {
      "sender_whitelist": ["*@github.com", "[email protected]"],
      "subject_keywords": ["线上故障", "生产", "urgent", "ASAP"]
    },
    "summary": {
      "labels": ["important"],
      "senders": ["linear.app"]
    },
    "ignore": {
      "labels": ["promotions", "social"],
      "sender_blacklist": ["[email protected]"]
    }
  }
}

四个层级(优先级从高到低):

  • 🤖 Copilot PR 通知 — 最高优先级,触发验收流程(见下)
  • 🔴 立即推送 — 白名单发件人或紧急关键词,立即发出,无视安静时段
  • 📋 每日汇总 — 每天最多一次,汇总「值得读但不紧急」的邮件
  • 🔇 忽略 — 推广、社交通知,永不浮出

文件里同时存着规则(config)和运行状态(last_urgent_idslast_summary_datelast_run)。这不是随意设计的——两者放在一起的好处是:OpenClaw 启动时读一个文件就能知道「规则是什么」和「上次做到哪里了」,完成后写回同一个文件。单一数据来源,跨 session 持久化,不会出现规则文件更新了但状态文件没更新的不一致。

Copilot Agent → 邮件 → 验收的闭合链路

这是邮件监控和项目管理工作流交叉的地方,也是整个自动化系统里最能体现「端到端闭环」的一段。

GitHub Copilot Agent 完成开发后会开一个 PR,GitHub 随即发一封邮件通知。邮件监控捕获这封邮件后,不是简单推送给你,而是自动触发后续的验收流程:

Copilot Agent 开 PR

      ├─ GitHub 发邮件通知(发件人 *@github.com,Subject 含 "github-copilot[bot]")


邮件监控捕获(heartbeat 或实时)

      ├─ 从 Subject 提取 {repo} 和 PR 编号
      ├─ gh pr view → 从分支名提取 Linear issue ID
      ├─ Linear issue → In Review


触发 Phase 4.5 验收 Gate

      ├─ 读验收标准 → 运行检查 → 写验收报告到 Linear comment

      ├─ 通过 → Linear Done,推送完成通知
      └─ 失败 → 自动修复循环(最多 2 次) → 仍失败则上报

整条链路不需要你介入:Copilot Agent 开 PR,OpenClaw 验收,通过则关闭,失败则上报。你只需要在最开始写好 Linear issue 的验收标准,以及在连续验收失败时做最终裁决。

唯一需要分支命名规范配合:分支名里必须包含 Linear issue ID(如 fix/PROJ-74-remove-bg),OpenClaw 靠这个建立 PR 和 issue 的关联。格式不对,自动链路断开,降级为普通推送通知。

执行流程(完整版):

Step 1:读取 MAILLIST.json,获取规则和状态
Step 2:拉取未读邮件(排除 promotions/social 分类)
Step 3:逐封按优先级匹配
        ├─ Copilot PR 通知 → 触发验收链路(Step 3.1)
        ├─ 立即推送 → 直接推 Telegram
        ├─ 汇总 → 加入每日摘要队列
        └─ 忽略 → 跳过
Step 4:推送普通紧急邮件(msg_id 去重)+ 每日汇总(last_summary_date 去重)
Step 5:更新 last_run、last_urgent_ids 写回 JSON

项目接入(Workflow 06)

每个新项目都需要:GitHub repo、Linear 项目、本地 clone、写入项目注册表、可选地加入心跳监控。以前这些步骤是手动的,每次都要想起来做哪几步,容易遗漏。

现在是六步 Playbook:

Step 1:确认 GitHub repo 存在(不存在则创建)
Step 2:确认 Linear project 存在(不存在则创建)
Step 3:clone 到本地
Step 4:注册到 PROJECTS.json
Step 5:加入 heartbeat 监控标志(如需要)
Step 6:推送完成摘要

为什么值得单独建一个 Workflow: 项目接入是最典型的「每一步单独都很显然、组合起来容易漏」的任务。漏掉 Step 5(heartbeat 监控标志),意味着这个项目的 PR 和 CI 可能在无人察觉的情况下运转好几周——你以为在监控,实际上没有。


flowchain 架构

这次最大的结构性改动:把所有 Linear 和 GitHub 操作抽象到 Python 脚本层(flowchain/projects.py)。

改造前: OpenClaw 直接在对话里调用 Skills(linear-cligithub),从上下文里拼命令字符串,解析各自不同格式的输出。

改造后: OpenClaw 调用 projects.py <命令>,读取格式统一的 stdout。

python3 flowchain/projects.py issue create "feat: 深色模式" --project MyApp
# stdout: [ok] PROJ-82 — feat: 深色模式

python3 flowchain/projects.py issue move PROJ-82 "In Progress"
# stdout: [ok] PROJ-82 → In Progress

python3 flowchain/projects.py sprint MyApp
# stdout: JSON(项目当前所有状态的快照)

统一输出协议:[ok] / [warn] / [error] 前缀。OpenClaw 只需检查第一个 token,不需要解析各个 CLI 工具各自不同的原始输出格式。

flowchain 解决的核心问题

1. Skills 漫游

这个问题值得展开说。

OpenClaw 有几十个 Skills,每个 Skill 都有自己的 SKILL.md——一份给 AI 看的使用说明。每次 OpenClaw 要调用一个 Skill,需要先读它的 SKILL.md,理解参数格式,再拼命令。这个过程消耗上下文 token,而且会漂移:

  • linear-cliissue list 参数在某个版本有细微变化,SKILL.md 没有及时更新
  • 不同 session 里 OpenClaw 对同一参数的理解可能略有不同
  • 多个 Skill 的输出格式各异,OpenClaw 需要在对话里「记住」怎么解析每一个

漂移积累到一定程度,你会发现 OpenClaw 在 session A 里能正确更新 Linear 状态,在 session B 里却拼出了错误的参数。

flowchain 做了一层隔离:OpenClaw 只知道 projects.py 的接口(几十行文档,稳定),不直接读各个 Skill 的说明文件。底层调哪个 Skill、怎么组合参数、怎么处理不同版本差异——全在脚本里,由 Python 处理。接口稳定,行为才稳定。

这也有一个直接的上下文节省效果:以前每次操作都要加载 linear-cli 的 SKILL.md(几百行);现在 OpenClaw 的工作上下文里只需要放 projects.py 的接口文档(几十行)。对于每小时跑一次的 heartbeat 而言,这个差距很明显。

2. 状态从 prompt 外移

以前「这个 PR 上次扫描时有没有见过」只能靠 OpenClaw 在上下文里「记」。但上下文会被压缩,会在 session 结束后消失。

现在这个状态存在 status/heartbeat-state.json 里:

{
  "seen_prs": {
    "user/BackClaw": [8, 9, 11],
    "user/MyApp": [1, 2]
  },
  "last_heartbeat": "2026-03-23T18:25:00+08:00"
}

持久,可查,可手动修改。即使 OpenClaw session 重启,下次 heartbeat 也知道哪些 PR 已经通知过了。

3. 错误处理统一

每个脚本命令都有明确的失败路径:stderr 上带 [error] 前缀,OpenClaw 统一检测后推送给你,不会出现「脚本静默失败,OpenClaw 继续假装成功」的情况。失败路径和成功路径一样可预期。

4. 可独立测试

脚本不依赖 OpenClaw 的上下文,可以独立运行。以前要验证监控逻辑,需要等 heartbeat 触发;现在直接:

python3 flowchain/projects.py heartbeat

在终端里看输出,调试,确认行为,再让 OpenClaw 去跑。这对系统稳定性来说是很大的改善——你可以在不影响 OpenClaw 正常运行的情况下迭代脚本逻辑。


status/ 目录

一个小但很重要的结构性补充:把运行时状态统一放到 status/ 目录。

status/
├── PROJECTS.json          # 项目注册表(权威来源)
├── heartbeat-state.json   # 上次运行时间、已见 PR、验收重试计数
├── MAILLIST.json          # 邮件监控规则和状态
└── candidates.json        # 招聘管道状态

完整数据流

一次 heartbeat 触发后,status/ 文件是如何被读取和更新的:

Heartbeat 触发(每 60 分钟)

      ├─ 读 PROJECTS.json
      │    └─ 获取所有 heartbeat: true 的项目和对应 repo 路径

      ├─ 读 heartbeat-state.json
      │    └─ 获取 seen_prs(去重用)、validation_retries(验收重试计数)

      ├─ 执行 flowchain/projects.py heartbeat
      │    ├─ 批量拉取每个 repo 的 open PR 和 merged PR
      │    ├─ 对比 seen_prs,过滤已通知的 PR
      │    ├─ merged PR → 提取 issue ID → 调 Linear API 标 Done
      │    ├─ CI 失败 → 记录到 ci_failures
      │    └─ 输出结构化 JSON

      ├─ OpenClaw 读取 JSON,按字段决定推送内容

      ├─ 写回 heartbeat-state.json
      │    └─ 更新 seen_prs(追加新 PR ID)、last_heartbeat、validation_retries

      ├─ 读写 MAILLIST.json
      │    ├─ 读取 config 规则 + last_urgent_ids + last_summary_date
      │    ├─ 拉取邮件,匹配规则,决定推送
      │    └─ 写回更新后的 last_urgent_ids、last_summary_date、last_run

      └─ 覆写 NOW.md(当前状态快照,供下次 session 快速恢复上下文)

为什么要和 memory/ 分开

memory/ 存的是 OpenClaw「知道的事」:踩过的坑、做过的决策、对你偏好的了解。这是认知层面的内容,以「我学到了什么」为主语。

status/ 存的是系统「做过的事」:上次心跳时间、已推送过的邮件 ID、正在监控的项目列表。这是操作层面的内容,以「系统执行了什么」为主语。

把两者混在一起会导致双重问题:

  • 检索噪音:搜索「某个项目的背景」时,seen_prs 这类操作日志会干扰结果
  • 操作不可靠:如果去重逻辑依赖 memory/ 里的内容,而记忆文件可能被提炼、压缩、改写,去重就会出现漏洞

设计规则:「OpenClaw 知道什么」→ memory/;「系统做了什么」→ status/

status/ 文件另一个重要特性:可以安全覆写memory/ 下的日记文件只能追加——覆写等于数据丢失。status/ 文件相反,每次操作后整个写回是正确行为,因为它们存储的是当前状态快照,不是历史记录。这两套文件在写入语义上完全相反,混在一起很容易出错。


几个观察

「写下来再执行」模式: 这轮改动里很多内容来自发现 OpenClaw 有隐性知识——怎么监控项目、怎么分级邮件——但从来没有写下来。把隐性知识显式化,写进 Workflow 文件、写进 CRUD 规则、写进验收标准,这就是「好的 AI 系统设计」的主要工作内容。不是调 prompt,是把知识外化到文件里。

幂等性被严重低估: 好的系统设计意味着同一个操作执行两次是安全的。last_urgent_ids 去重、last_summary_date 每日保护、validation_retries 计数——这些都是让操作可以安全重复执行的机制。heartbeat 每小时跑一次;没有去重逻辑的话,每次都会重复推送所有内容。

人机协作的设计原则: 几乎每个自动化操作都有明确的人工检查点。AI Agent 执行代码,但 OpenClaw 在标 Done 之前验收。OpenClaw 提议 Sprint 变更,但你确认后才执行。Workflow 更新 last_run,但 SOP 内容变更需要你确认后 OpenClaw 才写回。自动化是真实的,但升级路径是显式设计出来的——不是隐式 fallback。

评论