aptbot全景:分层架构与设计哲学

上一篇文章我们从”概念层”理解了agent是什么——ReAct循环、四大组件、agent loop。这篇文章切换到”工程层”:一个agent系统的代码应该怎么组织?层怎么划分、依赖怎么管理、各个模块之间如何协作而不互相纠缠?

我们会先用aptbot的四层架构作为具体例子,理解分层设计的真实动机;然后对比市面三种不同的agent架构路线,看它们各自取舍了什么;最后落到aptbot的设计哲学上——为什么它既不是框架也不是SaaS,而是”你的agent”。

一、四层架构:access / bus / core / infrastructure + shared

如果你打开aptbot的src目录,最先注意到的是四个顶层目录:access/bus/core/infrastructure/,外加一个被所有层引用的 shared/。这不是随意的文件夹命名,而是有明确意图的分层设计。

四层架构图

1.1 access(接入层):与外部世界对接

接入层是aptbot的”门面”。它负责与外部世界直接交互——接收外部请求、返回外部能消费的响应。具体内容包括:

  • HTTP路由:REST API端点,供WebUI或其他HTTP客户端调用
  • WebSocket处理:WebSocket连接的生命周期管理,支持流式事件推送
  • CLI终端界面:基于Ink(React终端渲染库)实现的交互式命令行界面
  • 落地页HTML:aptbot的Web管理界面

接入层的核心职责是翻译——把外部输入”翻译”成内部调用的参数,把内部事件”翻译”回外部能消费的格式。接入层不包含业务逻辑:它不做agent决策、不管理会话、不调用LLM。它只负责”怎么接入”。

这种隔离带来的好处在实际场景中非常明显:假如你要给aptbot添加一个Slack机器人接入,你只需要在access层加一个Slack路由处理逻辑,core层的agent循环完全不需要做任何改动。接入方式和业务逻辑解耦,意味着每增加一种接入方式,核心系统的风险增量趋近于零。

1.2 bus(总线层):事件分发的中枢

总线层解决一个核心问题:多端接入如何共享同一个agent会话

最简单的实现是”每个客户端一个agent实例”,但这是错的——如果用户在电脑上启动了一个任务让agent编辑文件,然后切换到手机查看进度,这两个客户端看到的是两个独立的agent会话,彼此不知道对方的存在。更糟的是,如果两个客户端先后执行了冲突的操作,agent状态会分裂。

bus层的方案是:agent只做一件事——运行循环并把所有输出(LLM流式token、工具调用结果、状态变化)作为事件发到bus。bus再把事件分发给所有绑定该session的channel。每个channel是一个接入端点(WebSocket、未来的Telegram等),它们通过bus共享同一个agent会话。

关键设计要点:

  • 类型化事件总线:bus上传递的不是散装的JSON字符串,而是类型化的 AgentEvent 联合类型。每个事件有明确的形状,消费方可以根据事件类型做精确处理。
  • Channel抽象:每个接入端点实现Channel接口,核心方法只有 onEvent(event)dispose()。这个接口足够小,使得添加一个新的Channel实现(比如Telegram Channel)成本极低。
  • Session绑定:一个session绑定到bus上,channel通过bus订阅session的事件流。

1.3 core(核心层):agent的心脏

core是aptbot最有技术含量的四层,它实现了上一篇文章描述的四个组件:

  • AgentLoop:ReAct循环的生成器实现。每次迭代:组装消息 → 调用LLM → 解析响应 → 执行工具 → 收集结果。循环约150行,保持清晰可读。
  • Provider:LLM服务接入的抽象层。Provider接口定义了 stream(model, context, options) 方法,返回 AsyncGenerator<AssistantMessageEvent>。所有LLM调用都通过这个接口,core不需要知道底层用的是OpenAI还是Anthropic。
  • Tool:工具注册与调度系统。每个工具是一个函数(或更复杂的实现),有明确的输入输出类型。ToolRegistry管理所有可用工具,AgentLoop通过它执行工具调用。
  • Memory:记忆系统。管理对话历史、短期上下文窗口、跨会话的长期记忆。
  • Skill:技能系统。管理可复用的技能模板,让agent能沉淀和复用成功解决问题的模式。
  • Hook:钩子系统。提供8个扩展点(beforeTurn、afterToolCall等),允许在不修改core代码的情况下插入自定义逻辑。

core对access和bus层一无所知。它不关心用户是通过CLI还是WebUI接入的,不关心事件是怎么分发的。它只负责”做好agent的本职工作”——接收输入、推理、行动、输出。

1.4 infrastructure(基础设施层):与具体技术对接

