
你有没有遇到过这种状况Agent 接的工具从三个变成三十个技能描述越堆越长任务一到关键环节就开始乱套——要么上下文被不相关技能占满要么两个工具抢同一个参数要么一个超时调用把整个流程拖住。我最初以为这是提示词工程问题改了大半个月后突然醒悟这哪是什么文案问题这是运行时架构问题。于是我把视角彻底反转把技能当成操作系统里的进程、把上下文当成内存、把工具调用当成系统调用从一套 Agent 运行时蓝图的角度做了次逆向拆解。这篇文章不聊 prompt 技巧也不罗列框架特性我会从操作系统设计逻辑出发拆掉技能管理这层窗户纸讲清楚 Agent 运行时到底在解决什么问题以及如何把这份蓝图落到自己的代码里。1. 为什么说技能层越来越像操作系统1.1 从工具箱思维到运行时思维最早我做 Agent 时习惯把技能当成一个工具箱写十个函数在 system prompt 里把每个函数的名称和用途罗列一遍模型需要用哪个就调哪个。工具少的时候这套逻辑还行工具一多问题立刻暴露。比如两个技能都需要读取当前时间一个技能的中间结果被另一个技能意外篡改或者某些技能执行到一半永远不返回导致整个循环卡死。这些事情靠“写清楚 prompt”根本解决不了因为它们本质上是资源管理和调度问题。很多人区分不开 skill 和 agent其实可以这么看技能是一个能力单元而 Agent 是承载技能、记忆、规划与外部交互的完整实体。当这个实体里只有一个技能时你不需要什么运行时当技能数量膨胀到几十个还涉及多轮协作、异步事件、权限校验时你就必须有一个中间层来管理它们。操作系统不是“软件工具的集合”而是一层管理系统资源、给上层应用提供稳定执行环境的运行时。Agent 的技能层也一样LLM 是计算核心技能是外设而中间那层决定“技能什么时候能被调用、调用时能访问什么、失败后怎么恢复”的胶水才是真正的运行时。从这个角度看把技能当操作系统不是文学比喻而是一种工程归位。1.2 技能管理要处理的三对核心矛盾技能层要处理的基本矛盾可以归纳成三对。第一能力丰富与上下文有限之间的矛盾技能越多LLM 能感知的上下文越拥挤但可执行任务也越复杂。第二自由调用与安全控制之间的矛盾我们希望模型充分发挥工具能力但又不能让它做出破坏性操作。第三多技能协作与运行隔离之间的矛盾技能需要共享数据但互相之间又不能随便踩脏数据。操作系统面对这三对矛盾时给出了现成的答案用进程隔离解决资源冲突用权限分级解决安全风险用调度算法解决资源分配。Agent 运行时完全可以复制这套逻辑——只是把“进程控制块”换成“技能控制块”把“虚拟内存”换成“上下文窗口”把“系统调用”换成“工具调用 API”。所以我说逆向拆解 Agent 运行时蓝图最有用的参照物不是某个框架的源码而是你大学里学过的操作系统书。很多 Agent 教程讲“工具调用、记忆、规划”讲得天花乱坠实际底层要解决的问题操作系统几十年前就研究完了。2. 技能即进程生命周期调度是运行时的心脏2.1 一张技能状态机把执行过程管住假设你已经在代码里实现了 30 个技能。现在任务来了模型决定调用“查询库存”技能这个技能需要读取数据库然后生成报告。如果运行时没有任何生命周期管理技能一旦被选中就只能“裸跑”遇事只能举手等模型问。这跟操作系统里没有进程调度程序直接跑在裸机上是一个效果——一块 CPU 只能跑一个程序互相踩踏。所以第一步给每个技能定义状态。我一般会至少定义五个dormant、ready、running、waiting、terminated。对应关系如下表技能状态操作系统类比触发时机dormant进程未创建技能已注册但未激活ready就绪态调度器选中候选技能等待执行资源running运行态技能正在执行占用上下文/API 资源waiting阻塞态技能等待外部 API、LLM 返回或用户确认terminated终止态执行完成、被杀死或超时退出状态转换由运行时统一驱动而不是让技能自己随意改状态。比如技能启动时运行时把状态从 ready 切到 running同时开始计时技能调用外部接口等待网络响应时切到 waiting收到响应后回到 running超时则直接进入 terminated顺手回收这个技能所占用的上下文窗口空间。没有这套状态机你很难回答“现在到底哪个技能在跑”这个问题。2.2 技能注册表像可执行文件一样被加载进程在执行前必须由 loader 把可执行文件加载到内存。Agent 技能也一样不能光“写一个函数”就完事它必须是可发现、可校验、可版本化的注册项。我建议把每个技能的元信息做成一式一份的“技能清单”至少包含名称、唯一 ID、描述、参数 JSON Schema、权限声明、超时时间、预估资源消耗。注册表加载时做 schema 校验非法技能直接拒绝加载而不是等运行时爆错。这样设计最重要的好处是模型选取技能时不是“凭记忆猜”而是由运行时从注册表里把候选列表渲染给 LLM技能执行前运行时还会根据注册表中的描述和权限再检查一次。等于把“函数指针数组”升级成了“带元数据的可执行文件”这恰恰是操作系统从“裸机程序”进化的关键一步。我见过有些团队把技能列表直接硬编码在 prompt 里每次加技能都要改 system prompt还要重新测试一遍提示词稳定性。用注册表之后新技能只需要往目录里丢一个 JSON 文件主程序一行不用动。2.3 抢占与恢复不能只会跑完很多 Agent 框架里的技能执行是“一次跑到底”中途不给外部打断的机会。但在真实场景里用户很可能在 Agent 执行长任务时插一句“等一下先做另一件事”。如果运行时没有抢占机制Agent 只能等当前技能结束体验非常差。操作系统的抢占式调度给了一个模板所有技能都设置成可挂起的单位。LLM 的执行过程恰好是天然可抢占的——只要一个技能在某个工具调用处切了 await运行时就能把控制权收回来把正在等待的事件挂起让另一个高优先级技能进入 running。你只需要在注册表里加一个“优先级”字段在调度器里维护就绪队列再给每个技能实现 suspend 和 resume 钩子即可。我自己的经验是不要试图去中断一个已经被外部 API 占用的请求而是等它的下一次 LLM 请求或工具请求时再挂起这样不用处理脏请求。3. 上下文即内存分层缓存与回收策略3.1 把 token 预算当成物理内存上下文窗口是 Agent 运行时最珍贵的资源比内存还要稀缺。很多开发者没意识到上下文窗口的容量就是 Agent 的“物理内存”。技能执行过程中产生的输入、输出、中间变量全都占着这个有上限的空间。操作系统内存不够时会用虚拟内存换页Agent 上下文不够时也得想办法换出、压缩、淘汰。我自己的上下文分配模型是三层核心指令段、工作记忆段、长期档案段。核心指令段保存 system prompt、技能注册表摘要、全局安全规则常驻上下文内容精炼且高价值工作记忆段是当前任务相关的输入输出、用户最近的交互、活动技能的中间数据会被不断淘汰和重写长期档案段则放用户偏好、历史任务摘要、知识库片段通常只在需要时被检索进上下文用完后降级为摘要。这个分层模型能让你对 token 消耗有清晰的预期而不是等整段上下文爆掉之后才开始手忙脚乱。3.2 上下文溢出时的回收机制上下文一旦超过模型限制最常见的做法是从 system prompt 开始截断这当然是错的做法——相当于机器还有足够内存时先杀了进程。更好的做法是模仿操作系统的虚拟内存把工作记忆里的低优先级条目“换出”到外部记忆层再为新的关键信息腾出空间。换出之前先让 LLM 生成一段摘要把这条信息的语义压缩成几行文字仍占用极少 token。淘汰策略可以结合 LRU 和重要性打分。我会为每个上下文条目维护两个属性最近访问时间和显式优先级。比如当前任务直接依赖的数据优先级拉满上一步技能输出的长 JSON如果已经提取成摘要直接标记为可淘汰。可以在代码里维护一个 ContextEntry 列表每次加入新条目时检查总 token 数超出上限就按“低重要度且最早未访问”的规则回收。这套机制对应操作系统里的页面置换算法——核心逻辑都是“在有限容量下保证高价值数据留在工作集”。3.3 共享内存别让技能只靠 prompt 传数据技能之间的数据传递如果全靠“模型读上下文再调用下一个技能”会有两个隐患一是大量中间结果反复进入上下文浪费 token二是技能间的数据耦合写在 prompt 里完全没法测试和调试。更好的方案是运行时维护一个“黑板/共享内存”区域技能通过键值接口读写结构化数据。比如技能 A 把分析结果写到 analysis.result技能 B 直接从该键读取而不必被塞进可能被截断的文本上下文。这个共享内存同样需要生命周期管理。技能启动时申请一块“内存段”技能结束时回收某些高优先级数据可以标记为“长期共享”跨任务保留。我在实际项目中犯过一个错误把临时性中间结果挂成长期共享键结果后续任务读到了过期数据。后来给每个键增加了 TTL 和写入者校验才把这类问题控制住。这就像操作系统里如果不好好管理共享内存一个进程改坏另一个进程的数据迟早会出事故。4. 工具调用即系统调用安全边界与权限治理4.1 从“函数调用”到“系统调用”的认知翻转普通编程里的函数调用是完全信任的你调用它它就可以访问整个进程地址空间。但 Agent 的工具调用完全不一样——调用的对象是一个外部服务、一个网络请求、一个文件读写副作用不可控且 LLM 本身并不可信可能被注入恶意提示词。如果直接把工具调用当成函数调用等于让一个不受信任的进程随意访问系统内核。所以 Agent 运行时必须像操作系统一样区分用户态与内核态LLM 的规划输出只是“用户态请求”真正的工具执行发生在“内核态”的沙箱内。所有外部调用都要通过运行时提供的系统调用接口由运行时进行参数校验、权限检查、资源配额限制后再转发到真实服务。这个设计能挡掉很多问题即使模型被骗着请求删除文件运行时权限模块也会因为技能没有该权限而拒绝。4.2 能力最小化每个技能一个权限指纹你不会给每个 shell 用户 root 权限也不应该给每个技能全部 API 密钥。给技能注册表加一个 permissions 字段例如 read_only、allow_network: get、allow_fs: /tmp/agent-workspace运行时在执行前检查这个字段。再往前一步做“能力最小化”一个技能如果只是查询天气就不给它写数据库的权限一个技能如果只需要读取用户上传的 PDF就不给它外网访问权。这里有个细节容易被忽略权限检查必须在技能内部实现之外做而不能信任技能本身。类似于操作系统不会把权限判断交给设备驱动自己。我见过不少 Agent 框架直接把 API client 递给技能让技能自己调这就是赤裸裸的“内核态漏洞”。正确做法是建立一个统一的 Gateway所有工具流量都经过它Gateway 做 schema 校验、费用限制、速率限制然后才访问外部服务。这样权限模型才真正可控。4.3 审计日志Agent 版 dmesg没有日志的系统调用是不存在的。以前我排查 Agent 问题时最痛苦的就是“模型说调了工具但不知道到底调没调、结果如何”。后来我把工具调用日志完整化每次调用记录发起技能、目标服务、请求参数摘要、返回码、耗时、异常堆栈、token 消耗。这一步价值极大。比如线上 Agent 从正常工作中突然开始疯狂调用某个 API我查日志很快发现是某个技能在循环中掉了进去模型把同一个搜索结果反复作为输入。这有点类似操作系统出问题时看 dmesg——每一行系统调用的 trace 都能帮你定位是哪一个进程、哪一段代码出了事。如果是在合规场景审计日志还是必备的追溯依据不光是调试工具更是安全边界的一部分。5. 中断与异步事件让运行时从轮询进化为响应式5.1 ReAct 循环的固有局限很多 Agent 的核心循环是经典的 ReAct推理、行动、观察反复迭代直到完成。这个循环结构简洁却很像是操作系统里最原始的忙轮询。问题在于当 Agent 在等待一个外部事件比如用户新消息、推送通知、另一个 Agent 的回传结果时ReAct 只能不断空转不断向 LLM 发起请求白白消耗 token 和 API 额度。如果一个 Agent 运行时只支持这种轮询式执行它就很难支撑多 Agent 协作和自动化工作流。因为现实世界是异步的不是所有事情都能在一个线性 prompt 循环里解决。我们要做的是把“单线程的 for 循环”改造成“事件驱动的事件循环”收到事件就唤醒就绪队列剩余时间可以保持轻量等待。这不是炫技而是 Agent 要成为一个长期运行的、能响应外部信号的实体就必须有这种机制。5.2 构建运行时事件队列我建议在运行时实现一个简单的事件队列支持三类事件用户输入事件、服务回调事件、定时器事件。调度器在每一轮中不是直接调用模型循环而是先检查事件队列如果有高优先级事件就打断当前任务将当前技能挂起并压入上下文栈然后处理新事件。事件处理完后恢复之前的技能这就像操作系统从 I/O 中断返回值继续执行用户程序。实际操作上在 Python 里可以用 asyncio.Queue 实现事件类型、优先级、回调函数、关联技能 ID 一起入队。处理事件时注意不要引入太多层级不然状态恢复会变得非常复杂。我的经验是一开始先只支持“用户输入事件”和“定时器事件”回调事件等基础设施稳定后再做否则会把事件循环变成意大利面条。先把骨架跑通再逐步加事件类型是高性价比的推进方式。5.3 协作死锁多技能并发时的路障两个技能并发执行且要访问共享数据时可能出现类似操作系统里的死锁技能 A 持有黑板的 order.lock等待技能 B 写入 order.payment技能 B 持有 payment.lock等待技能 A 释放 order.lock。没有超时控制的话两个技能互相等成饿死Agent 永远卡住。解决办法也照搬操作系统引入锁超时、强制回滚、统一事件循环中不要嵌套等待。我自己的代码里会给每个技能调用加 acquire_timeout超过设定秒数就报错并释放已有锁。更稳妥的是设计中避免两个技能同时写同一个共享键把写操作收敛进一个“管理者技能”这样本质上就消除了循环等待。很多 Agent 死锁问题不是模型理解力不够而是运行时没有把并发控制做好。6. 从蓝图到落地实现一个最小 Agent 运行时原型6.1 目录结构与组件拆分纸上谈兵没意思下面给出一个最小可落地的运行时骨架。我用 Python 写伪代码因为这个生态的异步支持最方便。agent_runtime/ ├── registry/ # 技能注册表 │ ├── skills.json # 技能元数据 │ └── loader.py # 校验、加载、热插拔 ├── scheduler/ # 调度器 │ ├── task.py # 技能控制块SCB │ └── loop.py # 事件循环 优先级调度 ├── memory/ # 上下文内存管理 │ ├── context.py # 上下文窗口分配与回收 │ └── blackboard.py # 共享内存黑板 ├── security/ # 安全边界 │ ├── gateway.py # 工具调用网关 │ └── audit.py # 审计日志 └── main.py # 启动入口目录结构本身就是一个缩小版操作系统内核镜像。你不用真的每个组件都写得很复杂先跑通最小闭环加载技能、调度执行、权限拦截、日志输出。只要这个闭环能跑后面往里面加记忆管理、异步事件都会顺手很多。6.2 技能描述协议与调度伪代码技能元数据我用 JSON 维护示例{ name: search_product, description: 根据关键词搜索商品目录返回匹配结果列表, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词} }, required: [query] }, permissions: [read:catalog], timeout_ms: 30000, priority: 10 }调度器的核心伪代码while True: event await event_queue.get() if event.priority current_task.priority: current_task.suspend() task create_task(event.skill_id, event.payload) task.run() current_task.resume()这个简版调度器已经能够实现抢占式执行。真正生产环境你还要加上 token 预算统计、错误重试、限流。原型阶段可以先不考虑这些先把“能抢占、能恢复、能记录”这三个点跑通远比一上来就搞复杂架构有用。6.3 实测中踩过的坑简单提几个我实际踩过的坑。第一个是把技能描述写得太长注册表里的描述超过 200 字模型经常选错技能后来我强制每个描述不超过 50 字把详细内容放进共享内存需要时再检索。第二个是技能运行时没有超时控制一个请求外部服务卡了 10 分钟把整个任务拖死后来所有技能调用都必须经过 Gateway由 Gateway 统一设 timeout。第三个是权限检查晚于实际调用等于先斩后奏正确的顺序一定是先校验、再执行、再审计。还有一个很容易忽略的点技能注册表的加载顺序会影响调度公平性。如果你在启动时按字母顺序加载有些技能会常驻在就绪队列前端优先级反而不明显。我建议调度器按优先级排序而不是按注册顺序排序否则高优先级技能会被低优先级技能堵住。如果你正在为 Agent 技能一多就乱套而头疼不妨像我一样先把“优化 prompt”这件事放下从运行时视角重新审一遍代码技能有没有生命周期上下文有没有回收机制工具调用有没有统一网关事件到达时能不能打断当前任务我自己的体验是这套操作系统隐喻不仅让架构清晰了很多连排查问题的思路都变了——从前是“这个 prompt 咋写才能让模型听话”现在是“这个调度器要怎么设计才能扛住乱序和超时”。后面我打算继续做资源计量和跨进程技能通信等有结果了再回来分享。