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由四个核心要素构成:
-
Entities(实体/节点):系统里的核心名词。比如
User,Order,PointAccount。定义它们的字段和类型。 -
States(状态):实体所处的生命周期。比如
Order有UNPAID,PAID,SHIPPED。 -
Edges/Transitions(边/流转):触发状态改变的动词。比如
pay()动作让UNPAID->PAID。 -
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: { type: integer, required: true, minimum: 0, description: "可用余额,绝对不允许为负" }version: { type: integer, required: true, description: "乐观锁版本号" }PointTransactionLog:description: "积分流水记录"fields:log_id: { type: string, required: true }user_id: { type: string, required: true }amount: { type: integer, required: true, description: "变动正负数" }reason: { type: string, required: true }states:PointAccount:- ACTIVE- FROZENtransitions:deduct_points:description: "扣减积分核心流转"from_state: ACTIVEto_state: ACTIVE # 扣减不改变状态,除非余额归零pre_conditions:- "account.balance >= deduct_amount"- "deduct_amount > 0"post_conditions:- "account.balance >= 0" # 铁律:绝对不能超卖side_effects:- "写入 PointTransactionLog"contracts:DeductPointsAPI:endpoint: /points/deductmethod: POSTrequest:user_id: { type: string, required: true }amount: { type: integer, required: true, minimum: 1, description: "扣减数量,必须大于0" }order_id: { type: string, required: true, description: "幂等键,同一订单号只扣一次" }response:success:code: 200data: { remaining_balance: integer }errors:- code: 40001message: "Insufficient balance"- code: 40002message: "Duplicate deduction (Idempotent hit)"- code: 40003message: "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 层代码(包含实体模型和业务逻辑)。
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: 0version = 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 = 40001; self.message = "Insufficient balance"class DuplicateDeductionError(Exception):def __init__(self): self.code = 40002; self.message = "Duplicate deduction (Idempotent hit)"class AccountFrozenError(Exception):def __init__(self): self.code = 40003; self.message = "Account frozen"# ================== 核心业务逻辑 (严格对应 OpenSpec Transitions) ==================class PointService:def __init__(self, session):self.session = sessiondef 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_versionupdated_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到底做对了什么?
-
双重防线防超卖:它不仅在业务层判断了
balance < amount,还在update语句的条件里加上了PointAccount.balance >= amount。这是极高段位的写法,即使并发下乐观锁放过,数据库层也会把最后一条超卖请求挡住。这是纯Prompt极难逼出来的细节。 -
极度精准的异常映射:异常类和错误码与YAML里的
40001、40002一一对应,分毫不差。 -
完整的幂等闭环:不仅做了防重表,还在流水生成时把 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章代码角斗场:毒药挑战
本章不搞简单的验证了,我们来个毒药挑战。
-
自己写一个简单的OpenSpec,定义一个“用户提现”的流转。约束条件:单次提现不超过5000,每日累计不超过20000,账户状态必须为ACTIVE。
-
把这份Spec喂给AI,让它生成代码。
-
故意在测试时传入一个提现6000的请求,看看AI写的代码是否按照你Spec里的约束,死死地把这个请求拦在门外。如果它拦住了,恭喜你,你已经成功给AI套上了缰绳,它成了你最可靠的偏执狂搭档。下一章,我们要带着这个偏执狂,去啃最硬的骨头——解剖祖传屎山代码!
夜雨聆风