第二部 原理篇:规则驱动的智能体设计

第 18 章:工程实践:从零构建业务智能体

本章为实践章,给出基于本书原理的落地路径。代码是主要的交流语言,因此本章以代码与操作步骤为主,原理见前文相应章节。

18.1 实现自定义智能体

from prog.runtime.base_agent import BaseAgent, AgentResponse

class GreetingAgent(BaseAgent):
    def __init__(self):
        super().__init__(agent_name="问候Agent", agent_type="greeting")

    def process(self, user_input, context):
        return AgentResponse(content=f"您好!我是{self.agent_name},很高兴为您服务。",
                             agent_name=self.agent_name)

agent = GreetingAgent()
resp = agent.process("你好", {"user": {}})
print(resp.content)

18.1.1 最小可运行示例:一条硬规则如何挡住一次操作

上面的 GreetingAgent 只回答了"能接上",尚未触及框架的核心——规则对生成的裁决。下面给出全书最短的完整链路(约 40 行):定义一条硬规则、挂到一个智能体上、跑两次请求,观察规则如何在生成之外独立作出裁决。

from prog.runtime.base_agent import BaseAgent
from prog.runtime.rule_registry import BaseRule, RuleRegistry, RuleResult


class AmountPositive(BaseRule):
    """硬规则:订单金额必须为正。"""

    def __init__(self):
        super().__init__()
        self.rule_name = "金额必须为正"
        self.is_hard = True                # ★ 硬规则:block 不可被任何角色覆盖
        self.rule_type = "hard_logic"      # 硬逻辑规则,不参与训练

    def check(self, data, *args, **kwargs):
        amount = float((data or {}).get("amount", 0))
        if amount <= 0:
            return RuleResult(status="block", rule_name=self.rule_name,
                              message=f"金额 {amount} 必须大于 0", is_hard=True)
        return RuleResult(status="pass", rule_name=self.rule_name)


class OrderAgent(BaseAgent):
    """订单智能体:声明自己要执行哪些规则。"""

    def __init__(self):
        super().__init__(agent_name="订单智能体", agent_type="order")
        self.applicable_rules = ["金额必须为正"]   # 按规则名挂载,与代码解耦


RuleRegistry.get_shared().register("order", AmountPositive())   # ① 注册规则
agent = OrderAgent()                                           # ② 实例化智能体

# ③ 违规请求:硬规则 block,LLM 输出不可覆盖
r1 = agent.process("下单", {"user": {"role": "manager"},
                            "data": {"amount": -5}})
print(r1.content, "|", r1.action, "|", r1.rules_violated)

# ④ 合规请求:规则通过,进入正常(此处为降级)回复
r2 = agent.process("下单", {"user": {"role": "manager"},
                            "data": {"amount": 100}})
print(r2.content, "|", r2.action, "|", r2.rules_violated)

在无 LLM、无数据库的降级模式下,实际输出为:

操作被规则引擎阻断:金额 -5.0 必须大于 0 | blocked | ['金额必须为正']
我已收到您的请求,但当前无法生成回复,请稍后重试。 |  | []

这段代码印证 8.2、8.3 与 11.2 节:

注意第 ④ 步的降级表现:未注入 LLM 时 content 是兜底文案、action 为空——框架在"能力缺失"时不报错、不阻断,而是给出最小可信回复(8.5 节)。这正是本书反复强调的降级模式:正确性验证不依赖外部能力。

可复现提示:上述代码在项目根目录以 python -m prog.<文件名> 运行;若直接 python <文件名>.py,会因脚本所在目录被优先加入 sys.path 而报 ModuleNotFoundError: No module named 'prog'——这是 18.4 节"框架统一以 prog.runtime.* 导入"的直接后果。

18.2 注册路由与上下文注入

from prog.runtime.coordinator import CoordinatorAgent

