标签归档:AIAgent架构

DeepSeek Harness 的 Cordis 插件架构

DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插件。

系统没有一块承载业务能力的固定内核,Cordis 只维护上下文、服务注册、事件分发、依赖激活和副作用回收;Agent 能做什么,由启动时挂入的一棵插件树决定。

传统的插件有注册容易,撤销困难;初始化容易,失败回滚困难;全局单例容易,局部覆盖困难等问题,而 Cordis 把这些困难收进了运行时语义,Harness 再用 session、agent 和 preset 的业务约束补齐它。

今天聊了一下 DeepSeek Harness 的核心 Cordis 架构以及 Cordis 所谓「时空可组合」,究竟怎样落到代码里,又为何适合 Agent Harness 这类持续变化的运行时。

插件的历史

插件架构的历史并不短。Eclipse 在二十多年前已经把 IDE 拆成插件、扩展点和扩展,宿主声明可扩展位置,其他插件通过清单贡献菜单、编辑器和处理逻辑。

OSGi 更进一步,给 bundle 配置安装、启动、停止、更新和卸载生命周期,再通过共享服务注册表完成发布、查找和绑定。服务离开注册表时,依赖方必须跟着处理动态变化。

今天看 Cordis,能找到这些设计的清晰痕迹:动态服务、生命周期、注册表、声明式依赖、事件通知都已有成熟先例。

Cordis 没有发明插件系统。

它简化并重新组合了这些概念,让它们适合 TypeScript 应用内的细粒度组装。OSGi 的部署单元是 bundle,模块、生命周期、服务和安全各有一层;Cordis 的执行单元是 Fiber,一个函数或一个 Service 子类就能成为插件。Eclipse 的扩展点通常由宿主定义结构化清单,Cordis 让服务和类型化事件直接成为扩展面。Agent 运行时里的工具、提示词片段、模型适配器和审批策略变化频繁,如果每次扩展都要引入重量级模块边界,团队很快会绕过框架,重新写回几个全局数组。

Cordis 官方仓库把自己定位为「Meta-Framework of Spatiotemporal Composability」。

从源码看,「元框架」表示它不规定 Agent、Web 服务或机器人该有什么组件,只提供构造框架所需的基础语义。DeepSeek Harness 在其上定义 sessionstoolsllmagents 等服务,又用这些服务组装产品。Cordis 与 Harness 的关系更接近运行时机制和领域框架,而非通用插件平台与插件集合。

DeepSeek Harness 还把 Cordis 源码直接放进 vendor/,固定在明确的上游提交,并将包名重映射到 @deepseek-ai 命名空间。

项目维护的本地修改包含 Fiber 重入卸载加固、配置更新事务、HMR 精确监听、延迟配置解析等。这增加了维护成本,也换来了框架层的可审计性。Agent 能执行 Shell、修改文件、访问网络,插件生命周期出错会留下进程、监听器、终端模式或权限状态。

此时依赖一个本地的代码白盒,会让人放心一些。

调用链路图

简单的调用图如下所示:

最小内核

Cordis 的根对象是 Context。它同时承担依赖容器、插件挂载入口和事件入口。

服务通过稳定名称出现在 ctx 上,例如 ctx.toolsctx.llmctx.sessions。插件声明 inject 后,Cordis 只有在所需服务可用时才激活它;服务消失,依赖插件也会进入卸载或等待状态。配置文件中条目的先后顺序因此不承担启动顺序,依赖关系才承担。

这种处理解决了传统插件系统的第一个顽疾:隐含初始化顺序。

常见实现会遍历插件数组,依次调用 init。当工具依赖文件系统、文件系统依赖沙箱、UI 又依赖会话时,数组顺序就变成一套没有类型、没有诊断的依赖图。后来插入一个插件,顺序约束可能跨越几十个文件。

Cordis 把需求写进 inject,Fiber 会为每项依赖保存当前实现;缺少依赖时保持 PENDING,依赖齐备后进入 LOADINGACTIVE。启动失败则进入 FAILED。状态机至少让故障有了准确位置。

Context 还是一个代理对象。

插件直接读取 ctx.tools 时,反射层会检查它是否声明过依赖,并沿 Fiber 父链解析实现。未声明就读取会抛错;声明了但当前上下文不可用,也会抛出另一类错误。这种约束防止依赖藏在任意函数深处。源码里依然提供 ctx.get(name) 读取可选服务,区别在于调用方显式接受服务可能不存在。

这里的「服务定义、服务提供方、消费方」三种角色被完整建模。比如文件系统能力不能只写一个接口,也不能只挂一个本地实现。定义方稳定调用协议,提供方接入本地目录或远程沙箱,消费方把能力变成模型可见工具。Harness 把这组关系称为 capability seam。替换沙箱时,Shell、PTY、LSP 只要依赖同一能力面,就能整体迁移到新的执行环境,消费方无需知道提供方运行在本机还是远端。

「一切皆插件」有一个前提是:一切产品能力皆由插件贡献,Cordis 自身仍保留插件得以存在的机制。上下文代理、Fiber 状态机、服务存储、事件总线和 Loader 属于元层。

这并不是系统里没有内核,更准确的说,应该是,内核不拥有模型、工具和循环等产品特权。

空间组合

同一个进程里,服务名称必须稳定,实例又不能全局唯一。两个 Agent 可能选择不同模型、不同工具集、不同 persona 和不同沙箱。如果 ctx.llm 永远指向一个全局对象,插件替换只发生在进程级,无法满足多会话并存。

Cordis 的 Context.isolate 为指定服务名创建一个 realm 标签。服务注册和查找都以该标签定位,同名服务可以在不同子上下文里各自存在。两个 isolate 调用传入同一标签时会加入同一 realm;使用新标签时彼此隔离。子上下文通过原型继承父上下文,隔离映射按层遮蔽,因此局部配置不需要复制整棵容器。

