1. 从"五个产品各管一摊"到"一个底座统管"的架构动机
做过企业级工具套件的人大概都有这种体会:产品线一旦超过三个,维护成本就不是线性增长,而是指数级爆炸。WorkBuddy 企业版在早期也是这个路子——每个产品各自维护一套 Agent 调度逻辑、各自的鉴权体系、各自的 Skill 加载机制。表面上看每个产品都能跑,但底层重复造轮子的代价在半年后集中爆发:同一个 Skill 在 A 产品能用、在 B 产品报错,排查半天发现是两边的 Agent 运行时版本不一致;OpenAPI 的鉴权逻辑改了三次,每次都要在五个仓库里同步改五遍,漏一个就出线上事故。
这套件化的核心思路,说白了就是把 Agent 从"每个产品内置的一个模块"提升为"所有产品共享的一个底座"。五个产品不再各自养一个 Agent,而是统一挂在一个 Agent 底座上,底座负责调度、鉴权、Skill 管理、上下文传递这些脏活累活,产品层只关心自己的业务逻辑。这个转变听起来简单,但真正落地时踩的坑比想象中多得多。
我先把这套架构要解决的核心问题列清楚,后面所有章节都围绕这几个问题展开:
- Agent 运行时的统一:五个产品共用一个 Agent 内核,版本、能力、行为完全一致,杜绝"同 Skill 不同表现"。
- Skill 的跨产品复用:一个 Skill 写一次,五个产品都能调,且能按产品做权限隔离。
- OpenAPI 的统一出口:对外只暴露一套 OpenAPI,内部路由到不同产品,鉴权和限流在底座层统一处理。
- 上下文与状态的隔离:共享底座不等于共享数据,产品之间的会话、缓存、凭证必须严格隔离。
- 可观测性:五个产品的 Agent 调用链路要能统一追踪,出问题能快速定位是底座问题还是产品问题。
这五个问题里,前三个是"为什么要套件化"的答案,后两个是"套件化之后怎么不出事"的保障。很多团队只盯着前三个做,结果上线后数据串了、日志乱了,返工成本极高。
提示:套件化不是把代码合并到一个仓库就完事了。真正的难点在于"共享"和"隔离"这对矛盾的平衡——共享得越多,隔离就越难做干净。这个平衡点必须在架构设计阶段就想清楚,不能边做边补。
2. Agent 底座到底该抽哪几层:一次务实的边界划分
2.1 底座层与产品层的职责切分
套件化最容易犯的错,是把底座抽得太"厚"。我见过有团队恨不得把 UI 渲染逻辑都塞进底座,结果底座变成了一个巨型单体,改一处牵动全身,比不套件化还难维护。WorkBuddy 这套底座最终收敛到四层,每层的边界都很明确:
| 层级 | 职责 | 不该管的事 |
|---|---|---|
| 调度层 | Agent 任务编排、Skill 调用链、超时重试 | 具体业务语义 |
| 能力层 | Skill 注册/发现/加载、OpenAPI 网关 | 产品特有的业务规则 |
| 状态层 | 会话上下文、缓存、凭证隔离 | 业务数据的持久化 |
| 观测层 | 链路追踪、指标采集、日志聚合 | 业务埋点 |
调度层是底座的心脏。它不关心"这个 Skill 是给哪个产品用的",只关心"给我一个 Skill 标识和入参,我负责把它跑起来并返回结果"。这种"无知"恰恰是它能被五个产品共用的前提——一旦调度层开始出现if product == 'A'这种判断,底座就开始腐化了。
能力层负责 Skill 的注册与发现。这里有个关键设计:Skill 的元数据里必须带产品可见性标签。同一个 Skill 可以标记为"仅产品 A 可见""A 和 C 可见""全部可见",底座在加载时根据调用方产品做过滤。这样既实现了复用,又做到了权限隔离,不需要为每个产品单独部署一套 Skill。
2.2 为什么状态层是最容易被低估的一层
大部分团队在套件化时会把注意力放在调度和能力上,状态层往往是最后才想起来补的。但恰恰是状态层决定了这套架构能不能安全地服务多个产品。
举个真实的场景:产品 A 和产品 B 都用了同一个"文档摘要"Skill。如果会话上下文不做隔离,A 用户上传的文档内容可能通过缓存 key 冲突泄漏到 B 用户的会话里。这种问题在测试环境几乎发现不了,因为测试时两个产品往往用的是同一批测试账号,数据本来就混着。
WorkBuddy 的做法是在状态层的所有 key 前面强制拼接产品命名空间,比如{product_id}:{tenant_id}:{session_id}:{resource}。这个拼接不是靠开发者自觉,而是封装在底座的上下文对象里,产品层拿到的上下文对象已经带好了命名空间,想绕过都绕不过去。这种"把正确做法变成唯一做法"的设计思路,比写十页规范文档都管用。
2.3 观测层:套件化之后你才知道它有多重要
单产品时代,出问题看一个服务的日志就够了。套件化之后,一个用户请求可能穿过产品层、底座调度层、Skill 执行层,任何一层出问题表现都是一样的"没反应"。没有统一的链路追踪,排查就是大海捞针。
底座在观测层做了一件很实在的事:给每次 Agent 调用生成一个贯穿全链路的 trace_id,并在每个关键节点打点。产品层拿到这个 trace_id 后,可以把它透传给前端,用户报障时直接提供 trace_id,运维就能一键拉出完整链路。这个设计把平均故障定位时间从小时级压到了分钟级。
3. Skill 跨产品复用的真实机制与踩坑记录
3.1 Skill 注册时到底发生了什么
很多人以为 Skill 就是一个函数,注册进去就能用。实际上一套能支撑五个产品的 Skill 机制,注册时要处理的事情远比想象中复杂。WorkBuddy 的 Skill 注册流程大致是这样的:
- 元数据校验:检查 Skill 的标识、版本、入参 schema、可见性标签是否完整。
- 依赖解析:如果 Skill 依赖其他 Skill 或外部 OpenAPI,先解析依赖树,确保依赖可用。
- 沙箱预加载:在隔离环境中试跑一次,确认 Skill 能正常初始化。
- 可见性注册:根据标签把 Skill 挂到对应产品的可见列表里。
- 版本快照:记录当前 Skill 的版本和依赖版本,方便回滚。
这里面第 3 步"沙箱预加载"是我强烈建议保留的。我们早期为了加快注册速度跳过了这一步,结果一个 Skill 因为依赖了一个不存在的环境变量,注册时没报错,运行时才崩,而且崩的时候报的是底层错误,排查了半天才发现是 Skill 本身的问题。加上预加载之后,问题在注册阶段就暴露了,代价小得多。
3.2 版本冲突:套件化最隐蔽的坑
五个产品共用一个底座,意味着它们共享同一套 Skill 运行时。但不同产品对同一个 Skill 的版本需求可能不一样——产品 A 还在用 v1.2 的接口,产品 B 已经升级到 v2.0 了。如果底座只支持单版本,必然有一方要妥协。
WorkBuddy 的解法是支持 Skill 多版本共存,由调用方显式指定版本。调用时如果不指定,默认走最新稳定版。这个设计的好处是产品可以按自己的节奏升级,不会被底座绑架。代价是底座要维护多份 Skill 实例,内存占用会上升,所以我们在底座里加了一个"冷版本回收"机制——连续 N 天没有被任何产品调用的旧版本,自动卸载,需要时再懒加载。
注意:多版本共存不是让你无脑保留所有历史版本。我们踩过的坑是早期没做回收,半年后底座里堆了四十多个 Skill 版本,启动时间从 3 秒涨到了 20 多秒。冷版本回收上线后,启动时间回到了 5 秒以内。
3.3 Skill 编码规范:那些文档里不会写的细节
热词里频繁出现"skill 编码"相关的搜索,说明很多人在 Skill 开发阶段就卡住了。我分享几条从实际项目里总结的编码经验,都是文档里不会写但特别容易踩的:
- 入参永远做防御性校验:不要假设调用方会传对参数。底座层虽然会做基础校验,但 Skill 内部的业务校验不能省。我们有个 Skill 因为没校验一个可选参数,调用方传了空字符串,结果触发了下游 OpenAPI 的异常,整个调用链挂了。
- 超时时间要显式设置:Skill 默认超时是底座给的全局值,但不同 Skill 的合理耗时差异很大。一个查数据库的 Skill 和一个调外部大模型的 Skill,超时设置能一样吗?显式设置超时,并在超时后做好清理,避免资源泄漏。
- 日志里不要打敏感信息:套件化之后日志是集中采集的,一个 Skill 的日志可能被多个产品的运维看到。入参里的用户凭证、文档内容这些,要么脱敏要么不打。
- 幂等性设计:Agent 调度层有重试机制,Skill 必须能承受被重复调用。写操作类的 Skill 尤其要注意,重复调用不能产生副作用。
4. OpenAPI 统一出口的鉴权与限流设计
4.1 为什么不能让产品各自暴露 OpenAPI
套件化之前,五个产品各自有一套 OpenAPI,对外调用方要记五套地址、五套鉴权方式、五套错误码。这不仅是调用方的噩梦,也是安全团队的噩梦——五套鉴权逻辑意味着五个可能出漏洞的地方。
统一到一套 OpenAPI 之后,对外只有一个入口,鉴权、限流、审计都在底座层做。产品层不再直接暴露接口,所有外部请求先到 OpenAPI 网关,网关根据请求里的产品标识路由到对应产品。这个架构的关键在于网关必须能准确识别请求属于哪个产品,否则路由就乱了。
4.2 鉴权链路的三层校验
WorkBuddy 的 OpenAPI 鉴权分三层,每层解决不同的问题:
| 校验层 | 校验内容 | 失败表现 |
|---|---|---|
| 接入层 | API Key / 签名有效性 | 401,请求被拒 |
| 产品层 | 该 Key 是否有权访问目标产品 | 403,无权访问 |
| Skill 层 | 该 Key 是否有权调用目标 Skill | 403,Skill 不可见 |
这三层缺一不可。我见过只做接入层校验的实现,结果一个产品的 Key 能调用另一个产品的所有 Skill,权限形同虚设。也见过只做产品层校验的,同一个产品内不同 Skill 的权限粒度控制不了。三层校验虽然多了一次查表,但换来的是清晰的权限边界,值得。
4.3 限流:套件化之后必须重新算的账
单产品时代,限流阈值是按单个产品的容量算的。套件化之后,五个产品共享底座资源,如果还按单产品算,底座会被打爆。正确的做法是在底座层做全局限流,同时给每个产品分配配额。
具体来说,底座维护一个全局令牌桶,总容量是所有产品配额之和。每个产品有自己的子令牌桶,从全局桶里预支令牌。这样既能保证单个产品不会超用,又能保证全局不超载。配额可以动态调整,某个产品做活动需要临时扩容时,从全局桶里多分一些给它就行。
提示:限流阈值不要拍脑袋定。我们上线前做了一轮压测,发现底座的瓶颈不在 CPU 而在 Skill 加载的 IO 上。如果按 CPU 使用率定阈值,实际能扛的 QPS 只有理论值的三分之一。压测数据才是定阈值的唯一依据。
5. 缓存目录与状态隔离:那些让人半夜爬起来的问题
5.1 缓存目录冲突引发的血案
热词里"workbuddy 缓存目录怎么更改""workbuddy 怎么更改系统缓存目录"出现频率很高,说明缓存目录问题是真实痛点。套件化场景下,缓存目录的管理比单产品复杂得多。
我们遇到过一个典型问题:产品 A 和产品 B 都用底座做文件缓存,默认缓存目录是同一个。某天产品 A 的一个批量任务把缓存目录写满了,产品 B 的缓存写入全部失败,表现为"产品 B 突然不可用"。排查时先看产品 B 的日志,一切正常,最后才发现是磁盘被产品 A 占满了。
解法是每个产品分配独立的缓存子目录,并设置独立的配额。底座在初始化时为每个产品创建cache/{product_id}/目录,并挂载磁盘配额。产品 A 写满自己的配额只会影响自己,不会波及产品 B。同时底座提供缓存清理接口,产品可以主动清理自己的缓存,也可以配置自动清理策略。
5.2 会话状态的隔离不能只靠命名空间
前面提到用命名空间隔离会话状态,这是基础。但实际场景里还有更细的隔离需求。比如产品 A 和产品 B 都用了同一个"多轮对话"Skill,如果只按产品隔离,同一个产品内不同用户的会话还是可能串。
完整的隔离维度应该是产品 + 租户 + 用户 + 会话四级。底座在生成会话 key 时把这四级都拼进去,任何一级不同就是不同的会话。这个设计在测试时看不出差别,但上线后多租户场景下能避免大量串话问题。
5.3 凭证隔离:最不能省的一环
五个产品可能各自对接了不同的外部服务,每个服务有自己的凭证。如果凭证在底座层共享,一个产品的凭证泄漏会影响所有产品。WorkBuddy 的做法是凭证按产品隔离存储,底座只负责加密和解密,不负责跨产品共享。
具体实现上,每个产品的凭证存在独立的加密空间里,加密密钥由底座统一管理但按产品派生。产品 A 的凭证即使被解密,也只能得到产品 A 的密钥,解不开产品 B 的凭证。这种"密钥派生"的设计比"一把钥匙开所有锁"安全得多。
6. 从单产品迁移到套件底座的实操路径
6.1 迁移顺序:先易后难还是先难后易
迁移五个产品到统一底座,顺序选择很关键。我们的经验是先迁一个功能最简单、依赖最少的产品,用它来验证底座的基本能力,把底座和产品之间的接口磨合好。这个阶段不要追求完美,能跑通就行。
第一个产品迁完之后,底座的核心接口基本稳定了。这时候再迁第二个产品,重点验证"多产品共存"场景——两个产品的 Skill 会不会冲突、缓存会不会串、限流会不会互相影响。第二个产品迁完,底座的隔离机制才算真正被验证。
剩下的三个产品可以并行迁移,因为底座已经稳定,迁移工作主要是适配产品特有的业务逻辑。整个迁移周期,第一个产品占了一半时间,后面四个加起来占另一半。这个时间分配是合理的,不要试图加速第一个产品的迁移。
6.2 迁移中的兼容性处理
迁移过程中,老产品还在跑,新产品要接入底座,两者必须能共存。WorkBuddy 的做法是在底座里加一层适配器,老产品的调用请求通过适配器转换成底座的标准调用格式。适配器是临时的,等所有产品迁完就下线。
适配器这层虽然增加了短期复杂度,但避免了"一刀切"迁移带来的风险。如果强行要求所有产品同时切换,任何一个产品出问题都会影响全局。有了适配器,可以一个产品一个产品地切,切一个验证一个,风险可控。
6.3 迁移后的回归验证清单
产品迁到新底座后,必须做一轮完整的回归验证。我整理了一份实际用过的验证清单,覆盖了最容易出问题的点:
- 功能回归:产品原有的所有 Skill 调用是否正常,返回结果是否一致。
- 权限验证:产品的 OpenAPI Key 是否只能访问自己的 Skill,跨产品调用是否被正确拒绝。
- 隔离验证:两个产品同时跑批量任务,缓存、会话、凭证是否互不干扰。
- 限流验证:单产品打满配额时,其他产品是否不受影响。
- 观测验证:trace_id 是否能贯穿全链路,日志是否能按产品过滤。
- 回滚验证:如果新底座出问题,能否快速切回老架构。
这份清单里的每一项都是我们踩过坑之后补上的。尤其是回滚验证,很多团队迁移时不做,真出问题时才发现回不去,只能硬扛。
7. 套件化之后的新问题与应对思路
7.1 底座成为单点故障怎么办
套件化最大的风险就是底座变成单点。五个产品都挂在底座上,底座一挂全挂。这个风险必须在架构设计阶段就正视。
WorkBuddy 的应对是底座本身做多实例部署,产品通过负载均衡访问底座。同时底座的关键状态(如 Skill 注册表、限流配额)做持久化,实例重启后能快速恢复。另外,底座提供"降级模式"——当底座不可用时,产品可以切换到本地缓存的 Skill 列表,用降级逻辑继续提供核心功能,虽然能力有损但不至于完全不可用。
7.2 底座升级如何不惊动产品
底座升级是套件化之后的常态,但每次升级都让五个产品跟着重启是不可接受的。解法是底座支持热升级:新版本底座实例先启动,健康检查通过后逐步接入流量,老实例在流量归零后下线。整个过程产品无感知。
热升级的关键是版本兼容。新底座必须能处理老产品发来的请求,不能因为底座升级就要求产品同步升级。这要求底座的接口设计保持向后兼容,新增能力用新接口,不改老接口的语义。
7.3 多产品需求冲突时的决策机制
五个产品共用一个底座,需求冲突是必然的。产品 A 想要底座支持某个特性,产品 B 觉得这个特性会影响自己的稳定性。这种冲突不能靠"谁声音大听谁的",需要一套决策机制。
我们的做法是底座维护一个公开的路线图,所有产品都可以提需求,底座团队评估后决定是否纳入。评估标准包括:这个需求是否通用、是否影响其他产品、实现成本多大。纳入路线图的需求,底座团队负责实现;不纳入的,产品可以自己在产品层实现,但不能要求底座为单个产品做定制。这套机制虽然不能消除所有冲突,但至少让决策过程透明,减少扯皮。
8. 一些关于 Agent 底座的经验之谈
做 Agent 底座这几年,最大的体会是底座的复杂度不在于技术,而在于边界。技术上要实现的调度、鉴权、隔离,都有成熟方案,难的是判断哪些能力该放进底座、哪些该留在产品层。放多了底座臃肿,放少了产品重复造轮子。
我的判断标准很简单:如果一个能力有两个以上产品需要,且实现方式基本一致,就放进底座;如果只有一个产品需要,或者不同产品的实现差异很大,就留在产品层。这个标准不是绝对的,但能覆盖大部分场景。
另一个体会是底座的稳定性优先级永远高于新功能。底座挂了五个产品全挂,这个代价太大。所以底座团队要有"宁可少做功能,也要保证稳定"的定力。新功能可以慢慢加,稳定性一旦出问题,信任就没了。
最后说个具体的:底座的日志和监控一定要做在产品接入之前。我们早期是先接产品后补监控,结果产品接入过程中出的问题全靠人肉排查,效率极低。后来把监控补齐了,再接新产品时问题定位快了很多。这个顺序不能反,监控是底座的基础设施,不是附加功能。