coordinator = CoordinatorAgent(
    agents={"greeting": greeting_agent},   # 业务通道:按 intent.target_agent 查找
    knowledge_assistant=greeting_agent,    # 咨询通道:兜底
)
routed = coordinator.route(
    "你好,帮我查一下今天的安排",
    {"user": {"role": "manager"}, "history": [], "data": {}},
)

业务接入时替换路由表为自身业务路由,业务规则以 BaseRule 子类 + RuleRegistry.register() 提供。

18.3 接入业务规则的完整路径

  1. 继承 BaseRule 实现业务规则(设置 rule_name / is_hard / spec_ref / rule_type,实现 check())并注册;
  2. 继承 BaseAgent 实现领域智能体(设置 agent_name / agent_type / applicable_rules,重写 process() 或复用默认生命周期);
  3. 实例化 CoordinatorAgent 并替换路由表;
  4. 可选注入 llm_provider / database / audit_log_dal(未注入降级模式运行);
  5. 高风险操作接入七层审核链,权限接入 PermissionSystem,安全接入 AgentSecurity,模块隔离接入 @require_module。

接入顺序本身遵循第 8 章原理:先让框架在降级模式跑通流程(验证正确性),再逐项注入真实依赖(接入能力)。

18.4 框架内嵌模块约定

prog/runtime/ 为框架本体(绳墨 Runtime),仅随本仓库维护;框架与业务共用同一仓库、统一以 prog.runtime.* 导入。原「开源独立副本 + 业务副本」双副本模式已取消,不再存在跨仓库同步义务;框架变更仅需修改本仓库 prog/runtime/ 自身。

原理: 框架内嵌于业务仓库后,"主运行 + 冷备用"的同步约束随之解除:不再维护可审计、可验证的框架快照,也无副本漂移成"另一套代码"的风险;框架改动直接作用于运行代码,回归由本仓库测试体系覆盖。

18.5 测试策略

# 框架自检(覆盖智能体/规则引擎/审核链/权限/安全/模块开关/意图识别)
python tests/test_framework.py

# 接口兼容性回归(核对 API_SPEC 兼容性基线)
python -m pytest tests/

原理: 框架可在无 LLM / 无数据库的 CI 中全量测试(降级模式),这是第 8.5 节"最小可信实现"的工程收益——正确性验证不依赖外部能力。接口兼容性回归把"版本演进不破坏下游"制度化:每一次改动都要回答"接口是否向后兼容"。

18.5.1 三步运营验收法(功能类)

单元测试与接口回归不足以证明"一套系统能真的用起来"。真实项目采用"三步运营模拟"来验收业务闭环:

  1. 组织初始化:用业务流程建立部门与岗位账户(HR 建档、总经理审批),验证组织树完整、角色就位、全员可登录;
  2. 部门内流程:各业务部门训练内部流程充分,逐条"训练 → 审批 → 生效 → 发起 → 审批 → 落库"闭环;
  3. 跨部门端到端:训练跨部门流程,跑通"订单 → 计划 → 采购 → 生产 → 质检 → 入库 → 发货 → 回款"全链协同。

判定要点:写操作成功须同时具备"成功语义 + 落库业务凭证(单号/审批号)"二者缺一不可(避免纯描述或中间态误判);业务数据全部经正常流程建立(数据自建),空数据友好引导视为通过而非缺陷;遇识别错误先对话修正或训练覆盖,经判断确属代码失误(训练入口缺失、正则误伤等)才改代码。

18.5.2 自动化端到端的六条铁律

大规模端到端回归的实况沉淀出几条自动化原则:

  1. 登录只在数据准备阶段发生一次,业务路径复用令牌——登录接口按 IP 限流,业务用例每步重新登录会触发限流,导致后段雪崩全失败;
  2. 判定必须以数据库落库为准——回复里出现"已成功"并不等于真的写入业务表,直查落库验证可防假通过;
  3. 话术"首轮无歧义词"——易被高优先级识别规则吞掉的词放到后续轮次补齐,靠多轮状态沿用意图;
  4. 改规则配置后须重启——配置有进程级缓存,热更新可能仍读旧值;
  5. 会话隔离——同一账号跨用例要重置会话,防历史上下文污染导致意图识别漂移;
  6. 失败先归类再处置——按"意图跑偏 / 数据缺失 / 判定口径 / 真实缺陷"分类,多数走训练修正,少数才是代码缺陷。