infrastructure层是core的”手脚”——它把抽象的操作落地到具体的技术实现:

  • 文件系统:JSONL持久化(对话历史存文件)、配置加载、日志写入
  • 子进程:bash工具执行命令时创建子进程,管理进程生命周期
  • HTTP客户端:Provider调用LLM API时发HTTP请求

这层之所以独立,是因为它是”最容易换”的一层。比如,今天用JSONL文件存对话历史,明天想换成SQLite——你只需要在infrastructure层重新实现Memory接口,core层一行代码都不用改。同样,今天用bash子进程执行命令,明天想换Docker容器执行,也只需要换infrastructure层的实现。

这种可替换性在生产环境中极其有价值:不同的部署场景需要不同的基础设施方案,而infrastructure层的存在让这些切换不影响agent的核心逻辑。

1.5 shared(跨层共享):纯类型与工具

shared层被所有其他层引用,但它不引用任何业务层。它包含:

  • 命令注册表(commands)
  • 共享类型定义(shared types)
  • 纯函数工具(如字符串处理、日期格式化)

shared层的核心约束是”不持有业务状态”。一个函数放在shared里,意味着它是纯的——同样的输入永远返回同样的输出,不依赖全局变量、不读写文件、不调用外部服务。这个约束让shared层的代码天然可测试、可推理。

二、严格单向依赖的意义

2.1依赖规则

四层架构不只是”把代码放到不同文件夹”,它更关键的是依赖规则。aptbot的依赖方向是严格单向的:

单向依赖方向图