这解释了「空间可组合」的第一层:组合具有位置。插件挂在哪个上下文,决定它提供的服务、注册的监听器和持有的副作用在哪个范围可见。传统依赖注入容器也有 singleton、request、session 等 scope,Cordis 的差异在于作用域与插件树、事件过滤、资源所有权使用同一个 Context 表达。开发者无需在四套 API 之间同步身份。

DeepSeek Harness 又增加了 dsh-scope。它用不透明对象作为 scope key,维护父子关系,并创建带路由身份的事件 receiver。注册视图沿父链向下继承:Agent 能看到所属 preset 的提示词和工具。事件则沿链向上接纳:preset 级监听器能收到其下 Agent 的事件,兄弟 Agent 互不串线。这是第二层空间结构,解决的是领域身份,而非单个服务名的 realm。

为何需要两层?isolate 处理「哪个 tools 服务实例」,dsh-scope 处理「同一工具注册表里,哪些注册项对当前 Agent 可见」。前者适合替换提供方,后者适合对注册内容分层。把所有差异都做成独立服务实例,会放大内存和初始化成本;把所有差异都塞进一个全局注册表,又会让过滤规则散落在调用点。这两层模型把实例隔离和内容路由分开了。

Agent preset 是空间组合的完整应用。一个 preset 的 Cordis 配置会挂在一个长期存在的 scope 下,Agent 创建时把自己的 scope key 绑定到该 preset。相同 preset 的并发首次使用通过 single-flight 共享一次挂载,后续 Agent 复用这份组合。文件变化后,新会话加入新一代组合,已有会话保留原来的代际。这个策略避免会话运行中途突然更换工具或提示词,代价是旧代际要保留到整棵运行时退出,配置频繁变化时内存会按代际增长。

源码还专门审计 preset 子树是否把服务发布到了 root realm。发生这种泄漏时,第二个会话挂载同一 preset 会与第一个冲突,所谓会话级组合也会退化成进程全局状态。DeepSeek Harness 在发布 Agent 前拒绝这种配置。空间隔离若只靠约定,迟早会被一个没有 isolate 的 provider 穿透;运行时审计比文档警告可靠。

这套空间模型存在认知成本。插件作者要同时理解 Context 父链、服务 realm、业务 scope 父链和事件过滤方向(当然,在当前 Vibe Coding 盛行的时代,也可以作者不理解,直接让 AI 来搞)。

在认知不清楚的时候,常见的错误往往表现为某项能力「看不见」或意外泄漏,类型系统无法证明运行时挂载位置。DeepSeek Harness 用 preset 挂载审计、包级 invariant 和真实组合测试降低风险,却没有消除模型复杂度。团队若只需要单进程单 Agent,直接引入整套空间语义会显得过重。

时间组合

插件能挂载只是静态组合。运行期间服务上线、配置改变、插件失败或上下文销毁,系统还要回到一致状态。Cordis 用 Fiber 和 effect 管理这条时间轴。

每次插件应用都会产生一个 Fiber。Fiber 记录父上下文、原始配置、解析后配置、依赖实现快照、生命周期状态和 disposables。插件调用 ctx.effect() 注册副作用,effect 的执行结果返回 disposer。事件监听、服务提供、子插件和访问器最终都进入这套所有权体系。Fiber 卸载时按注册的逆序执行清理,并等待异步清理达到静止状态。

逆序本身不是一个特别要讲的事情,因为这是必须的。

若插件先启动子进程,再注册输出监听,最后暴露服务,销毁时应先撤销服务,停止新请求,再移除监听,最后结束进程。资源创建顺序的反向通常就是依赖安全的拆卸顺序。

传统插件常给出一个 deactivate() 钩子,把所有清理责任推给作者;漏掉一个定时器或事件监听,热加载几次便出现重复执行。Cordis 让每次注册同时产生撤销动作,框架持有所有权。

DeepSeek Harness 的 vendor 版本进一步处理了重入场景:effect 在执行 setup 前先登记所有者包装;插件发布事件时,观察者可能同步卸载它;异步 cleanup 已经开始后,其他调用者仍能等待同一次清理;Fiber 处于 UNLOADING 时拒绝创建新 effect。这些代码看起来全是繁琐的边界处理,但为了保证生命周期并发的安全,一个都不能少。否则插件的热卸载就只是个玩具,没法真正落地。

依赖变化也属于时间组合。某个服务被提供后,反射层通知所有声明该依赖的 Fiber 重新检查;条件满足便激活。服务撤销后,依赖方会卸载并回到等待。OSGi 早已采用动态服务注册表,Cordis 的新意主要在于把动态依赖、作用域上下文和 effect 所有权压进一个很小的进程内模型。代价同样继承自 OSGi:任何持有服务引用越过生命周期的代码,都可能在提供方撤销后继续调用过期对象。Cordis 保存加载时的实现快照,却无法替业务代码管理逃逸引用。

Loader 把时间组合延伸到配置。DeepSeek Harness 的 profile 由多层 bundle patch 叠加:基础 bundle、模式 bundle、profile patch、home patch、命令行 overlay。patch 按 id 定位条目,配置采用整块替换。整块替换要求用户重述保留字段,使用上稍显笨重,却避免深度合并规则在数组、表达式和删除语义上制造歧义。

配置热更新时,Loader 先导入候选插件,再卸载旧实例并应用候选;候选失败会恢复旧插件或旧配置。Group 对一批子条目并发启动,收集全部结果,只要一项失败就删除新增项并重建旧配置。Include 读取候选文件、在副本上应用 patch、完成树协调后才提交缓存。用户 patch 语法错误或新插件启动失败时,最后一棵可用树继续运行。

这已经接近配置事务,却不能等同于数据库事务。

插件 effect 可能调用外部 API、创建远端资源、发送消息;disposer 只能做补偿,无法保证外部世界回滚。Loader 能保证自己管理的树和服务注册恢复,无法撤回所有不可逆副作用。插件开发规范仍要限制初始化期行为,把外部写入延迟到真正的业务请求,或者设计幂等键和补偿路径。

