第 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 节:
- 契约统一(8.2 节):两次调用返回的都是
AgentResponse,action字段直接给出裁决结果(blocked),调用方无需解析自然语言; - 裁决后置(8.3 节):
process()的生命周期是"生成 → 裁决 → 格式化"。r1的content之所以是"操作被规则引擎阻断"而非模型输出,正因为规则在生成之后独立接管了终态; - 三态裁决(11.2 节):
check()返回RuleResult(status="block"),而is_hard=True使这条阻断不可被任何角色覆盖——这正是标题所说"挡住"的确切含义。
注意第 ④ 步的降级表现:未注入 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 接入业务规则的完整路径
- 继承
BaseRule实现业务规则(设置rule_name / is_hard / spec_ref / rule_type,实现check())并注册; - 继承
BaseAgent实现领域智能体(设置agent_name / agent_type / applicable_rules,重写process()或复用默认生命周期); - 实例化
CoordinatorAgent并替换路由表; - 可选注入
llm_provider/database/audit_log_dal(未注入降级模式运行); - 高风险操作接入七层审核链,权限接入
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 三步运营验收法(功能类)
单元测试与接口回归不足以证明"一套系统能真的用起来"。真实项目采用"三步运营模拟"来验收业务闭环:
- 组织初始化:用业务流程建立部门与岗位账户(HR 建档、总经理审批),验证组织树完整、角色就位、全员可登录;
- 部门内流程:各业务部门训练内部流程充分,逐条"训练 → 审批 → 生效 → 发起 → 审批 → 落库"闭环;
- 跨部门端到端:训练跨部门流程,跑通"订单 → 计划 → 采购 → 生产 → 质检 → 入库 → 发货 → 回款"全链协同。
判定要点:写操作成功须同时具备"成功语义 + 落库业务凭证(单号/审批号)"二者缺一不可(避免纯描述或中间态误判);业务数据全部经正常流程建立(数据自建),空数据友好引导视为通过而非缺陷;遇识别错误先对话修正或训练覆盖,经判断确属代码失误(训练入口缺失、正则误伤等)才改代码。
18.5.2 自动化端到端的六条铁律
大规模端到端回归的实况沉淀出几条自动化原则:
- 登录只在数据准备阶段发生一次,业务路径复用令牌——登录接口按 IP 限流,业务用例每步重新登录会触发限流,导致后段雪崩全失败;
- 判定必须以数据库落库为准——回复里出现"已成功"并不等于真的写入业务表,直查落库验证可防假通过;
- 话术"首轮无歧义词"——易被高优先级识别规则吞掉的词放到后续轮次补齐,靠多轮状态沿用意图;
- 改规则配置后须重启——配置有进程级缓存,热更新可能仍读旧值;
- 会话隔离——同一账号跨用例要重置会话,防历史上下文污染导致意图识别漂移;
- 失败先归类再处置——按"意图跑偏 / 数据缺失 / 判定口径 / 真实缺陷"分类,多数走训练修正,少数才是代码缺陷。
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["审阅后更新快照"]
失败防范: 两条纪律各有其失效方式,须显式防住——
- 快照失效:用手写的"乐观值"充当快照,等于用错基线掩盖回归;快照值必须来自真实运行的
--capture(18.5.3 节),且既有快照永不被静默覆盖。 - 提交失效:把多个不相关的改动塞进一个提交,出错时无法二分定位;提交点须"一改一提交",提交信息写清"改了什么、为什么"。
- 顺序失效:先改后补快照,等于拿改动后的行为当基线——快照必须在改动之前冻结,才能充当"旧行为"的证词。
18.10 本章小结
- 自定义智能体只需继承基类并实现约定方法,路由与上下文注入通过注册表完成,无需改动框架内核;
- 业务规则的接入路径是"定义 → 注册 → 挂载到意图",规则与代码解耦,新增规则不改业务代码;
- 测试策略分层:降级模式全量自检 + 接口兼容性回归 + 路由评测,验收门槛为
principled_error = 0且pass_rate > 98%;
- 会话黄金快照提供防回归基线,使"本次改动是否破坏了旧行为"可自动判定,而非依赖人工回忆;
- 三层粒度(流程 / 算子 / 原子函数,图 18-1)把"可训练的编排"与"不可训练的动作能力"钉在不同层上,统一执行流水线(18.8 节)是它的落点;
- AI 协作编程的两条纪律(18.9 节)——改动前冻结黄金快照、动手前留下 Git 恢复点——让"改坏了"可判定、可回退;
- 工程验收清单(表 18-2)把全书原理逐条转为可勾选项,是上线前的最后一道门禁。