具体规则如下:

  • access/* → 可引用 bus/* core/* infrastructure/* shared/*
  • bus/* → 可引用 core/* infrastructure/* shared/*
  • core/* → 可引用 infrastructure/* shared/*不可引用 access/* bus/*
  • infrastructure/* → 只引用 shared/* 与外部依赖
  • shared/* → 不引用任何业务层

核心要求是:依赖方向从接入层指向基础设施层,不能反过来

2.2为什么不能反过来?

用反例来说明最直观。假设core层直接引用了access层的WebSocket实现,会出现什么问题?

第一个问题:可替换性崩塌。你想把aptbot从Web接入改成Telegram接入,但agent循环里到处都是”如果是WebSocket客户端就…”的条件分支。改接入方式变成了改core——而core是agent循环本体,任何改动都可能影响agent的推理行为。接一个Telegram,结果把agent搞傻了,这是不可接受的。

第二个问题:可测试性崩溃。你想给AgentLoop写一个单元测试——测试它收到一个工具调用请求后能否正确解析响应。但如果AgentLoop依赖WebSocket实现,你跑测试前必须先拉起一个WebSocket服务端,再连一个客户端。本来5行能写完的测试,变成了50行的环境搭建。测试变成了集成测试,跑一次慢、改一次痛,最终团队会放弃写测试。

第三个问题:逻辑污染。agent循环的代码里出现了接入层的概念——“如果是WebSocket,用这种方式发送错误;如果是CLI,用那种方式提示”。业务逻辑和接入细节纠缠在一起,代码的可读性急剧下降。新成员要理解agent循环,必须先理解所有接入方式——这和”关注点分离”的原则背道而驰。

单向依赖解决了所有这三个问题:

  • 可替换性:core层不知道access层存在,加新接入方式core不动。换infrastructure层(JSONL → SQLite),core接口不变。
  • 可测试性:core层测试只需要mock Provider和ToolRegistry两个接口,不需要拉起HTTP服务或WebSocket连接。一个AgentLoop的核心测试可以被控制在20行以内。
  • 逻辑纯净:agent循环的代码只包含”做决策”的逻辑,不混入”如何传输”的细节。读代码的人只需要理解agent本身,不需要理解所有接入方式。

2.3反方向依赖的实际案例

曾经有一个阶段,aptbot的session管理中包含了对access层WebSocket状态的直接引用——session需要知道”当前有几个客户端连着”来做某些决策。这在依赖规则上是违规的(core引用了access)。

后果很快显现:测试session逻辑时必须mock WebSocket连接,测试变慢;后来想加一个CLI接入,发现session把WebSocket当作”默认接入方式”硬编码了。最终重构时把”连接计数”的逻辑从session移到bus层,session只关心自己的事件流,不关心谁在消费它。重构后,session的测试从需要3个mock降到了0个mock(纯函数测试),CLI接入也只需在bus层加一个Channel实现,session层零改动。

这个经历验证了:单向依赖不是教条,是经过实际教训总结出来的工程纪律

三、市面其他agent架构方案对比

aptbot的四层架构不是唯一的组织方式。市面上的agent项目在架构上存在几种不同的设计路线,这里用方案A/B/C代表三种典型思路(它们对应某些开源项目,但本文关注设计思路本身)。

3.1方案A:极简内核SDK路线

这条路线的哲学是”少即是多”——内核足够小,上层可以自由组合。

核心设计:

  • 无状态生成器内核:agent loop是一个无状态的async generator函数,核心100-150行。它不持有任何状态,状态由上层的session/harness管理。
  • 全链路类型安全:用TypeScript + schema校验库(TypeBox/Zod)保证每个环节的类型安全——工具定义、配置、事件流都有完整的类型约束。编译期就能捕获大部分接口不一致问题。
  • 事件流模型:agent输出不是字符串,而是 EventStream<AgentEvent>——每个事件是类型化对象。上层UI通过订阅感兴趣的事件类型来渲染,不需要轮询或解析。
  • 复杂度上移:内核极简,但上层可能包40+ 组件来实现完整交互体验。这是一种”把选择权交给使用者”的策略——你不需要的功能就不引入。

优点: 内核纯粹,可组合性强,类型安全让重构有信心。

代价: 内核虽小,但要构建一个可用的产品需要在上层补大量胶水代码;事件流模型对简单”问答”场景偏重;类型系统的学习门槛不低。

3.2方案B:自演化 + 极低token路线

这条路线的哲学最激进——不给agent预置任何技能,让它自己在解决问题的过程中积累经验。agent用得越多越聪明。

核心设计:

  • 任务结晶机制:agent完成一个任务后,自动把成功路径抽象成skill,存入skill库。下次遇到相似任务时直接调用,不需要从头推理。
  • 极致token控制:通过”只传新消息”(而非全量历史)+ tag截断 + 工作记忆checkpoint,把每轮上下文控制在30K token以内,远低于动辄200K-1M的方案。
  • 原子工具集:只用9个原子工具覆盖所有能力——其中 code_run 一个工具就包揽了Python执行和bash执行,工具之间可以自由组合。
  • 自举性:项目自身的代码就是agent创建的——agent不仅使用工具,还能理解自己的代码并提出修改。

优点: 长期使用后越来越贴合个人习惯;token消耗极低,对按量计费的用户友好;agent能力的进化路径自然。

代价: 初次使用时skill库为空,表现不如预置技能的方案;自演化路径不可控,可能结晶出低质量的skill;threading + generator的并发模型在多端接入时比async/await复杂。

3.3方案C:全栈工程化配置驱动路线

这条路线的哲学是”生产就绪”——从IM渠道到WebUI到定时任务全覆盖,配置驱动一切,不改代码。

核心设计:

  • Channel抽象 + 丰富实现:把每个聊天平台(Telegram、Discord、Slack…)抽象成Channel,通过统一的MessageBus接收和发送消息。内置20+ Channel实现。
  • 配置驱动:30+ 内置provider、20+ 内置channel,全部通过YAML/TOML配置文件切换。用户不需要写代码,改配置就能切换模型、增减平台。
  • 循环内嵌恢复:agent loop单方法约400行,内置orphan修复(工具调用后未返回的处理)、backfill(补全遗漏的上下文)、microcompact(上下文压缩)等多种错误恢复路径。
  • 弱类型:运行时大量dict/JSON传递,类型校验仅覆盖配置层。核心逻辑中类型约束不严格,重构时依赖测试覆盖。

优点: 开箱即用,功能完整;多平台接入覆盖广;配置驱动的使用门槛低。

代价: 单方法400行的循环可读性差,新成员理解成本高;弱类型在重构时风险大(改一个字段可能引发多处运行时错误);功能全但每个模块的精炼度不够。

3.4架构方案对比

维度 方案A(极简SDK) 方案B(自演化) 方案C(全栈工程)
核心哲学 少即是多,可组合 不预置技能,用中演化 生产就绪,配置驱动
核心循环规模 ~150行 ~100行 ~400行
类型安全 强(全链路TypeBox/Zod) 弱(几乎无类型约束) 弱(仅配置层Pydantic)
token策略 中等(全量上下文) 极低(<30K,截断+checkpoint) 高(依赖大context window)
工具策略 用户手写 + 内置组合 原子工具 + 自动结晶 内置30+ 工具
多平台 无(纯SDK,由使用者集成) 有限(多前端非Channel) 20+ Channel
测试难度 低(纯函数 + mock接口) 中(有状态,但单进程) 高(多平台依赖复杂)
适合谁 开发者嵌入自己产品 个人长期使用的桌面助理 团队多平台运维

四、aptbot的设计特点

4.1定位决定取舍

在上一篇文章中我们说过,aptbot有双重身份:学习型项目个人助理。这双重身份决定了aptbot的架构风格必须在上述三种方案之间找到自己的位置。

对比来看:

  • 不能像方案A那样只做SDK,因为学习者需要看到一个完整的产品——他们需要理解”agent从启动到服务的完整生命周期”,而不是只有一个内核。
  • 不能像方案C那样堆功能,因为学习者会被淹没在400行的循环和20+ 的Channel实现中,找不到主线。
  • 不能像方案B那样追求自演化,因为初学者无法理解不可控的skill结晶过程,而且自演化的路径依赖太强,不适合教学。

4.2关键架构决策

基于这一定位,aptbot在几个关键维度上做了以下选择:

类型安全(向方案A看齐):选择TypeScript + Zod作为类型系统。这既是工程质量保证,也是教学优势——读代码就能看出每个数据的形状。一个 AgentEvent 联合类型定义,比十页文档更能让人理解agent输出什么。类型安全带来的编译期检查,也降低了学习者修改代码时”改崩了不知道”的风险。

核心循环(参考方案A的分层思路):AgentLoop保持无状态生成器风格,约150行。复杂度上移到session层(管理状态)和harness层(管理生命周期)。这样新手可以先理解最简单的”输入 → 推理 → 工具 → 输出”循环,再逐步看上层如何管理状态和生命周期。

事件流(区别于方案B的字符串yield):采用类型化EventStream,每个事件有明确的type和payload。这让UI层能以类型安全的方式订阅事件,也让学习者在读代码时能精确理解”agent输出流中可能包含哪些东西”。

工具系统(区别于方案B的原子工具自结晶):预置文档齐全的工具集,每个工具有清晰的定义和参数说明。学习者一看就知道agent能做什么、不能做什么。

多端接入(区别于方案C的20+ Channel):MVP聚焦CLI + WebSocket两种接入。但保留Channel抽象和bus层,为未来扩展留空间。架构上保持了方案C的扩展性,但实现上控制了复杂度。

记忆与可靠性:这是aptbot投入最多精力的领域——Provider故障转移、工具安全边界、记忆压缩、Hook插件机制,每项都在解决”如何让agent在真实环境中更可靠”的问题。这部分在后续文章中会逐一深入。

4.3教学优先的可读性约束

aptbot和其他方案相比,一个独特的约束是”教学优先的可读性“。

其他方案优化的是性能(低延迟)、token(低成本)、功能覆盖(多平台)。aptbot在考虑这些的同时,还有一条额外的标准:代码和架构必须能让初学者读懂

这意味着aptbot会主动放弃一些”聪明但晦涩”的实现技巧。举个具体的例子:aptbot的配置加载没有采用”运行时反射 + 配置类自动绑定”的魔法实现,而是用显式的 readConfig()validateConfig()applyConfig() 三步流程。前者更”优雅”(少写代码),后者更”朴素”(读代码的人能一步一步跟着看到配置怎么被加载和应用的)。

这不是技术上的退让——从项目定位来看,一个让人学习agent的项目,自己的代码不能是黑盒。如果aptbot自己都让人看不懂,那就失去了存在的意义。

五、发展方向

四层架构为aptbot的演进提供了清晰的路线图。每一层都可以独立进化,不影响其他层。

access层:未来的重点是IM集成(Telegram作为首通道)。这会验证Channel抽象的设计是否真的够用——当接入的不是”WebSocket客户端”而是”IM Bot”时,Channel接口是否需要修改。

bus层:规模化方向是”事件路由”——当单个实例服务多个用户、多个session时,bus需要从简单的事件广播升级为事件路由。用户A的事件只到用户A的channel。

core层:持续深化的方向是”更智能的agent行为”——更好的上下文压缩策略、更可靠的错误恢复、多轮推理的深度控制。core层的进化是aptbot长期的核心主线。

infrastructure层:扩展方向是存储后端的多样性——从JSONL到SQLite到S3,让aptbot适应不同规模的部署场景。

shared层:保持”不持有状态”的纯函数集合,持续提炼各层共用的工具函数。

小结

这篇文章从工程视角拆解了aptbot的架构设计:

  1. 四层架构(access/bus/core/infrastructure)+ shared层,每层有明确的职责边界。access管接入、bus管分发、core管推理决策、infrastructure管技术对接、shared管纯类型与工具。
  2. 严格单向依赖是”可替换性 + 可测试性”的工程纪律。反例证明:核心依赖接入层会导致替换成本高、测试变慢、逻辑污染。
  3. 三种架构路线对比:方案A(极简SDK)追求可组合性,方案B(自演化)追求自主积累,方案C(全栈工程)追求生产就绪。
  4. aptbot的选择:取各方案之长,加”教学可读性”约束。TypeScript + Zod保证类型安全,清晰分层保证可读性,面向学习的定位决定了每一项取舍。

架构是骨架,下一篇文章看血肉——Provider系统,它负责让agent的大脑(LLM)真正运转起来,处理多协议、多模型、故障转移。