已发布接口:存下来的日志,也是给未来代码的接口
本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。
重构时把 amount 改成 total,编译通过,单元测试也通过,发布后却有一批等待审批的任务再也恢复不了。原因不在今天的请求,而在昨天落盘的数据里。旧任务还在用旧字段,新代码已经忘记了怎样读取它。
Published Interface强调一种区别:代码里标为 public 的接口,不一定已经被外部依赖;一旦无法同步修改所有调用方,变更就需要更谨慎。持久记录可以看成时间上的外部调用方,今天的代码无法回到昨天修改已经产生的历史。
这个角度对于 Agent 很实用。长期任务可能跨越多次发布,调用参数、工具结果和模型输出都有机会进入历史。只验证新请求能否运行,覆盖不了旧任务的恢复。即使数据库字段没有变化,一个枚举值的含义变化,也可能改变后续业务行为。
下面模拟两种金额事件。旧格式只有整数分,新格式显式带币种。解码器先识别版本,再转换成内部统一结构。旧数据的币种来自当时的固定业务契约,不能在没有依据时默认猜测。未知版本会失败,让问题暴露出来。
def decode(record):
version = record.get("schema", 1)
if version == 1:
# 示例旧契约明确规定:amount 是人民币分。
amount, currency = record["amount"], "CNY"
elif version == 2:
amount = record["money"]["minor"]
currency = record["money"]["currency"]
else:
raise ValueError("不支持的数据版本")
if type(amount) is not int or amount < 0:
raise ValueError("金额必须是非负整数最小单位")
if currency != "CNY":
raise ValueError("此用例尚未支持该币种")
return {"minor": amount, "currency": currency}
old = {"schema": 1, "amount": 1250}
new = {"schema": 2, "money": {"minor": 1250, "currency": "CNY"}}
assert decode(old) == decode(new)
for bad in ({"schema": 99}, {"schema": 1, "amount": 12.5}):
try:
decode(bad)
except ValueError:
pass
else:
raise AssertionError("应拒绝未知版本或错误单位类型")
print("旧记录和新记录保持相同语义")
完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。
这种适配器可以被称为升级转换,但它不应该修改事件曾经表达的事实。如果旧记录只知道金额,不知道批准人,新代码不能为了凑齐字段就补上当前操作者。缺失信息要么能从可信历史补全,要么明确保留未知,再由业务决定是否继续任务。
也不能把“能成功解析”当作兼容。字段仍然叫 amount,单位却从元改成分,JSON 解析不会报错,结果可能差一百倍。兼容性测试需要验证语义:单位、时区、默认值、枚举含义,以及一个字段缺省时到底代表什么。对金额与权限尤其如此。
我会保存少量脱敏的历史样本作为契约测试。每次发布,使用新代码读取这些样本,验证转换后的业务含义和后续状态。样本要覆盖真正存在的版本,而不是根据新代码反向生成。后者很容易形成“生产数据不兼容,测试数据永远完美”的假象。
代码包版本和数据结构版本最好分开。一个部署包可能同时读取三个数据版本;同一个结构也可能经历多次实现修复。把版本号直接写成 Git 提交号虽然能追踪来源,却未必适合表达兼容协议。需要一个稳定、可文档化的结构版本,并记录它支持到什么时候。
对于外部工具返回的原始响应,我倾向于先转换成我们负责维护的业务结果,再存入任务历史。这样供应商增加一个字段或调整接口布局,不会立即污染所有长期任务。确实需要保存原始证据时,可以单独存储,并明确访问权限与保留期限。
这个模式也提醒我们控制对外承诺的范围。内部临时函数可以快速重构,公开工具参数、事件结构和审批消息则需要更稳健的演进方式。不是所有字段都要永久兼容,而是必须知道谁仍在依赖它,何时能够迁移,以及何时可以删除。
我会在准备重命名时多问一句:这个名字只存在于代码里,还是已经写进数据、消息或任务历史?前者通常可以借助重构工具一次修改,后者需要适配、迁移和验证。把这条边界识别出来,才能让发布速度和长期任务的可靠性同时成立。