乐于分享
好东西不私藏

AI原生开发:ClaudeCode与国产大模型协同实战 第5章:给野马套上缰绳——用OpenSpec把变成死磕契约的偏执狂

AI原生开发:ClaudeCode与国产大模型协同实战 第5章:给野马套上缰绳——用OpenSpec把变成死磕契约的偏执狂

老兵们,上一章我们学会了用C.O.D.E框架和思维链给AI下指令,效果立竿见影,对吧?但如果你真的在大型项目里用AI写过核心业务,你一定经历过这种绝望:你明确告诉了AI“必须用Redis做分布式锁”、“返回体必须是Result<T>”,它前三次写得无比完美。第四次,你让它加个小功能,它一激动,把Redis锁忘了,返回体直接给你甩了个裸对象出来。你愤怒地质问,它委屈地道歉,然后改完这个,另一个地方又漏了。为什么?因为Prompt是“软约束”,它是用自然语言写的,没有强制力。AI本质上是个概率机器,写着写着概率偏移了,它就“忘了”你的规矩。资深工程师最怕的不是没规矩,而是规矩定了一半有人不守。今天,我们就来彻底解决这个问题,给这匹野马套上工程级的紧箍咒——OpenSpec

5.1 救命稻草:什么是OpenSpec?为什么它是AI协同开发的基石?

很多同学第一次听OpenSpec觉得很玄乎。其实一点不玄乎。回想一下,在微服务架构里,前端和后端怎么协同?靠口头约定吗?不可能。我们靠Swagger/OpenAPI。后端定义好接口路径、参数类型、返回结构,前端照着调,谁也别越界。这就叫“契约驱动”。OpenSpec也是同样的逻辑,只不过它是人类与AI之间的契约如果说Prompt是用自然语言跟AI“商量”,那么OpenSpec就是用结构化语言给AI“下死命令”。它把模糊的业务需求,拆解成AI无法误读的节点、数据流、状态机和接口契约

核心差异:自然语言的Prompt里写“余额不能为负”,AI可能会忽略;但在OpenSpec里,你把 balance 定义为 minimum: 0,AI生成的代码里如果没有校验,它自己都会觉得违背了物理法则,因为它必须对着Spec的字典逐字翻译。

5.2 解构OpenSpec:节点、边、状态与契约

到底怎么写一份OpenSpec?它不是什么外星语言,它就是你平时做系统设计时画的架构图,只不过我们用YAML/JSON把它严格定义下来。一个完整的OpenSpec由四个核心要素构成:

  1. Entities(实体/节点):系统里的核心名词。比如 UserOrderPointAccount。定义它们的字段和类型。

  2. States(状态):实体所处的生命周期。比如 Order 有 UNPAIDPAIDSHIPPED

  3. Edges/Transitions(边/流转):触发状态改变的动词。比如 pay() 动作让 UNPAID -> PAID

  4. Contracts(契约):对外暴露的接口。定义入参、出参、异常码,极其严格。我们拿电商里最臭名昭著的“积分扣减”来开刀。这玩意并发一高就超卖,业务逻辑稍微没理清就资损。

5.3 【实操】从模糊PRD到严苛OpenSpec:亲手写出规约文档

产品经理的PRD通常是这样的:

“用户可以在结算时用积分抵扣,1积分抵1分钱。积分不够不能扣,扣完要记流水。”这要直接丢给AI,绝对给你扣出负数来。我们来看看资深工程师是怎么把它转成OpenSpec的。创建一个文件 point_deduct_spec.yaml

# point_deduct_spec.yamlopen_spec_version: "1.0"module: PointsDeductionentities:  PointAccount:    description: "用户积分账户"    fields:      user_id: { type: string, required: true }      balance: { typeinteger, required: true, minimum: 0, description: "可用余额,绝对不允许为负" }      version: { typeinteger, required: true, description: "乐观锁版本号" }  PointTransactionLog:    description: "积分流水记录"    fields:      log_id: { type: string, required: true }      user_id: { type: string, required: true }      amount: { typeinteger, required: true, description: "变动正负数" }      reason: { type: string, required: true }states:  PointAccount:    - ACTIVE    - FROZENtransitions:  deduct_points:    description: "扣减积分核心流转"    from_state: ACTIVE    to_state: ACTIVE  # 扣减不改变状态,除非余额归零    pre_conditions:      - "account.balance >= deduct_amount"      - "deduct_amount > 0"    post_conditions:      - "account.balance >= 0" # 铁律:绝对不能超卖    side_effects:      - "写入 PointTransactionLog"contracts:  DeductPointsAPI:    endpoint: /points/deduct    method: POST    request:      user_id: { type: string, required: true }      amount: { typeinteger, required: true, minimum: 1, description: "扣减数量,必须大于0" }      order_id: { type: string, required: true, description: "幂等键,同一订单号只扣一次" }    response:      success:        code: 200        data: { remaining_balance: integer }      errors:        - code: 40001          message: "Insufficient balance"        - code: 40002          message: "Duplicate deduction (Idempotent hit)"        - code: 40003          message: "Account frozen"