18.5.3 会话黄金快照防回归(FD-04)

端到端回归的另一难题是"依赖变化后如何快速发现语义漂移"。

框架引入会话黄金快照防回归:把一次真实会话的可判定字段(recognized_intent/routed_agent/extracted_slots/action/pending_state/http_status)投影为黄金快照,此后每次回归把当前会话结果与该快照比对、只输出字段级 delta,据此早发现意图/路由/槽位漂移。

设计原理: 快照对标 zcode 的 projection+replay 思路,但坚持"纯测试基建、不侵入主链路"——只消费会话结果字典这一契约,不 import 产品代码。判定上"只投影可判定字段,free-text 一律忽略";采集上半自动(--capture 生成候选、--commit 才入库);diff 只报"增/删/改"而非全量 dump;既有快照永不被静默覆盖。

失败防范: 黄金快照是回归红线而非文档:字段归一化避免格式噪声误报;快照缺失/结构非法时 fail-closed 显式失败,不静默跳过;快照值须用真实运行结果的 --capture 采集(而非手写乐观值),否则快照本身即失真。

真实用例(prog/eval/session_golden.py): 以 contract_parallel_e2e 场景(三连击"提交合同审批 → 销售经理会签通过 → 财务经理会签通过")为例。回归时直接比对当前会话结果与既有快照:

# 比对:正常应只输出字段级 delta,全绿则 ok=true
python prog/eval/session_golden.py diff contract_parallel_e2e \
  --input run_result.json --extra '{"recognized_intent":"contract_approval","operation":"write"}'

比对判据来自已入库快照 prog/eval/golden/contract_parallel_e2e.json——只投影 golden 下的可判定字段(recognized_intent / operation / http_status 列表),ignored_fields 列出的 body / content / guidance / message / raw / trace 等 free-text 一律忽略。

采集新快照走半自动:先用 diff --input 看 delta 确认当前结果可信,再 commit --input ... --inputs '["提交合同审批(金额 350000)",...]' 入库;既有快照不覆盖,须显式 --force。失败防范落到命令上:diff 返回非零即回归红线,CI 门禁据此 fail-closed。

18.5.4 在线评测闭环与 LLM-as-Judge

黄金快照守住的是"离线基准",但真实流量依然会产出离线语料没覆盖到的错误。框架把评测从一次性的离线基准扩为在线评测闭环:离线基准 → 在线评测 → 模型裁判初筛 → 人工抽样校准 → 失败沉淀回回归集。

关键工程点是给每一次真实会话绑定 run_id(trace_id)关联键:线上一次错误答案,连同"输入话术 + 目标上下文 + 错误类型 + 修复说明"沉淀为可重复资产,回流进离线回归集——让每一次线上失败都变成下一次回归的守护用例,而不是沉睡在日志里。

分层评测控制成本: 全量用 LLM 裁判成本不可控。按风险分层:规则可全覆盖的确定性断言(意图/路由/落库凭证)用规则全量跑;大多数人肉难判的用轻量裁判(LLM-as-Judge)初筛放行;只有高风险操作(写库、越权、合规)才上高能力模型或人工抽样复核。LLM-as-Judge 定位是初筛漏斗而非终审:它给标号,人来复核标号;裁判自身的漏报由人工抽样兜底。

任务级 SLO: 评测指标不写"接口可用率",而写审批流水线的业务语义 SLO——「首响应及时」(对话 P95 预算)、「任务可交付」(写操作拿到落库凭证)、「安全不让步」(越权率 = 0)。把性能与服务质量的承诺翻译成业务可感知的判据,与 17.4.2 的延迟预算一脉相承。

