
把技能当操作系统这个想法我第一次说给别人听的时候对方以为我在玩概念。后来我陆续跑了几个Agent项目技能从三个加到三十个问题就全暴露出来了技能之间互相抢上下文、某个技能挂了整个流程跟着崩、新技能加载进来根本不知道该由谁调度。这时候我才意识到这不是单个技能写得不够好而是整套体系缺一个“运行时”。操作系统能干的事情技能体系一样得干甚至更急迫。这篇文章算是我自己项目里的一次逆向拆解记录。我会把Agent运行时的蓝图拆开讲清楚插件、技能、连接器分别是什么、各自负责什么、又是怎么配合的然后给出一套可以直接照抄的最小实现。内容不挑框架不管是Claude Code还是自建的Agent底层那套逻辑都是通用的。适合正在做Agent开发、被技能调度和上下文管理折腾得头疼的朋友。1. 为什么说技能应该被当作操作系统来设计1.1 一次让我崩溃的Agent调参经历先讲个真实的翻车现场。我最早做一个文档处理Agent技能非常朴素读文件、写文件、调用Python脚本、请求内部接口。一共四个技能串成一个工作流跑得挺顺。直到业务要求加“解析PDF”“提取表格”“压缩图片”这三个技能麻烦立刻来了。新增的三个技能单独测都没问题合在一起就跑不通而且报错的位置每次都在变今天挂在读文件明天挂在表格解析。第一个问题是上下文打架。技能A从用户输入里萃取出一个文件路径技能B跑的时候又把路径覆盖最后Agent拿着一个乱码字符串去读文件报错报得莫名其妙。第二个问题是状态不同步技能B把文件写进临时目录技能C却不知道这个目录存在自己又建了一个两个目录里的文件互相看不到。第三个问题是崩溃传导技能D里一个超时异常没被捕获整个Agent直接终止前面几步的结果全部作废。我那时候的直觉是去改每个技能的实现把异常处理写得滴水不漏。改了两天问题照旧。后来换个角度想这一堆问题操作系统早就全部解决了上下文打架不就是进程内存空间没隔离吗状态不同步不就是进程间通信没做好吗崩溃传导不就是缺少进程隔离和崩溃回收吗想通这一点我立刻把思路转向“给技能造一个运行时”。这段经历想说明一个关键区分技能本身是业务逻辑而怎么让一堆技能在有限的上下文里活着、协作、不互相搞破坏这是运行时的问题不是你多写几个try except就能解决的。很多人栽跟头就是分不清这两层。1.2 技能与操作系统的映射关系操作系统管理进程Agent运行时管理技能。这层映射关系我整理成了一张表每次设计新技能都会对着核对一遍操作系统概念Agent运行时对应物职责进程控制块PCB技能状态记录存技能当前状态、参数、生命周期进程表技能注册表知道系统里有哪些技能可用内存空间上下文窗口技能能读写的上下文数据区域调度器运行时调度器决定哪个技能先跑、跑多久设备驱动连接器把外部工具封装成统一接口系统调用库插件扩展技能可用的基础能力进程间通信IPC技能间消息传递让技能能安全地交换结果用户态/内核态沙箱/权限层限制技能能碰什么资源这张表是我整个运行时设计的核心蓝图后面所有代码都是围绕它来的。你可能会问为什么非得用操作系统这套框架因为它在真实世界里被验证了六七十年进程调度、内存管理、死锁预防这些难题都有成熟的解法。Agent这边的技能编排本质上是在重新发明同一套东西与其自己造轮子不如把这套成熟蓝图拿过来改造。这也是标题里“逆向拆解”的意思先看懂操作系统怎么做再回去看Agent缺什么。2. 蓝图拆解一个Agent运行时由哪几层组成2.1 内核层调度器、状态机、生命周期如果把Agent运行时看作一个微型操作系统内核层就是最小存活单位。我实现的内核汇总三样东西调度器、状态机、生命周期管理器。这三样也正好对应操作系统里“哪个进程先跑”“这个进程现在活得好不好”“这个进程什么时候该回收”三个核心问题。调度器负责回答一个很朴素的问题当前这一步该把控制权交给哪个技能最粗糙的写法是写死一个顺序技能A完了跑技能B。但真实场景里技能是有优先级的有些技能比如“读取配置”需要在一开始执行且不能被跳过有些技能比如“发送通知”即使失败也不影响主线。所以调度器至少要支持两种策略顺序执行和条件跳转。再进阶一点可以支持抢占式调度——用户中途打断时正在跑的长任务应该被挂起而不是被杀死。状态机管的是单个技能的生命周期。我用了五个状态pending、running、paused、completed、failed。每个技能从注册到最后销毁必然在这五个状态之间流转。状态机存在的意义是让运行时随时说得清“现在世界处于什么状态”这个能力在最难排查的时候会救你的命。很多诡异Bug就是因为你根本不知道某个技能当时是不是还在跑、是不是已经死了。生命周期管理器管的是技能的加载与卸载。加载不只是把代码读进内存还要做依赖检查、验证技能元数据、绑定连接器。卸载处理资源释放、取消超时计时器、清理上下文里的临时变量。我见过很多Agent项目完全不卸载技能上下文像内存泄漏一样越跑越大最后模型直接犯迷糊。这跟进程不回收是一个道理。2.2 进程层技能的本质是一段可被挂起的计算技能在运行时里到底是个什么东西我的答案是一段可以被打断、被恢复、被单独计时的计算单元。换句话说技能就是一种协程有自己的入口、自己的局部状态、自己的返回方式。你不能把它当成一个普通的函数随便调因为函数一旦跑起来就没法从外部干预而Agent需要的中途干预能力。给技能建模的时候我参考了进程的三要素代码、数据、上下文。技能代码是固定的数据是技能自己保存的中间结果上下文是运行时给它的一块读写区域。关键在于这些要分开。很多Agent出问题就是因为技能直接把临时变量写在全局状态里下一个技能跑进来看到的是被污染过的世界。这个现象我后面会专门展开因为它是最隐蔽的坑。“可被挂起”这一点是技术含量最高的部分。大模型执行技能通常要几秒到几十秒在这期间如果用户发来新的指令运行时不应该傻等而应该把当前技能挂起处理新指令再恢复。这在编程上意味着技能不能是一长串同步的代码而要被拆成可恢复的步骤。像Claude Code里那些工具调用之所以能中途被校准本质上就是这个机制在起作用。2.3 设备层连接器是怎么工作的连接器是我认为最被低估的一层。很多人写Agent直接让技能用requests去请求HTTP接口、用open()去读文件看起来快实际把技能和外部环境焊死在一起换一个部署环境整条链就断了。我在第一个项目里吃过这个亏本地跑得好好的一到服务器上路径全不对临时改配置改得人仰马翻。连接器的作用是给“外部世界”提供统一入口很像操作系统的设备驱动。技能要读文件不直接open而是通过“文件系统连接器”去读要调用内部服务不直接写死URL而是通过“API连接器”做参数校验和错误转换。这样技能写的不是具体的外部调用而是一组语义化的指令比如“读取文件(path/tmp/xxx)”。好处是明显的第一连接器可以单独做重试、超时、资源限制技能代码保持干净第二换环境的时候只换连接器实现技能一行不用改第三连接器能统一记录所有外部交互日志这在排查Agent行为时极其重要。真实操作系统的做法也是这样进程从不直接操作硬件一切都是经过驱动抽象后的系统调用。3. 核心机制逐个拆插件、技能、连接器分别负责什么3.1 技能可执行的“进程镜像”技能是运行时里真正干活的单元。它的元数据最少要有这几个字段字段用途示例name技能唯一名称file_readerdescription给调度器和模型看的说明读取指定文本文件内容parameters入参Schemapath: string, encoding: stringdependencies依赖的其他技能file_resolvertimeout超时上限30sside_effects对外部的影响类型read_only / write_file / network我把技能类比成“进程镜像”是有原因的。镜像里不仅包含代码还写着运行时需要的全部元信息。调度器看到name知道调谁看到description知道什么时候该调它看到dependencies知道要按什么顺序加载。如果没有这套元数据Agent运行时就跟裸机上乱跑的一堆bin文件一样互不通信、无法管理。技能树的依赖关系也是从这里来的技能可以像技能树一样逐级展开父技能依赖子技能运行时负责按依赖顺序把它们全部准备好。3.2 插件运行时的“可加载模块”插件和技能的边界我磨合了很久才搞清楚。现在我的定义是技能是面向任务的插件是面向能力的。技能解决“用户要什么”插件解决“运行时能提供什么基础能力”。这句话我建议反复读三遍它能省掉你后面一大半纠结。举个最直白的例子。“解析PDF”是技能它需要用到字符串处理、正则匹配、文件缓存这些基础能力这些基础能力就由插件提供。插件可以在多个技能之间共享就像内核里那些动态加载的模块一样一个解压插件谁都能用没必要每个技能重新造一遍。很多新手把“JSON解析”也写成一个技能其实这应该是插件把它放在技能层纯属污染。设计插件的时候注意接口要稳定。插件面向技能暴露的能力函数集合就是“系统调用集合”一旦上线就不要随意变动不然依赖这个插件的所有技能都会崩。历史上真实操作系统维护API稳定也是这个原因。插件版本的兼容管理是你早晚要面对的课题我的经验是给插件接口加版本号技能声明自己需要哪个版本运行时按最小可行版本加载。3.3 连接器外部资源的“设备驱动”连接器是运行时与外界交汇的地方。我在项目里固定维护四类连接器文件系统连接器、网络连接器、数据库连接器、命令执行连接器。每个连接器背后都有一套统一的错误码和超时策略。连接器的数量不贪多够用就好但是每一类都要保证接口稳定、日志完整。为什么不能直接在技能里调SDK我踩过一次很蠢的坑。一个技能用Python的boto3直接操作存储桶当时一切正常后来项目换了一个云存储厂商我把boto3换成新SDK结果技能里散落的几十处调用全要改。如果当时有一个“对象存储连接器”我只需要替换连接器这唯一一个文件。这就是操作系统的即插即用思想驱动层一变上层应用毫不知情照样工作。连接器还有一个容易被忽视的职责把外部错误翻译成运行时统一的错误结构。外部服务返回一个HTTP 500技能看不懂连接器把它转成“network_unavailable”加一个友好提示技能就能根据错误码做重试或者降级。这一层翻译工作做得越好上层技能的逻辑就越简单。3.4 三者关系的正确打开方式用一句话概括技能是用户能看到的任务执行单元连接器是技能和外界交互的唯一通道插件是支撑技能运转的基础能力库。三者各管一摊界面清晰。如果理不清最常出现的症状就是什么都往技能里塞最后技能、插件、连接器的代码混成一团运行时形同虚设。流程是这样的调度器选中技能 - 技能执行时调用插件提供的基础能力 - 需要访问外部资源时通过连接器发出请求 - 连接器把外部结果转成运行时统一的数据格式返回给技能 - 技能更新自身状态把结果写入上下文通知调度器进入下一轮。这个流程跟操作系统里“进程-系统调用-驱动-内核”的调用链几乎一模一样。我建议大家记住一个原则技能只关心“做什么”插件关心“能用什么”连接器关心“怎么访问”。说句实际的如果你在代码评审里看到技能直接import了一个第三方库去访问外部系统那基本就是分层错了。4. 从零搭一个最小可运行的Agent运行时4.1 第一步定义技能接口规范理论说够了直接上代码。我用Python定义一个最小技能接口足够支撑后续的调度和生命周期管理。from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Awaitable class SkillState(Enum): PENDING pending RUNNING running PAUSED paused COMPLETED completed FAILED failed dataclass class SkillContext: 技能可读写的上下文区域相当于进程的内存空间 variables: dict[str, Any] field(default_factorydict) session_id: str user_input: str dataclass class Skill: name: str description: str parameters: dict # 入参 JSON Schema dependencies: list[str] field(default_factorylist) timeout: float 30.0 state: SkillState SkillState.PENDING context: SkillContext | None None handler: Callable[[SkillContext, dict], Awaitable[Any]] | None None async def run(self, params: dict) - Any: self.state SkillState.RUNNING try: result await self.handler(self.context, params) self.state SkillState.COMPLETED return result except Exception: self.state SkillState.FAILED raise这个接口的重点在于每个技能都收到一个独立的SkillContext而不是共享全局字典。代码里我刻意让上下文归属技能实例就是为了避免前面提到的“上下文污染”。你可以在上下文里放session_id让技能知道自己在为哪个会话工作。当然这个接口是最精简版真实项目里你还要加优先级字段、重试次数、计费信息但核心骨架就是这样。4.2 第二步实现注册表与调度器接着写注册表。注册表维护一份“系统里有哪些技能”的表提供注册和按名查取能力。它对应操作系统的进程表。class SkillRegistry: def __init__(self): self._skills: dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(f技能重复注册: {skill.name}) for dep in skill.dependencies: if dep not in self._skills: raise RuntimeError(f依赖缺失: {skill.name} - {dep}) self._skills[skill.name] skill def resolve(self, name: str) - Skill: return self._skills[name]注意register里做了依赖检查技能A依赖技能B技能B必须先注册。这就是我拿到技能树那个概念后加进去的强制约束——没有依赖关系的技能树注册阶段就该报错而不是等运行时跑一半才崩。依赖检查还有一个隐藏好处它能让你在启动时就知道整个Agent到底支不支持当前计划而不是运行到中间才发现缺技能。调度器用最基础的顺序调度加上一个简单的条件指令支持。class RuntimeScheduler: def __init__(self, registry: SkillRegistry): self.registry registry async def execute_plan(self, plan: list[str], context: SkillContext): for step in plan: skill self.registry.resolve(step) await skill.run({input: context.user_input})别嫌这个调度器简单它已经具备最小内核的样子能被注册表管理、能按顺序执行、能拿到独立上下文。你在上面加优先级队列、加跳转条件、加超时控制都是开放扩展。想做条件跳转的时候可以这样扩展让计划里的每个步骤带一个“下一步技能名列表”技能返回的结果决定走哪条分支。这跟操作系统的中断处理有点像发生什么事件跳去哪个处理函数。4.3 第三步把上下文当内存来管理上下文管理是整套运行时最容易忽略又最要命的部分。我的经验是上下文必须先分配再使用。每个技能启动前调度器要为它创建自己的变量空间技能结束后写回主上下文的只有它声明的返回值其他变量一律回收。这一点和操作系统管理用户态内存是一个思路——进程创建时分配地址空间退出时整体回收避免碎片泄漏。class ContextManager: def __init__(self): self._main: dict[str, Any] {} def spawn_context(self, session_id: str, user_input: str) - SkillContext: return SkillContext( variables{}, session_idsession_id, user_inputuser_input ) def commit(self, ctx: SkillContext, exported: dict[str, Any]) - None: self._main.update(exported) def read_main(self) - dict[str, Any]: return dict(self._main)spawn_context负责给技能创建“干净的房间”commit负责把技能明确导出的结果写回主区域。有人觉得这多此一举直接传一个字典不就行了吗等你遇到技能B把技能A的中间变量当成输入给改了、整个流程跟着走偏的时候就知道这层隔离值多少钱了。上下文隔离是运行时设计里成本最低、收益最高的一个决策没有之一。提示上下文回收一定要做到技能级别。技能结束只允许导出明确标记的字段其他一律扔掉。这比事后清理垃圾数据简单得多也可靠得多。4.4 第四步挂载一个真实的技能测试最后我挂一个实际技能验证整套运行时能不能跑。就写一个“读取并统计文件行数”的技能它通过文件系统连接器访问外部资源。# 文件系统连接器 class FileConnector: def read_text(self, path: str) - str: with open(path, r, encodingutf-8) as f: return f.read() # 技能本体 async def read_file_count_lines(ctx: SkillContext, params: dict) - dict: path params[path] connector FileConnector() text connector.read_text(path) return {line_count: len(text.splitlines())} # 注册与运行 registry SkillRegistry() skill Skill( namefile_line_counter, description统计文本文件行数, parameters{path: {type: string}}, handlerread_file_count_lines, ) registry.register(skill) ctx ContextManager().spawn_context(session-001, 请统计 /tmp/demo.txt 行数) scheduler RuntimeScheduler(registry) await scheduler.execute_plan([file_line_counter], ctx)这个例子最值得看的不是技能本身而是“技能没有直接操作文件系统之外的东西它通过connector读文件”“它把结果包成dict返回没有污染其他变量”。等你把连接器换成对象存储版本、把read_text改成HTTP远端读取技能本体依然不用改。这就是即插即用的现场演示。整套代码加起来不到一百行但该有的隔离、注册、调度、连接四层都有了具体到真实项目里你再按需往上加东西就行。5. 逆向拆解实践怎么从报错里读懂运行时5.1 看到“execution terminated”先查资源上限聊完正向搭建说说逆向的功夫。我经常被朋友拉去帮忙排查Agent框架其中一个高频报错是agent execution terminated due to error。新手第一个反应是翻技能代码老手第一反应是看运行时资源。这个错本身不特殊它只是说“运行被终止了”真正的病根在别处。我的排查顺序是固定的先看是不是上下文超限再看技能超时设置最后查连接器的重试配置。这三样东西才是运行时的资源上限绝大多数terminated都跟它们有关。就像写二叉树程序时总报运行时错误你第一件事是查空指针和数组边界而不是怀疑编译器出了毛病。用操作系统那个“运行时错误”的体验类比一下系统弹窗给出的信息很有限真正的原因在事件日志里。Agent的报错信息也好不到哪去一定要学会看运行时日志日志里才有技能状态转换记录和连接器调用耗时。我的经验是至少要把“技能开始”“技能结束”“技能失败”“连接器超时”这四类日志打出来排查效率能翻一番。5.2 上下文污染的本质是内存泄漏上下文污染是我在Agent项目里见到最多的慢性病相当于操作系统里的内存泄漏。症状是Agent跑得越久越糊涂前面几轮的变量在后面的技能里反复出现最后模型被一堆垃圾输入干扰得答非所问。最烦人的是这种病不报错它只是让Agent的产出质量慢慢下降。查这种问题有个笨但有效的办法把每次技能执行前的上下文快照打出来跑完再打一次对比变化。上下文不该变的地方变了就是污染源。我做过一个最小复现一个技能在variables里存了个临时变量result没回收下一轮调度时调度器以为result是主上下文里的正式变量顺手传给了另一个技能当参数。这个错误的根源就在运行时缺少隔离和回收机制。修复手段就是第4.3节那套commit模型技能结束只允许导出明确标记的字段其他一律扔掉。运行时还要在调度循环里周期性检查上下文大小超过阈值就触发一次“GC”把无引用的历史变量清理掉。这跟操作系统的内存回收逻辑完全一致只是回收对象从内存页变成了上下文里的变量。5.3 技能启动超时从“替换缺失文件”的报错想到的有一次技能启动一直超时报错信息很像系统里那个“替换缺失的文件时出错”的弹窗看起来像文件问题。我顺藤摸瓜发现根本不是文件缺失是技能依赖的一个插件初始化加载了10秒默认超时只给了5秒。从一开始就判错了方向浪费了不少时间。这个经历给我的教训很具体技能的timeout字段不仅要用在技能主体执行上还要覆盖依赖加载、连接器建立、插件初始化这些隐形成本。就像真实操作系统调度进程进程创建包括加载镜像、初始化运行环境、建立进程控制块这些全算启动时间。很多人只给主体执行设了超时结果前置环节一慢整个启动判定失败。我后来把timeout拆成startup_timeout和execution_timeout两个字段分开配置好查也好调。6. 避坑实录Agent运行时最常见的五个问题6.1 问题速查表把我在多个项目里踩过的坑整理成一张速查表建议直接存在笔记里现象可能原因优先排查思路Agent执行到一半消失技能超时或上下文超限查timeout配置和上下文大小技能拿到的参数是垃圾值上下文被其他技能污染查看上一轮技能写回字段新技能加载后老技能报错插件接口版本变动检查插件API是否破坏兼容同一个技能多次执行结果对不上连接器内部缓存过期清理连接器缓存并重试Agent越来越慢历史上下文堆积触发上下文化清理压缩旧变量这张表的价值在于把问题归因到运行时层而不是让你去一行行读技能代码。我发现大多数Agent问题都不是“单个技能写错了”而是“运行时环境出了问题”。设备驱动出错的时候你会去改硬件吗不会你先看驱动日志。Agent这边也是一样的道理。6.2 我坚持的几条设计纪律说到最后分享几条我从实战里提炼出来的设计纪律。第一条任何技能都必须在注册时声明自己的side_effects只读、写文件、网络访问分清楚运行时才能决定要不要沙箱隔离。这条跟操作系统给进程标注权限是同一个逻辑权限都不知道隔离就是空话。第二条连接器永远单例复用不要每个技能new一个。否则文件指针、连接池、缓存状态会在技能之间交叉状况比不隔离还乱。连接器一旦设计成可共享内部状态管理就得做精细要么无状态要么加锁没有第三条路。第三条调度器要暴露一个“当前状态导出接口”需要的时候把整个运行时的快照打出来。这个功能成本很低但排障效率能翻倍。我就靠它抓过一个特别隐蔽的竞态两个技能同时往同一目录写文件最后互相覆盖快照里一眼就看出了重叠的时间窗口。另外多说一句技能树这个思路我后来也用了。给技能配置依赖关系让运行时能完整推导一套技能的加载顺序而不是靠人肉排列。技能树能预防一类特别隐蔽的问题两个技能互相依赖形成一个环。注册阶段如果只检查单向依赖是查不出来的我后来加了一个简单的环路检测检测到就直接拒绝加载坚决不给运行时留雷。6.3 把技能当操作系统的最终体会如果你问我这套类比最大的价值是什么我会说它提供了一整套现成的提问方式。遇到技能调度乱你会想问进程优先级怎么排遇到上下文不够用你会想问内存碎片怎么整理遇到技能协同出问题你会想问IPC该怎么设计。操作系统几十年的经验库一下子变成了Agent开发的参考手册。我个人实际操作中的体会是不急着把系统做得很大先按最小内核把技能接口、注册表、调度器、连接器这四样立起来让技能在规范的轨道里跑起来后面再逐步扩展插件、沙箱、优先级这些高级能力。顺序反了先造一堆插件和高级特性底层调度稀烂最后还是要推倒重来。最后再分享一个小技巧。我给每个技能都设了一条“汇报线”日志技能开始、结束、失败时运行时自动写一行结构化日志包含技能名、耗时、状态、导出字段数量。看起来简单但这套日志积累了两个月之后我排查一个莫名其妙的“Agent换了一个问题之后就犯傻”的Bug只花了十分钟——日志显示第三个技能读到的user_input还是上一次会话的内容上下文没随会话切换清理干净。很多诡异的问题不是模型不够聪明是运行时把不该留的东西留给了它。管好运行时Agent的表现上限才会被真正释放。