时间组合还带来可观的测试面积。挂载成功只是第一条路径,还要覆盖依赖晚到、依赖撤销、初始化失败、卸载重入、异步清理、热更新失败、回滚再次失败。DeepSeek Harness 要求注册项证明 disposal,产品可见插件还要通过真实 Loader 组合测试。这个成本无法靠框架消失,只能被框架集中暴露。相比线上出现幽灵监听器和半更新状态,还是愿意支付这部分测试费用。

事件契约

插件之间只靠服务调用,会把所有扩展都变成接口方法。宿主每增加一项策略,就要修改服务定义。Cordis 同时提供类型化事件,并区分 emitparallelserialbailwaterfall

DeepSeek Harness 最依赖的是 waterfall。它的监听器拿到 next,调用后把控制权交给下一层,不调用就截断链条。模型请求、工具执行和轮次控制都能被插件包裹。审批策略可以在工具执行前拒绝,重试插件可以包围模型流,日志插件可以观察前后状态。它与 Koa 一类中间件链相似,但事件名和 Context 过滤让同一个分发器覆盖多个能力域。

waterfall 也最容易出错。一个只想记录日志的监听器忘记调用 next(),整条能力链便被短路。DeepSeek Harness 把「必须调用 next()」写进项目级规则,并在事件文档里记录 dispatch mode。类型能保证参数,却很难保证 continuation 一定执行。代码评审和组合测试仍是主要防线。

