两个 AI 产品都需要检索、证据校验和受控写入时,抽一个公共包很自然。但公共包装进各自进程后,谁负责运行执行器、保存数据凭据、维护检索状态?如果每个产品仍要回答一遍这些问题,复用的可能只是代码,运行责任依然散落在各处。
反过来,把公共包搬到独立服务,也会增加网络失败、版本协调和迁移成本。什么时候拆包已经足够,什么时候需要进一步服务化? TraceWise 的演进提供了一个观察角度:先区分希望共享的规则、需要统一的权威,以及产品必须保留的业务决定,再选择边界。
TraceWise 是一个围绕项目资料、关系图和证据展开调查与审核的 AI 产品。Agent Foundation 为它提供受治理的数据和执行能力。早期 a12 完成了物理包拆分与两个受控本地项目的权威采用;截至 2026 年 9 月 13 日,a17 已完成公开服务客户端的本地代码与交付验证,真实项目向共享实例的切换仍未完成。这两段经历分别说明安装边界和运行边界,不能合并成一次已经完成的生产迁移。
先判断自己要解决的是哪一种耦合
Martin Fowler 在 2015 年的《Microservice Trade-Offs》中把更强的模块边界与分布式调用、数据一致性和运维复杂度放在一起讨论,并指出单体同样可以保持良好的模块化。这提醒我:评估拆分时,应当明确要减少哪一种耦合,以及为此愿意增加什么成本。Microservice Trade-Offs
下面是我结合项目整理的比较方法,不是必须逐级升级的成熟度模型。
| 主要压力 | 可以先选的边界 | 仍需承担的成本 |
|---|---|---|
| 一套产品内部难以理解和修改 | 进程内模块、明确接口 | 用检查约束内部依赖;仍一起部署 |
| 多个采用方复用规则,但可以各自运行 | 独立包、公开 API、精确版本 | 每个采用方仍需管理自己的运行环境与升级 |
| 数据权威、执行和维护责任需要统一 | 独立服务、受限客户端 | 网络故障、服务运维、契约兼容与数据迁移 |
例如,纯文本解析器没有长期权威状态,多个产品安装同一个库通常已经有意义。而证据撤销之后,哪些图对象还可以使用,涉及持续维护的状态;如果每个产品分别解释这件事,就需要保证每条路径都及时、正确地跟随变化。这是不同的压力。
拆包首先解决安装与发布身份
TraceWise 早期面临的一个具体风险,是产品与平台如果共同拥有同一顶层 Python namespace,安装与卸载就可能互相影响。拆分后,产品包拥有 app/**,Foundation 包拥有 agent_foundation/**;产品只从 agent_foundation.public.* 采用能力。安装顺序、卸载重装和 wheel 文件归属的受控检查,给这条边界提供了实际依据。
这种拆分值得做,因为它让团队能够回答“安装的是哪一版、谁拥有这些文件、公共能力来自哪里”。但它没有自动回答“谁启动 Executor、谁保存底层凭据、谁维护查询视图”。把公共包放进产品进程,仍可能让产品承担平台的运行工作。
因此,拆包可以是一个完整选择。若采用方需要离线运行、数据必须各自保管,而且团队能够承担各自运维,它不必再向独立服务演进。只有运行责任本身也需要改变时,第二次拆分才有明确目的。
服务化改变运行责任,产品仍要作出业务决定
a17 的产品环境只依赖 tracewise-memory==0.1.100 公开客户端,通过 HTTP 消费 Foundation 能力,不再安装 Foundation 服务端包,也不承载产品本地的 Executor、直接 Graph 写入以及底层存储或索引工作进程凭据。
这条边界并没有把所有检索逻辑交给平台。当前节点检索由 Foundation 提供语义候选与当前图内容,TraceWise 保留九信号权重、业务排序、有界邻域重排和结果展示。完整语料统计绑定到检索代次;整图刷新限于代次或维护视图标识变化,普通查询不反复下载全图。
| 读者真正关心的决定 | TraceWise 保留的责任 | Foundation 承担的责任 |
|---|---|---|
| 这个问题需要哪些资料 | 资料选择、查询范围、业务排序与上下文预算 | 按公开契约提供候选和受治理内容 |
| 这条依据现在还能否使用 | 展示状态、解释业务影响 | 已登记对象的版本、撤销与依赖资格 |
| 是否把建议变成正式变更 | 业务授权、审核人选择、差异呈现 | 校验请求,执行适用约束并记录结果 |
| 如何生成回答 | Host Model、提示词、模型凭据与成本策略 | 不替产品决定回答目标 |
我据此采用的原则是:业务差异留在产品中;对于平台已经管理的对象,其权威状态和持续维护由平台统一承担。是否共享,取决于对象的责任,而不是某段代码看起来是否通用。
本地交付验证确认了这次依赖调整:两次干净构建产生相同 wheel,独立安装环境没有 Foundation 服务端包,变更范围内的 Python 检查通过 102 项、跳过 3 项外部夹具,前端 160 项检查通过。这些检查支持代码与交付边界;它们尚不能说明真实采用方已迁移,也不能证明跨产品运行成本已经下降。
身份与数据不会跟着依赖声明自动搬家
早期第二个项目暴露过一个很小、却决定迁移结果的反例。第一个 Pilot 的业务项目 ID 与 Graph ID 恰好相同,错误复用一个 ID 也能通过。第二个项目的业务 ID 是 model-post-training,Graph ID 是 demo,偶然相等的条件消失了。
业务项目 ID 用于路由、授权和历史,Graph ID 标识图作用域,Foundation Registration 还需要绑定它承接的权威身份。它们不能因为都叫 project_id 就互相替代。a12 修复身份映射后,第二个受控本地项目形成独立 Registration,迁移 107 条 Evidence,并完成精确重放与重开检查;第一个 Pilot 和未采用项目得到保留。
这个反例对服务化仍然适用。改成 HTTP 客户端只改变访问方式,原有提案、审核决定、执行回执和项目身份还必须保持连续。a17 的集成没有改写这些已有记录;实际准入、数据转移和旧写入者停用是后续迁移要分别完成的工作。
迁移方案因此至少要能回答:这一批对象由谁负责,旧路径何时失去写入资格,重复请求如何识别,失败时从哪份已保存状态继续。若这些问题没有答案,换了连接地址也不能视为切换完成。
远程调用会让“成功”需要更多解释
一次审核通过,表达的是批准决定;一次请求被服务接受,表达的是服务接到了工作;最终图发生变化,还需要执行结果。产品若把三者压成同一个成功提示,用户在超时后就无法判断是否应该重试。
历史受控路径曾出现 Graph 已应用、Foundation 后续同步失败的情况,状态保留为 applied_with_foundation_pending;恢复依赖后,对同一决定重试推进同步,再次重试得到去重结果。这个案例支持保留部分成功与执行身份的必要性,不等于多个存储具有原子回滚。
服务化后的读取也有类似取舍。节点候选尚未就绪、视图过期或服务不可用时,a17 保留类型化状态,不静默替换为文本搜索。对于一次明确要求语义检索的请求,悄悄换算法会改变任务含义;是否允许降级,应由产品明确决定并让用户知道。
代价是产品需要解释更多等待和失败状态,服务需要可诊断、可恢复的运维路径。客户端变薄,并不表示整个系统变简单。
哪些情况会让我重新考虑这个选择
如果只有一个采用方、发布节奏一致,而且模块边界已经稳定,独立服务可能只增加一次网络调用和一套运维。若产品要求可靠离线运行,集中服务的可用性依赖也可能不合适。这些情况下,保持公开接口和精确包版本,可以比继续拆分更经济。
即使选择服务化,也要观察它有没有达到原来的目的:产品迭代是否仍频繁要求平台同步发版,故障能否定位到明确责任方,采用方是否仍持有绕过服务的数据库写权限。如果这些现象持续存在,就应重新审视契约粒度和所有权,而不是仅凭仓库数量判断拆分完成。
TraceWise 目前得到的结论是,安装边界与运行边界需要分别设计。选择边界之前,先说明要减少哪一种耦合;完成之后,再用实际依赖、身份连续性和失败恢复检查它是否成立。独立服务的价值,要由它承担了什么责任来解释。