用了一个月speckit后放弃了—存量项目spec的真实体验

10 阅读9分钟

首先抛出一个观点,在我使用了将近一个月的speckit之后,我个人理解speckit这种基于瀑布流理念构建的AI开发流程对于存量项目是不合适的。

对于一个新的项目可能比较合适,没有任何约束,就像一个一望无际的大草原,你可以策马奔腾。

但是对于一个存量系统,就像是在一个随处都有坑或者山头的地方行走,一不留神就会深陷泥潭。

你需要对当前系统的边边角角有全面了解,同时对代码仓库无法直接反馈的业务决策信息,特殊时代的背景信息等有全面了解才能保证整体业务交付质量。

但是这些都无法在speckit这个体系中无法体现,大量背景信息每次都要补充,补充之后无法沉淀,下次还要要补充,每次通过AI都是冷启动开发。

时间久了随着你个人对系统熟悉程度的增加,你可能越来越觉得还不如直接告诉AI怎么做,让AI彻底进入执行层,LLM的决策能力无法彻底释放。

系统介绍

犁剑是一个海外贷后催收系统,基于Spring生态构建的多微服务业务系统:

Server.Sword.Case/      ← 案件生命周期:罚息、还款、减免、入催
Server.Sword.Allot/     ← 案件流转:分案、撤案、留案
Server.Sword.Regain/    ← 催收作业:邮件、短信
Server.Sword.Home/      ← 业务网关:鉴权 + 聚合接口
Server.Sword.User/      ← 用户管理 + 审批引擎
Server.Sword.Datalink/  ← 数据同步

因为一个系统要支持公司所有海外贷后催收业务,所有对于整个团队来讲,多业务线并行,每个需求要改动多个微服务战点是一个非常常见的事情。

因为业务增速过快,但是整体系统技术建设没有跟上的原因,导致服务之间已经出现了环形调用,这就导致对每个需求实施阶段来讲,技术方案的设计和兼容性要求特别高。

我试着用 speckit 来规范 辅助开发流程。以下是真实经历,不是理论推演。

任何需求实施的第一阶段是"考古"

speckit 的五阶段流程有一个隐含的前提——你知道自己在造什么:

Constitution 定义原则 → Specify 定义需求 → Plan → Tasks → Implement

但犁剑这种存量项目的每个需求实施,第一步都是搞清楚现在到底怎么跑的:

这个委案流程现在走几步?
HandleChain 里哪些步骤有跨服务调用?
各业务线的实现差异在哪?
我们之前因为什么业务背景信息做了这种决策?
上次超时是什么原因?

拿委案流程来说,光是梳理调用链就发现了 Case → Allot → Regain 三个服务间的 6 步状态机、远程调用、补偿任务、分布式锁,还有事务内远程调用的风险。这些信息不在任何文档里,全在代码和历史排查记录里。

speckit 没有"考古"阶段。但存量项目的每个需求都得先考古,搞清楚影响面,才能开始谈方案。

第一次我可以接受在对话窗口补充这些所有的信息或者驱动LLM去阅读代码熟悉系统,在当前上下文窗口有一个相对完整且清晰的业务和系统认知,但是这些没有沉淀下来。随着对话窗口的结束这些都没有了。

下一次你开发关联需求还是要补充,相当于每次开发都是一个冷启动的过程。过去LLM学习到的知识没有保存下来,下次LLM也无法自动根据知识进行决策。

Spec 只描述一个系统

speckit 的 Spec 描述的是单个 Repo 的行为。犁剑的现实是多服务编排:

graph LR
    subgraph speckit的假设
        direction LR
        R["一个 Repo"] --> C["Constitution"] --> S["Specify"] --> I["Implement"]
    end

    subgraph 犁剑的现实
        direction LR
        Case["Case 服务"] <-->|Feign| Allot["Allot 服务"]
        Allot <-->|Feign| Regain["Regain 服务"]
        Home["Home 服务"] -->|Feign| Case
        Home -->|SPI| Plugin["业务线插件"]
    end

一个委案需求涉及跨服务编排、分布式事务边界、补偿机制。这种复杂度用一份 Spec 文档装不下。

在运行中的审批引擎上加留案审批

这个案例最能说明问题。

犁剑的审批流系统一开始只给减免审批用,四层模型:模板(Template)→ 实例(Instance)→ 节点(Node)→ 任务(Task)。拿到留案审批需求时,真正的问题不是"怎么做留案审批",而是"怎么加上去还不把正在跑的减免审批搞崩"。

审批引擎现状

