我在家里跑着一个私人智能体。它有一份配置文件,记着我的尺码、版型、颜色偏好和常买的品牌,然后去我真正会下单的店铺抓货。
后来我把抓取引擎单独开源成了一个工具。它有自己的配置文件,自己的店铺清单,自己对"我的尺码"的理解。
大约三周里,这两套系统一直在打架。不是大吵大闹那种。私人智能体说我的裤子是 XL,公开工具说 XL 或 XXL。其中一个是对的,但我完全不知道是哪个——因为工具输出的样子,和智能体输出的一样权威。
我先把公开工具写了出来,回头再看私人智能体,发现是同一件事:同样二十家 Shopify 店铺,同样的 /products.json 接口,同样的"按变体库存判断你的尺码有没有货"逻辑。
每家店都被打了两次,一次来自智能体,一次来自工具,时间表不同,解析方式也不同。每周两个数据池就漂移得更远一点,因为私人这边修好了一个尺码标签,公开那边没有;反过来也一样。
公平地说,私人智能体老得多也聪明得多:它多了六个适配器,建立在无头渲染器上,还有图片匹配、价格错误检测、订单历史。但这些全都架在我已经复制过一遍的那二十家店上面。
同一个抓取器我写了两遍,然后管它叫两个功能。
显而易见的修法是"让一个调另一个"。调哪个?这个问题一开始就问错了。
该问的是:谁拥有这些事实?
我真正会去编辑的文件,是智能体的 profile.yaml。尺码变了,我改的是它。所以它才是事实来源——而公开工具的配置是副本的副本,手写的,早就过期了。
于是修法长这样:智能体的 profile.yaml 是我唯一编辑的文件,一个桥接脚本把它投影成工具需要的格式,生成工具的 profile.yaml 和 config.yaml,后者包括店铺清单。生成出来的文件永远不手改。
桥接脚本大约一百行,干的全是不体面的活:复制尺码、映射版型规则、把"绝不买 Zara"翻译成品牌过滤器,再丢掉工具不该有的字段。
它故意丢两个字段。工具的 schema 拒收它们,智能体自己的规则也不允许。智能体存着我的身高体重用于版型推理,并且被禁止把这些数字透露给任何人。所以桥接不复制,工具的校验器见到就拒。两条独立规则在同一个边界上达成一致,这正是你想要的效果——意味着以后不可能被意外越过。
然后我改了抓取路径。那二十家共享店铺现在由工具抓——有测试、有打包的那个——智能体改为读取工具的数据池,不再自己去抓这些店。智能体保留它多出来的六个适配器和无头渲染的鞋店,只是不再重复抓那二十家。
意外的是,之后几乎没怎么变。智能体的数据池格式和工具完全不像——字段名不同,结构不同,其中一个把尺码存成 JSON 字符串。中间大约四十行映射,然后……它就跑通了。智能体的每日摘要开始包含工具抓来的条目,看不出任何接缝。
那四十行就是全部诀窍。两套系统从来没有不同的事实,它们只是形状不同,而形状翻译起来很便宜。
我反复重新学到的规则是:如果两个系统需要同样的事实,其中一个就不该去采集它们。不是"应该共享",是共享。在拥有事实的地方采集一次,其他人只读。
智能体系统的诱惑在于,每个"灵魂"都觉得自己有权拥有自己的抓取器,因为自己抓数据的灵魂感觉自给自足。但自给自足和重复是两回事,而重复不会自己宣告:两份副本看起来都对,漂移要几个月后才以"为什么工具觉得我的尺码不一样"的形式冒出来。
做这件事还学到两个小点:
- 生成文件需要一个会跑的生成器,而不是一句"它应该被生成"的备注。我第一版是记得才手动跑一次的脚本,两周内就错了。能用的版本是每周任务里的三行:先跑桥接,再抓取。现在它漂不了,因为它和使用它的任务同一个节奏重新生成。
- 把私人配置挡在公开仓库外。桥接和合并方案一进公开仓库,我就得把它们拿回来——接线属于那台机器,不属于项目。小事,但这是可复用库和别人带公开 URL 的私人配置之间的区别。
如果要给过去的我一句话:当你准备把驱动自己智能体的引擎开源时,先定好哪一边拥有配置,另一边必须是生成的,而且生成器要接进使用它的任务里。
工具因为公开而更好:它有了当私人脚本时不会有的测试,打包过程逼我修好了一个从来没工作过的抓取器。但私人智能体因为作为事实来源而更好,因为那才是我真正会去改文件的地方。
两套系统,一份配置,一条抓取路径。把它们粘起来的代码是最无聊的部分,而所有重复以前就住在这个无聊的部分里。
热门跟贴