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的架构设计:
- 四层架构(access/bus/core/infrastructure)+ shared层,每层有明确的职责边界。access管接入、bus管分发、core管推理决策、infrastructure管技术对接、shared管纯类型与工具。
- 严格单向依赖是”可替换性 + 可测试性”的工程纪律。反例证明:核心依赖接入层会导致替换成本高、测试变慢、逻辑污染。
- 三种架构路线对比:方案A(极简SDK)追求可组合性,方案B(自演化)追求自主积累,方案C(全栈工程)追求生产就绪。
- aptbot的选择:取各方案之长,加”教学可读性”约束。TypeScript + Zod保证类型安全,清晰分层保证可读性,面向学习的定位决定了每一项取舍。
架构是骨架,下一篇文章看血肉——Provider系统,它负责让agent的大脑(LLM)真正运转起来,处理多协议、多模型、故障转移。