graph TB
    subgraph Home["Home 服务(BFF 层)"]
        WFC["WorkFlowController<br/>审批流 API 入口,7 个端点"]
    end

    subgraph User["User 服务(审批引擎)"]
        WFI["WorkFlowInstanceBiz<br/>审批实例管理"]
        DH["DeductWorkFlowHandler<br/>减免回调处理器"]
    end

    subgraph Case["Case 服务(业务方)"]
        DA["DeductApi<br/>减免业务"]
    end

    WFC -->|Feign| WFI
    WFI -->|审批通过| DH
    DH -->|syncWorkFlowState| DA

    style Home fill:#e3f2fd
    style User fill:#fff3e0
    style Case fill:#e8f5e9

读完代码后发现四件事:

  • WorkFlowTemplateCodeEnums 只有 3 个枚举值,全是减免相关
  • WorkFlowController.pageList() 硬编码了 DEDUCT_CODES 做模板筛选
  • instanceInfo() 里的 populateDeductInfo() 只处理减免的业务详情
  • DeductWorkFlowHandler 是唯一的 Handler 实现

每一条都是从代码里读出来的。没有文档记录过这些。

speckit 会怎么做 vs 实际发生了什么

graph TB
    subgraph speckit的流程
        direction TB
        SK1["Specify:写用户故事和验收标准"]
        SK2["Plan:新增 Handler + 枚举 + 接口"]
        SK3["Tasks:T001 加枚举 → T002 写 Handler → T003 测试"]
        SK4["Implement:按序实现"]
        SK1 --> SK2 --> SK3 --> SK4
    end

    subgraph 实际发生的事
        direction TB
        SW0["第 0 步:考古<br/>审批引擎四层模型怎么转?<br/>Handler 回调机制?补偿流程?"]
        SW1["需求本身清晰,但决策点不在需求层面:<br/>留案端点放 Home 还是 Case?<br/>回调失败怎么补偿?"]
        SW2["初版方案:在 WorkFlowController 加端点<br/>→ 发现 pageList 硬编码了 DEDUCT_CODES<br/>→ 改 DTO 风险太大<br/>→ 改为留案走 Case 独立端点"]
        SW3["编码时发现:<br/>→ Feign 接口还没定义<br/>→ 审批通过要触发 6 步状态机<br/>→ 状态机失败时审批已标记完成<br/>→ 需要额外补偿逻辑"]

        SW0 --> SW1 --> SW2 --> SW3
        SW3 -.->|补偿逻辑| SW2
        SW2 -.->|方案调整| SW1
    end

    style SK1 fill:#c8e6c9
    style SK2 fill:#c8e6c9
    style SK3 fill:#c8e6c9
    style SK4 fill:#c8e6c9
    style SW0 fill:#e1bee7
    style SW1 fill:#ffcdd2
    style SW2 fill:#ffcdd2
    style SW3 fill:#ffcdd2

注意右边的虚线箭头——实施阶段的发现推翻了方案,方案的调整又反过来修正了需求理解。这不是意外,这是存量项目的常态。

每个阶段的实际落差

speckit 缺一个"考古"阶段。

正常我们实施这个需求的一件事是阅读当前审批流程代码并真理出整体架构:四层模型、Handler 回调、补偿流程、queryCode 路由。

  • Specify 阶段,假设需求本身清晰,但要做的决策不在需求文档能覆盖的范围内

    • 端点放哪个服务?

    • 回调失败怎么办?

    • Spec 模板没法表达"在现有系统上扩展"这类约束。

  • Plan 阶段,初版方案直接被否了,因为我们得到业务通知要调整,导致实施方案要调整,也就意味着通过speckit产出的所有文档已经不能完全起作用,需要推倒重来一遍。

  • Implement 阶段,写着写着发现 Feign 接口没定义、状态机失败需要补偿、queryCode 需要新增分支。Spec 和 Plan 都没覆盖这些。

需求交付的难点

这不是在做一个新功能。是在一台运转中的引擎上加新业务类型,难的地方全在留案之外

  • 现有的待审批列表查询参数硬编码默认查询所有的减免审批,写这段代码的人压根没想过审批还能用在别的地方。留案上线前得先把这些隐式假设全找出来,绕开它们,不然减免流程先崩。
  • 还有回调链:审批通过 → Handler 回调 → Case 创建 Active → 启动 6 步状态机。中间任何一步挂了都需要补偿,但审批那头已经标记完成了。Spec 描述不了这种跨服务的故障传播。
  • 最后一点可能最容易被忽略——最终方案选择留案查询走 Case 独立端点、不改 Home 的 WorkFlowController。"决定不做什么"这个判断来自对代码的了解。Spec 里找不到。