看到没?这叫工程化。 余额不能为负、必须传orderId防重、各种异常码全安排得明明白白。AI拿到这份文档,连胡思乱想的空间都没有。

5.4 【深挖】封神时刻:看GLM-5.1如何在OpenSpec约束下秒变偏执狂

现在,见证奇迹的时刻到了。我们把这份OpenSpec喂给GLM-5.1,看看它写出来的代码和纯Prompt有什么天壤之别。👉 请在ClaudeCode中输入以下指令(注意我们是如何注入OpenSpec的):

# Role你是一个极度严谨的支付结算后端工程师。# Task请根据我提供的 OpenSpec 规约文档,实现 `deduct_points` 的核心业务逻辑。# OpenSpec 规约(将上面 point_deduct_spec.yaml 的内容完整粘贴在这里)# Directive1. 必须使用 Python + SQLAlchemy (同步模式)。2. 必须严格遵循 OpenSpec 中的 `transitions` 和 `contracts` 定义。3. 针对 `pre_conditions`,必须使用数据库层面的乐观锁(基于 version 字段)来保证并发安全,绝对不能出现超卖(余额为负)。4. 必须实现基于 order_id 的幂等逻辑。5. 异常必须严格按照 contracts 中定义的 errors 抛出。请直接输出核心的 service 层代码(包含实体模型和业务逻辑)。
大模型拿到这份指令后,它的注意力机制会被强制聚焦在Spec的每一个字段上。它会像强迫症一样,逐条核对 minimum: 0,核对 errors 列表。下面是GLM-5.1在这套约束下输出的代码。我进行了微调以确保直接可运行,你可以看到这绝对是工业级的水准:
import hashlibfrom sqlalchemy import Column, String, Integer, create_enginefrom sqlalchemy.orm import declarative_base, sessionmakerfrom sqlalchemy.exc import IntegrityErrorBase = declarative_base()# ================== 实体模型 (严格对应 OpenSpec Entities) ==================class PointAccount(Base):    __tablename__ = 'point_account'    user_id = Column(String(64), primary_key=True)    balance = Column(Integer, nullable=False# OpenSpec: minimum: 0    version = Column(Integer, nullable=False# OpenSpec: 乐观锁版本号class PointTransactionLog(Base):    __tablename__ = 'point_transaction_log'    log_id = Column(String(128), primary_key=True)    user_id = Column(String(64), nullable=False)    amount = Column(Integer, nullable=False)    reason = Column(String(255), nullable=False)class IdempotentRecord(Base):    __tablename__ = 'idempotent_record'    order_id = Column(String(128), primary_key=True)    user_id = Column(String(64), nullable=False)# ================== 自定义异常 (严格对应 OpenSpec Contracts.errors) ==================class InsufficientBalanceError(Exception):    def __init__(self): self.code = 40001self.message = "Insufficient balance"class DuplicateDeductionError(Exception):    def __init__(self): self.code = 40002self.message = "Duplicate deduction (Idempotent hit)"class AccountFrozenError(Exception):    def __init__(self): self.code = 40003self.message = "Account frozen"# ================== 核心业务逻辑 (严格对应 OpenSpec Transitions) ==================class PointService:    def __init__(self, session):        self.session = session    def deduct_points(self, user_id: str, amount: int, order_id: str):        # 1. 幂等校验 (OpenSpec Contract: order_id 幂等键)        existing_record = self.session.query(IdempotentRecord).filter_by(order_id=order_id).first()        if existing_record:            raise DuplicateDeductionError()        # 2. 基础校验 (OpenSpec Contract: amount minimum 1)        if amount <= 0:            raise ValueError("Deduct amount must be greater than 0")        # 3. 查询账户        account = self.session.query(PointAccount).filter_by(user_id=user_id).with_for_update().first()        if not account:            raise ValueError("Account not found")        # 4. 前置条件校验 (OpenSpec Transitions: pre_conditions)        if account.balance < amount:            raise InsufficientBalanceError()        # 5. 执行扣减与乐观锁更新 (OpenSpec Transitions: post_conditions balance >= 0)        # UPDATE point_account SET balance = balance - :amount, version = version + 1         # WHERE user_id = :user_id AND version = :old_version        updated_rows = self.session.query(PointAccount).filter(            PointAccount.user_id == user_id,            PointAccount.version == account.version,            PointAccount.balance >= amount # 数据库层最后防线        ).update({            "balance": PointAccount.balance - amount,            "version": PointAccount.version + 1        })        if updated_rows == 0:            # 乐观锁冲突或余额不足,重试或抛异常            raise InsufficientBalanceError()        # 6. 记录流水 (OpenSpec Transitions: side_effects)        log_id = hashlib.md5(f"{order_id}:{user_id}".encode()).hexdigest()        new_log = PointTransactionLog(            log_id=log_id,            user_id=user_id,            amount=-amount, # 负数表示扣减            reason=f"Deduction for order {order_id}"        )        self.session.add(new_log)        # 7. 记录幂等键        idempotent_rec = IdempotentRecord(order_id=order_id, user_id=user_id)        self.session.add(idempotent_rec)        self.session.commit()        # 8. 返回 (OpenSpec Contract: response success)        return {"remaining_balance": account.balance - amount}# ================== 测试运行 ==================if __name__ == "__main__":    # 初始化内存 SQLite 数据库测试    engine = create_engine('sqlite:///:memory:')    Base.metadata.create_all(engine)    Session = sessionmaker(bind=engine)    session = Session()    # 准备测试数据    session.add(PointAccount(user_id="U001", balance=100, version=1))    session.commit()    service = PointService(session)    print("--- 正常扣减 ---")    res1 = service.deduct_points(user_id="U001", amount=30, order_id="ORD1001")    print(f"扣减成功,剩余余额: {res1['remaining_balance']}")    print("\n--- 幂等拦截 ---")    try:        service.deduct_points(user_id="U001", amount=30, order_id="ORD1001")    except DuplicateDeductionError as e:        print(f"拦截成功: {e.message} (Code: {e.code})")    print("\n--- 余额不足 ---")    try:        service.deduct_points(user_id="U001", amount=999, order_id="ORD1002")    except InsufficientBalanceError as e:        print(f"拦截成功: {e.message} (Code: {e.code})")