失败防范: 在线评测不能自动改配置(改配置走第 14 章训练审批链);失败样本回流须去重、防"同一错误反复入集";LLM-as-Judge 的判据要用离线标定的基准确认其自身通过率,避免"裁判带偏回归闸门"。

真实用例(prog/eval/route_eval.py): 离线路由评测直接压真实 HTTP 链路,以语料分层抽样做"语义族"判定:

python prog/eval/route_eval.py --sample-per-intent 3 --max 10
# 关键判据:principled_error(跨语义族误判)= 0;pass_rate(同族合格)> 98%
# 写类回复须同时含「成功动词 + 落库凭证」(write_proof);缺参补发/合规拦截视为路由正确

与黄金快照互补:route_eval 用规则全量覆盖确定性判据(语义族/路由/写凭证),正是"分层评测"里成本最低的第一层;模型裁判与人工抽样只在规则判不出、或 --correct 开启对话纠正复测时介入,把每一次线上失败按"意图跑偏/数据缺失/判定口径/真实缺陷"归类后回流语料,形成闭环。

18.6 常见设计问题

表 18-1 常见设计问题与原理归属

问题 原理归属 建议
规则不生效 第 11 章 检查规则注册、applicable_rules、引擎步骤是否存在
多轮被打断 第 10 章 检查只读查询是否挂起状态;漂移判定词表
审批推进无通知 第 16 章 检查事件总线 handler 注册、通知主题
并发身份错乱 第 17 章 确认无共享可变智能体状态;上下文经线程本地存取
训练不生效 第 14 章 检查审批通过、重载/缓存失效是否调用

18.7 工程验收清单

专家级工程评审以验收清单收尾。按本书原理逐项核对,可作为项目上线前的门禁,见表 18-2:

表 18-2 工程验收清单

类别 验收项 对应原理
正确性 每个写操作是否经过规则校验?高风险操作是否经审批? 第 8 / 11 / 13 章
契约 层间是否统一使用响应契约?有无裸字典传递? 第 8 章
路由 每类输入是否有出口?误路由是否有度量? 第 9 章
状态 多轮状态挂起/清除时机是否验证?并发请求是否互斥? 第 10 章
规则 硬规则是否 bypass=false?规则异常是否 fail-closed? 第 11 章
安全 STRIDE 六类威胁是否有防线?认证密钥是否非弱密钥? 第 12 章
流程 流程定义与实例是否分离?守卫五维是否齐备? 第 13 章
训练 变更是否走审批?热更新是否生效?有无回归基线? 第 14 章
记忆 窗口裁剪是否配置化?过程性数据是否隔离出知识库? 第 15 章
可观测 全链路是否单一 trace_id?日志 schema 是否稳定? 第 16 章
性能 延迟预算是否分解?快路径命中率是否度量? 第 17 章
测试 降级模式全量测试是否通过?接口兼容性回归是否执行? 第 18 章

原理: 验收清单是"原理的可操作化"——每一验收项都对应一个设计原则,使评审从"凭感觉"变为"逐项核对"。清单本身也应随框架演进更新(如新增机制时补充对应验收项),保证验收与设计同步。

18.8 实现剖析:流程、算子与原子函数——统一执行流水线

18.3 节列出接入业务规则的完整路径,其中第 5 步是"高风险操作接入七层审核链"。本节从 runtime/operation_registry.py(79 行)、runtime/workflow_executor.py(98 行)与 runtime/operations/(原子函数族)摘取四段,先讲清框架的三层粒度,再看"一次业务操作"如何被编排成一条声明式的统一流水线。

框架把"一次业务动作"按粒度拆成三层,各层的状态、审批与复用单元都不相同,如图 18-1 所示:

图 18-1 流程 / 算子 / 原子函数的三层粒度