事件的持久性也被刻意分层。agent/*tools/* 事件用于活跃运行时的拦截;会话事件追加到日志,承担恢复、fork、UI 回放和模型历史投影。DeepSeek Harness 规定「模型可见即已记录」:进入模型请求的输入必须能从 session log 重建。插件若偷偷修改 prompt 却不产生会话事实,重放结果会漂移,问题也无法审计。

这条约束说明,一切皆插件并不意味着一切皆事件。直接能力调用放进 Service,策略和拦截放进实时事件,需要持久化的事实进入 session log。三类通信各自承担调用、扩展和历史。将它们混成一个全局 event bus,短期代码更少,长期会失去时序语义和数据权威。

Agent 适配

Agent Harness 比普通 Web 服务更需要动态组合。模型供应商会变,工具权限随工作区变化,子 Agent 的执行环境可能与父 Agent 不同,UI 还要消费同一份流式过程。如果主循环直接 import 每个能力,任何替换都会改循环;循环逐渐成为依赖最多、风险最高的文件。

DeepSeek Harness 的默认 agent loop 仍然存在,但它也是一个服务提供方。循环读取 session log 生成历史,组装插件贡献的 prompt 和 tool schema,通过 agent/request 进入模型适配器,再经过工具流水线记录结果。扩展点围绕步骤、请求、工具和停止阶段布置。新功能通常挂到这些事件或注册表,项目规则甚至要求修改 agent loop 时同步更新架构文档。

我赞成这种限制。循环承担控制流,频繁承载产品策略后会迅速腐化。压缩上下文、重试、审批、工具超时、计划模式、子 Agent 调度都可以有自己的生命周期与配置。它们进入循环的接口必须少且稳定。插件化不会自动获得解耦,真正起作用的是 DeepSeek Harness 为能力选择了明确的 seam,并拒绝消费方特有逻辑污染定义层。

UI 作为插件也有实际意义。Web 和 headless profile 在共享 base bundle 上增加不同条目,后者可以完全不启动服务器。UI 驱动 ctx.agents,订阅 session/event 渲染状态,没必要成为循环的一部分。服务端、CLI、ACP 和浏览器界面可以共享会话及 Agent 语义,同时保留自己的传输和展示逻辑。

插件树还提供自省基础。Fiber 保留名称、状态、依赖和 effect 元数据,DeepSeek Harness 能实现查看、挂载、卸载自身插件的能力。对 Agent 而言,这比普通应用更敏感:模型可以通过工具改变运行时,错误配置可能直接扩大权限。源码中的 sandbox 和 approval 仍是独立能力,插件架构没有天然安全性。自修改必须受工具权限、配置校验和作用域审计约束。

架构代价

第一项代价是启动与运行时开销。每个插件产生 Fiber,服务访问经过 Proxy 和反射解析,事件分发要执行作用域过滤,注册项还要保存 disposer 与诊断元数据。对于 LLM Agent,单次模型请求通常以百毫秒到秒计,这些 JavaScript 调度开销很难成为主要瓶颈;高频 token chunk、文件扫描或终端字节流若全部穿过通用事件总线,成本会被放大。DeepSeek Harness 把流式模型输出定义为能力语义,但大量数据处理仍应留在具体 provider 内,事件只承载必要扩展点。

第二项代价是故障面扩大。插件初始化可以同步抛错、异步拒绝、等待缺失服务,也可能在 disposer 中失败。Group 并发启动缩短时间,却会带来多个兄弟同时失败的 AggregateError。DeepSeek Harness 的 boot 会等待 Loader 结算,审计所有启用条目是否加载与激活,失败时先 dispose 部分构造的上下文,再退出。少做任何一步,都可能让终端停留在 raw mode,或者让后台 watcher 继续运行。

第三项代价是配置成为编程接口。cordis.yml 支持 !!js 表达式,条目可以按环境禁用,配置会在依赖激活后求值。灵活性很高,静态分析能力随之下降。表达式读取服务时,求值时机与上下文位置都会影响结果。DeepSeek Harness 只允许 configdisabled 使用插值,其他元数据保持字面量,并让错误尽早暴露。这仍要求运维人员理解插件依赖和 patch 覆盖语义,配置文件已经超出普通 YAML 参数表的复杂度。

第四项代价是生态兼容。Cordis 的服务名和事件类型提供了源码级协议,插件版本升级仍可能修改配置、事件 payload 或生命周期假设。DeepSeek Harness 目前处于预发布阶段,仓库规则允许直接拒绝旧磁盘格式,也没有承诺 session 格式兼容。现在的可组合性主要服务于同一发行版内的替换和扩展,尚不能推导出跨版本插件 ABI 稳定。

第五项代价是组织治理。一切都能成为插件后,团队容易把每段十几行逻辑都拆成包,形成依赖图膨胀、文档分散和测试启动缓慢。DeepSeek Harness 用 package 分组、Service Definition/Provider/Consumer 角色、每包 README、运行时 invariant 和真实组合测试控制边界。这些规范本身就是成本。小团队若没有维护扩展生态或多 profile 的需求,模块化函数加显式依赖可能更合适。

历史对照

从 OSGi 看 Cordis,动态服务和生命周期联动已有先例。服务注册、发现、撤销后触发依赖变化,这条主线几乎一致。Cordis 值得学习的新点,是把资源清理统一成 effect,并让子插件、服务、监听器都归属于同一个 Fiber。OSGi 的 bundle 生命周期更完整,也更重;Cordis 选择应用内细粒度对象,失去了类加载隔离、标准版本解析和安全层,获得了低门槛组合。

从 Eclipse 看 DeepSeek Harness,profile 与 bundle patch 很像部署时组装,Service 和事件类似扩展点。差异在于 Eclipse 扩展通常围绕稳定宿主能力,DeepSeek Harness 连 agent loop 和 UI 都可替换,核心插件与第三方插件共享同一挂载机制。这个平权减少了特权内核,风险也更集中到协议治理:循环能被替换,不代表任何替代循环都遵守 session log、权限和工具时序约束。

从依赖注入容器看,Cordis 的服务解析并不陌生。新的组合来自 DI 与生命周期的绑定。普通容器负责构造对象,定时器、监听器和子进程仍由业务代码清理;Cordis 把注册动作收敛成 effect。空间 scope 与时间 ownership 同时存在,插件才能在某个 Agent 范围内挂载,并随该 Agent 完整撤销。

从微内核看,DeepSeek Harness 的内核确实很轻,但其 Loader 和配置协调已经承担不少平台职责。它要解析模块、等待依赖、处理 HMR、执行事务式更新、保存最后可用树、输出诊断。这提醒我,微内核减少的是业务特权,不会减少生命周期复杂度。能力越动态,内核对失败语义的要求越高。

Cordis 的贡献可以概括为一种紧凑的组合坐标:Context 决定插件位于哪里,Fiber 决定插件在何时有效,Service 负责直接能力,Event 负责横切协作,effect 负责撤销。DeepSeek Harness 在这个坐标系上加入 Agent scope、持久会话事件和配置事务,使其能承载多会话、多 profile 与热更新。

工程取舍

当我想要用类似架构时,会先确认三个条件,基于这三个条件判断来是否采用。

其一,能力确实需要独立替换,至少存在两个生产提供方或明确的外部扩展需求。

其二,同一进程需要多套组合并存,或者运行期间需要可靠更新。

其三,团队愿意为卸载、回滚和真实组合测试持续付费。

缺少这些条件,插件框架容易沦为复杂的工厂模式。

能力切分要从消费方开始。先列出谁调用、调用时需要哪些稳定语义,再设计 Service Definition;随后实现 provider,最后把面向模型的 schema、展示和错误文本留给 consumer。接口只有一个内部调用者时,保留私有闭包,不急着升格为公共 service。可替换性必须由真实替代者证明。

所有注册都要有明确所有者。注册工具、提示词片段、适配器、监听器时同步返回 disposer;卸载测试要观察注册项确实消失。涉及异步资源时,dispose 完成的定义要包含子进程退出、队列排空和 watcher 关闭,不能只发出取消信号。DeepSeek Harness 对 Fiber quiescence 的处理值得照搬。

配置更新要区分内存一致性和外部副作用。Loader 可以恢复旧树,插件初始化若已经创建云资源,回滚需要业务补偿。规则可更保守一些:挂载阶段只做校验和本地注册,外部写操作放到带幂等标识的运行阶段。确实要在初始化创建资源时,必须让 disposer 能识别部分完成状态。

作用域要控制在两种以内。实例隔离与注册可见性已经足够表达多数 Agent 场景,再增加租户、请求、工作区、角色四套独立 scope,调试成本会失控,需要根据实际业务需求和必要性来判断。当需要新增维度时,先判断它属于服务实例选择、注册过滤、持久数据权限还是请求参数。很多所谓新 scope,其实只是一次显式参数传递。

事件也要克制。需要唯一返回值的调用用 Service,需要持久恢复的事实写 session log,需要多个插件观察或包裹的阶段才用事件。waterfall 必须把短路当成公共协议,监听器顺序也要有测试。事件名虽然能降低导入耦合,却会增加时序耦合;后者通常更难排查。

小结

DeepSeek Harness 选择 Cordis,解决的主要矛盾是 Agent 能力变化速度与运行时一致性之间的冲突。模型、工具、沙箱和 UI 都可以替换并不稀奇;这些能力可以在局部作用域内共存,能随依赖变化启停,能在配置失败后保留旧树,卸载时还能回收监听器、服务和子插件,才构成可用的插件架构。

它也没有抹平工程风险。Proxy 隐藏了查找路径,双层 scope 增加理解成本,热更新无法回滚任意外部副作用,预发布阶段也缺少跨版本兼容承诺。团队采用这套思路时,应复制它的生命周期纪律和能力边界,别只复制「一切皆插件」的目录结构。

源码给我们一些启发,把插件设计的评审顺序倒过来:先问如何撤销,再问如何注册;先证明局部挂载不会泄漏,再讨论全局复用;先定义失败后保留哪一棵树,再讨论热更新速度。做到这些,时空可组合才是一组可以验证的运行时语义。

这套架构个人理解是想往 Agent 的实时「自生长」,通过 AI 的能力在使用过程中让自己更强大。

逼逼这么多,主要还是在这个过程中学习一下,说实话,有了 DeepSeek Harness,想自己开发一个专属的 Agent ,快捷方便了很多,eg且这是 MIT 协议的。

以上。

参考资料

 

AI Agent 进阶架构:渐进式披露和动态上下文管理

当 Agent 做到一定复杂度,问题往往不在模型能力本身,而在上下文怎么给、工具怎么给、流程怎么控。同一套模型,有的团队能把它用成「能稳定交付的执行系统」,有的团队只能得到「偶尔灵光一现的聊天机器人」,差距就在架构。

早期提示词工程里,上下文基本是静态的:一次性把提示词写好,然后让 LLM 自己发挥。随着架构的演化,,上下文变成动态的,它会「收」和「放」:

收(Contract):渐进式披露。屏蔽无关信息,减少 Token 消耗,聚焦注意力。(解决“准确性”)
放(Expand):动态注入。根据交互状态,主动引入外部话题、记忆片段或世界观设定。(解决“丰富性”与“持续性”)

这是一种系统架构策略:用有限 Token 去管理无限信息,用非确定性模型去执行标准化流程

1. 三个典型瓶颈:Context、工具、SOP

复杂 Agent 基本都会遇到三个主要的问题:

  1. 上下文爆炸(Context Explosion)
    文档、代码、历史对话、用户画像、任务状态……你不可能全塞进 Prompt。硬塞进去也会出现“Lost in the Middle”,关键信息被淹没。

  2. 工具过载(Tool Overload)
    工具越多,定义越长,Token 越贵;更严重的问题是:工具选项越多,模型选择正确工具的概率越低,尤其是多个工具功能相近时。

  3. 执行不可控
    当我们希望它按 SOP 做事(先检查、再验证、最后提交),它却容易跳步、漏步,或者为了“把话说圆”而瞎编执行结果。

「渐进式披露 + 动态上下文管理」就是对这三件事的统一解法:不要一次把世界交给模型,而是让模型在每一步只看到它此刻需要看到的东西。

2. 渐进式披露

渐进式披露不是少给信息,是分阶段给信息

有人把渐进式披露理解成省 Token。省 Token 是结果,不是核心。

核心是:把一次性的大上下文,拆成多轮的决策—反馈—再决策。每一步只给与当前决策相关的最小信息面,减少噪音,让模型的注意力更集中,也让系统更可控。

一个直观的工程化表述:

  • 不是构建一个「全量 Context」
  • 而是维护一个「可增长的 Context」,并且增长受控

你会看到两个动作交替出现:

  • Contract(收缩):隐藏、裁剪、摘要、替换为索引
  • Expand(扩张):按需加载片段、工具子集、记忆、世界观、流程状态

3. 数据层

传统做法,使用 RAG 很容易走向粗暴:检索到的内容直接拼进 Prompt,能拼多少拼多少(可以配置)。结果通常是两种:

  • Token 变贵,延迟变长
  • 模型注意力被稀释,反而更不准

渐进式披露在数据层的落地方式,是把「获取信息」做成连续的动作序列,而不是一次性拉满。

参考 AI Conding 很贴近工程实际的步骤:

  • 初始 Prompt 只有任务描述
  • AI 发现信息不足,发起 lsgrep 请求
  • 系统只返回 ls 的结果(文件名列表),而不是文件内容
  • AI 选中目标,发起 read_file
  • 系统这时才披露文件内容

这里关键点不是 ls/grep/read_file 这些名字,而是信息披露粒度

  • 先给目录/索引(低成本,低噪音)
  • 再给片段(命中后才扩大)
  • 最后给全文(只在确认需要时才给)

3.1 披露层级建议:L0 到 L3

可以把上下文分成几层,这里定义的层级不是标准答案,但思路是这么个思路:

  • L0:任务和约束
    用户需求、输出格式、禁止事项、成功标准。L0 必须稳定,尽量短,长期驻留。

  • L1:证据索引
    文件列表、章节目录、数据库表名、日志摘要、搜索结果标题。只给“在哪里”。

  • L2:证据片段
    命中的段落、代码片段、表结构、关键日志区间。只给“相关部分”。

  • L3:证据全量
    全文档、完整文件、长对话历史。尽量少用,只在确实需要通读时开放。

系统要做的事是:让模型先用 L1 做定位,再用 L2 做判断,最后才允许 L3 进场。这样不仅省 Token,还可以减少模型在噪音里自我发挥的空间

3.2 动态注入

动态注入常见误区:用户问 A,你检索 A;用户又问 B,你把 A+B 都塞进去;几轮后上下文就乱了,且不可控了。

比较常用的做法是引入「上下文预算」和「淘汰策略」:

  • 每轮允许注入的 Token 上限(硬预算)
  • 驻留区(长期有效,例如用户身份、偏好、当前任务)
  • 工作区(当前步骤的证据片段)
  • 冷存区(旧证据移出,保留索引或摘要)

淘汰的对象通常是“旧证据全文”,不是“任务状态”。任务状态丢了,模型就会重复问、重复做;证据全文丢了,大不了重新检索。

4. 工具层

工具越多越强这件事,在 Agent 里是反的:工具越多,模型越容易犹豫、选错,甚至编造「我已经调用了某某 工具」。

渐进式披露在工具层的做法是:分层路由,按需可见

参考一个很实用的层级披露思路:

  • Root 层只披露 5 个大类工具:代码类文档类部署类数据库类通知类
  • 模型先选大类,例如“我要查数据”-> 数据库类
  • 下一轮 Prompt 才披露数据库相关的具体工具,例如 sql_query, get_table_schema

我们可以把它当成「工具菜单」:

  • 第一屏:只显示一级菜单
  • 点进去:才显示二级菜单
  • 系统控制可见性,而不是让模型在 100 个工具里裸选

4.1 工具披露带来的三个工程收益

  1. Token 控制更直接
    大量工具的 schema 描述会花费大量的 Token。层级分发能把「工具定义成本」分摊到多轮,而且只在需要时支付。

  2. 工具选择准确率提升
    选项少,模型更容易做对;更重要的是,减少「近义工具」同时出现。

  3. 安全策略更好落地
    不该给的能力,默认不可见。你不需要在 Prompt 里反复警告“不要调用某某工具”,直接让它看不见。

4.2 「工具可见性」本质是一种权限系统

很多团队权限做在网关、做在后端鉴权,但 Agent 的权限还应该做在“可见性”上:

  • 看不见:降低误用概率
  • 看得见但不可用:模型会反复尝试,浪费回合
  • 可用但有条件:需要把条件变成流程状态的一部分(下一节讲 SOP)

5. SOP 层

SOP 层就是当前很火热的 Skills,且不仅仅是 Skills,它是把流程写进披露逻辑,而不是写在提示词里

企业场景里,最怕的是「看似完成、实际没做」,而这在大模型的输出中很常见。让模型「请遵循 SO」”意义不大,它会漏步骤,而且它很擅长把漏掉的步骤用语言补上。

渐进式披露在 SOP 上的落地方式,是在我们的系统里做“流程锁”:上一步没通过,下一步的工具就不出现

参考一段很清晰的流程控制(关键点直接引用):

  1. 阶段一(Lint):系统只披露 Lint 工具和当前 Diff,隐藏 Commit 工具
  2. 阶段二(Test):Lint 返回 Success 后,系统才披露 Test 工具
  3. 阶段三(Commit):只有测试通过,系统才披露 git_commit

这套逻辑解决的是“话术不可信”的问题:模型可以说“我已经测试通过”,但系统的状态机不会因为它一句话就放行。放行只能来自可验证的工具回执

5.1 SOP 控制要点

把「检查点」设计成机器可判定

SOP 最容易失败的地方是检查点含糊,比如“确保无问题”“确认完成”。Agent 体系里要改成:

  • 有工具回执的:以回执为准
  • 没有工具回执的:以人工确认或外部系统状态为准
  • 不能验证的:不要当作放行条件

能自动化判定,就不要让模型自评。

6. 为什么要引入 Agent Skill

这里本质是一种是工程分层,当然也是概念包装。

很多人会问:这些用代码控制不就行了,为什么还要提 Agent Skill?

把 Skill 当成一个架构抽象,会更容易把系统做稳:它解决的是解耦、复用、状态感知

这里把关键逻辑说透:

6.1 Skill 是「上下文的容器」,用完即走

没有 Skill 时,你往往会得到一个越来越大的系统提示词:把所有话题、所有工具、所有规则都塞进去。结果就是注意力迷失、指令冲突、Token 爆炸。

有 Skill 后,你把「某一类任务需要的提示词 + 可用工具 + 知识入口」封装到一起:

  • 需要时加载
  • 不需要时卸载
  • 上下文保持干净

这和「渐进式披露」是同一件事:Skill 是披露的载体

6.2 Skill 是「动态注入」的边界

动态注入真正难的是边界:注入多少、注入什么、何时撤回。

Skill 让边界清晰:

  • 注入不是“往 Prompt 拼字符串”
  • 注入是“激活某个 Skill”,让它把需要的最小信息面带进来

系统因此更容易做预算、做审计、做回放。

6.3 Skill 让路由变成可维护的系统,而不是靠直觉写 prompt

复杂 Agent 一定会路由:用户一句话可能触发“查资料 / 写代码 / 安抚情绪 / 改流程 / 发通知”。

Skill 体系下,路由的输出是“激活哪些 Skill”,而不是“写一段更长的提示词”。这会直接改善维护体验:

  • 你能统计每个 Skill 的触发率、成功率、平均 Token
  • 你能对某个 Skill 单独迭代,而不牵一发动全身
  • 你能为不同用户、不同权限加载不同 Skill 组合

7. 动态上下文管理

动态上下文管理要管理「状态 + 证据 + 权限」。

把上下文当成一段文本来拼接,迟早失控。更合理的视角是:上下文是系统状态在模型侧的投影。

建议把上下文拆成四类对象,每一类有不同的生命周期:

  1. 任务状态
    当前处于哪个阶段、已完成哪些检查点、下一步允许做什么。它要短、稳定、结构化,尽量每轮都带。

  2. 证据
    检索片段、工具输出、外部信息。它要可引用、可追溯、可淘汰。

  3. 偏好与长期记忆
    能影响输出风格或长期策略的东西。它不该频繁变化,变化要可控,最好有写入门槛。

  4. 能力与权限
    工具可见性、工具可用性、流程放行条件。它是约束,不是参考建议。

8. 可执行的架构清单

按“先做什么更值”排:

  1. 先做工具可见性控制
    工具分层,默认只给 Root 类目;按分支披露具体工具。

  2. 把 SOP 变成状态机放行
    上一步成功回执出现,下一步工具才可见。失败就停在当前阶段,不要让模型口头放行。

  3. 把上下文分区:驻留区 / 工作区 / 冷存区
    驻留区短且稳定;工作区有预算;冷存区只保留索引/摘要。

  4. 先索引后片段的披露策略
    任何大文本资源都先给目录、标题、命中位置,再给片段,不要一上来就全文。

  5. Skill 化你的上下文与工具组合
    让“动态注入”从拼 Prompt 变成“加载/卸载 Skill”。一开始不需要 100 个 Skill,把高频的 5–10 个先做稳。

  6. 把观测补齐
    记录每轮:披露了哪些证据、开放了哪些工具、触发了哪些 Skill、用了多少 Token、是否命中检查点。没有这些数据,后面很难迭代。

9. 小结

一个成熟的 Agent 系统,外观上像在聊天,内部其实在跑一套受控的执行架构:

  • 信息不是一次塞满,而是按步骤披露
  • 工具不是全量开放,而是按层级开放
  • 流程不是靠自觉,而是靠状态机约束
  • 记忆不是越多越好,而是可写入、可淘汰、可追溯

把这四件事做好,Agent 会越来越像一个靠谱的执行系统:该问的问清楚,该查的查到证据,该做的按流程做完,做不到就停下来,不会硬编。

这就是「渐进式披露 + 动态上下文管理」的价值:不是让 Agent 说得更像,而是让它做得更稳。

以上。

聊下 AI Agent 的上下文记忆和遗忘

最近 DeepSeek 的 OCR 论文里有个有趣的想法:用光学压缩来模拟人类的记忆遗忘机制。

这个思路很巧妙。他们注意到人类记忆和视觉感知都有个共同特点:距离越远,信息越模糊。一小时前的事记得清楚,一年前的事几乎忘光;10 厘米的东西看得清楚,20 米外的东西就模糊了。

DeepSeek 把这个生物学现象转化成了工程实现:近期对话用高分辨率保存,一周前的对话降到中等分辨率,久远的记忆压缩到最小。信息随时间自然衰减,就像人类遗忘一样。

这让我想到 Agent 记忆系统设计的本质问题。

1. 为什么 Agent 需要记忆

上下文窗口和记忆是两码事。

上下文窗口只是让模型一次看到更多对话,像是扩大了工作台。但记忆不同,它让 Agent 能保存、更新和选择性回忆信息。没有记忆,Agent 就像得了失忆症,每次对话都从零开始。

现在大家都在追求更大的上下文窗口,从 8K 到 32K,再到 128K、1M。但这种暴力扩张有个问题:计算成本呈二次方增长。处理 100K tokens 的成本是 10K tokens 的百倍。而且,把所有信息一股脑塞给模型,反而可能让它迷失在细节中。

记忆系统的价值在于选择性保留。

不是所有信息都值得记住,也不是所有信息都需要同样的精度。

2. Agent 记忆的层次结构

AI Agent 的记忆系统可以分成几个层次:

短期记忆(工作记忆)
这是 Agent 的记事本,保存当前对话和正在处理的任务。典型容量在几千到几万 tokens。没有它,Agent 会在对话中途失去思路,问你刚才说了什么。

长期记忆
这是跨会话的持久化存储。用户下次回来,Agent 还记得之前的交互。长期记忆又可以细分:

  • 事实记忆:保存确定的信息,比如用户姓名、偏好、角色定义。这些信息需要随情况更新,保持”当前真实状况”。
  • 情景记忆:记录具体经历,什么时候发生了什么,结果如何。这给 Agent 一种时间连续感,能回顾过去的决策。
  • 语义记忆:组织概念和关系的知识网络。让 Agent 理解”数据库慢”和”查询延迟高”是相关问题。

3. 遗忘机制是有用的

人类会遗忘,不是大脑容量不够,而是遗忘让我们更高效。

想象一下,如果你记得生活中的每个细节:每顿饭的味道、每次呼吸的感觉、路过的每个行人的脸。这些信息会淹没真正重要的记忆。遗忘是一种过滤机制,帮我们保留有价值的信息。

Agent 也需要这种机制。随着交互增加,历史数据会无限增长。如果不加选择地保存所有内容,会面临几个问题:

  1. 存储成本:每个用户的历史数据都完整保存,存储需求会爆炸式增长。
  2. 检索效率:在海量历史中找到相关信息越来越慢。
  3. 注意力分散:太多无关信息会干扰 Agent 的决策。
  4. 隐私风险:永久保存所有对话增加了数据泄露的风险。

4. 用分辨率模拟时间衰减

DeepSeek 的做法是:把历史对话渲染成图像,然后用不同的分辨率来编码。

近期对话用高分辨率(Gundam 模式,800+ tokens),保留完整细节。

一周前的对话用中等分辨率(Base 模式,256 tokens),保留主要内容。

久远的历史用低分辨率(Tiny 模式,64 tokens),只留个大概印象。

这样做的好处是:

  1. 不需要丢弃历史信息,所有对话都保留着
  2. 但远期信息自然”淡化”,占用的 token 越来越少
  3. 模拟了人类记忆的衰减曲线

具体实现上,他们会:

  1. 把超过一定长度的历史对话渲染成图像
  2. 第一次压缩,让 token 减少 10 倍
  3. 上下文再次超长时,进一步降低分辨率,再压缩 10 倍
  4. 随着时间推移,图像越来越小,内容越来越模糊,模型也就逐渐”读不清”了

这种方式不是简单的截断或删除,而是让信息随时间衰减——就像人类记忆一样。

5. 理论上无限的上下文

如果这个思路走通了,就能实现”理论上无限的 context window”。

这里的无限不是真的无限,而是通过分层压缩,让历史信息的存储成本趋近于常数。

我们不需要保持所有信息的高保真度,只需要让信息按重要性和时效性衰减。

最近的对话,全保留。
一周前的,压缩一次。
一个月前的,再压缩一次。
半年前的,只留个印象。

这样,计算资源始终聚焦在最重要的”近期”信息上,而久远的历史虽然还在,但只占用很少的 token。

从成本角度看,这比无脑扩大上下文窗口要合理得多。

6. 记忆管理的工程实践

在实际的 Agent 系统里,记忆管理通常分几个层次:

会话级记忆——当前对话的上下文,存在内存里,对话结束就清空。这部分可以直接用模型的上下文窗口。

用户级记忆——持久化存储,用向量数据库或 KV 存储。每次对话时,根据当前问题检索相关的历史记忆,注入到 prompt 里。

全局知识库——所有用户共享的知识,比如产品文档、技术规范、FAQ。这部分通常用 RAG(检索增强生成)来处理。

关键是如何在这些层次之间做好信息流转和优先级管理。

比如,用户刚说过的话,优先级最高,直接放在 prompt 前面。一周前的对话,需要检索后才注入。一个月前的,可能只保留摘要。

这种分层策略,和 DeepSeek 的分辨率衰减思路是一致的——让资源消耗和信息重要性成正比。

7. 遗忘不是丢失

这里需要明确的是,遗忘不等于删除。

人类的遗忘,是提取变难了,不是信息消失了。在特定的提示下,很多”遗忘”的记忆还能被唤起。

Agent 的记忆也应该这样设计。

低分辨率的历史对话,在大部分场景下不会被激活,但如果用户明确提到某个时间点的事情,系统可以重新加载那段历史的高分辨率版本。

这需要一个索引机制,能根据时间、主题、关键词快速定位到历史片段。

像 Manus 所使用的文件系统就是这样一种索引机制。

遗忘是常态,召回是特例。这样才能在效率和完整性之间找到平衡。

8. 什么值得记住

并不是所有对话都需要长期保存。

大量的对话是重复的、临时的、没有上下文依赖的。比如简单的问候、重复的确认、无关紧要的闲聊。

这些内容可以在会话结束后直接丢弃,不需要进入长期记忆。

真正值得记住的,是那些包含决策、偏好、关键信息的交互。

比如:

  • 用户明确表达的需求和偏好
  • 重要的决策节点和原因
  • 反复出现的问题和解决方案
  • 用户的角色、职责、技术栈等基础信息

这需要在记忆写入时做判断和过滤。可以用一个小模型或规则引擎,评估每轮对话的重要性,决定是否持久化。 Gemini Code 就是这么干的。

不是所有东西都值得记住,筛选本身就是一种优化。

9. 记忆的更新和冲突

长期记忆不是只写不改的日志,它需要能更新。

用户的偏好会变,角色会变,之前的信息可能过时或错误。

比如用户之前说喜欢中古风的家具,后来又说喜欢北欧风,这个信息需要更新,而不是简单地追加一条新记录。

如果只追加不更新,记忆会越来越冗余,甚至出现矛盾。

一个好的记忆系统,需要能识别冲突,做合并和覆盖。

这可以通过实体识别和关系抽取来实现。把对话内容结构化成三元组(主体-关系-客体),然后在写入时检查是否和已有记忆冲突。

如果冲突,可以根据时间戳、置信度等因素决定是更新还是保留多个版本。

记忆管理的复杂度,不亚于写一个小型数据库。

10. 压缩不只是技术问题

回到 DeepSeek 的光学压缩思路,它的价值不只是技术实现,更重要的是提供了一个思维框架。

我们习惯于把上下文长度当作硬指标——越长越好。但这个论文提醒我们,长度和质量不是一回事。

有时候,适度的遗忘反而能提升系统的整体表现。

有如下的好处:

  1. 减少了无关信息的干扰
  2. 降低了计算成本
  3. 让模型专注于最相关的内容

这和人类的认知机制是一致的。我们不会带着所有历史记忆去做每一个决策,而是根据当前情境激活最相关的那部分。

Agent 应该学会做同样的事。

11. 成本和效果的平衡

从成本角度看,无限扩展上下文是不可持续的。

假设一个 Agent 系统,每天处理 1000 次对话,每次对话平均 10 轮,每轮 500 tokens。

如果所有历史对话都全量保留,一个月下来,单个用户的上下文就会达到 1500k tokens。

如果有 1000 个活跃用户,每次推理都带上完整上下文,token 消耗会是天文数字。

但如果用分层记忆 + 遗忘机制:

  • 最近 3 天的对话,全保留
  • 一周到一个月的,压缩 50%
  • 一个月以上的,压缩 90%

token 消耗可能降低到原来的 10%-20%,而对实际效果的影响很小。

因为大部分场景下,用户关心的就是最近的对话。

12. 记忆应该是系统能力

很多团队把记忆功能当作一个独立的我来开发——加个数据库,存一下历史,检索一下注入。

但这样做的效果往往不好。

因为记忆不是一个功能模块,而是整个 Agent 系统的底层能力。

它需要和 prompt 设计、工具调用、推理链路、错误处理深度集成。

比如:

  • Prompt 需要为记忆预留位置和格式
  • 工具调用的结果需要写回记忆
  • 推理过程中需要动态检索记忆
  • 错误发生时需要回溯历史上下文

记忆管理做得好,Agent 的整体表现会有质的提升。做得不好,就只是一个会话机器人。

13. 一些建议

以下是一些可以尝试落地的建议:

1. 不要把所有历史都塞进 prompt

很多团队的做法是,每次调用都把所有历史对话拼接到 prompt 里。这在对话轮数少的时候没问题,但规模上去后会成为瓶颈。

改成检索式注入——根据当前问题,从历史记忆里检索最相关的几条,而不是全量加载。(这里就会考量检索的能力了)

2. 区分会话记忆和持久记忆

当前对话的上下文,存在内存里就行,不需要持久化。

只有那些需要跨会话保留的信息,才写入数据库。

这样可以大幅减少存储和检索的开销。

3. 给记忆加上过期时间

就像缓存一样,记忆也可以有 TTL(Time To Live)。

一般性的对话,可以设置 7 天或 30 天过期。重要的信息,可以标记为永久保留。

定期清理过期记忆,防止数据无限膨胀。

4. 用好向量数据库

向量检索是目前最适合做语义记忆的技术。

把历史对话做 embedding,存到向量数据库里,每次根据当前问题做相似度检索。

但要注意,向量检索的召回率不是 100%,需要结合关键词索引和时间过滤。

5. 记忆要能解释

用户问”你为什么这么回答”,Agent 应该能说清楚是基于哪段历史记忆做的判断。

这需要在记忆检索时保留溯源信息——这条记忆来自什么时间、什么对话、可信度如何。

可解释性不仅是用户体验问题,也是调试和优化的基础。

14. 最后

上下文记忆和遗忘,本质上是资源分配问题。

我们只有有限的 token 预算,有限的计算资源,有限的响应时间,需要在这些约束下做出最优决策。

无限扩展上下文听起来很美好,但实际上不可行。

真正的解决方案,是学习人类的记忆机制——让重要的信息留下来,让次要的信息自然衰减。

DeepSeek 的光学压缩思路提供了一个很好的启发:遗忘不是缺陷,而是一种优化策略。

如果能把这个思路落地到 Agent 系统里,不仅能降低成本,还能提升整体的智能水平。

因为真正的智能,不是记住所有东西,而是知道什么值得记住,什么可以忘记。