1. 为什么我们要自己动手搭一个Agent框架
很多人第一次接触Agent这个概念,脑子里浮现的都是"调个大模型API,套个循环,能跑就行"。我一开始也是这么想的,直到我试着用这种思路去支撑一个稍微复杂点的场景——比如让Agent自己决定先查资料、再算数、最后写报告——才发现问题一大堆:工具调用结果怎么回传?多轮对话的上下文怎么裁剪?某个步骤失败了要不要重试?重试几次?重试的时候上下文要不要回滚?这些问题堆在一起,代码很快就变成了一坨谁都不敢碰的意大利面。
这就是我决定认真设计一套Agent框架的起点。不是因为它时髦,而是因为没有框架的Agent代码,本质上是一次性的。你写完一个Demo很爽,但想加一个新工具、换一个模型、或者让两个Agent协作,就得把原来的逻辑推倒重来。框架的价值就在于把这些反复出现的脏活累活抽象出来,让你专注于"这个Agent到底要干什么"。
这一章我要做的事情,是把一个Agent框架从零到一拆开讲清楚。不是讲某个现成开源库怎么用,而是讲如果你要自己设计一套,整体架构应该怎么摆。我会围绕几个核心问题展开:Agent框架到底由哪几层组成、每层的职责边界在哪里、层与层之间用什么数据结构通信、以及为什么很多看起来"能跑"的设计在真实场景下会崩。这套思路我把它叫做HelloAgents的架构雏形,名字不重要,重要的是背后的取舍逻辑。
适合谁看?如果你已经能写一个简单的Agent循环,但一到多工具、多轮次、多Agent就开始手忙脚乱,那这篇就是写给你的。如果你还没写过Agent,也没关系,我会把每个概念用生活化的例子讲清楚,你跟着思路走一遍,也能理解一个框架为什么长这样。
2. Agent框架的四层骨架与职责切分
2.1 从"一个函数"到"一套系统"的认知转变
最开始写Agent,大多数人写的是一个函数:输入用户问题,循环调用模型,模型返回工具调用就执行工具,把结果塞回去,直到模型不再调用工具为止。这个函数大概五十行,跑起来也没问题。但当你想要加日志、加超时、加并发、加多Agent协作的时候,你会发现所有逻辑都缠在一起,改一处动全身。
我后来想明白一件事:Agent框架的本质,是把"决策"和"执行"彻底分开。模型负责决策——下一步该调用哪个工具、该说什么话;框架负责执行——真正去调工具、管理状态、处理异常。这两件事混在一起,就是灾难的开始。
基于这个认知,我把整个框架切成四层。这四层不是拍脑袋分的,而是按照"谁依赖谁、谁不该知道谁"的原则划出来的。下面一层永远不知道上面一层的存在,这样任何一层都可以被替换、被测试、被单独调试。
2.2 四层结构:接口层、编排层、能力层、基础设施层
接口层是最外面那层,负责和外界打交道。用户输入从这进来,最终结果从这出去。它不关心Agent内部怎么运转,只负责把输入规范化成框架内部能理解的结构,再把内部结果渲染成用户能看懂的格式。这一层最容易被人忽略,但它决定了你的框架能不能同时支持命令行、HTTP接口、WebSocket推送等多种接入方式。
编排层是框架的大脑,也是整个设计里最核心的部分。它管的是"流程":一次任务从开始到结束,中间要经过哪些步骤、每一步的输入输出是什么、失败了怎么办、要不要并行、要不要人工介入。编排层不直接调用工具,也不直接调用模型,它只负责调度。你可以把它理解成一个项目经理,自己不干活,但知道谁该在什么时候干什么。
能力层是真正干活的地方,分成两块:一块是模型能力,封装各种大模型的调用;另一块是工具能力,封装搜索、计算、文件读写、代码执行这些具体动作。能力层对外暴露统一的接口,编排层不需要知道背后是哪个模型、哪个工具,只需要按接口调用就行。这就是为什么换模型、加工具不需要动编排逻辑。
基础设施层是最底下那层,管的是日志、配置、存储、监控、错误处理这些横切关注点。它被上面三层共用,但上面三层不需要知道它的具体实现。比如日志,编排层只管打日志,至于日志写到文件还是发到远端,那是基础设施层的事。
| 层级 | 核心职责 | 不该做的事 | 可替换性 |
|---|---|---|---|
| 接口层 | 输入规范化、结果渲染 | 不碰业务逻辑 | 高,可换接入方式 |
| 编排层 | 流程调度、状态管理 | 不直接调模型和工具 | 中,流程逻辑相对稳定 |
| 能力层 | 模型调用、工具执行 | 不管流程和状态 | 高,可换模型和工具 |
| 基础设施层 | 日志、配置、存储 | 不参与业务决策 | 高,可换实现 |
这张表是我在实际重构中反复验证过的。每次我想偷懒把某个逻辑塞错层,最后都会付出代价。比如我曾经把重试逻辑写在能力层,结果编排层想控制重试次数就做不到,因为能力层已经自己重试完了。后来把重试挪到编排层,能力层只负责"调一次,成功返回结果,失败抛异常",整个世界就清爽了。
2.3 层与层之间的通信契约
分层容易,难的是定义层与层之间传什么。我踩过的最大坑,就是早期让各层直接传原始字典,结果字段名满天飞,改一个字段要全局搜索替换。后来我定了一条死规矩:层与层之间只传明确定义的数据结构,不传裸字典。
具体来说,编排层和能力层之间传的是"任务单元"。一个任务单元包含:任务类型(是调模型还是调工具)、输入参数、超时时间、重试策略、上下文引用。能力层执行完,返回一个"执行结果",包含:状态(成功/失败/超时)、输出内容、消耗的资源(token数、耗时)、错误信息。
这个契约一旦定下来,能力层就可以随便换实现,只要它认这个输入、吐这个输出。我甚至做过一个实验:把能力层的模型调用从一家换成另一家,编排层一行代码没改,因为契约没变。这就是分层的红利。
提示:定义通信契约的时候,一定要把"失败"当成一等公民。很多人设计数据结构只考虑成功路径,结果一出错就到处打补丁。我的做法是,执行结果里状态字段必须有,而且失败时错误信息要足够详细,能让编排层决定是重试、跳过还是终止。
3. 编排层:Agent框架里最容易写崩的地方
3.1 编排的本质是状态机,不是while循环
大部分人写Agent编排,写的是一个while循环:只要模型还在返回工具调用,就继续循环。这个写法在单Agent、单轮任务里没问题,但一旦任务变复杂,比如"先搜索,根据搜索结果决定要不要再搜索,最后总结",while循环就表达不了了,因为循环条件不再是简单的"有没有工具调用",而是依赖中间结果。
我后来把编排层重写成状态机。每个Agent任务有一组状态:初始化、思考中、等待工具执行、工具执行完成、生成最终回答、结束、异常。状态之间的转移由明确的规则决定,而不是靠循环条件硬凑。这样做的好处是,任何时刻我都能知道任务处于哪个状态,也能在任意状态插入钩子——比如在"等待工具执行"状态加超时检查,在"生成最终回答"状态加内容审核。
状态机还有一个隐性好处:它天然支持中断和恢复。如果任务执行到一半服务重启了,只要把状态持久化,重启后从上次的状态继续就行。while循环做不到这一点,因为循环的进度藏在调用栈里,栈一没就全丢了。
3.2 上下文管理:不是把所有历史都塞给模型
上下文管理是编排层最容易被低估的部分。新手最常见的做法是把所有对话历史、所有工具返回结果一股脑塞进下一次模型调用。短对话没问题,一旦轮次多了,token爆炸,成本飙升,而且模型还会因为上下文太长而"注意力涣散",开始胡说八道。
我在HelloAgents里用的是分层上下文策略。把上下文分成三层:系统层(Agent的角色设定、可用工具说明,基本不变)、任务层(当前任务的目标、已完成的步骤摘要)、即时层(最近几轮的原始对话和工具结果)。每次调模型,系统层和任务层全带上,即时层只带最近N轮,更早的内容压缩成摘要放进任务层。
压缩摘要这件事我试过两种做法。一种是让模型自己总结,好处是语义保留好,坏处是每次总结都要额外调一次模型,有成本和延迟。另一种是用规则提取关键信息,比如只保留工具调用的名称和结果的关键字段,好处是快且便宜,坏处是可能丢信息。我最后的选择是混合:关键节点用模型总结,普通轮次用规则提取。这个取舍没有标准答案,取决于你的场景对信息完整度的要求。
注意:上下文裁剪一定要在编排层做,不要在能力层做。因为只有编排层知道当前任务的全局状态,能力层只看到单次调用,它没有足够信息判断哪些历史重要、哪些可以丢。
3.3 错误处理与重试:区分"能救"和"救不了"
Agent执行过程中出错是常态,不是异常。模型可能返回格式错误、工具可能超时、网络可能抖动。如果每次出错都直接失败,用户体验会很差;但如果无脑重试,又可能陷入死循环,烧钱又烧时间。
我的做法是把错误分成三类,分别处理。第一类是瞬时错误,比如网络超时、限流,这类错误重试大概率能成功,策略是指数退避重试,重试次数设上限。第二类是逻辑错误,比如模型返回的工具参数格式不对,这类错误重试同样的输入没用,需要把错误信息反馈给模型,让它重新生成,这叫"纠错重试"。第三类是致命错误,比如工具根本不存在、权限不足,这类错误重试多少次都没用,直接终止并上报。
区分这三类的关键,是能力层返回错误时要带上错误类型标记,而不是只返回一个错误字符串。编排层根据错误类型决定动作。这个设计我在多个项目里用过,能显著减少无效重试,也能让失败原因更清晰。
3.4 多Agent协作时的编排复杂度
单Agent的编排已经够复杂了,多Agent一上来,复杂度是指数级增长的。我见过很多多Agent设计,本质上就是让几个Agent互相调用,结果调用链一深,谁在等谁、谁该负责、出错了算谁的,全乱套。
我的经验是,多Agent协作必须有一个明确的"协调者"角色,其他Agent都是"执行者"。协调者负责拆解任务、分配任务、汇总结果,执行者只负责完成自己被分配的那一小块。执行者之间不直接通信,所有信息都经过协调者中转。这样做虽然看起来效率低一点,但可控性高得多,出问题也容易定位。
协调者和执行者之间的通信,走的是和单Agent一样的任务单元契约,只是多了一个"目标Agent"字段。这样编排层的代码可以复用,不需要为多Agent单独写一套调度逻辑。
4. 能力层:模型和工具怎么封装才不添乱
4.1 模型封装:统一接口,隔离差异
不同大模型的API差异其实挺大的:有的用messages数组,有的用prompt字符串;有的工具调用返回结构化对象,有的返回一段需要解析的文本;有的支持流式,有的只支持一次性返回。如果编排层直接对接这些差异,那换模型就是噩梦。
能力层的模型封装,核心目标就是把这些差异全部吃掉,对外暴露一个统一的调用接口。这个接口的输入是:消息列表、可用工具定义、生成参数;输出是:模型回复内容、工具调用请求(如果有)、token消耗。至于内部怎么把统一格式转成各家API的格式,那是封装内部的事。
我封装的时候特别注意一点:工具调用的解析要足够健壮。有些模型返回的工具调用参数是合法JSON,有些会带多余的文字,有些干脆返回一段自然语言描述它想调用什么。封装层要能处理这些情况,实在解析不了就返回一个明确的错误,让编排层去决定是纠错重试还是放弃。千万不要在封装层里"猜"模型的意图,猜错了比直接报错更麻烦。
4.2 工具封装:描述比实现更重要
工具封装看起来简单,不就是包一个函数吗?但我踩过的坑告诉我,工具的描述(description)比实现重要得多。因为模型是根据描述来决定要不要调用这个工具的,描述写得含糊,模型要么不调用,要么乱调用。
我写工具描述有个模板:先说这个工具是干什么的,再说什么时候该用、什么时候不该用,最后说参数的含义和格式。比如一个搜索工具,描述里要写清楚"用于查询实时信息,当问题涉及最新事件或你不确定的事实时使用;不要用于查询常识性知识"。这样模型判断起来就准得多。
工具的参数定义也要严格。能用枚举就用枚举,能加约束就加约束。我见过一个工具参数是"日期",没写格式,结果模型一会儿传"2024-01-01",一会儿传"Jan 1, 2024",工具内部还得做兼容。后来我把格式写死在描述里,并加了参数校验,问题就没了。
| 工具描述要素 | 反例 | 正例 |
|---|---|---|
| 用途说明 | "搜索工具" | "查询实时网络信息,用于获取最新事件、数据或你不确定的事实" |
| 使用时机 | 无 | "当问题涉及近期事件或需要外部数据时使用" |
| 禁用场景 | 无 | "不要用于常识问答或纯计算" |
| 参数格式 | "日期" | "日期,格式为YYYY-MM-DD,例如2024-01-01" |
4.3 工具执行的安全边界
工具执行是Agent框架里风险最高的环节,因为工具可能读写文件、执行代码、调用外部服务。如果不加限制,一个被提示注入攻击的Agent可能做出危险操作。
我在能力层加了几道防线。第一道是白名单,只有注册过的工具才能被调用,模型就算编出一个工具名也没用。第二道是参数校验,每个工具定义自己的参数约束,不满足直接拒绝执行。第三道是沙箱,对于执行代码这类高危工具,放在隔离环境里跑,限制它能访问的资源和能运行的时间。第四道是人工确认,对于删除、发送这类不可逆操作,执行前要人工确认。
这几道防线不是每个场景都要全上,但你要知道它们存在,根据场景选择。比如一个只读的查询类Agent,白名单加参数校验就够了;一个能操作生产环境的Agent,四道都得上。
4.4 能力层的可观测性设计
能力层是框架里和外部交互最多的地方,也是最需要可观测性的地方。每次模型调用、每次工具执行,都应该留下记录:调了什么、传了什么参数、花了多长时间、消耗了多少token、返回了什么、有没有出错。
这些记录不只是为了排查问题,更是为了优化。比如你发现某个工具调用特别频繁但返回结果很少被用到,那可能是工具描述有问题,模型误以为它有用。再比如你发现某类任务的token消耗特别高,那可能是上下文管理策略需要调整。
我在实现的时候,把可观测性做成了能力层的默认行为,而不是可选功能。每个能力调用自动记录,记录格式统一,这样后续做分析的时候不用为每个工具单独写解析逻辑。
5. 基础设施层:那些不显眼但决定成败的细节
5.1 配置管理:别把密钥写进代码
Agent框架要连模型、连工具、连存储,配置项一大堆。新手最容易犯的错是把API密钥、数据库连接串直接写在代码里,然后一不小心提交到代码仓库。这种事我见过不止一次,后果很严重。
正确的做法是配置和代码分离。敏感配置走环境变量或专门的配置服务,非敏感配置走配置文件。配置文件要分环境,开发、测试、生产各一套,避免开发时的配置误用到生产。配置的读取要集中在一个模块,不要散落在各处,否则改一个配置要全局搜索。
我还会给配置加校验。框架启动时检查必需配置项是否存在、格式是否正确,缺了就启动失败并给出明确提示。这比运行到一半才发现配置错了要好得多。
5.2 日志与追踪:一次任务的全链路还原
Agent执行一次任务,可能涉及多次模型调用、多次工具执行、多个Agent协作。出了问题,你需要能把整条链路还原出来,知道哪一步慢了、哪一步错了。
我的做法是给每次任务分配一个唯一的追踪ID,这个ID贯穿任务的所有环节。每条日志都带上这个ID,这样你就能通过ID把所有相关日志串起来。日志内容要包含时间戳、层级、组件名、追踪ID、日志级别、消息。格式统一,方便后续用工具解析和检索。
除了日志,关键节点还要打点,记录耗时和结果状态。这些打点数据可以用来做监控和告警,比如某个工具的平均耗时突然上升,或者某个错误类型的出现频率突然变高,都能及时发现问题。
5.3 存储抽象:别让框架绑死在某一个数据库上
Agent框架需要存的东西不少:对话历史、任务状态、工具调用记录、配置。如果直接把这些存到某个具体数据库,那换数据库就是大工程。
我的做法是在基础设施层定义存储接口,把"存什么"和"怎么存"分开。接口定义的是操作,比如"保存任务状态""读取对话历史",具体实现可以是内存、文件、关系数据库、文档数据库。框架上层只依赖接口,不依赖实现。这样开发阶段用内存实现快速迭代,生产阶段换成数据库实现保证持久化,切换成本很低。
5.4 并发与资源控制
Agent框架经常要处理并发任务,比如同时服务多个用户,或者一个任务内部并行调用多个工具。并发带来两个问题:资源竞争和成本失控。
资源竞争方面,模型调用和工具执行都可能有限流,需要做并发控制。我用的是信号量加队列的方式,限制同时进行的调用数量,超出的排队等待。这样既能充分利用资源,又不会因为并发过高被限流。
成本失控方面,要给每个任务设置资源预算,比如最多调用多少次模型、最多消耗多少token、最长运行多长时间。预算用完就终止任务,避免一个失控的任务烧掉大量资源。这个预算机制在多Agent场景下尤其重要,因为Agent之间互相调用很容易失控。
6. 从架构到代码:一个最小可运行骨架
6.1 目录结构怎么摆
架构讲完了,落到代码上,第一件事是目录结构。我的习惯是按层分目录,每层一个包,层内的模块再细分。比如接口层一个包,编排层一个包,能力层下面再分模型和工具两个子包,基础设施层一个包。这样一眼就能看出代码属于哪一层,也方便做依赖检查——比如编排层不应该import能力层的具体实现,只应该import能力层的接口。
依赖方向要严格控制。上层可以依赖下层,下层不能依赖上层。基础设施层被所有层依赖,但它不依赖任何业务层。这个规则用代码检查工具可以自动 enforce,比如在构建脚本里加一条规则,发现反向依赖就报错。
6.2 核心接口定义
框架的核心接口不多,但每个都要想清楚。编排层对外暴露的是"执行任务"接口,输入是任务描述和上下文,输出是任务结果。能力层的模型接口是"生成",输入是消息和工具定义,输出是回复和工具调用。工具接口是"执行",输入是参数,输出是结果。
这些接口定义的时候,参数和返回值都用明确的数据结构,不用裸字典。数据结构定义在一个公共模块里,各层都引用它。这样字段名统一,改的时候也只需要改一处。
6.3 一个最小Agent的完整执行流程
把上面的东西串起来,一个最小Agent执行一次任务的流程是这样的:接口层收到用户输入,规范化后交给编排层;编排层创建任务,初始化状态为"思考中";编排层从能力层拿到模型接口,传入系统提示、用户输入、可用工具定义;模型返回工具调用请求,编排层把状态转为"等待工具执行",把任务单元交给能力层的工具接口;工具执行完返回结果,编排层把结果加入上下文,状态转回"思考中",再次调用模型;模型这次返回最终回答,编排层状态转为"结束",把结果交给接口层;接口层渲染结果返回给用户。
整个流程里,编排层是唯一知道全局状态的,能力层每次只处理一个任务单元,基础设施层在背后默默记录一切。这个分工清晰,任何一环出问题都容易定位,任何一环想替换也不影响其他环。
6.4 跑通之后先别急着加功能
骨架跑通之后,很多人会迫不及待地加功能:加更多工具、加多Agent、加记忆、加规划。我的建议是先别急,先把骨架的健壮性做扎实。具体来说,做三件事:一是补全错误处理,把每类错误都走一遍,确保不会崩;二是补全日志和追踪,确保出问题能查;三是补全测试,核心流程要有自动化测试覆盖。
这三件事做完,再加功能就稳得多。因为新功能一定会引入新问题,如果骨架本身不健壮,新问题会和老问题混在一起,排查起来非常痛苦。我吃过这个亏,后来学乖了,宁可慢一点,也要先把地基打牢。
7. 几个我踩过的坑和对应的解法
7.1 把编排逻辑写进工具里
早期我为了让某个工具"聪明"一点,在工具内部加了判断逻辑:如果参数是A就做这个,是B就做那个。结果这个工具越来越复杂,最后变成了一个小型Agent,但它又不受编排层控制,出了问题很难查。
解法是把决策逻辑全部上移到编排层。工具只做一件事:给定明确参数,执行明确动作,返回明确结果。需要判断的地方,由编排层根据上下文决定调用哪个工具、传什么参数。这样工具保持简单,编排层集中管理决策,职责清晰。
7.2 上下文无限增长导致成本失控
有个项目上线后,我发现成本比预期高很多。排查后发现是上下文管理没做好,长对话的历史全部塞给模型,token消耗随轮次线性增长。一个跑了二十轮的任务,最后一轮的输入token是第一轮的十几倍。
解法是前面说的分层上下文加摘要压缩。实施之后,token消耗基本稳定在一个区间,不再随轮次无限增长。这里的关键是摘要压缩的时机和粒度要调好,太频繁浪费调用,太稀疏又起不到压缩效果。我的经验是每五到十轮压缩一次,具体看任务的平均轮次。
7.3 多Agent互相等待导致死锁
做多Agent协作的时候,我遇到过两个Agent互相等对方输出,结果谁都不动,任务卡死。原因是协调逻辑没设计好,A等B的结果才能继续,B等A的结果才能继续,形成循环等待。
解法是明确依赖方向,禁止循环依赖。协调者分配任务时,要保证任务之间有明确的先后顺序,后一个任务依赖前一个任务的输出,但不会反过来依赖。如果确实需要双向交互,那就不该拆成两个Agent,而应该是一个Agent内部的多轮对话。
7.4 工具调用结果格式不统一
不同工具返回的结果格式五花八门,有的返回JSON,有的返回纯文本,有的返回文件路径。编排层处理这些结果时,要写一堆if-else判断格式,很乱。
解法是在能力层统一工具返回格式。所有工具执行完,都返回一个标准结构:状态、内容、元数据。内容可以是任意类型,但外层结构统一。这样编排层处理结果时只需要看标准结构,不用关心具体工具返回什么格式。如果某个工具的原始返回需要转换,转换逻辑放在该工具的封装里,不污染编排层。
8. 架构演进:从能跑到好用要补哪些课
8.1 可测试性:让每个组件都能单独测
框架刚搭起来的时候,测试很痛苦,因为组件之间耦合紧,测一个要启动一堆依赖。后来我强制要求每个组件都能单独测试:模型封装可以用mock模型测,工具封装可以用假数据测,编排层可以用假的能力层测。这样单元测试跑得飞快,改代码也敢改。
可测试性的关键是依赖注入。组件不自己创建依赖,而是从外部传入。这样测试时传入mock,生产时传入真实实现。这个模式在框架里到处都用,效果很好。
8.2 可扩展性:加新工具不改核心代码
框架好不好用,一个重要的指标是加新工具要不要改核心代码。如果加一个工具要动编排层、动接口层,那这个框架的扩展性就有问题。
我的设计里,加新工具只需要做两件事:实现工具接口,注册到工具注册表。编排层和接口层完全不用动,因为它们只依赖工具接口,不依赖具体工具。模型也是同理,加新模型只需要实现模型接口并注册。这种插件式的设计,让框架能快速适应新需求。
8.3 可观测性:从日志到指标到追踪
前面提过日志和追踪,这里补充指标。指标是可观测性的第三根支柱,和日志、追踪并列。日志记录发生了什么,追踪记录一次请求的完整链路,指标记录系统的整体状态,比如每秒请求数、平均延迟、错误率。
Agent框架的指标要特别关注几个:模型调用的延迟和token消耗、工具调用的成功率和耗时、任务的完成率和平均轮次。这些指标能帮你发现性能瓶颈和异常模式。比如任务平均轮次突然上升,可能是某个工具的描述变了导致模型频繁调用;工具成功率下降,可能是外部服务出了问题。
8.4 安全加固:从输入到输出的全链路防护
Agent框架的安全问题比传统应用更复杂,因为输入不只是用户输入,还有模型输出、工具返回,这些都可能包含恶意内容。防护要覆盖全链路。
输入端要做输入校验和清洗,防止提示注入。模型输出端要做内容审核,防止生成不当内容。工具执行端要做权限控制和沙箱隔离,防止危险操作。输出端要做敏感信息过滤,防止泄露。每一环都不能省,因为攻击者只需要找到一个薄弱环节就能突破。
我在实际项目里,把安全防护做成了框架的内置能力,而不是每个Agent单独实现。这样新加的Agent自动继承安全防护,不会因为开发者疏忽而留下漏洞。
9. 写在最后的一点个人体会
搭Agent框架这件事,我最大的体会是:架构设计不是一开始就想清楚的,而是在不断踩坑中逐步清晰的。我第一版框架只有两层,后来发现不够用,拆成三层,再后来发现还是有问题,才拆成现在这四层。每一层都是被真实问题逼出来的,不是凭空设计的。
所以如果你现在正在搭自己的框架,我的建议是不要追求一步到位。先搭一个能跑的最小版本,然后在实际使用中发现问题、解决问题、调整架构。架构是长出来的,不是画出来的。你踩的每一个坑,都会变成架构里的一块砖。
另外一点体会是,框架的价值不在于功能多,而在于边界清晰。一个功能少但每层职责明确的框架,比一个功能多但到处耦合的框架好用得多。因为前者你能改,后者你不敢改。做框架的人,要克制自己加功能的冲动,先把边界划清楚。
最后分享一个小技巧:每次你想往框架里加东西的时候,先问自己一个问题——这个东西属于哪一层?如果答不上来,说明你的分层可能有问题,或者这个东西不该加。这个问题帮我避免了很多次架构腐化。