flowchart TD
    A["流程(Workflow):一条完整业务链路<br>识别 → 审批 → 生效;有生命周期,可训练"]
    B["算子(OperationDef):一个业务步骤的编排壳<br>七回调位;一次性执行,可注册、可复用"]
    C["原子函数:一次落库的纯副作用<br>transfer_stage / _create_xxx_in_db / insert_row<br>无状态,最小复用单元"]
    A --> B --> C

三层各自的边界见表 18-3:

表 18-3 流程 / 算子 / 原子函数的三层对照

层 状态 审批 可否注册 复用单元
流程 有生命周期(草稿→审批中→终态) 链式 Gate、签字留痕 训练写 workflow_configs 每条业务一个
算子 一次性执行 RBAC(action_name)+ post_execute 挂审批 OperationRegistry 可被多流程复用
原子函数 无状态 无 不独立注册 最小复用单元

这条分层的工程意义,在于"把哪些能改、哪些不能改钉在不同层上":流程与算子的编排(走哪几步、按什么顺序、挂哪条审批)是数据,原子函数的动作能力是代码——正对应 11.1 节"动作能力不可训练、动作组合可训练"的红线。

三者中只有算子会被 OperationRegistry 注册(runtime/operations/__init__.py 由 register_domains 汇总各领域算子),原子函数只被算子调用、不单独暴露:改配置能改变行为,却造不出新的落库动作。

18.8.1 操作即数据:OperationDef 的七个回调位

class OperationKind:
    WRITE = "write"      # 写核落库
    QUERY = "query"      # 查询(兼用 execute_workflow_query)
    FLOW = "flow"        # 审批链流程 + flow_effect
    BYPASS = "bypass"    # 保留原 handler 原路径


class OperationDef:
    """算子(OperationDef)= 一个业务步骤的编排壳,处于「流程」与「原子函数」之间。"""

    def __init__(self, name, *, kind=OperationKind.WRITE, action_name=None,
                 feature_flag=None, parse_hook=None, steps=None,
                 data_guard=None, post_execute=None, execute=None):
        self.name = name                        # 操作名(注册表键)
        self.kind = kind                        # 通道类型(决定走哪条流水线)
        self.action_name = action_name or name  # ③ 操作级 RBAC 权限键
        self.feature_flag = feature_flag        # P6 灰度开关
        self.parse_hook = parse_hook            # ① 领域前置解析(不终止)
        self.steps = steps or []                # ② 有序步骤 [(step_key, fn)]
        self.data_guard = data_guard            # 数据级校验(fail-closed)
        self.post_execute = post_execute        # ⑥ 审批挂接(仅 write 通道)
        self.execute = execute                  # ⑦ 生效(原子函数)

OperationDef 即图 18-1 的中间层——算子:它把一次业务操作的各步回调挂到固定槽位。这段代码印证 18.3 节:"接入一个业务操作"被化简为"填一张声明表",而不是"改一处控制流"。

七个回调位与第 8 章的生命周期一一对应:parse_hook 对应前置解析、steps 对应分步校验、data_guard 对应数据级守卫、execute 对应生效、post_execute 对应审批挂接。

其中 steps 的元素签名固定为 (value, ctx) -> AgentResponse | None——返回 None 表示"继续下一步",返回响应表示"终态短路"。这条约定让"步骤顺序"成为可配置数据([(step_key, fn), ...]),与第 11 章"编排可训练、动作能力不可训练"的红线一致:顺序可改,动作本身仍是代码。

18.8.2 统一编排:WorkflowExecutor.execute() 的固定七步