深挖时刻:看看AI到底做对了什么?

  1. 双重防线防超卖:它不仅在业务层判断了 balance < amount,还在 update 语句的条件里加上了 PointAccount.balance >= amount。这是极高段位的写法,即使并发下乐观锁放过,数据库层也会把最后一条超卖请求挡住。这是纯Prompt极难逼出来的细节。

  2. 极度精准的异常映射:异常类和错误码与YAML里的 4000140002 一一对应,分毫不差。

  3. 完整的幂等闭环:不仅做了防重表,还在流水生成时把 order_id 融合进去了。这就是OpenSpec的威力——它消除了大模型的方差,把生成代码变成了精密的翻译过程。

5.5 契约驱动开发:从OpenSpec直接生成测试用例与骨架代码

既然有了契约,我们完全可以把开发流程倒过来:先定Spec,再写测试,最后填代码。 也就是AI时代的规格驱动开发。流程如下:

你可以直接在ClaudeCode里执行第二步:

“根据我上面的 point_deduct_spec.yaml,请生成覆盖所有 contracts.errors 和 pre_conditions 的 Pytest 测试用例,使用 mock 替代真实数据库。”你会发现,AI生成的测试用例,不仅包含了正常路径,连余额不足、幂等拦截、账户冻结这些边界Case,都按照Spec里的定义写得清清楚楚。人不再需要去想测试用例怎么写,人只需要确保Spec是完美的。 Spec就是架构师的图纸,AI就是照着图纸干活的施工队。图纸画对了,楼就歪不了。


🏆 第5章代码角斗场:毒药挑战

本章不搞简单的验证了,我们来个毒药挑战。

  1. 自己写一个简单的OpenSpec,定义一个“用户提现”的流转。约束条件:单次提现不超过5000,每日累计不超过20000,账户状态必须为ACTIVE。

  2. 把这份Spec喂给AI,让它生成代码。

  3. 故意在测试时传入一个提现6000的请求,看看AI写的代码是否按照你Spec里的约束,死死地把这个请求拦在门外。如果它拦住了,恭喜你,你已经成功给AI套上了缰绳,它成了你最可靠的偏执狂搭档。下一章,我们要带着这个偏执狂,去啃最硬的骨头——解剖祖传屎山代码