前几讲从人的意图(Intent)、规格说明(Spec)和实现(Implementation)之间的缝隙出发,讨论了版本管理、需求分析与架构设计。Git、Jujutsu 帮助我们管理变化,架构帮助我们组织复杂性;到了 Agent Native 开发,我们又把工作区看成状态,把 Agent 的工作看成状态迁移,用持续集成与交付(CI/CD)检查和发布这些变化。
这一讲把这些线索收拢到一个具体系统:DeepSeek Harness,简称 dsh。Harness 是模型周围的运行与协作环境,负责把模型调用、工具、状态、交互和执行控制组织起来。我们关心的不是它提供了多少按钮,而是一个软件工程问题:一个很小的 Agent 循环,怎样容纳不断增加、甚至突然改变的需求?
上一讲展示的 mini-agent 只有大约 150 行。它的核心机制很简单:接收用户目标,把当前上下文交给模型;模型提出工具调用,程序执行工具,把结果放回上下文;继续请求模型,直到这一轮结束。模型不只是生成一段回答,还能通过工具改变工作区,再根据结果决定下一步。
于是,我们可以交给它一个看起来颇有挑战的任务:
生成一个相对真实的南大仙林校区 3D 图。
课程中的项目用 OpenStreetMap 的建筑与道路轮廓构造三维场景:把经纬度转换成平面坐标,根据建筑多边形生成楼体,再加上道路、水域、绿地和相机交互。建筑高度在资料缺失时需要估计,立面、屋顶、树木与路灯也有程序化的示意成分。因此,“相对真实”有具体含义:校园的空间布局有数据依据,但它不是逐栋实测的数字孪生。这样的结果,已经足以让人感受到一个简单 Agent 循环能完成多少工作。
不过,前一个版本很快用实际故障反驳了“它完全可以工作”:网络或者模型服务中断,程序就直接断了。原来,它悄悄假设了每次请求都能顺利结束。
如果需求只是“在网络正常时演示一次”,这个假设或许可以接受。如果需求是“让用户把真实工作交给它,遇到中断也能接着做”,故障处理就是问题本身的一部分。这里的本质复杂性(essential complexity),来自我们对产品行为的要求。把主循环写得更短,不会让网络变得永远可靠。
“失败就重试”也没有听起来那么简单。暂时断网、服务限流,可以保留进度并有限重试;输入损坏、权限不足,原样再来几遍通常没有帮助。更麻烦的是结果未知:没有收到成功消息,不等于操作没有执行。 假设文件已经移动,工具结果还没来得及记录,进程就崩溃了。恢复时若直接重放,可能再次移动、覆盖文件,或者误把“原文件不存在”当成另一个错误。
幂等性(idempotency)就是处理这类问题的重要工具:同一个逻辑操作重复提交,不应产生额外的业务效果。比如用稳定的操作标识识别重试,记录已执行的结果,并让重复请求得到相应回复。记录标识和执行修改还必须协调好,不能留下“记录说做过,实际上没做”或相反的缝隙。AWS 的重试设计经验讨论了这种接口设计。对 Agent 来说,“接着做”必须建立在对实际状态的判断上。
可靠恢复只是需求清单的开头。用户还会要求项目管理、工具和 Skills、沙箱和权限、上下文压缩、记忆和索引、文件编辑、远程访问、App 同步,以及备份和恢复。Skills 是可复用的任务流程和操作知识;沙箱则限制代码与工具能够接触的资源。每增加一种能力,都会出现新的状态与边界。
以课程中的办公 Agent 需求规格说明书为例,用户说“把本周项目记录整理成周报”,期望的是找到资料、核对内容、生成可以继续编辑的文件。只在聊天框里输出一段建议,还不能算完成。生成了文件也不够:它能否打开,是否包含要求的内容,数字与来源有没有核对,哪些地方还缺资料,都属于交付。
执行过程中,用户还可能改变主意。“补充这个文件”“暂停一下”“不要修改原件”“先把已经完成的部分给我”,分别意味着什么?暂停通常要保留进度并停止发起新步骤;取消之后,已经完成的文件修改不会自动消失;恢复则要检查外部文件是否发生变化。一个“停止”按钮的背后,已经有一组需要讲清楚的状态迁移。
类似地,预算不是界面右上角的一个数字。模型请求、工具调用和执行时间要计量,到达限制后要有明确行为。权限也不能只写进提示词:读取一个目录、修改其中的文件、把内容发给外部服务,是不同的动作。网页或文档里出现的命令,只是待处理资料,不能自行变成新的用户授权。
历史与同步还会把问题拉长:关闭窗口后任务是否继续?重启之后怎样恢复?两个设备看到的是不是同一个进度?一份产物被用户手工修改后,再从旧检查点恢复,应该保留谁的版本?这些要求彼此交叉,不能靠每个按钮各写一份状态来解决。
最小 Agent Loop 仍然重要,它让我们看到能力的核心。但周围这些要求不会因为核心简洁就消失。处理需求时,重复保存状态、到处复制恢复逻辑,又会引入附带复杂性(accidental complexity),也就是实现方式额外制造的麻烦。软件工程要做的,是让复杂性有合理的位置,让一类问题可以统一处理,而不是每增加一个功能,就把整个循环重新改一遍。
需求不只是越列越长,还会换方向。
课堂以 Grok Bot 的走红引出这种变化:界面里不再只有一次性的问答,而是出现“论文晨报”“每日新闻”“编程助手”等不同身份和用途的 Bot。用户开始期待持续工作的助手、定时任务,以及不同助手之间的协作。原来围绕“打开一个项目,发起一次编码任务”组织起来的产品设想,就需要重新审视。
“之前的需求全崩了”,是一句夸张但贴切的感叹。底层模型未必换了,用户理解产品的方式却变了。大型团队可能开始重新划分职责、调整路线、拉通对齐;而 dsh 的扩展生态很快出现了 dsh-bot。它提供不同 Bot 的配置、定时例程和相互通信,课堂中的协作界面还让“执行”“审阅”“改进”三个角色围绕同一项工作接续发言。
这个对照关心的是变化的成本。新需求出现时,能否复用已有的会话、模型执行和工具能力,再组织出新的交互方式?还是必须从产品入口一路修改到最深处的循环?
把时间拨回 2026 年 6 月 10 日 22:57,DeepSeek Harness 的仓库刚刚建立。最初的提交创建 README、AGENTS.md 和 CLAUDE.md 符号链接,随后把最小可行产品的需求分析与微内核架构文档接入开发入口。当时已经有 Claude Code、OpenCode、Trae、CodeBuddy 等不同形态的编码工具。这个项目从起点就在面对同一个问题:今天可以选定一组需求,但怎样为明天的变化留下空间?
架构是“受限”和“自由”之间的平衡。“在某个架构下开发”意味着接受它的表达方式、接口和边界。你不能随意做所有事情,但也因此获得一批不必自己重写的能力。
操作系统中的进程就是一个熟悉例子。普通进程在自己的地址空间里运行,通过系统调用申请资源,不能任意修改别的进程的内存。接受这些限制,编辑器、编译器和播放器才能同时运行,一个程序的非法访存也不至于直接改坏另一个程序的数据。操作系统替我们组织隔离、调度和资源访问。
但“仿佛拥有自己的机器”,并不等于真的拥有无限资源。当活跃数据放不进物理内存,频繁换页会让程序慢下来;多个程序竞争 CPU 和磁盘,也会互相影响。独立地址空间隐藏了地址映射,却没有消灭物理资源的容量和竞争。想解释性能问题时,我们仍要走到抽象背后。
高级语言也作了类似交换。以 Java 这样的托管语言为例,程序员接受对象、类型与引用的规则,把对象布局和垃圾回收交给运行时。写课程论坛时,可以集中处理帖子和评论,不必逐个判断对象何时释放。但如果论坛加入低延迟互动,垃圾回收的暂停与 CPU 开销就可能进入用户体验。降低暂停、提高吞吐、减少内存占用之间有取舍,运行时隐藏的成本又回到了开发者面前。HotSpot 的垃圾回收调优说明明确讨论了这些目标。
LAMP,即 Linux、Apache、MySQL 和 PHP 的组合,曾让大量 Web 应用有了直接的组织方式:接收 HTTP 请求,执行应用逻辑,读写数据库,返回页面。课程报名、信息查询等业务可以复用成熟的 Web 服务、事务和备份设施。它也鼓励我们按请求、业务与数据来划分职责。
可是,新增“所有人一直在线的课堂讨论”之后,请求结束、释放资源的惯常节奏就变了。假设让 PHP-FPM 的工作进程一直等消息,它就持续占用处理名额,而进程池限制了同时服务的请求数。这是从 PHP-FPM 的进程管理机制可以推得的容量约束。系统可以扩展长连接、消息广播和后台任务,但连接与共享状态需要重新安排。进一步接入外部支付时,MySQL 回滚也不会替你撤销已经完成的外部扣款。
事件溯源(Event Sourcing)则要求把发生过的事实记录下来,再从这些事实计算状态。例如,校园卡先充值 100 元,消费 12 元,再退款 12 元,最终余额与只充值 100 元相同,历史却不同。接受事件记录的约束,就能同时回答“现在是多少”和“为什么是这个数”。
它的边界也很明确:回放消费记录以重算余额,不能再扣一次钱;要重现历史外币消费的计算,不能偷偷换成今天的汇率。外部副作用、历史输入与规则变化仍然需要处理。Fowler 对 Event Sourcing 的讨论分析了这些问题。保存事件给了我们重建的依据,怎样解释事件仍是设计责任。
这些例子共同说明:边界与需求吻合时,我们得到解耦、复用、可测试性,以及团队共同理解的接口;假设改变时,被隐藏的细节重新影响上层,这就是抽象泄漏。架构开始成为阻力,往往不是因为它突然“变坏”,而是它替我们固定下来的选择不再合适。
MapReduce把这种交换表现得非常清楚。它要求开发者将计算组织成 map → 按 key 分组 → reduce。例如统计网站访问量,每条日志输出 (URL, 1),相同 URL 的记录被分到一起,再由 reduce 求和。
接受这种形式,就能把切分输入、并行执行、任务调度、负载均衡和失败重试交给运行系统。程序员描述的是计算,框架负责把它放到许多机器上执行。某台工作机器坏了,相关任务可以重新调度。
不过,重试之所以有用,是因为计算能够按约定重新执行。如果 map 在处理记录时顺手发一封邮件,任务重试可能再发一封。原论文的副作用部分明确把额外输出的原子性和幂等性留给应用作者。再比如某个热门 key 汇聚了大量数据,框架的自动调度也不能替应用任意拆开它的归约逻辑。便利建立在限制上。
当需求变成反复迭代同一批数据,一轮轮 MapReduce 作业之间交换中间结果就可能成为负担。Spark 用更一般的计算依赖组织多阶段处理,并允许缓存可复用的数据。RDD,即弹性分布式数据集,可以分区存放;丢失分区时,系统根据产生它的转换重新计算。哪些数据值得缓存、怎样分区、内存不足时怎么办,则成为新的取舍。Spark RDD 文档。
如果问题又变成“持续显示最近五分钟的访问量”,我们不能等所有数据到齐再开始。Flink 的流处理让算子持续接收记录、维护状态。但日志可能乱序,也可能迟到:按事件发生的时间统计,还是按系统收到它的时间统计?等多久才输出结果?Flink 用水位线表达事件时间的处理进度,迟到记录则需要相应策略。时间与水位线。
变化因此不只是换一个计算 API。输入可能改成可重放的消息日志,算子长期运行,恢复时还要把输入读取位置与算子状态一起恢复到一致的检查点。Flink 的有状态流处理说明展示了这组配套机制。新的需求会带着整个技术栈一起重组。
从架构向外看,产业和组织也在使用某种“架构”:岗位怎样分工,流程怎样审批,谁掌握客户与信息,哪些事情值得投入成本。它们同样建立在一组假设上。
如果实现一个小工具要几个月,很多个人需求根本不会进入开发计划。假如 AI 让表达、搜索和试错便宜很多,给一个班写工具、给某个设备做专用界面,就可能突然值得做了。供给方式改变,也会催生原先没有被表达出来的需求。
沿着这个变化推演,过去“需求相对稳定、实现相对昂贵”的组织方式,可能开始把大量时间花在等待审批和交接上。瓶颈也可能转移到验收、信任和物理执行:方案容易生成了,谁来判断它是否正确,谁来把它落实到真实世界?
这也是“AI 是产业的超级加速器”所包含的更大判断。新产业诞生,旧产业衰退,人才和社会阶层随之流动;有人更容易跨过技能门槛,也有人因为掌握数据、客户和渠道而扩大优势。这些是值得警惕和探索的可能方向,不能从一次成功的代码生成就推导出确定的社会结局。对工程师来说,眼前可以做的,是不断检查:自己的系统和组织还在依赖哪些已经松动的前提?
回到 DeepSeek Harness,它选择的第一条原则是:Everything is a plugin。 模型适配器、工具、会话日志,乃至默认的 Agent Loop,都可以作为插件组织。希望增加行为时,优先使用共同的扩展接口,而不必每次修改一个越来越大的中心程序。
底层 Cordis 负责服务解析、依赖激活、作用域与卸载。服务解析回答“这个插件需要的能力由谁提供”,依赖激活处理“所需能力何时可用”,作用域界定注册项在哪些上下文里生效,卸载则撤销相应的注册与监听。我们可以把这种职责类比为操作系统管理进程生命周期和资源访问,但插件框架的作用域并不等于操作系统级的安全隔离。
每个插件可以有自己的私有状态。接下来,真正困难的事情才开始:它们怎样协同?
不协同,工具、界面、记忆和执行循环就是一堆孤岛;随便协同,大家直接修改彼此内部状态,系统又会变成无法管理的混乱。把文件分成很多包,并不会自动得到解耦。插件需要共同理解少量稳定的概念,并通过有含义的接口交换信息。
操作系统为程序提供进程、文件和网络这些基本对象。Agent Harness 也需要自己的“世界模型”,也就是软件对领域对象的组织方式。在本讲的抽象层次上,dsh 的几个核心概念是 Session、Agent,以及装配界面的 slots。
Session 记录发生过什么;Agent 接收输入并推动执行;slot 是界面预留的插入位置,让插件贡献按钮、面板或其他组件。底层的服务、工具和模型提供执行能力,业务插件围绕这些共同对象组合。一个插件不必知道另一个插件的全部内部结构,就能参与同一段工作。
可以把 Session 的核心表示写成 Session = list[Event]:会话是一串有序事件。它不只是“用户说一句、模型说一句”的聊天记录,还可以包含工具操作、执行边界与插件负责的业务事实。
当前模型应该看到哪些消息,界面应该显示什么,某项业务处于什么状态,都可以从事件历史推导。这个过程叫投影(projection):按一组规则处理同一份事实,得到某种用途需要的表示。模型消息历史是一种投影,业务状态和界面也可以有各自的投影。前几讲的 Event Sourcing,在这里又出现了。
接口可以分成两组。session.append(type, data) 追加插件负责的业务事件;监听 session/event,则能观察已经提交到会话中的事实。这里的“提交”表示事件已进入会话历史,是否已经写入持久存储,还取决于相应的持久化机制。
另一组接口负责解释事实。session.deriveMessages() 生成模型消息历史;插件通过 ctx.sessionProjections.register(definition) 注册业务投影,再用 ctx.sessionProjections.stateOf(session, key) 读取相应状态。理解它们时,关键不是记住函数名,而是区分“记录一件事”和“计算这件事意味着什么”。
例如,某个工具执行完成后,会话留下对应结果。聊天界面可以显示工具结果卡片,业务插件可以据此更新任务状态,下一次模型请求也可以获得这项结果。它们使用共同的事实依据,不必靠某个界面组件顺手通知另外几个模块,才能勉强维持一致。
这还提供了诊断路径:如果模型看到了某段上下文,应该能找到它进入请求的依据;如果界面显示“任务完成”,应该能追问它是从哪些事实得出的。重建状态依然需要正确的投影规则,外部操作的恢复也仍需核实,但系统至少保留了可检查的起点。
Session 负责历史,Agent 负责推动执行。为了让不同插件能表达各自的意图,“给模型一段话”需要进一步区分。
agent.followup(message) 提交普通的后续输入,排入新的一轮,并唤醒执行。用户发出下一个任务,适合这种语义。agent.inject(message) 补充模型上下文,等待后续步骤使用,本身不唤醒空闲 Agent。例如插件获得一项补充资料,并不意味着应该立即开始一项新任务。agent.steer(message) 将中途引导交给最近的后续步骤;Agent 空闲时也可以唤醒它。例如用户补充“先处理第二份文件”,希望正在进行的工作尽快调整方向。这里的“尽快”仍然有执行边界。正在进行的模型调用或外部操作,不会因为一句新消息就自动倒退。把三种入口分开,恰恰是为了明确消息何时进入执行,以及它是否应当启动工作。
插件还可以持续贡献动态上下文。通过 ctx.systemPrompt.context(...) 注册一个读取当前状态的文本函数,就能在组装上下文时提供最新信息。例如,画布当前选中了哪些对象、编辑器打开哪个文件,都可能随用户操作改变。如果只在插件启动时把它们写入一次提示词,模型很快就会面对过期状态。
最后一类接口参与尚未执行的决策。Hook 是挂在明确执行节点上的处理逻辑。比如下面这个处理器把决策交给后续处理链,再原样返回;其中 ctx 是插件上下文:
ctx.on('agent/pre-step', async (payload, next) => {
const decision = await next()
return decision
})
next() 的意义是委托后续处理链。真正的插件可以按接口约定检查或调整待执行的决策;如果不调用它,就可能短路后续处理。这不是无关紧要的回调细节,而是决定多个插件怎样共同影响执行的规则。
要特别区分两种时间方向:session/event 面向已经发生的事实,补充或纠正应留下新的记录;agent/pre-step 面向尚未进入的步骤,可以参与决定下一步怎样走。过去的事实和未来的决策如果混在一起,“插件想改变行为”就很容易变成“插件悄悄改写历史”。
这些接口让插件像挂在一条总线上:可以观察往来的事件,贡献输入和上下文,在约定节点参与决策,也可以注册工具与界面组件。Cordis 管理相应注册的生命周期,插件卸载时撤销已登记的监听与其他 effects。这里的 effect 指注册等需要清理的作用;它不会自动撤回插件此前已经发出的邮件或修改过的文件。
这种组合方式允许不同产品外形共享同一套能力。终端用户界面(TUI)可以完全不使用图形界面的 slots,直接驱动 Session 和 Agent API;pdf-reader 可以声明依赖 tools 服务,再向其中注册 PDF 阅读工具;dsh-better-sidebar 则可以把会话信息重新组织成类似 VS Code 的工作界面。它们不需要都长成同一个聊天窗口。
但没有免费的午餐。框架能管理插件何时加载、何时卸载,却不能自动裁决所有业务语义。
假设插件 A 改写输入,插件 B 检查输入。先检查再改写,与先改写再检查,检查到的就不是同一份东西。两个 Hook 也可能各自合理,却一个要求继续执行、另一个要求停止。或者,插件表面上没有依赖彼此,实际上已经依赖另一个插件产生的事件、注册顺序或某项副作用。
一个夸张的思想实验是安装十个自动上下文压缩插件,每个都把前一个的输出压缩到一半。如果它们确实依次作用于同一份上下文,剩余长度就是 (1/2)^10 = 1/1024。这个算式不是对真实压缩算法的实验结论,却很好地说明了组合风险:每个插件单独都觉得自己在节省上下文,合起来可能把有用信息压没了。即使不存在顺序争议,局部合理也不保证整体合理。
因此,“彻底解耦合”并不意味着不需要共同规则。哪些事件由谁负责,哪些行为可以同时存在,冲突时如何决定,卸载后有哪些状态需要处理,都需要明确。接口把协作集中到了可讨论的位置,也把原来藏在实现里的语义冲突暴露了出来。
DeepSeek Harness 的吸引力之一,是共同接口足够小,许多个性化需求可以直接通过插件尝试。这里的 vibe,就是用自然语言表达想法,让 Agent 快速实现,再观察和修改:“我想要这样的侧边栏”“给这个应用加一种编辑方式”“让几个角色接力处理任务”。
沿着这个思路,插件开发也可以进入 Agent 的工作范围:用户许愿,Agent 编写并装配插件,用户在实际环境里体验。这正是 Creator Mode 所指向的方向。重要的反馈发生在使用现场,用户不用先把所有细节整理成一份完美规格,再等待别人排期。
讲者对社区方向甚至有一句尖锐的评价:“我觉得 dsh 的开源社区走远了。”放在这里,更值得继续追问的是:一个开放架构究竟是在鼓励大家收集一批现成插件,还是在让每个使用者有能力满足自己的小需求?已有插件当然有复用价值,但如果只围绕成品功能竞争,就容易忽略“需求来了,可以自己改”的能力。
这并不免除维护责任。越容易生成插件,越需要明确它影响哪些对象、怎样观察结果、失败后怎样退出。共同接口与可验证的行为,让快速试错有机会积累成长期可用的扩展。
第一个值得探索的方向,是把 Harness 接入已有应用。很多应用早已有成熟的对象模型和编辑能力:画布里有图形,文档里有段落,表格里有单元格。我们可以保留这些能力,在用户意图与应用对象之间接入一层很小的抽象。
课堂把它概括成三件事:lock objects + serialize objects + UI。
锁定对象,是在模型修改期间明确它负责的范围,避免用户与模型同时改动相同对象。序列化对象,是把应用内部数据转换成模型和工具能处理的表示,例如 JSON。UI 则负责让用户选择对象、表达要求、查看结果,并在必要时停止工作。
课程中的 Excalidraw 插件把这个思路落实到了画布。用户选中若干图形,点击 AI,输入修改要求。系统锁定选区及其绑定文字,把这些对象的 JSON 交给相应 Session;画布其他部分仍可继续编辑。
模型通过 replace_selection 工具提出替换内容。工具检查输出,禁止修改选区外的对象,先暂存通过检查的结果,在本轮成功结束后应用到画布。停止、超时或失败时,系统解锁并保留原选区。每张画布对应一个会话,因而可以继续讨论、切换和恢复。
这个过程比“把整张画布交给模型随便改”多了一点约束,却给人和 Agent 留出了清楚的协作边界。应用负责对象含义与编辑规则,Harness 负责会话、模型执行和工具协作。失败恢复也从一开始就进入设计,而不是等用户图画丢了再补。
“还要什么 WorkBuddy?”背后是一种带着挑衅的产品判断:许多 AI 增强可以直接生长在用户已经工作的应用里。现有应用提供对象和操作,模型根据人的意图临时组合它们,小的连接层就可能创造新的工作方式。
第二个方向是生成式界面(Generative UI,GenUI)。除了让模型输出文字,还可以让它生成 HTML 与 JavaScript 构成的交互卡片,并在卡片与 Session 之间建立双向通道。
单向生成一个页面,只完成了一半。更有意思的是,卡片中的代码能够把用户操作和当前状态送回上下文,模型再据此回应或更新界面。这样,界面不只是回答的装饰,而是用户参与工作的入口。
下棋就是一个直观例子。用户在棋盘上点击落子,卡片把走子与棋局送入会话;模型读取局面,选择应手;卡片据此更新。于是,“我甚至可以和 DeepSeek 下棋了”不再要求每一步都手动输入坐标、再在脑中重建棋盘。
这里仍然要分清职责:棋盘表示、回合顺序与合法走子可以由程序维护,模型负责选择和解释应手。把棋盘画出来,并不证明模型棋力高,也不保证它永远不会提出非法走法。明确对象和规则,才能让互动持续进行。
同样的方法可以用于图表、任务卡片或参数面板:点击精确表达操作,自然语言表达目的,状态变化回到同一个会话。用户不必在“聊天”与“使用软件”之间反复搬运信息。一个小的双向接口,便为原本没有预先设计好的交互留下了空间。