上下文窗口:绕不过去的物理限制

就算流程问题都解决了,上下文窗口的容量限制也会卡住你。

graph TB
    subgraph Window["上下文窗口"]
        direction TB
        Fix["固定开销<br/>命令模板 + tasks + plan + 可选产物<br/>占窗口 30-50%"]
        Need["委案流程需要的代码<br/>3 个服务 × 多层调用链<br/>+ 6 步状态机 + SPI 实现<br/>+ Feign 依赖 + 补偿 Job<br/>需要窗口 80%+"]
    end

    Fix --- Conflict{"装不下"}
    Need --- Conflict

    Conflict -->|保产物| LoseCtx["代码上下文被截断<br/>LLM 缺少信息,生成质量低"]
    Conflict -->|保代码| LoseRule["产物被挤出去<br/>LLM 忘记规则,输出不合规"]

    style Fix fill:#ff9999
    style Need fill:#99ccff
    style Conflict fill:#ffcc00
    style LoseCtx fill:#ff6666
    style LoseRule fill:#ff6666

保产物就丢代码上下文,保代码上下文就丢产物。两头堵,没有中间态。

在LLM当前硬性上下文的约束之下,很难做到两个都保证。

及时当前有1M大小的上下文窗口,但是我们同样会面临LLM的中间丢失和上下文腐烂的问题处理。

为什么不合适

回头看,原因其实不复杂。

预先设定在存量项目很难起作用

speckit 从 Constitution 开始,假设你在定义新系统。

犁剑的每个需求都得先摸清现状,不读代码根本不知道有什么。speckit 没给这步留位置,从需求描述直接跳到技术方案,影响面一定不完整。

影响面不完整就意味着后面的方案设计无法兼顾所有场景,会出现返工。

然后是线性流程的问题。存量项目的节奏是这样的:

graph TB
    subgraph 存量项目的真实节奏
        direction TB
        R1["需求会变<br/>某业务线临时加合规要求"] -->|推翻| S2["方案会改<br/>读代码发现接口要调整"]
        S2 -->|阻塞| D["依赖会卡<br/>等 DBA 加字段、等外部联调"]
        D -->|并行| P2["资源会抢<br/>多个分支改同一段代码"]
        P2 --> R1
    end

    style R1 fill:#ffcdd2
    style S2 fill:#ffcdd2
    style D fill:#ffcdd2
    style P2 fill:#ffcdd2

speckit 假设 Specify 完了不会改、Plan 定了不会变。虽然有 /speckit.clarify 处理歧义,但进了 Plan 就没有回头改 Spec 的机制。我们在编码阶段推翻过 Spec,不止一次。

spec文档和代码文档都要维护

还有 Spec 的详细度问题。SDD 要求 Spec 详细到"像代码一样"。但维护一份这么详细的 Spec 等于维护两套系统。

多业务线并行迭代的项目里,Spec 在第一次跨团队对齐会后就可能过时了。越详细越接近代码本身,维护两份"代码"不划算。

放弃 speckit 之后做了什么

没有完全扔掉 speckit 的想法,但做了三个改造。

第一个是 subagent 并行。用 Claude Code 的 subagent 机制把任务分散到多个独立上下文窗口,每个 subagent 只加载自己那部分上下文,不用把所有产物塞进同一个窗口。犁剑现在的 context/ 目录就是为此设计的——按服务、按流程、按域组织知识,agent 按需取用。

第二个是把"考古"成果固化下来。每次排查、每次梳理调用链、每次踩坑,都结构化记录到 context/knowledge/ 下。委案流程的调用链路、线程分析、故障模式,都有独立文档。这些不是 speckit 那种"应该是什么"的 Spec,是"实际是什么"和"为什么"的记录。下次遇到相关需求,agent 直接加载,跳过重复的考古。

第三个是按需加载。我们建了一套索引(context/INDEX.md),agent 按"阶段→域→关键词"过滤后只加载匹配的文件。窗口里的每个 token 都有用,不会被"以防万一"的约束文档占满。

结语

如果你在做全新项目、需求清晰、复杂度可控,speckit 的五阶段流程确实能帮上忙。

但如果你面对的是一个跑了几年的存量系统——多服务编排、多业务线并行、代码里埋着各种历史假设——"先写规范再写代码"在第一个需求就碎了。存量项目的复杂度不在 Spec 里,在过去的决策中,在业务背景信息中;当下这些知识都无法通过代码库直接传递给LLM。

最后想明白的事情很简单:问题不是"怎么写出更好的 Spec",是"怎么让 AI 搞懂一个已经跑了几年的系统长什么样"。