def execute(self, operation: str, value: dict, ctx: dict) -> Any:
    op = self._registry.get(operation)
    if op is None:
        return self.BYPASS + (operation,)        # 未注册 → 回退原 handler
    if not _op_switch.feature_flag_enabled(op.feature_flag):
        return self.BYPASS + (operation,)        # P6 灰度未开 → 回退原路径

    if op.kind == OperationKind.BYPASS:          # P5 旁路清单:原 handler 原路径
        return op.execute(value, ctx)

    if op.kind in (OperationKind.QUERY, OperationKind.FLOW):
        # 查询/流程:权限由各自层内管控(查询 step 级 / 审批链三道校验),
        # 跳过操作级 RBAC(权限双权威消解)
        v = op.parse_hook(value, ctx) if op.parse_hook is not None else value
        for _key, fn in op.steps:
            out = fn(v, ctx)
            if out is not None:
                return out                       # ★ 终态短路
        if op.data_guard is not None:            # 数据级校验对 query/flow 同样生效
            guard = op.data_guard(v, ctx)
            if guard is not None:
                return guard
        return op.execute(v, ctx)

    denied = self.authorize(op.action_name, value, ctx)   # ③ 权限:唯一 RBAC 点
    if denied is not None:
        return denied

    v = op.parse_hook(value, ctx) if op.parse_hook is not None else value
    for _key, fn in op.steps:                    # ② 有序步骤
        out = fn(v, ctx)
        if out is not None:
            return out
    if op.data_guard is not None:                # ④ 数据级校验(fail-closed)
        guard = op.data_guard(v, ctx)
        if guard is not None:
            return guard
    result = op.execute(v, ctx)                  # ⑦ 生效
    if op.post_execute is not None:              # ⑥ 审批挂接(不进原子函数)
        attached = op.post_execute(result, v, ctx)
        if attached is not None:
            return attached
    return result

这段代码印证 18.3 与 13.10 节:流水线的"统一"体现在"所有写操作共用同一段编排代码"——门禁、字段收集、权限、条件、规则、审批链、生效七步的顺序不再散落在各业务 handler 里,而是收敛到 execute() 一处。

三条工程约束在此可见:① 未注册 / 灰度未开一律返回 BYPASS 哨兵回退原路径,保证上线切换可随时撤销;② QUERY / FLOW 通道显式跳过操作级 RBAC,避免"查询层 step 级权限 + 操作级 RBAC"二次授权相互冲突(但 parse_hook / steps / data_guard 三步对三条通道一致生效——跳过的是授权,不是编排);③ post_execute 被注释明确标注"不属于原子函数体"——审批挂接是编排动作而非纯副作用,不能污染原子函数。

18.8.3 权限判定的 fail-closed:getattr 默认值不能写 True

def authorize(self, action_name: str, value: dict, ctx: dict) -> Any:
    """构造注入 PermissionSystem 时的操作级唯一 RBAC 点。"""
    if self._permission_system is None:
        return None                      # 未注入 → 放行,交回 op 自身处理
    user = ctx.get("user", {}) if isinstance(ctx, dict) else {}
    result = self._permission_system.check_permission(
        user=user, action=action_name, resource=value.get("slots", {}))
    # check_permission 返回 bool 或带 .allowed 的结果对象:取 .allowed 优先,
    # 否则取 bool 本身。
    # ★ 不能用 getattr(result, "allowed", True):对 bool 值 .allowed 恒缺失,
    #   默认 True 会把"拒绝"误判为"放行"(fail-open,权限失效)。
    allowed = getattr(result, "allowed", result)
    if not allowed:
        return AgentResponse(content="您没有操作权限,请联系管理员。",
                             action="permission_denied", agent_name="op_executor")
    return None

这段代码印证 12.8 与 18.6 节:权限判定的默认值方向决定了安全边界。getattr(result, "allowed", result) 与 getattr(result, "allowed", True) 只差一个默认值,但后者在 check_permission 返回裸布尔 False 时会把"拒绝"读成"放行"——这是典型的 fail-open 缺陷。

作者把这一反例钉在代码注释里,作为"默认值即安全策略"的活教材:第 18.6 节的"常见设计问题"与第 12.8 节的"输入防线"都指向同一条纪律——默认值必须朝安全方向倾斜。

18.8.4 参数载体与返回值契约:value / ctx 与 AgentResponse

三层之间靠两条约定通信;把它们写死,算子与原子函数才能各自独立演进。

参数载体:value 与 ctx 贯穿所有 fn(value, ctx),分工见表 18-4:

表 18-4 参数载体 value 与 ctx 的分工

载体 类型 关键字段 谁写
value(v) dict slots(用户槽位)、_raw(原文)、_user(当前用户 id)、_perms(权限集);解析阶段可就地追加 _order_data 等派生字段 coordinator 注入初值,parse_hook / steps 补充
ctx dict user、环境与运行时上下文 统一执行层注入,各步以读为主

传参风格:原子函数不收 value 字典、也不靠深拷贝传递,而是收具名参数——由算子把从 value / slots 归并出的字段显式传入。

runtime/operations/primitives.py 的半原语 transfer_stage 即典型:parse_hook 先把槽位归并成 v["_product_code"] / v["_quantity"] / v["_from_stage"],execute 再逐个具名传给 provider.transfer_stage(...)。

职责边界因此清晰:算子负责拆参,原子函数只收确定参数;落库产生的 id 由算子写回 v["_xxx_id"],供 post_execute 使用。

返回值契约:算子的各步骤与 execute 返回 AgentResponse(或 None——None = 继续,非 None = 短路返回);原子函数通常返回业务结果(新单号、(affected, 状态)),不返回 AgentResponse,由外层算子把结果包成 AgentResponse 或写回 v。

异常约定同样分层:原子函数抛业务异常(OrderInputError / ValueError)不吞不包——能补正的转 request_info(补参后续推),不能补正的(数据库失败)转 error,禁止"吞错假成功"(8.7.2 节)。这条"算子包契约、原子函数抛异常"的分工,使同一个原子函数既能被写通道调用、也能被流程的生效步骤复用,而不必各自重写一遍错误处理。

18.9 AI 协作编程的两条纪律:黄金快照与 Git 提交点

本书描述的系统由人机协作迭代而成:AI 改代码,人判断"改对了没有"。实践中真正省事的,不是"让 AI 一次写对",而是让每一次改动都可判定、可回退。支撑这两点的,是两条几乎零成本的纪律。

纪律一:先冻结行为基线,再让 AI 动手。 AI 改代码最典型的失败不是"改错了目标",而是"改对了目标、改坏了别处"——识别 / 路由 / 执行是 LLM 主导的链路,同一句话的输出天然漂移,靠人眼很难发现静默回归。

18.5.3 节的会话黄金快照正为此设计:把一组真实会话的可判定字段(意图 / 路由 / 操作 / 槽位 / 状态 / 落库凭证)冻结为基线,改完只跑一次 diff,字段级漂移立即报错。它的价值不在"测试更全",而在把"有没有改坏"从回忆题变成判断题。

纪律二:让 AI 动手前,先提交一次。 Git 提交点的作用是把"撤销"从一场考古变成一条命令。AI 一次改动往往横跨多个文件,出错时若没有干净的提交点,"退回上一版"就要靠人肉辨认哪些行该留、哪些该弃。

实践约定是:每次交给 AI 改动前先 commit(哪怕只写"改动前基线"),改动验证通过后再 commit——于是任何一次 AI 改动都只对应一个 diff,回退即回到上一个提交点。小提交 = 小爆炸半径。

两条纪律的分工如图 18-2 所示:黄金快照管"改完怎么判",Git 提交点管"判错了怎么退",一前一后构成 AI 改动的安全网。

图 18-2 AI 协作编程的两条纪律:先提交、改完比对

flowchart LR
    A["改动前<br>git commit 形成恢复点"] --> B["冻结行为基线<br>黄金快照"]
    B --> C["交给 AI 改代码"]
    C --> D["跑 golden diff<br>只输出字段级 delta"]
    D -->|全绿| E["git commit<br>形成新恢复点"]
    D -->|有漂移| F["判定:真回归 or 预期变更"]
    F -->|真回归| G["回到上一个恢复点"]
    F -->|预期变更| H["审阅后更新快照"]

失败防范: 两条纪律各有其失效方式,须显式防住——

18.10 本章小结