上周我去找一个小 bug,却发现同一软件有五个副本。
这个软件是 hermes-memory-pgvector,这是我维护的一个开源插件。它为一组 AI 代理在 PostgreSQL 和 pgvector 上提供共享、持久的记忆——这样,一个代理学到的东西,其他代理也能回忆起来。它发布在 PyPI 上。你可以用 pip 安装它。
可是在我自己的服务器上,它却是从某个 git checkout 里运行的,藏在另一个项目的源代码树中,只差一次例行清理就会被删掉。我的基础设施仓库里还有一个没人使用的 vendored 副本,一个过时的构建目录,一个包缓存,以及几分钟前刚手工创建的符号链接,用来阻止整个安排彻底崩掉。
这就是那种你六个月后接手自己留下的设置,却一时无法为它辩护的东西。
为什么 pip install 还不够
下面这部分其实解释了这团乱麻。
主机框架通过扫描目录来发现内存插件。它会查看自己打包的插件文件夹,以及一个用户插件文件夹,寻找包含 __init__.py 且其中提到了正确类的子目录。发现机制就只有这些。它从不检查已安装的 Python 包,也没有 entry-point 注册。
所以 pip install 确实把我的插件正确地装到了机器上,也装在完全正确的环境里——但框架就是看不到它。安装得非常完美。完全不可见。
面对这种情况,显然的做法是把副本放到扫描器会看的地方。把文件夹复制到部署仓库里。把它检出到框架旁边。给它做一个符号链接。这些做法单独看都算合理的本地决策,合在一起就是你最终拥有五个副本,以及一个没人能解释的部署。
修复方法:别再和扫描器对着干
我这周发布的版本增加了一个命令,在安装后运行一次:
pip install hermes-memory-pgvector
hermes-pgvector install
第二个命令会在扫描器读取的文件夹里写入一个小型 shim——几行代码,唯一的任务就是导入真正通过 pip 安装的包。扫描器找到了一个目录,于是满意了。Python 则把导入解析到正确安装的库上。
这一个间接层把五个副本压缩成了一个。升级就变成了 pip 升级和重启。回滚则意味着固定到前一个版本。没有 vendored 内容,没有检出代码,没有漂移,别人仓库里的例行维护也再不能把我的记忆层一起带走。
这件事还意外暴露了什么
把部署收拢之后,就意味着要认真重新读一遍代码,而这又找出了几个值得点名的真实 bug:
一个并非真正进行子串搜索的搜索。 编辑已存的记忆时,系统用 SQL LIKE 把你的文本和数据库进行匹配,其中 % 是通配符。包含 "revenue grew 15% YoY" 的记忆,可能会匹配——并修改——不相关的行。反斜杠则有相反的问题:一个 Windows 文件路径会悄无声息地完全匹配不到任何内容。
一个在声称完成时却丢弃工作的队列。 后台写入器接收写入,然后在关闭时,如果它碰巧正忙,就一声不吭地丢下所有仍在队列里的内容。
一个误导性的错误。 如果把插件指向输出维度不对的 embedding 模型,你得到的会是来自无关回退逻辑的 404,而不是“预期 768 维,得到 1024”。
这些都不稀奇。它们都是认真阅读能发现、而测试套件不会碰到的问题。
我不断重学的教训
当部署一团糟时,这团糟通常在说一些关于设计的真实问题。
这五个副本不是粗心大意。每一个都是理性的权宜之计。它们是一个症状,而根本问题在于,我的包不能以它本该被安装的方式被发现。如果一个个去修补这些权宜之计,只会得到一个更整洁的乱摊子。修复发现机制的缺口,才让这些权宜之计变得不再必要。
如果你维护的东西,用户一种方式安装、另一种方式部署,那么这个缺口值得补上。它很可能正试图告诉你什么。
