<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; DeepSeekHarness</title>
	<atom:link href="https://www.phppan.com/tag/deepseekharness/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 03 Oct 2026 08:08:29 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>开发同学在 vibe coding 后，面对代码黑盒应该做什么</title>
		<link>https://www.phppan.com/2026/10/vibe-coding/</link>
		<comments>https://www.phppan.com/2026/10/vibe-coding/#comments</comments>
		<pubDate>Sat, 03 Oct 2026 08:08:29 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[AI 编程]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2543</guid>
		<description><![CDATA[最近半年，我们在代码库中看到越来越多这样的提交：一个功能完整的 PR，包含数千行代码，单测覆盖率达到 85%  [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #191b1f;" data-first-child="" data-pid="spZNmcpK">最近半年，我们在代码库中看到越来越多这样的提交：一个功能完整的 PR，包含数千行代码，单测覆盖率达到 85% 以上，本地集成测试全绿。然而在提测演示或做方案复盘时，提交代码的工程师无法在不借助模型的情况下，准确描述出某条核心业务链路在异常分支下的具体状态扭转过程。</p>
<p style="color: #191b1f;" data-pid="gvz4LzWc">代码由大模型直接生成，工程师负责提供提示词、粘贴报错、运行命令以及验收功能。这种被称为 vibe coding 的开发模式正在研发团队中快速蔓延。开发人员正在迅速丧失对代码内部实现细节的掌握。</p>
<p style="color: #191b1f;" data-pid="Fv5TBOvk">如果一个开发人员完全不需要知道代码内部发生了什么，只要在外部通过输入输出来验证系统，这项工作换成一个不懂技术的产品经理或业务人员，结果并没有本质区别。 当编写代码的动作被模型接管之后，技术人员本身的专业壁垒就会受到直接冲击。</p>
<p style="color: #191b1f;" data-pid="RJSUlxiS">在实际工程推进中，不懂底层原理的使用者在与模型交互时，会迅速遇到一道无法逾越的墙。当系统行为偏离预期，或者出现跨多个子系统的复合型偶发故障时，缺乏系统内部认知的人甚至无法组织出有效的排查提示词。他们只能把控制台的错误堆栈反复扔给模型，陷入尝试、失败、再尝试的低效死循环。</p>
<p style="color: #191b1f;" data-pid="KwZVgWlZ">当然，这种情况在模型和 Harness 越来越强的情况下，越来越少了。</p>
<p style="color: #191b1f;" data-pid="djeNisIs">当前，判断开发工程师价值的关键指标，在于具体业务子领域是否还需要我们深入理解系统的内部实现，以及需要理解到何种程度。</p>
<h2 style="font-weight: 500; color: #191b1f;">复杂系统的黑盒沉降</h2>
<p style="color: #191b1f;" data-pid="zs9kfW9W">我们原本设想黑盒开发只会停留在低风险的边缘地带。那些生命周期以周计的营销脚本、孤立的数据管道、或者一次性的内部报表工具，被开发者直接当成黑盒丢给模型演化。只要输入和输出符合预期，内部堆叠了多少冗余逻辑，人类完全不需要过问。</p>
<p style="color: #191b1f;" data-pid="wx1PahnO">这种边界在过去半年里被全面打破。</p>
<p style="color: #191b1f;" data-pid="PbvqLQvM">在真实的研发场景中，复杂的长周期核心系统同样正在被迅速黑盒化。系统的实现逻辑已经开始脱离了我们的掌控。</p>
<p style="color: #191b1f;" data-pid="kY9RNigU">有些系统已经没有任何一个工程师完整读过底层实现，所有人都在依靠行为验收来驱动迭代。</p>
<p style="color: #191b1f;" data-pid="ubT04B-7">这一变化的驱动力来自工程吞吐量的极端失衡。过去由一个六人资深团队耗时三个月才能搭建完的复杂状态机，现在借助高阶推理模型，两天内就能生成数万行代码，并配套数千个通过的单元测试与端到端用例。当代码生成的速率超越人类视网膜与大脑工作记忆的物理极限时，人工逐行审查机制就失去了事实上的防御能力。</p>
<p style="color: #191b1f;" data-pid="8TlakflU">我们开始被迫接受这种现实。 在面对极其庞大的调用拓扑和复杂的上下文依赖时，我们已经放弃了对抽象语法树的微观控制，转而把整个复杂系统视作一个自组织的、不透明的动力学网络。</p>
<p style="color: #191b1f;" data-pid="rxfo_DOe">这种转变直接剥夺了传统工程学给我们带来的安全感。当不懂内部机理的团队开始掌控这种复杂黑盒系统时，表面上的研发效率提升与潜伏的系统性崩溃风险就绑定在了一起。</p>
<h2 style="font-weight: 500; color: #191b1f;">演化哲学的工程代价</h2>
<p style="color: #191b1f;" data-pid="uRHD6plh">把软件代码视同生物 DNA 序列，认为系统可以在目标函数的约束下自然演化，是黑盒派的核心立论。生物演化积累了大量的无用突变、历史包袱与内含子序列，依然能在残酷的自然选择中维持机体运转。在很多开发者的设想里，只要外部的行为约束足够严格，内部的代码哪怕堆积成山，系统也能通过模型的自我修剪持续向后演化。</p>
<p style="color: #191b1f;" data-pid="IR2Saaln">在复杂系统中使用演化哲学，会遭遇物理层面的硬约束。</p>
<p style="color: #191b1f;" data-pid="1TQvfq1E">软件系统与生物体存在本质区别。生物体拥有物理世界施加的绝对法则限制，而软件的目标函数是由人类通过测试套件和契约规范手工定义的。人类工程师编写的测试用例，无论规模多么庞大，都只能覆盖有限的状态空间。</p>
<p style="color: #191b1f;" data-pid="m12Usug9">当模型在复杂的业务系统里自我演化时，它会不断寻找使测试用例全绿的阻力最小路径。在处理一个涉及分布式两阶段提交的业务分支时，模型为了修复一个并发竞争的偶发报错，可能会在关键路径上引入一段极其隐蔽的全局读写锁，或者擅自放宽事务隔离级别。从行为验收的角度看，那十几个报错的测试用例确实顺利通过了。</p>
<p style="color: #191b1f;" data-pid="SNdO04F6">这种微小的局部优化在多轮提示词和版本迭代后，会引发全局架构的雪崩。并发瓶颈从数据库行锁被硬生生搬运到了应用层内存，系统的吞吐量在特定流量脉冲下出现断崖式下跌。由于团队没有人理解这段被模型演化出来的内部调度机制，监控指标只能呈现出 CPU 软中断升高与连接池耗尽，排查人员根本无法把这种宏观症状与某个被模型悄悄修改的同步原语对应起来。</p>
<p style="color: #191b1f;" data-pid="TA4JLzG5">生物演化历经数十亿年，其代价是无数个体的死亡与物种灭绝。在商业软件工程中，任何一次演化走入死胡同，换来的都是真实的资损、核心数据损坏与长达数小时的服务不可用。</p>
<h2 style="font-weight: 500; color: #191b1f;">重写的大坑与暗知识</h2>
<p style="color: #191b1f;" data-pid="9oG8d6EP">当代码产出如此快速时，以前不敢做的重构操作，现在频频出现在现实之中。</p>
<p style="color: #191b1f;" data-pid="zdMFwx_o">并且对于黑盒的代码，当无法维护时，会有人开始直接让 AI 来重写了。</p>
<p style="color: #191b1f;" data-pid="KEeccMgZ">对于极度依赖隐性规则的复杂系统，重写会是一个大坑。</p>
<p style="color: #191b1f;" data-pid="kiv0vGr1">复杂系统的内部实现中，沉淀了海量的暗知识。这些暗知识由线上历史事故、冷门协议的缺陷规避、上下游陈旧系统的怪异行为，以及特定硬件环境下的性能妥协交织而成。在漫长的迭代周期里，这些细节几乎不可能被完整记录在需求文档或提示词工程的上下文中。</p>
<p style="color: #191b1f;" data-pid="td5miB45">当一个承载核心业务的复杂黑盒系统遭遇架构死锁时，工程师试图命令模型从零重写一套全新的系统。模型根据现有的规范文档和接口契约，迅速生成了一个结构干净、抽象完美的新架构。但在接入真实生产流量的瞬间，系统会被各种意想不到的边缘异常彻底击穿。</p>
<p style="color: #191b1f;" data-pid="BRqVSo3M">老系统中那些看似丑陋的防重试逻辑、硬编码的延时等待以及反常的类型转换，恰恰是系统在生产环境存活多年的抗体。在黑盒演化过程中，这些抗体没有被人类工程师转化为显性的设计原则，而是散落在无法辨识的代码废墟中。</p>
<p style="color: #191b1f;" data-pid="OxA-QEzG">当模型完全接管了代码，人类不再阅读实现细节，这些暗知识就随着上下文窗口的更迭永久失传了。重写一个复杂黑盒系统，意味着要重新把过去五年踩过的所有生产故障从头经历一遍。这种重写代价根本不是代码生成速度能够弥补的。</p>
<h2 style="font-weight: 500; color: #191b1f;">工程师的剩余价值</h2>
<p style="color: #191b1f;" data-pid="ya-uSEaV">在复杂的长周期系统也逐步被黑盒代码吞噬的周期里，技术团队内充斥着工程师技能贬值的焦虑。天天面对自己看不懂或者懒得去看的自动化生成代码，开发人员的专业尊严受到了直接侵蚀。</p>
<p style="color: #191b1f;" data-pid="Oh_yzmAI">开发人员的剩余价值非但没有消失，反而在系统复杂度失控的悬崖边缘被急速放大。只不过，这种价值的落点发生转移。</p>
<p style="color: #191b1f;" data-pid="CN3pFVA5">编写算法、拼装框架、修补语法的低阶体力劳动被彻底剥离。工程师的核心价值，收敛为以下几项不可替代的底层能力：</p>
<p style="color: #191b1f;" data-pid="ztaGaHnU">第一，是定义系统物理边界与失败容忍度的系统论设计能力。当实现细节彻底黑盒化，整个系统的生存完全取决于外部边界是否足够坚固。如何设计熔断逻辑，如何划分数据强一致性与最终一致性的边界，如何定义灾难恢复时的降级降速策略，这些决策直接关乎企业的商业生死，不可能托付给缺乏全局上下文责任感的模型。</p>
<p style="color: #191b1f;" data-pid="7Tr8M8Mm">第二，是穿透软件抽象层、理解物理基础设施底层的能力。不论上层代码被模型演化得多像一个不可名状的黑盒，当它最终被编译成汇编指令落到物理 CPU、内存条、网卡与固态硬盘上时，它依然必须服从物理世界的客观定律。</p>
<p style="color: #191b1f;" data-pid="xPyMRUHb">在跨机房专线延迟突增、底层存储硬件出现坏块、操作系统内核调度产生微秒级锁死等极端场景下，缺乏真实物理世界感知的黑盒系统会瞬间丧失全部应对能力。此时能够挽救全局的，永远是那些清楚 Linux 内核参数配置、深刻理解 TCP 拥塞控制算法、明白数据库 B+ 树与 LSM-Tree 存储引擎底层权衡的工程师。</p>
<p style="color: #191b1f;" data-pid="fuZFHL84">第三，是决定何处必须保留白盒的战略决断力。在复杂的业务全景图中，我们必须划出一条绝对的红线。在这条红线之外，允许模型疯狂试错、野蛮生长、黑盒演化，以换取交付速度；而在红线之内，在涉及资产结算、核心密码学协议、权限鉴权核心以及数据持久化原子性的基石模块上，我们必须死守白盒阵地，每一个状态位、每一行锁逻辑、每一个并发屏障，都必须由人类大脑完全理解、推演与严格审计。</p>
<p style="color: #191b1f;" data-pid="xOKFz_XF">拥抱黑盒演化是应对代码生产力爆发的必然妥协，但盲目迷信黑盒则是工程理性的自杀。</p>
<h2 style="font-weight: 500; color: #191b1f;">白盒边界的落地</h2>
<p style="color: #191b1f;" data-pid="u6u-2D7y">不过，把红线画出来，还远远不够。</p>
<p style="color: #191b1f;" data-pid="3TOVZB7c">如果白盒意味着所有代码都要由人逐行理解，那么几万行的关键模块很快就会耗尽团队的审查能力。如果白盒只意味着安排一位资深工程师签字，它又会退化成一种形式上的责任分配：代码已经合入，签字的人却无法解释失败路径。</p>
<p style="color: #191b1f;" data-pid="2iM7k00x">从实际出发，白盒要求可以落实到几个可以检查的问题上：</p>
<p style="color: #191b1f;" data-pid="8ExoQysA">这个模块维护哪些状态？哪些状态转换绝对不允许发生？一次操作在什么位置产生不可撤销的影响？执行中断以后，如何判断它究竟完成到了哪里？修改这里，会影响哪些调用方的既有假设？</p>
<p style="color: #191b1f;" data-pid="uYIgU_Qh">负责的工程师需要能够独立回答这些问题，并且指出对应的实现位置。</p>
<p style="color: #191b1f;" data-pid="QDkcBXIS">这里仍然允许模型生成代码。我们限制的是未经理解的关键变更进入系统。生成与理解可以由不同的过程完成，但理解不能被一份自动生成的说明替代。</p>
<p style="color: #191b1f;" data-pid="jCIU3fgp">边界还需要沿着依赖关系检查。</p>
<p style="color: #191b1f;" data-pid="EfRrb8xE">假设一个关键模块本身经过了严格审查，但它依赖的公共组件被模型修改了失败处理方式，原有结论就可能失效。白盒边界如果只按目录划分，很容易漏掉这种变化。把关键模块依赖的行为约定一起纳入审查范围，尤其是超时、重试、错误返回和状态写入的语义。</p>
<p style="color: #191b1f;" data-pid="6bXlZnyf">这样做会拖慢部分公共组件的修改速度。我们需要接受这笔成本，也需要控制它的规模。如果一个普通改动总要召集半个团队评审，说明关键模块依赖了过多外部细节，应该考虑收缩接口和状态共享范围。</p>
<p style="color: #191b1f;" data-pid="kdju40Tz">白盒区域也不必永久固定。一个模块的状态被拆出、接口变得稳定、替换方式得到验证之后，可以降低内部审查强度。一个原本独立的工具开始承接共享数据写入，就应该重新评估。</p>
<p style="color: #191b1f;" data-pid="Tk1PBKdg">「核心业务」四个字无法覆盖整个代码库。范围画得太大，最后往往只能整体降低执行标准。</p>
<h2 style="font-weight: 500; color: #191b1f;">验证独立</h2>
<p style="color: #191b1f;" data-pid="D2_U59l8">黑盒能接受到什么程度，取决于我们能够从外部验证什么。</p>
<p style="color: #191b1f;" data-pid="Rly4DqfF">这里最容易出现的问题，是实现、测试和验收结论都来自同一条生成链路。模型先理解需求，再生成代码，随后根据代码补齐测试，最后告诉工程师所有测试已经通过。</p>
<p style="color: #191b1f;" data-pid="aZs4ZKH-">如果最初的理解遗漏了一个条件，这个条件就可能同时从实现与测试中消失。</p>
<p style="color: #191b1f;" data-pid="-ebw-xIh">覆盖率无法自动发现这种遗漏。某一行代码被执行过，只能证明测试经过了那里。异常分支执行以后留下的状态是否正确，仍然需要另外检查。</p>
<p style="color: #191b1f;" data-pid="lFgeY2Bo">因此，关键验收条件在实现之前确定。至少把正常结果、禁止出现的结果，以及失败后允许留下的状态写清楚。</p>
<p style="color: #191b1f;" data-pid="yrmpY1-g">尤其要检查那些跨越多个动作的过程：前一个动作已经完成，后一个动作失败，系统应该停在哪里？用户重新发起请求时，允许重新执行哪些部分？超时之后，调用方能否把结果当成失败？</p>
<p style="color: #191b1f;" data-pid="Y22T_BDW">这些问题没有答案时，继续增加测试数量并不能补上设计缺口。</p>
<p style="color: #191b1f;" data-pid="e2UmrkSa">模型可以帮助枚举场景、生成数据、执行验证。涉及业务承诺的条件，需要由了解系统的人确认。验收阶段还应保留独立于当前实现的依据，避免模型通过修改预期结果，让实现重新获得通过。</p>
<p style="color: #191b1f;" data-pid="jVieyvOO">测试本身的变更也要审查。</p>
<p style="color: #191b1f;" data-pid="HIE5vX1g">删除一个失败用例，有时是因为旧需求确实失效，有时却只是当前实现暂时无法满足它。二者在测试报告上没有区别。对稳定约束的删除、放宽和跳过，我会要求单独解释，不能夹在几千行功能改动里一起合入。</p>
<p style="color: #191b1f;" data-pid="zzQdOCQn">验证强度当然有成本。复杂故障场景需要准备环境，大规模数据验证消耗资源，长时间运行的检查会增加反馈延迟。我的做法是分开执行频率：局部检查跟随每次修改，影响面较大的验证放在合入和发布环节，昂贵的故障验证集中覆盖关键路径。</p>
<p style="color: #191b1f;" data-pid="TvXYcJBx">目标是让不同风险都有对应的检查位置，同时避免每次修改都等待一套庞大的验证流程。</p>
<h2 style="font-weight: 500; color: #191b1f;">限制单次变化</h2>
<p style="color: #191b1f;" data-pid="Y58ARTod">代码生成速度上来以后，团队很容易把更大的任务直接交给 agent。</p>
<p style="color: #191b1f;" data-pid="uKjtTjSI">一个需求里同时包含接口调整、状态机修改、数据结构迁移和历史代码整理。模型可能一次完成，最终差异也显得相当统一。但人工审查时，很难再把每一处变化与最初的动机对应起来。</p>
<p style="color: #191b1f;" data-pid="EiUnH35Q">发生回归以后，定位范围同样会扩大。</p>
<p style="color: #191b1f;" data-pid="nbM6g0dw">我们要控制单次变更所包含的独立决策数量。功能增加、结构整理和历史兼容性清理，尽量分开进行。每一部分都需要有能够单独解释的目的，以及对应的验证结果。</p>
<p style="color: #191b1f;" data-pid="wUE1mK6R">这里不适合机械地规定 PR 行数。一次自动生成的重复性修改可能涉及很多文件，实际语义变化很少；一个几行的条件调整，也可能改变整个系统的状态约束。</p>
<p style="color: #191b1f;" data-pid="anRazQXE">这次只改变了哪些行为，哪些证据支持这些变化，以及如何撤销？</p>
<p style="color: #191b1f;" data-pid="L8dzShB7">让 agent 先提交修改计划，也有帮助。人可以在生成大量代码之前检查它准备触及哪些模块，是否引入新的共享状态，是否扩大已有接口的职责。</p>
<p style="color: #191b1f;" data-pid="QCfVe4mz">当然，计划不能被当成执行事实。模型在修复过程中可能偏离原计划，最终仍然需要核对实际改动。尤其要检查那些为了让测试通过而新增的兼容分支、默认值和异常处理。</p>
<p style="color: #191b1f;" data-pid="plA-m70t">另外，权限应该跟着风险划分。修改实现、调整验收标准、操作生产数据，是三个不同等级的动作。不能因为 agent 已经获得代码仓库的写权限，就顺带允许它自行决定另外两件事。</p>
<h2 style="font-weight: 500; color: #191b1f;">保留接管能力</h2>
<p style="color: #191b1f;" data-pid="gpAd5mTC">即便验证做得足够认真，团队仍然需要准备一种情况：系统出了问题，agent 连续几轮都没有找到原因。</p>
<p style="color: #191b1f;" data-pid="IJ_JjIsx">这时工程师需要接管什么？</p>
<p style="color: #191b1f;" data-pid="VV2ZX85_">首先要接管的是故障判断和损失控制。当前还有哪些请求正在写入？哪些动作可以暂停？哪些状态已经无法直接撤销？继续重试是否会重复产生副作用？</p>
<p style="color: #191b1f;" data-pid="MU-aqOMZ">如果这些问题无法回答，贸然修复代码可能让后续处理更困难。</p>
<p style="color: #191b1f;" data-pid="iobUF_SY">因此，关键路径上的可观测信息需要围绕状态变化设计。只记录异常堆栈，通常不足以判断一个业务动作完成到了哪里。需要能够把一次操作的开始、关键写入和最终结果关联起来，同时区分「没有执行」与「执行完成但没有返回」。</p>
<p style="color: #191b1f;" data-pid="u6Z80aW-">信息越多，记录与检索的成本也越高。涉及数据内容时，还需要限制记录范围。我不会要求把所有局部变量都写进日志，而会优先保留能够判断执行阶段和副作用的信息。</p>
<p style="color: #191b1f;" data-pid="3S_VEuXj">恢复方案也要区分代码与数据。</p>
<p style="color: #191b1f;" data-pid="Maz34_BR">代码版本退回以后，新版本已经写入的数据仍然存在。旧实现能否读取这些数据，正在执行的任务能否继续，外部已经发生的动作如何处理，都需要提前确认。一个发布平台上存在的回滚按钮，不能证明业务状态能够恢复。</p>
<p style="color: #191b1f;" data-pid="rm2kcc6N">团队可以让模型生成恢复方案，但需要通过演练检查其中的假设。演练时暴露出一个无法判断完成状态的步骤，就应该补充相应的记录或操作方式。</p>
<p style="color: #191b1f;" data-pid="MpV55fKi">这些工作会占用原本可以拿来交付功能的时间。我们要优先安排给那些失败后难以恢复、影响范围又大的模块。对于可以直接丢弃并重新生成的结果，没有必要采用同样的投入标准。</p>
<p style="color: #191b1f;" data-pid="Z3T2_yAL">我们也不应该假设所有故障最终都需要人手工解决。agent 能完成的排查可以继续交给它。人需要保留的是判断它是否仍在有效推进的能力，以及必要时改变排查方向、停止危险操作的权限。</p>
<h2 style="font-weight: 500; color: #191b1f;">把暗知识留下</h2>
<p style="color: #191b1f;" data-pid="UIwzKZRF">关于暗知识，还有一个地方需要说得更准确。</p>
<p style="color: #191b1f;" data-pid="IpHt2JEf">只要代码、历史记录和运行证据仍然存在，很多知识就有机会被重新找回来。但随着版本累积，重新发现它们需要的时间会增加。故障发生时，团队未必有这个时间。</p>
<p style="color: #191b1f;" data-pid="i3Jgfsmt">在每次关键修改中，顺手留下三类信息：这段特殊处理保护了什么约束，当初在什么条件下出现过问题，以及什么证据能够证明它将来可以被删除。</p>
<p style="color: #191b1f;" data-pid="HiP-dZPS">单独写一句「兼容历史逻辑」，对后来的维护者帮助很有限。模型也无法据此判断，这段逻辑究竟承担了必要的保护，还是早已失效的补丁。</p>
<p style="color: #191b1f;" data-pid="-2joDuOd">记录需要贴近决策发生的位置。约束可以进入验收条件，历史问题可以转化为回归场景，难以自动验证的条件则保留在模块说明和变更记录里。没有必要把所有知识都塞进一份不断膨胀的架构文档。</p>
<p style="color: #191b1f;" data-pid="YkLALh4Z">尤其要防止另一种浪费：模型生成了大量说明，团队却没有检查其中哪些是事实，哪些只是根据代码推测出来的意图。</p>
<p style="color: #191b1f;" data-pid="rxuHUBop">「当前代码这样执行」和「系统必须这样执行」需要分开记录。前者描述实现，后者约束未来修改。混在一起，模型可能把历史偶然行为永久固化，也可能把必须保留的规则当成可整理的细节删除。</p>
<p style="color: #191b1f;" data-pid="KIxgcZ73">重写之前，这些记录应当成为核对清单。找不到来源的特殊逻辑，需要进一步追踪，不能仅凭实现难看就判定它没有价值。</p>
<p style="color: #191b1f;" data-pid="8ZcaEWMr">同样，也不能把所有历史补丁都当作不可触碰的要求。长期保留失效约束，会让新系统继续背负旧系统的复杂度。删除它们需要证据，保留它们也应该能够说明理由。</p>
<h2 style="font-weight: 500; color: #191b1f;">重新分配时间</h2>
<p style="color: #191b1f;" data-pid="7dgu19QQ">这会改变工程师日常工作的时间分配。</p>
<p style="color: #191b1f;" data-pid="P8Cz1irA">一部分原本用于编写常规实现的时间，可以转移到约束定义、关键路径审查、失败恢复和演化设计上。另一部分确实可以节省下来，用于交付更多需求。我们没有必要为了证明专业价值，把节省的时间全部重新填满。</p>
<p style="color: #191b1f;" data-pid="F1AqUNOy">不要求所有工程师都深入内核、网络协议和存储引擎。基础设施知识在某些问题上非常关键，但大量故障仍然来自业务状态、依赖关系和错误的边界假设。团队需要根据系统的风险分布建立能力，不能用一套底层知识清单替代具体判断。</p>
<p style="color: #191b1f;" data-pid="3R_ly8xo">对于个人：能否识别模型没有回答的问题，能否发现验证结论依赖了未经确认的假设，能否在陌生实现中定位关键状态，以及能否解释一次技术选择会给后续修改增加什么成本。</p>
<p style="color: #191b1f;" data-pid="Iq7CZ4dh">这些能力也需要通过实际接触代码培养。如果年轻工程师长期只负责转发需求和粘贴错误，却没有机会追踪实现、分析失败、参与关键决策，团队几年后可能会出现知识传承的问题。</p>
<p style="color: #191b1f;" data-pid="lpfV9T9e">因此，我们需要让工程师对完整的局部问题负责：提出约束、使用模型实现、解释关键行为、验证异常路径，并参与上线后的问题处理。模型可以承担其中大量工作，但负责的人需要能够检查结果。</p>
<p style="color: #191b1f;" data-pid="G_CY-3wb">团队层面的评价也要变化。生成了多少代码、完成了多少轮对话，都不适合作为主要指标。我们需要观察一次需求从提出到稳定运行花了多久，评审与返工占用了多少时间，线上问题是否反复出现，以及一个模块是否越来越难由其他人接手。</p>
<p style="color: #191b1f;" data-pid="d9TbmMXL">如果编码时间缩短了，排查和返工时间却持续增长，就需要调整当前流程。交付数量暂时增加，也不能掩盖恢复能力和知识覆盖的下降。</p>
<p style="color: #191b1f;" data-pid="h28O_liH">回到最初那个数千行代码、测试全绿的 PR，我们不会因为提交人无法背出全部实现就否定它。</p>
<p style="color: #191b1f;" data-pid="6P82LnqA">但如果他无法解释核心异常分支的状态变化，我们要要求补上这部分理解：追踪关键写入，核对失败后的状态，检查重复执行的后果，再决定是否需要增加验证或修改实现。</p>
<p style="color: #191b1f;" data-pid="0OBqAuG8">这个 PR 可以继续由模型完成修改。合入之前，负责的工程师需要知道哪些结论已经得到验证，哪些风险仍然存在，以及出问题以后应该从哪里开始处理。</p>
<p style="color: #191b1f;" data-pid="IUhl1_zR">以上。</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/10/vibe-coding/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DeepSeek Harness 的 Harness 到底做了什么</title>
		<link>https://www.phppan.com/2026/09/deepseek-harness-and-what-is-harness/</link>
		<comments>https://www.phppan.com/2026/09/deepseek-harness-and-what-is-harness/#comments</comments>
		<pubDate>Sat, 26 Sep 2026 04:51:36 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>
		<category><![CDATA[DSH]]></category>
		<category><![CDATA[harness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2541</guid>
		<description><![CDATA[DeepSeek Harness 解决的核心问题，是多轮模型调用、工具执行与会话状态之间的协调：输入何时进入请 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">DeepSeek Harness 解决的核心问题，是多轮模型调用、工具执行与会话状态之间的协调：输入何时进入请求，工具结果如何写回历史，请求失败后从哪里重试，上下文压缩后保留哪些信息，以及任务中断后能够恢复到什么状态。</p>
<p data-tool="mdnice编辑器">围绕这些问题，框架将运行过程拆分为插件装配、回合驱动、会话日志、工具管道和异常处理。分析其架构的关键，在于各部分的状态边界，以及这些边界提供的保证与限制。</p>
<p data-tool="mdnice编辑器">本篇文章限定在提交 477b4f4205（2026 年 9 月 25 日）对应的实现。</p>
<h1 data-tool="mdnice编辑器"><span class="content">1. 插件装配</span></h1>
<p data-tool="mdnice编辑器">插件是 DeepSeek Harness 的核心能力，插件的配置决定 Agent 的运行能力。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 通过 Cordis 插件组织运行能力。Cordis 提供 Context、Service、Plugin 和 Event 等抽象，插件通过依赖注入与事件系统协作，并由框架管理生命周期。</p>
<p data-tool="mdnice编辑器">LLM 服务、Session、Agent、AgentLoop 和工具注册表等基础组件，由 bundle 统一挂载。启动时，<code>dsh</code> 以 profile 为入口加载配置，按 id 定位配置行，并替换对应的整段 config。</p>
<p data-tool="mdnice编辑器">这里的配置语义是<strong>整段替换，而非字段级合并</strong>。覆盖某个组件的配置时，原配置中未被保留的字段也可能随之消失。</p>
<p data-tool="mdnice编辑器">插件装配因此直接影响 Agent 的实际行为：模型服务是否可用、哪些工具被注册、哪些事件处理器参与执行，都取决于最终加载的插件与配置。仓库包含某项能力，并不等于当前运行实例已经启用该能力。</p>
<p data-tool="mdnice编辑器">这种设计便于替换组件和组合能力，但也让执行逻辑分布在多个插件中。分析一次请求的行为，需要同时查看核心循环、插件注册关系和最终生效的配置。</p>
<h1 data-tool="mdnice编辑器"><span class="content">2. 核心循环</span></h1>
<p data-tool="mdnice编辑器">Agent 循环是 Agent 的核心逻辑，其主要区分回合、步骤与请求重试。</p>
<p data-tool="mdnice编辑器">从实际的工作逻辑上来看，<code>agent-loop</code> 负责驱动模型与工具之间的循环。其主要执行路径包括：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>准备模型请求，解析配置并选择适配器。</section>
</li>
<li>
<section>投影系统提示词，提交本次需要进入历史的用户输入。</section>
</li>
<li>
<section>从会话构造请求，接收并累积流式响应。</section>
</li>
<li>
<section>将 assistant 消息写入 Session。</section>
</li>
<li>
<section>执行模型提出的工具调用，并将结果交给后续模型请求。</section>
</li>
<li>
<section>在没有工具调用或满足结束条件时，结束本轮执行。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">理解这条路径，需要分三个层次：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>turn</strong>：由用户输入驱动的回合。</section>
</li>
<li>
<section><strong>step</strong>：回合内部的执行边界。</section>
</li>
<li>
<section><strong>请求尝试</strong>：一次实际发往模型的调用，失败后可以在同一个 step 内重试。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">因此，step 不能简单等同于一次网络请求。重试可能产生多次请求尝试，但不应重复消费输入或重复提交用户消息。</p>
<p data-tool="mdnice编辑器">Agent 维护 <code>next-turn</code> 和 <code>next-step</code> 两个队列，分别承接下一回合与下一步的输入。队列修改以 <code>agent/inbox/spliced</code> 事件写入 Session，待处理输入可以从日志投影恢复。</p>
<p data-tool="mdnice编辑器">这一设计区分了「系统已经收到消息」和「模型已经消费消息」。</p>
<p data-tool="mdnice编辑器">执行过程中到达的补充要求，需要在相应边界进入后续请求。它不能自动改变已经发出的模型请求，也不能撤销已经发生的工具副作用。输入排队与执行取消因此属于不同机制。</p>
<p data-tool="mdnice编辑器">将队列变化写入日志，还能避免恢复时只找回聊天记录，却丢失尚未处理的输入。</p>
<h1 data-tool="mdnice编辑器"><span class="content">3. Session</span></h1>
<p data-tool="mdnice编辑器">Session 机制实现了运行记录与模型历史分离。</p>
<p data-tool="mdnice编辑器">Session 被设计为只追加的事件日志，事件通过递增序列号确定顺序，以 <code>type</code> 区分类型，以 <code>body</code> 保存内容。</p>
<p data-tool="mdnice编辑器">日志不仅记录用户与 assistant 消息，也记录请求失败、输入队列变化、工具执行和压缩过程等运行事实。</p>
<p data-tool="mdnice编辑器">但<strong>日志中存在的事件，不一定进入模型上下文</strong>。</p>
<p data-tool="mdnice编辑器">真正发送给模型的 <code>messages</code>，由 Session 的 surface 通过 <code>deriveMessages()</code> 重建，再冻结为不可变请求。只有进入 surface 的消息事件参与历史投影；<code>assistant/attempt</code> 等运行记录不会自动成为模型消息。</p>
<p data-tool="mdnice编辑器">这一分离有两个作用：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>保留排障信息</strong>：请求失败与恢复过程可以完整记录，而不必全部占用上下文窗口。</section>
</li>
<li>
<section><strong>允许调整可见历史</strong>：压缩或替换模型上下文时，仍能保留原始事件。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Session 的 append 还会检查 JSON 可序列化性、事件规则及 surface 替换的合法性，以减少无法持久化或无法正确投影的数据进入日志。</p>
<p data-tool="mdnice编辑器"><strong>日志恢复不等于外部动作回滚</strong></p>
<p data-tool="mdnice编辑器">只追加日志为审计与状态重建提供了基础，但不能单独保证任意中断点都能无损恢复。</p>
<p data-tool="mdnice编辑器">例如，工具已经修改文件，但结果尚未写入 Session 时进程退出，外部环境与日志之间就可能出现状态差异。恢复日志可以还原已提交的记录，却不能仅凭「缺少结果」判断工具没有执行。</p>
<p data-tool="mdnice编辑器">因此，恢复能力需要区分：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>已提交事件及其投影的恢复；</section>
</li>
<li>
<section>外部副作用的确认与处理。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">后者还依赖工具的幂等性、状态检查能力或额外的事务机制，不能仅由 Session 的追加式设计推导出来。</p>
<h1 data-tool="mdnice编辑器"><span class="content">4. 工具管道</span></h1>
<p data-tool="mdnice编辑器">工具管道实现了从模型意图到受控执行。</p>
<p data-tool="mdnice编辑器">工具注册表只向模型暴露 name、description、parameters 等白名单字段。执行函数、超时设置和并发属性保留在运行时。</p>
<p data-tool="mdnice编辑器">这将模型侧的工具描述与运行时的执行控制分开：模型负责提出调用，框架负责判断调用是否可执行，以及如何执行。</p>
<p data-tool="mdnice编辑器">一次工具调用依次经过：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>解析模型生成的参数。</section>
</li>
<li>
<section>查找当前 Agent 可见的工具。</section>
</li>
<li>
<section>通过 <code>tools/pre-execute</code> 进行执行前检查，允许或拒绝调用。</section>
</li>
<li>
<section>进入 <code>tools/execute</code> 包装链。</section>
</li>
<li>
<section>通过 <code>tools/post-execute</code> 处理结果。</section>
</li>
<li>
<section>将结果写回 Session，供后续模型请求使用。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">权限检查、执行包装和结果处理因而拥有独立的介入位置，不必全部写进工具函数。</p>
<p data-tool="mdnice编辑器"><strong>并发执行与顺序提交分离</strong></p>
<p data-tool="mdnice编辑器">同一条 assistant 消息可能提出多个工具调用。调度器通过工具的 <code>isConcurrencySafe</code> 属性分类：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>明确返回 <code>true</code> 的调用进入并行池。</section>
</li>
<li>
<section>其他调用作为独占屏障处理。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">并发是显式声明的能力，未声明安全的工具不会默认并行执行。</p>
<p data-tool="mdnice编辑器">工具本体可以并发，但准备、后处理、<code>tool/result</code> 写入及附加 context 入队，按模型调用顺序提交。这样可以避免工具完成时间的随机性直接改变会话中的结果顺序。</p>
<p data-tool="mdnice编辑器">代价是可能出现等待：后面的工具即使先完成，也需要等待前面的调用提交。并发能够缩短部分执行时间，却不保证结果可以立即进入历史；提前完成的结果还可能需要暂存。</p>
<p data-tool="mdnice编辑器">此外，<strong>提交顺序稳定不等于外部副作用确定</strong>。并发工具是否真正安全，仍取决于它们是否访问共享状态、是否存在执行依赖，以及并发声明是否准确。</p>
<h1 data-tool="mdnice编辑器"><span class="content">5. 安全隔离</span></h1>
<p data-tool="mdnice编辑器">DSH 在实现上让审批与执行限制分别生效，以实现安全隔离。</p>
<p data-tool="mdnice编辑器">Harness 的沙箱模式包括：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">type</span> SandboxMode =
  | <span class="hljs-string">'read-only'</span>
  | <span class="hljs-string">'workspace-write'</span>
  | <span class="hljs-string">'danger-full-access'</span>
</code></pre>
<p data-tool="mdnice编辑器">策略涉及文件路径权限、命令执行限制和权限提升。工具可以通过 <code>justification</code> 与 <code>sandbox_permissions</code> 请求提升权限，再由 approval 机制处理审批。</p>
<p data-tool="mdnice编辑器">基础 bundle 默认挂载 <code>workspace-write</code> 文件沙箱与 <code>ask</code> 审批策略；<code>sandbox-policy</code> 将当前文件操作模式解析给实际后端。</p>
<p data-tool="mdnice编辑器">这一结构包含三个不同层次：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>工具可见性</strong>：模型能够提出哪些调用。</section>
</li>
<li>
<section><strong>执行审批</strong>：某次调用是否获得许可。</section>
</li>
<li>
<section><strong>后端隔离</strong>：获准执行后，实际能够访问哪些资源。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">三者不能相互替代。审批通过只说明动作获得许可，不代表执行环境具有无限权限；模型看不到某个工具，也不能据此证明其他工具无法访问相同资源。</p>
<p data-tool="mdnice编辑器">安全边界最终取决于策略配置与实际后端的共同约束，而不只是沙箱模式的名称。</p>
<h1 data-tool="mdnice编辑器"><span class="content">6. 异常收敛</span></h1>
<p data-tool="mdnice编辑器">异常是应用中很常见的东西，也是 Harness 的重点处理对象。</p>
<p data-tool="mdnice编辑器">DSH 通过重试、压缩与取消各自处理不同问题。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.1 请求重试</span></h2>
<p data-tool="mdnice编辑器">通过请求重试来保留失败记录，避免重复提交输入。</p>
<p data-tool="mdnice编辑器">模型请求失败后，系统先写入 <code>assistant/attempt</code>，再交给 <code>agent/request-error</code> 瀑布链处理。</p>
<p data-tool="mdnice编辑器"><code>llm-retry</code> 根据适配器提供的 retry policy，判断错误类型、重试次数与退避方式。计划重试与实际启动分别记录为 <code>llm/retry</code> 和 <code>llm/retry-started</code>。</p>
<p data-tool="mdnice编辑器">重试在同一个 step 中重新准备请求，不重复提交首次用户消息。这一边界避免了网络层重试演变成会话层的重复输入。</p>
<p data-tool="mdnice编辑器">分别记录重试计划与启动，也使日志能够区分“决定重试”与“已经开始下一次尝试”。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.2 上下文压缩</span></h2>
<p data-tool="mdnice编辑器">上下文压缩实现了替换可见历史，保留原始事件。</p>
<p data-tool="mdnice编辑器">自动压缩在 <code>agent/pre-step</code> 检测上下文压力，并记录：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>compaction/start</code></section>
</li>
<li>
<section><code>compaction/summary</code></section>
</li>
<li>
<section><code>compaction/end</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">随后，系统将选中的连续历史区间替换为一个 checkpoint 用户消息。</p>
<p data-tool="mdnice编辑器">这里发生的是 <strong>surface replace</strong>：模型可见历史缩短，原始日志仍然保留。因此，压缩减少的是请求上下文，不等于同步减少持久化存储。</p>
<p data-tool="mdnice编辑器">压缩也引入了信息损失的可能。原始约束即使仍在日志中，只要没有进入摘要或剩余 surface，后续模型就无法直接使用。压缩是否有效，除了看上下文长度，还取决于任务目标、限制条件和未完成事项是否得到保留。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.3 取消</span></h2>
<p data-tool="mdnice编辑器">通过取消来传递停止信号，而非撤销既有结果。</p>
<p data-tool="mdnice编辑器">AbortSignal 贯穿回合、请求和工具，使取消信号能够沿执行链传递。</p>
<p data-tool="mdnice编辑器">但信号传递不等于立即停止所有动作。工具是否及时退出，取决于其是否检查并响应取消；已经完成的文件写入或远端操作，也不会因 AbortSignal 自动回滚。</p>
<p data-tool="mdnice编辑器">取消机制提供的是停止继续执行的控制路径，外部副作用的撤销仍需要独立实现。</p>
<h1 data-tool="mdnice编辑器"><span class="content">7. 小结</span></h1>
<p data-tool="mdnice编辑器">DSH 的 Harness 实际上是一种架构的取舍，其取舍考量的是可追溯性与运行成本。</p>
<p data-tool="mdnice编辑器">如下的机制共同形成了 DeepSeek Harness 的主要工程取舍：</p>
<section class="table-container" data-tool="mdnice编辑器">
<table data-draft-node="block" data-draft-type="table" data-size="normal">
<tbody>
<tr>
<th>机制</th>
<th>提供的能力</th>
<th>成本或边界</th>
</tr>
<tr>
<td>Cordis 插件装配</td>
<td>组件替换与能力组合</td>
<td>行为分布在配置和多个处理器中</td>
</tr>
<tr>
<td>turn / step 与输入队列</td>
<td>明确输入归属和执行边界</td>
<td>增加队列状态与事件维护</td>
</tr>
<tr>
<td>追加式 Session</td>
<td>审计与已提交状态重建</td>
<td>日志持续增长，不保证外部动作恰好执行一次</td>
</tr>
<tr>
<td>surface 投影</td>
<td>分离运行记录与模型上下文</td>
<td>需要维护投影一致性与替换规则</td>
</tr>
<tr>
<td>并发执行、顺序提交</td>
<td>利用并发，同时稳定历史顺序</td>
<td>可能产生提交等待与结果暂存</td>
</tr>
<tr>
<td>沙箱与审批</td>
<td>分层控制动作权限</td>
<td>实际保证依赖配置和执行后端</td>
</tr>
<tr>
<td>自动压缩</td>
<td>降低上下文窗口压力</td>
<td>增加摘要成本，并可能丢失任务信息</td>
</tr>
<tr>
<td>AbortSignal</td>
<td>统一传递取消意图</td>
<td>依赖工具配合，不能自动回滚副作用</td>
</tr>
</tbody>
</table>
</section>
<p data-tool="mdnice编辑器">DeepSeek Harness 的核心价值，是将 Agent 执行中的关键状态显式化：输入有队列，执行有边界，请求有可见历史，工具有调度与审批，失败有独立的处理路径。</p>
<p data-tool="mdnice编辑器">这些机制使多轮任务能够被追踪、检查和恢复，同时也明确了框架的能力边界：日志恢复不能替代副作用确认，顺序提交不能替代并发安全，权限审批不能替代环境隔离，上下文压缩也不能保证信息无损。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/09/deepseek-harness-and-what-is-harness/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DeepSeek Harness 的上下文管理、记忆和知识库剖析</title>
		<link>https://www.phppan.com/2026/09/an-analysis-of-deepseek-harnesss-context-management-memory-and-knowledge-base/</link>
		<comments>https://www.phppan.com/2026/09/an-analysis-of-deepseek-harnesss-context-management-memory-and-knowledge-base/#comments</comments>
		<pubDate>Sun, 06 Sep 2026 11:48:53 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2536</guid>
		<description><![CDATA[DeepSeek Harness 里，上下文、记忆和知识库并没有被设计成三个彼此独立的重型系统。它们围绕 Se [&#8230;]]]></description>
				<content:encoded><![CDATA[<div class="Post-RichTextContainer" style="color: #191b1f;">
<div class="css-1od93p9">
<div class="css-376mun">
<div class="RichText ztext Post-RichText css-1petdff">
<p data-first-child="" data-pid="ZVS7zDT9">DeepSeek <a class="RichContent-EntityWord css-b7erz1" style="color: #09408e;" href="https://zhida.zhihu.com/search?content_id=283001209&amp;content_type=Article&amp;match_order=1&amp;q=Harness&amp;zhida_source=entity" target="_blank" data-za-not-track-link="true" data-paste-text="true">Harness</a> 里，上下文、记忆和知识库并没有被设计成三个彼此独立的重型系统。它们围绕 Session Log 协同工作。</p>
<p data-pid="bUf-mryy">用三句话概括：</p>
<ul>
<li data-pid="nzNNv1Y7">上下文管理负责决定当前这一轮模型能看到什么。系统提示词、动态环境、工具定义和会话消息，会在模型调用前完成装配。</li>
<li data-pid="du7DZHN-">记忆机制负责在上下文窗口不足时折叠旧历史。原始会话日志继续保留，模型当前可见的历史被替换成结构化摘要。</li>
<li data-pid="IO-ekMll">知识获取负责把仓库文件和外部信息送入模型。文件通过路径引用和读取工具进入，网络信息通过搜索、抓取工具进入，所有结果最终写回 Session Log。</li>
</ul>
<p data-pid="2531wJkA">三个模块共享一条约束：</p>
<p data-pid="Ol_jid7N">模型看到的内容，需要拥有可追踪的来源，并且能够从会话记录中重新构造。</p>
<p data-pid="sdoo15aJ">这条约束决定了 DeepSeek Harness 的整体形态。上下文不能由控制器随手拼接，工具结果不能停留在进程内存，压缩不能覆盖原始记录，检索结果也不能绕过会话日志直接塞给模型。</p>
<p data-pid="Ztch0Epe">它由此形成了一条完整链路：</p>
<p data-pid="gYE12SPk"><code>SystemPrompt.assemble()</code> 组装当前上下文，<code>ReactLoopAgent.preStep()</code> 决定动态内容是否进入会话，<code>Session.append()</code> 保存事件，<code>Session.deriveMessages()</code> 生成模型历史，<code>buildRequest()</code> 构建最终请求，<code>llm.stream()</code> 完成模型调用。</p>
<p data-pid="Lxwlklrz">理解了这条链路，就大概能了解 DeepSeek Harness 的知识和记忆相关的逻辑了。</p>
<h2 style="font-weight: 500; font-style: inherit;">上下文管理</h2>
<p data-pid="-iIki_pi">上下文管理解决的是一个具体问题：每次调用模型时，系统应该选择、组织并发送哪些内容。</p>
<p data-pid="1OxMbrL6">现在大多数 Agent 系统不会简单维护一个持续增长的字符串，而是使用结构化消息列表管理上下文。较成熟的系统还会根据任务状态，动态选择系统提示词、历史消息、工具定义、运行环境和检索结果。</p>
<p data-pid="T4yW9dCg">真正的难的不是 「是否使用消息列表」，而在于：</p>
<ul>
<li data-pid="7hW92SbN">哪些内容应该进入本轮请求；</li>
<li data-pid="3WylJe-n">不同内容由哪个模块提供；</li>
<li data-pid="TEQOcncN">动态状态发生变化后如何更新；</li>
<li data-pid="Fo-aSMbf">重复信息是否需要再次发送；</li>
<li data-pid="yXNiuvMo">长会话中哪些历史应该保留或压缩；</li>
<li data-pid="7a2E9ue9">服务重启后能否恢复模型当时看到的内容；</li>
<li data-pid="MUCJcQJR">最终请求能否被追踪、审计和重放。</li>
</ul>
<p data-pid="68fGdd0A">DeepSeek Harness 同样以结构化消息为基础，但它没有让 AgentLoop 直接维护所有上下文，而是将上下文拆分为不同来源，在每个 step 开始前统一装配。</p>
<h3 style="font-weight: 500; font-style: inherit;">1. 分层组织上下文</h3>
<p data-pid="WIz0eDZE">DeepSeek Harness 中的上下文主要分为四类：</p>
<ul>
<li data-pid="h36hu1TF">系统规则：角色设定、行为约束和工具使用规范；</li>
<li data-pid="-CNOVSI9">动态状态：当前工作目录、运行环境和终端状态；</li>
<li data-pid="ROvWEBtd">会话历史：用户消息、模型回复和工具调用结果；</li>
<li data-pid="7JX2q7Pw">工具定义：当前允许模型调用的工具及其参数结构。</li>
</ul>
<p data-pid="CpfzbkmD">这些内容分别由 SystemPrompt、Runtime Context、Session 和 ToolRuntime 管理，最后在模型调用前合并。</p>
<p data-pid="df8iKdLJ">这种设计的重点不是改变消息格式，而是明确上下文的来源和职责。AgentLoop 只负责控制执行流程，不直接承担提示词拼接、历史管理和工具注册等工作。</p>
<h3 style="font-weight: 500; font-style: inherit;">2. 动态装配本轮内容</h3>
<p data-pid="sgu7nVD0">每个 step 开始前，<code>ReactLoopAgent.preStep()</code> 会调用 <code>SystemPrompt.assemble()</code>，收集本轮所需的系统提示、动态上下文和工具定义。</p>
<p data-pid="tqSj8DGU">其中：</p>
<ul>
<li data-pid="mY1XQCN0"><code>sections</code> 保存相对稳定的系统提示；</li>
<li data-pid="ZgLFFNa3"><code>contexts</code> 提供会随运行状态变化的环境信息；</li>
<li data-pid="BNF4s-T5"><code>tools</code> 描述当前可用的工具；</li>
<li data-pid="9xk_iDCF"><code>variables</code> 提供提示词渲染需要的变量。</li>
</ul>
<p data-pid="ZfuI-WKm">各插件只负责注册自己的内容，最终由 SystemPrompt 统一生成本轮快照。</p>
<p data-pid="EaCZoEMA">这意味着上下文并不是固定不变的。例如，插件启停、工作目录变化或工具权限调整后，下一次模型调用可以获得最新状态，不需要由 AgentLoop 编写额外的业务分支。</p>
<h3 style="font-weight: 500; font-style: inherit;">3. 避免重复注入动态状态</h3>
<p data-pid="03_Jujqq">动态上下文并不需要每个 step 都重复写入。</p>
<p data-pid="_bCGGseH"><code>RuntimeContextProjection</code> 会比较本轮快照和上一轮快照：</p>
<ul>
<li data-pid="dTQ2-qjZ">如果内容没有变化，就继续沿用已有信息；</li>
<li data-pid="bbu23Z-t">如果内容发生变化，就生成新的上下文消息并写入 Session。</li>
</ul>
<p data-pid="91vc9doZ">这种机制有两个作用。</p>
<p data-pid="zoPD8mBJ">第一，减少重复内容带来的 token 消耗。工作目录和运行环境可能连续多个 step 保持不变，没有必要反复加入会话历史。</p>
<p data-pid="AlGd9TJm">第二，保留状态变化轨迹。当环境发生变化时，新快照会进入 Session，后续可以追踪模型从哪一轮开始看到了新的状态。</p>
<p data-pid="9v-SU_7A">投影状态还可以从已有 Session 中恢复。因此，即使服务重启，系统也能判断某段动态上下文是否已经注入，避免因为进程内状态丢失而重复写入。</p>
<h3 style="font-weight: 500; font-style: inherit;">4. 统一派生会话历史</h3>
<p data-pid="T_QS3lDg">用户消息、模型回复和工具结果不会由控制器临时拼入请求，而是先写入 Session Log，再由 <code>Session.deriveMessages()</code> 派生为模型协议需要的消息列表。</p>
<p data-pid="iSoyk-o2">这使系统能够区分三个层次：</p>
<ul>
<li data-pid="eu_nI505">Log：完整保存原始会话事件；</li>
<li data-pid="AhfnXZpc">Surface：决定当前哪些事件参与模型上下文；</li>
<li data-pid="CmnI0n7m">Messages：最终发送给模型的结构化消息。</li>
</ul>
<p data-pid="5aa8i5ac">例如，工具执行结果先作为 <code>tool/result</code> 写入 Session，下一轮再由 <code>deriveMessages()</code> 转换成模型可以读取的消息。上下文压缩也只调整 Surface，不直接删除原始事件。</p>
<p data-pid="-E0H6G9I">因此，模型输入不是由多个模块临时修改的消息数组，而是由统一的会话记录派生出来的。</p>
<h3 style="font-weight: 500; font-style: inherit;">5. 构建并记录最终请求</h3>
<p data-pid="sl3tcmpK">上下文装配完成后，<code>buildRequest()</code> 会生成最终的模型请求，主要包括：</p>
<ul>
<li data-pid="ixYuPn8-"><code>system</code>：系统提示；</li>
<li data-pid="KtLWEBbm"><code>messages</code>：会话历史和动态上下文；</li>
<li data-pid="DFNV-EZY"><code>tools</code>：当前可用的工具定义；</li>
<li data-pid="Ioyqh8NZ"><code>sessionId</code>：当前会话标识；</li>
<li data-pid="2vAVk5LP"><code>signal</code>：调用控制信号。</li>
</ul>
<p data-pid="hrmW_wLH">同时，系统会记录 <code>request/header</code> 和 <code>request/context</code>，保存本次调用使用的关键上下文信息。</p>
<p data-pid="9eQeRYbp">这样可以回答几个重要问题：</p>
<ul>
<li data-pid="X2p3DZiu">模型当时看到了哪些历史；</li>
<li data-pid="CZAABVm5">使用了哪一版系统提示；</li>
<li data-pid="8t_iHaWU">当时有哪些可用工具；</li>
<li data-pid="saQ8_8Wk">动态环境是否已经发生变化；</li>
<li data-pid="DH5Ke_TC">某次异常是模型推理问题，还是输入上下文问题。</li>
</ul>
<p data-pid="mRQvX3q1">DeepSeek Harness 的上下文管理并不是简单地将内容放进消息列表，而是围绕分层管理、动态装配、变化检测和统一派生建立完整流程。</p>
<p data-pid="PKxIbdE1">其核心链路可以概括为：</p>
<p data-pid="EPI8iPtm">各模块提供上下文 → SystemPrompt 动态装配 → RuntimeContextProjection 检测变化 → Session 派生历史消息 → buildRequest 构建并记录最终请求。</p>
<p data-pid="X0YKAYYv">这样，每段上下文都有明确来源，动态信息不会被无意义地重复发送，模型输入可以从 Session 中恢复和追踪，AgentLoop 也不需要承载复杂的提示词与历史管理逻辑。</p>
<h2 style="font-weight: 500; font-style: inherit;">记忆压缩</h2>
<p data-pid="dsNB2BuQ">DeepSeek Harness 当前的记忆能力主要由 Compaction 提供。</p>
<p data-pid="LFWvoSd7">这里需要控制术语。Compaction 服务于当前会话的连续运行，它会把旧历史折叠成结构化检查点。跨 Session 的用户偏好、项目经验和长期事实存储，目前没有形成完整系统。</p>
<p data-pid="hNkisSRL">因此，我更愿意把它称为「会话记忆」。</p>
<h3 style="font-weight: 500; font-style: inherit;">1. 压缩对象</h3>
<p data-pid="QC-JxdGS">Session 内部存在三个层次：</p>
<ul>
<li data-pid="ycBa8_uy">Log：所有原始事件组成的追加日志；</li>
<li data-pid="qCfBJKyK">Surface：当前参与模型消息派生的事件视图；</li>
<li data-pid="jvQttHWA">Messages：发送给模型的协议消息。</li>
</ul>
<p data-pid="BFg8Zlwu">Compaction 修改的是 surface。</p>
<p data-pid="UCAJoucE">原始事件仍然保留在 Log 中，模型当前看到的历史则由摘要节点替代。这个结构同时满足了两个要求：</p>
<ul>
<li data-pid="Wkm6gkrd">模型输入得到缩短；</li>
<li data-pid="pot3sY_R">原始会话记录没有被覆盖。</li>
</ul>
<p data-pid="ko1qemzy">它比直接删除前 N 条消息安全很多。</p>
<p data-pid="VwKAhN6p">删除历史以后，调试系统无法知道模型之前看过什么。线上出现问题时，只剩下截断后的残缺记录。Compaction 保留原始事件，后续仍可审计压缩范围和摘要来源。</p>
<h3 style="font-weight: 500; font-style: inherit;">2. 触发条件</h3>
<p data-pid="cBCflI7k"><code>BasicCompactionEngine</code> 会通过 <code>tokenMeter.measure(session)</code> 估算当前会话的 token 压力，再与模型的 <code>contextWindow</code> 比较。</p>
<p data-pid="wjOHifKk">自动压缩拥有两个主要触发入口：</p>
<ul>
<li data-pid="wMv0knXY"><code>agent/pre-step</code> 阶段的常规压力检查；</li>
<li data-pid="Gk5b7txM">模型返回上下文溢出错误后的重试处理。</li>
</ul>
<p data-pid="LtB9n0TG">常规检查负责提前处理窗口压力，错误重试负责兜底。</p>
<p data-pid="Wwuadzpi">压缩区域由 <code>selectCompactableRange()</code> 选择。它会优先折叠较旧历史，并保留近期上下文。近期消息通常包含当前修改进度、最新工具结果和下一步操作，保留它们可以减少摘要对短期推理的影响。</p>
<p data-pid="iBa_vtM9">区域选择还需要保护工具调用链。</p>
<p data-pid="GDghhKSB">Assistant 发出的 tool-call 和对应的 tool-result 属于一组完整语义。若压缩边界将它们切开，后续模型可能看到孤立的调用或孤立的结果。部分模型适配器甚至会直接拒绝这种消息结构。</p>
<p data-pid="UU-WE4Zd"><code>validateSurfaceRegion()</code> 会检查压缩区域，避免破坏工具调用与结果的配对关系。</p>
<p data-pid="-lZ2Re3R">这类结构校验比简单的「保留最近 N 条消息」可靠。Agent 历史已经超出普通聊天记录的范畴，消息之间存在协议级关联，切割时必须理解这些关联。</p>
<h3 style="font-weight: 500; font-style: inherit;">3. 摘要生成</h3>
<p data-pid="HJCoBTOu">压缩区域选定后，<code>buildSummarizationInput()</code> 会恢复对应的模型消息，并取回当时的 system 和 tools。<code>summarizeWithLlm()</code> 随后调用 LLM 生成摘要。</p>
<p data-pid="KU3wMmSv">摘要受到固定模板约束，主要包含：</p>
<ul>
<li data-pid="amzfL5CH"><code>Primary Request and Intent</code></li>
<li data-pid="puqjUsET"><code>Files and Code</code></li>
<li data-pid="FmP0biWl"><code>Errors and Fixes</code></li>
<li data-pid="qK7lgmGd"><code>Next Step</code></li>
<li data-pid="FCQK469s"><code>Critical Context</code></li>
</ul>
<p data-pid="SEgF1nXL">结构化模板可以防止摘要退化成普通的对话概述。</p>
<p data-pid="OhSegI69">编程 Agent 的历史里，有用的信息通常集中在几类内容：</p>
<ul>
<li data-pid="NfNO3ymE">用户最初要解决的问题；</li>
<li data-pid="1A7RJ55N">已经读取或修改的文件；</li>
<li data-pid="ZuvW4Vjc">执行过的命令；</li>
<li data-pid="nETeOMJj">失败原因和修复过程；</li>
<li data-pid="GfwrSkyU">当前未完成步骤；</li>
<li data-pid="g60F_bNA">不能违反的环境约束。</li>
</ul>
<p data-pid="gFLu4uY4">如果只要求模型「概括以上对话」，输出往往会省略文件名、错误信息和失败尝试。摘要读起来流畅，后续执行却接不上。</p>
<p data-pid="8-nTKN9R">固定结构会提高这些信息的保留概率，但无法保证语义完整。</p>
<h3 style="font-weight: 500; font-style: inherit;">4. 替换事务</h3>
<p data-pid="WxSs0QW9">摘要生成以后，会被包装进 <code>&lt;compacted-summary&gt;</code>，随后作为新的 <code>user/message</code> 写入 Session，并通过 <code>surfaceOp: replace</code> 替换旧区域。</p>
<p data-pid="dF8F4HY4">参考实现中的主流程如下：</p>
<div class="highlight">
<pre><code class="language-ts"><span class="c1" style="font-style: italic; color: #9196a1;">// compaction-basic/region.ts 的逻辑简化
</span><span class="nx">session</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s1" style="color: #d95350;">'compaction/start'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>

<span class="kr" style="font-weight: 600;">const</span> <span class="nx">summary</span> <span class="o" style="font-weight: 600;">=</span> <span class="k" style="font-weight: 600;">await</span> <span class="nx">summarizeWithLlm</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">config</span><span class="p">,</span> <span class="nx">input</span><span class="p">,</span> <span class="nx">agent</span><span class="p">,</span> <span class="nx">signal</span><span class="p">)</span>
<span class="kr" style="font-weight: 600;">const</span> <span class="nx">framed</span> <span class="o" style="font-weight: 600;">=</span> <span class="nx">frameSummary</span><span class="p">(</span><span class="nx">summary</span><span class="p">.</span><span class="nx">summary</span><span class="p">)</span>

<span class="kr" style="font-weight: 600;">const</span> <span class="nx">summaryEvent</span> <span class="o" style="font-weight: 600;">=</span> <span class="nx">session</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s1" style="color: #d95350;">'compaction/summary'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>

<span class="nx">session</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span>
  <span class="s1" style="color: #d95350;">'user/message'</span><span class="p">,</span>
  <span class="nx">createUserMessage</span><span class="p">({</span>
    <span class="nx">content</span>: <span class="kt" style="font-weight: 600; color: #09408e;">framed</span><span class="p">,</span>
    <span class="nx">source</span>: <span class="kt" style="font-weight: 600; color: #09408e;">compactCheckpointSource</span><span class="p">(...),</span>
  <span class="p">}),</span>
  <span class="p">{</span>
    <span class="nx">surfaceOp</span><span class="o" style="font-weight: 600;">:</span> <span class="p">{</span> <span class="nx">op</span><span class="o" style="font-weight: 600;">:</span> <span class="s1" style="color: #d95350;">'replace'</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">end</span> <span class="p">},</span>
    <span class="nx">sourceEventSeqs</span><span class="o" style="font-weight: 600;">:</span> <span class="p">[</span><span class="nx">startEvent</span><span class="p">.</span><span class="nx">seq</span><span class="p">,</span> <span class="nx">summaryEvent</span><span class="p">.</span><span class="nx">seq</span><span class="p">,</span> <span class="p">...</span><span class="nx">shadowedSeqs</span><span class="p">],</span>
  <span class="p">},</span>
<span class="p">)</span>

<span class="nx">session</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s1" style="color: #d95350;">'compaction/end'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>
</code></pre>
</div>
<p data-pid="oDC-ylQr">整个过程会记录：</p>
<ul>
<li data-pid="U-aEeBwl">压缩开始；</li>
<li data-pid="lErGaeYK">摘要内容；</li>
<li data-pid="yYYEz4ey">replacement message；</li>
<li data-pid="lcxkb6F-">被覆盖的事件序列；</li>
<li data-pid="7H6ZondC">压缩结束。</li>
</ul>
<p data-pid="DOHBd6kw"><code>sourceEventSeqs</code> 建立了检查点与原始历史之间的关联。排查错误摘要时，开发者可以反查它覆盖了哪些事件。</p>
<p data-pid="u5i0F0Xk"><code>Session.deriveMessages()</code> 检测到 <code>replaceGeneration</code> 发生变化后，会重建消息缓存。后续模型看到的是新检查点和保留下来的近期历史。</p>
<p data-pid="8W5Xh3G1">当前的设计保证了事务的完整性。压缩失败时，旧 surface 仍然可以继续使用。摘要生成成功且提交完成后，模型视图才发生变化。</p>
<h3 style="font-weight: 500; font-style: inherit;">5. 有损风险</h3>
<p data-pid="EbSf2fmc">Compaction 无法绕开有损问题。</p>
<p data-pid="PmrNGKPj">假设旧历史中出现过一条约束：测试环境使用特定环境变量，变量为空时必须跳过某项操作。摘要模型漏掉这条信息以后，后续 Agent 可能执行错误命令。</p>
<p data-pid="Oh8s4BOJ">原始事件虽然还在 Log 中，当前模型无法自动访问。对推理过程而言，这条信息已经离开热上下文。</p>
<p data-pid="o7DmBcyd">因此，Session Log 的完整性解决了审计问题，没有完全解决信息恢复问题。</p>
<p data-pid="3Zr5ws-X">我会在现有 Compaction 上增加一层历史召回能力。摘要中保留关键事件引用，模型缺少细节时，可以调用类似 <code>recall_history</code> 的工具读取旧历史。</p>
<p data-pid="c2MrZod8">召回参数可以采用结构化维度：</p>
<ul>
<li data-pid="KpD1eV5V">turn 范围；</li>
<li data-pid="FszCvF2t">step 范围；</li>
<li data-pid="v_YuQSCX">event seq；</li>
<li data-pid="M-uEjIcR">tool call id；</li>
<li data-pid="utyaSRh1">文件路径；</li>
<li data-pid="HmUxQXKs">compaction id。</li>
</ul>
<p data-pid="lt3jM4DU">这种方式比单纯的语义搜索更稳定一些。Session 已经拥有完整事件顺序和来源关系，应优先利用现有结构。</p>
<p data-pid="RzgNrWMq">召回结果也要写成 <code>tool/result</code>。这样系统可以知道模型恢复了哪段历史，以及这段历史怎样影响后续决策。</p>
<h3 style="font-weight: 500; font-style: inherit;">6. 调度延迟</h3>
<p data-pid="mGNvns0D">当前压缩会调用一次 LLM。压缩发生在主流程上时，用户需要等待摘要完成。</p>
<p data-pid="uK38qW36">会话较短时，这个延迟可以接受。长会话的摘要输入很大，调用时间会明显增加。编程 Agent 还可能连续执行多个工具，压缩恰好卡在下一步之前，交互体验会出现停顿。</p>
<p data-pid="P8yYqH2p">可以引入两级水位：</p>
<ul>
<li data-pid="dSmiaE4t">接近窗口上限时，后台生成候选 checkpoint；</li>
<li data-pid="pzOUr8Tu">到达硬上限时，提交已有 checkpoint；</li>
<li data-pid="m4u4YITb">候选结果过期时放弃；</li>
<li data-pid="QgDYfGIm">没有可用结果时回退到同步压缩。</li>
</ul>
<p data-pid="yvTt5-Qy">异步压缩的难点集中在一致性。</p>
<p data-pid="MzgMb-Og">生成摘要期间，Session 还会继续追加事件。提交前必须校验它对应的 surface generation，确保被压缩区域没有发生冲突。检查点过期后不能强行替换，否则可能覆盖新的工具结果或用户输入。</p>
<p data-pid="wuZWyAhF">当前已有的压缩重入保护、surface generation 和事务事件，为异步化提供了基础。早期保持同步更稳，长任务比例上升以后，再逐步引入后台 checkpoint。</p>
<h2 style="font-weight: 500; font-style: inherit;">知识获取</h2>
<p data-pid="3Zni8Gfa">DeepSeek Harness 没有传统意义上的统一向量知识库。</p>
<p data-pid="6knaQaz1">它的知识入口由三部分组成：</p>
<ul>
<li data-pid="mZwO47nm">文件路径引用；</li>
<li data-pid="h7CAILu8">Web 搜索与抓取；</li>
<li data-pid="mZByYlxr">工具结果写入 Session。</li>
</ul>
<p data-pid="ITb0RONj">这种组合适合编程 Agent。代码仓库里的信息具有路径、符号、定义和引用关系。统一切块并写入向量数据库，会损失一部分结构信息，还要承担索引更新成本。</p>
<h3 style="font-weight: 500; font-style: inherit;">1. 文件引用</h3>
<p data-pid="UVWk65o1"><code>file-reference-local</code> 提供工作区文件和目录候选。</p>
<p data-pid="LwC_-47d">它会根据 <code>session.header.cwd</code> 创建 <code>WorkspaceFileSearch</code>，然后扫描当前工作区。结果只包含：</p>
<ul>
<li data-pid="_XfNjtTK"><code>path</code></li>
<li data-pid="A0ittOcn"><code>kind</code></li>
</ul>
<p data-pid="R5vJfsfF">用户在前端选择文件后，输入框里会插入 <code>@path</code> 或 <code>@"path with spaces"</code>。</p>
<p data-pid="7akSAKmK">文件内容不会在这个阶段进入模型。</p>
<p data-pid="0Rg1X9nJ">系统提示会告诉模型，<code>@</code> token 表示工作区路径。模型需要读取内容时，应调用 <code>read</code> 工具。</p>
<p data-pid="UWdY7um2">这个分层处理了两个不同问题：</p>
<ul>
<li data-pid="ndMMFBs0"><code>file-reference-local</code> 负责找到路径；</li>
<li data-pid="2uUP6r2f"><code>read</code> 工具负责读取内容。</li>
</ul>
<p data-pid="O9YeUKjB">如果选择路径时就把整个文件自动塞进消息，系统会遇到几类麻烦。</p>
<p data-pid="f0Jb38WJ">第一，大文件会快速占满上下文。</p>
<p data-pid="G3e59NMN">第二，用户无法知道系统展开了多少内容。</p>
<p data-pid="jh80ovsF">第三，读取行为缺少独立记录。</p>
<p data-pid="XtGR3-zr">第四，文件权限和读取范围可能绕过工具 guard。</p>
<p data-pid="C8YbYvPF">第五，文件修改以后，很难判断模型当时看到的是哪个版本。</p>
<p data-pid="jVmlGDOA">路径引用保留了用户意图，工具调用保留了实际读取行为。两类信息都会进入 Session，审计链更完整。</p>
<p data-pid="hWkae2qB"><code>WorkspaceFileSearch</code> 还会控制目录边界、排除路径、最大条目和候选数量，并拒绝通过 <code>..</code> 或符号链接跳出 workspace。</p>
<p data-pid="-t-NIkd4">这些限制属于安全边界。文件路径来自用户输入，也可能由模型生成，不能默认可信。</p>
<h3 style="font-weight: 500; font-style: inherit;">2. 精确检索</h3>
<p data-pid="zi6Vf8X1">文件路径补全只能解决「大致知道文件在哪里」的问题。</p>
<p data-pid="KZXBexpk">大型仓库中，开发者和模型经常需要回答另一类问题：</p>
<ul>
<li data-pid="RnYecD6s">接口定义位于哪个文件；</li>
<li data-pid="VVcMC8wF">某个符号有哪些引用；</li>
<li data-pid="LsAIvOSw">哪些实现依赖当前类型；</li>
<li data-pid="uJzWfnix">一次修改会影响哪些调用点；</li>
<li data-pid="ejCKmWP0">编译诊断关联到哪些符号。</li>
</ul>
<p data-pid="ZadaIexL">这些查询适合交给 LSP。</p>
<p data-pid="54-7xxBH">项目已经存在 <code>packages/lsp/</code>，可以继续扩展为代码检索能力。优先提供：</p>
<ul>
<li data-pid="Vxe0Zj_E">Go to Definition；</li>
<li data-pid="u4ltMKDF">Find References；</li>
<li data-pid="3SdAFfqC">Workspace Symbols；</li>
<li data-pid="v_B0WNlg">Diagnostics；</li>
<li data-pid="5u28Ji9V">调用关系。</li>
</ul>
<p data-pid="nssrfBB2">向量检索依赖语义相似度。两个函数命名和注释很接近，并不能证明它们具有依赖关系。LSP 返回的是语言服务器维护的定义和引用关系，适合代码修改场景。</p>
<p data-pid="Pd3VaVyG">可以把仓库检索分成四层：</p>
<ol>
<li data-pid="82PUHsGQ">已知路径，直接读取文件；</li>
<li data-pid="-RZKY-5O">已知文本，使用 <code>grep</code>；</li>
<li data-pid="WBFMexCi">已知符号，使用 LSP；</li>
<li data-pid="BeIPUWQa">只有概念描述时，再考虑语义检索。</li>
</ol>
<p data-pid="e6FXxjEs">这个逻辑会优先消耗确定性信息。代码仓库已经提供路径和符号结构，没有必要先把问题转换成向量相似度。</p>
<p data-pid="OPMQfTLY">LSP 也有运行成本。语言服务器需要启动和预热，多语言仓库要维护多个进程，大型 monorepo 的全局引用查询可能很慢。它适合作为工具按需调用，不适合把所有结果常驻上下文。</p>
<p data-pid="sZAaCgGt">查询结果仍然要通过 ToolRuntime 写入 Session。这样模型读取过哪些定义、哪些引用，可以在后续回放中找到。</p>
<h3 style="font-weight: 500; font-style: inherit;">3. Web 检索</h3>
<p data-pid="nZ8WuNim">外部知识通过 <code>web_search</code> 和 <code>web_fetch</code> 进入模型。</p>
<p data-pid="oLWBQq9_">整体分为三层：</p>
<ul>
<li data-pid="Brje1A-X"><code>WebRuntime</code> 定义统一能力；</li>
<li data-pid="OcfNNWWT">search/fetch provider 负责具体请求；</li>
<li data-pid="8KrooT_R"><code>tool-web</code> 把能力注册成模型可见工具。</li>
</ul>
<p data-pid="i4EFcx3j">这种分层使工具 schema 与供应商解耦。模型只需要理解 <code>web_search</code> 和 <code>web_fetch</code>。底层可以选择 DeepSeek、Exa、Perplexity 或 HTTP fetch provider。</p>
<p data-pid="qH9a0PZA">Provider 选择规则保持严格：</p>
<ul>
<li data-pid="a_m7wBWJ">配置了 provider id，使用指定 provider；</li>
<li data-pid="W3mohG3w">未配置时，系统要求只有一个可用 provider；</li>
<li data-pid="2pwfGnET">多个 provider 同时可用时直接报错。</li>
</ul>
<p data-pid="QGXoBcxO">这可以避免插件加载顺序影响线上行为。</p>
<p data-pid="8ExFuhVE">Web 搜索结果会被格式化成模型可见文本，并带有外部内容不可信和引用 URL 的提示。Web fetch 会把 HTML 转换成 Markdown，普通文本则直接进入结果。</p>
<p data-pid="iPs_7DhE">提示只能影响模型行为，网络边界还要由 provider 控制。HTTP fetch provider 已经包含多项限制：</p>
<ul>
<li data-pid="n88b_gVf">只允许 HTTP(S)；</li>
<li data-pid="XJ6odxWr">拒绝 URL credentials；</li>
<li data-pid="keofVZlT">拒绝私网和非公网地址；</li>
<li data-pid="YuCc0-_J">控制同源重定向；</li>
<li data-pid="DxuIBNPR">限制重定向次数；</li>
<li data-pid="qRYiGZ8O">限制响应字节数；</li>
<li data-pid="I7hHqWHz">限制正文长度；</li>
<li data-pid="Xp7KWCPK">固定经过校验的 DNS 地址；</li>
<li data-pid="3SGk3Jfu">timeout 由部署配置管理。</li>
</ul>
<p data-pid="KecUMm0Q">模型生成的 URL也属于不可信输入。网页中的 Prompt Injection 可能诱导模型请求内网地址、云元数据地址或带有凭证的 URL。网络策略需要在模型之外执行。</p>
<h3 style="font-weight: 500; font-style: inherit;">4. 工具入库</h3>
<p data-pid="_xVJZrCj">文件读取、Web 搜索和 Web 抓取获得的信息，都通过同一条工具链进入模型。</p>
<p data-pid="shNKpQa9">流程可以概括为：</p>
<ol>
<li data-pid="jH3mCR8Q">ToolRuntime 把工具 schema 注册到 SystemPrompt；</li>
<li data-pid="daKDvxxC">模型返回 tool-call；</li>
<li data-pid="4yE2Ibqg">AgentLoop 调用 <code>executeToolCalls()</code>；</li>
<li data-pid="5KDj7Jtd">Session 写入 <code>tool/call</code>；</li>
<li data-pid="1u9a0Gmz">ToolRuntime 执行工具；</li>
<li data-pid="lvRGQ_EC">Session 写入 <code>tool/result</code>；</li>
<li data-pid="GZEqR6Gv">下一轮 <code>deriveMessages()</code> 把结果投影给模型。</li>
</ol>
<p data-pid="XjzIWmrO">参考实现中的调用路径如下：</p>
<div class="highlight">
<pre><code class="language-ts"><span class="c1" style="font-style: italic; color: #9196a1;">// 模型返回 tool-call
</span><span class="kr" style="font-weight: 600;">const</span> <span class="nx">toolCalls</span> <span class="o" style="font-weight: 600;">=</span> <span class="nx">message</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">block</span> <span class="o" style="font-weight: 600;">=&gt;</span> <span class="nx">block</span><span class="p">.</span><span class="kr" style="font-weight: 600;">type</span> <span class="o" style="font-weight: 600;">===</span> <span class="s1" style="color: #d95350;">'tool-call'</span><span class="p">)</span>

<span class="c1" style="font-style: italic; color: #9196a1;">// Agent 执行工具
</span><span class="kr" style="font-weight: 600;">const</span> <span class="p">{</span> <span class="nx">concluded</span> <span class="p">}</span> <span class="o" style="font-weight: 600;">=</span> <span class="k" style="font-weight: 600;">await</span> <span class="nx">executeToolCalls</span><span class="p">(</span>
  <span class="k" style="font-weight: 600;">this</span><span class="p">.</span><span class="nx">loopCtx</span><span class="p">,</span>
  <span class="nx">turn</span><span class="p">,</span>
  <span class="nx">step</span><span class="p">,</span>
  <span class="nx">toolCalls</span><span class="p">,</span>
  <span class="nx">signal</span><span class="p">,</span>
  <span class="nx">context</span> <span class="o" style="font-weight: 600;">=&gt;</span> <span class="k" style="font-weight: 600;">this</span><span class="p">.</span><span class="nx">inbox</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="s1" style="color: #d95350;">'next-step'</span><span class="p">,</span> <span class="k" style="font-weight: 600;">this</span><span class="p">.</span><span class="nx">inbox</span><span class="p">.</span><span class="nx">nextStep</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="mi" style="color: #1772f6;">0</span><span class="p">,</span> <span class="p">[</span><span class="nx">context</span><span class="p">]),</span>
<span class="p">)</span>

<span class="c1" style="font-style: italic; color: #9196a1;">// 工具结果进入 session log，下一 step 被 deriveMessages() 投影给模型
</span></code></pre>
</div>
<p data-pid="NtNcofEY">工具结果不会只停留在当前执行栈，也不会直接修改临时 messages 数组。</p>
<p data-pid="Ax7Auz5q">这对 Web 信息尤其关键。搜索结果和网页内容会随时间变化。如果系统只记录搜索参数，重放时重新请求一次，模型得到的内容可能已经不同。</p>
<p data-pid="L7z0iJ-U">文件读取也一样。Agent 可能在读取后修改文件。历史推理需要保留当时真正发送给模型的内容。</p>
<p data-pid="c4pcStOq">当然，完整保存所有工具输出会增加 Session 体积。工程上可以保存模型实际看到的渲染结果，同时在 meta 中记录：</p>
<ul>
<li data-pid="9WnIPTWN">是否截断；</li>
<li data-pid="4UGG5Maw">原始内容长度；</li>
<li data-pid="LWXGzSlV">来源路径或 URL；</li>
<li data-pid="mNdu6kpE">content type；</li>
<li data-pid="uFAqNPwg">工具调用参数；</li>
<li data-pid="qsWaiZ8f">provider 信息。</li>
</ul>
<p data-pid="jBYSgmdP">模型没有看到的正文，无须全部伪装成上下文历史。回放能力关注的是当时的模型输入。</p>
<h2 style="font-weight: 500; font-style: inherit;">三者关系</h2>
<p data-pid="FUcSeOHW">上下文、记忆和知识获取分别控制模型输入的三个阶段。</p>
<ul>
<li data-pid="xcwQICsc">上下文决定当前输入： SystemPrompt 收集系统约束、动态状态和工具 schema。<code>preStep()</code> 判断动态内容是否变化，<code>buildRequest()</code> 构建最终模型请求。它解决的是「这一轮应该携带什么」。</li>
<li data-pid="CiEh5b89">记忆控制历史体积： Compaction 观察 token 压力，选择旧历史区域，生成结构化摘要，再替换当前 surface。它解决的是「历史太长以后保留什么」。</li>
<li data-pid="AGBLgN4N">知识补充外部信息：文件引用帮助定位路径，读取工具获得仓库内容，Web 工具获取外部信息，未来还可以用 LSP 提供符号级查询。它解决的是「当前会话缺少的信息从哪里获得」。</li>
</ul>
<p data-pid="gxdjQl2i">三者最终都回到 Session。</p>
<p data-pid="TWppKY0B">动态上下文通过 <code>user/message</code> 进入日志，工具信息通过 <code>tool/result</code> 进入日志，压缩通过 <code>compaction/*</code> 事件和 replacement message 改写 surface。</p>
<p data-pid="6ZIh_YDu">所以，DeepSeek Harness 的数据主线可以压缩为：</p>
<p data-pid="12oL3cl6">装配上下文，记录事件，派生消息，调用模型，执行工具，写回结果，必要时折叠 surface。</p>
<p data-pid="Oq91MSYk">插件可以增加新的上下文来源、新的工具和新的压缩策略，但不能绕过这条主线。</p>
<p data-pid="YvZljiWk">直接向 <code>GenerateOptions.messages</code> 塞内容，会产生无法回放的隐形上下文。</p>
<p data-pid="F8wOkw-6">检索模块绕过 ToolRuntime，会失去权限、审计和结果记录。</p>
<p data-pid="IWCMYGUD">压缩模块覆盖原始事件，会破坏历史追踪。</p>
<p data-pid="xdnpu4MF">这几条边界比具体使用哪种模型、搜索服务或向量数据库更影响系统的维护成本。</p>
<h2 style="font-weight: 500; font-style: inherit;">小结</h2>
<p data-pid="RHY4GYaJ">DeepSeek Harness 的三个模块可以归纳为三句话：</p>
<ul>
<li data-pid="EDKSSzOw">上下文管理负责装配当前输入。</li>
<li data-pid="6cl53pXr">Compaction 负责压缩当前会话历史。</li>
<li data-pid="aLrrUR15">文件和 Web 工具负责补充当前缺失的信息。</li>
</ul>
<p data-pid="Tru6Pjsy">三者共享 Session Log：</p>
<ul>
<li data-pid="Kr_tKYLj">模型看到的动态状态要进入日志；</li>
<li data-pid="xWGS9G8Y">模型获得的工具结果要进入日志；</li>
<li data-pid="rSsYcT0d">压缩只调整 surface，原始事件继续保留；</li>
<li data-pid="G3j7W_Pr">后续模型输入由 <code>Session.deriveMessages()</code> 统一派生。</li>
</ul>
<p data-pid="lkDcl9dP">现有设计的优势集中在可回放、可审计和模块边界。主要缺口也很具体：Compaction 存在语义损失，压缩调用会阻塞主流程，文件路径检索缺少符号级能力，跨 Session 长期记忆尚未形成独立体系。</p>
<p data-pid="LrBqFP7v">Session Log 继续保存事实，surface 控制模型当前看到的历史，工具负责按需获取信息。三个模块沿着这条数据链协作，系统才能在上下文窗口、运行延迟和信息完整性之间保持可控。</p>
<p data-pid="cDtiwkJV">以上。</p>
</div>
</div>
</div>
</div>
<div class="Reward" style="color: #81858f;"></div>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/09/an-analysis-of-deepseek-harnesss-context-management-memory-and-knowledge-base/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DeepSeek Harness 和 Pi 的 Agent Loop 循环结束判断架构细节解析</title>
		<link>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/</link>
		<comments>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/#comments</comments>
		<pubDate>Sun, 30 Aug 2026 05:26:34 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agent Loop]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>
		<category><![CDATA[harness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2533</guid>
		<description><![CDATA[本篇文章代码基线采用 DeepSeek Harness 版本 0.1.2-alpha.1，提交时间为 2026 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">本篇文章代码基线采用 DeepSeek Harness 版本 <code>0.1.2-alpha.1</code>，提交时间为 2026 年 8 月 28 日；Pi 采用 <code>853a80d2</code>。这里的 Pi 指 <code>pi-agent-core</code> 中的官方 Agent Loop。</p>
<p data-tool="mdnice编辑器">就循环究竟在什么时刻结束这个问题，很多 Agent 框架把结束判断写成一个条件：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">while</span> (hasToolCalls) {
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p data-tool="mdnice编辑器">在 Demo 中这种循环也够用。进入生产环境后，你会发现会很多问题：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>模型没有调用工具，是否代表任务完成？</section>
</li>
<li>
<section>工具执行完了，模型是否还需要解释结果？</section>
</li>
<li>
<section>用户在工具执行期间发来 steering，应该进入当前轮还是下一轮？</section>
</li>
<li>
<section>输出触达 token 上限后，工具参数还能不能执行？</section>
</li>
<li>
<section>某个工具宣布任务完成，其他并行工具怎么办？</section>
</li>
<li>
<section>Agent 进入 idle 后，Goal Driver 能不能再次唤醒它？</section>
</li>
<li>
<section>Plan mode 退出，是结束当前循环，还是切换下一次请求的策略？</section>
</li>
<li>
<section>进程崩溃后，日志里尚未闭合的 turn 应当如何解释？</section>
</li>
</ul>
<p data-tool="mdnice编辑器">DeepSeek Harness 和 Pi 的工程路线不同，或者说 DeepSeek Harness 更复杂，也更能满足复杂场景的需求。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 将结束拆成多层状态，并把事件日志作为重建依据。Pi 保留了更扁平的循环结构，把 steering、follow-up 和 post-turn stop 暴露成回调。前者适合多插件、持久恢复和长期任务；后者适合轻量嵌入，控制路径也更容易读懂。</p>
<p data-tool="mdnice编辑器">两种实现都能工作。它们承担的系统复杂度不同，结束语义自然不会相同。</p>
<h1 data-tool="mdnice编辑器"><span class="content">五层边界</span></h1>
<p data-tool="mdnice编辑器">讨论 Agent Loop 时，我们需要先定义好「结束」，否则代码里的 <code>completed</code>、<code>turn_end</code>、<code>agent_end</code> 很容易被混为一谈。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 实际存在五层边界。</p>
<p data-tool="mdnice编辑器">第一层是模型请求结束。</p>
<p data-tool="mdnice编辑器">Provider 流式输出完成，可能给出正常停止、工具调用、最大 token、错误或取消。这里的判断逻辑是：当前 HTTP 请求或模型流结束了吗？</p>
<p data-tool="mdnice编辑器">第二层是 step 结束。</p>
<p data-tool="mdnice编辑器">一个 step 包含一次模型调用，以及该响应发起的一整批工具执行。工具执行结束后，step 才算关闭。模型若调用了普通工具，系统通常还欠一次模型回访，因此 step 结束不代表 turn 结束。</p>
<p data-tool="mdnice编辑器">第三层是 turn 结束。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 中，一个 turn 可以包含多个 step，大概如下：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">用户输入
→ 模型调用
→ 工具批次
→ 模型回访
→ 工具批次
→ 模型最终输出
</code></pre>
<p data-tool="mdnice编辑器">以上为一个 turn。turn 结束需要同时满足两项条件：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>模型不再欠一次回复；</section>
</li>
<li>
<section><code>next-step</code> inbox 没有待处理输入。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">第四层是 driver activity 结束。</p>
<p data-tool="mdnice编辑器">一个 driver 可以连续处理多个 turn。当前 turn 关闭后，如果 inbox 里还有普通 follow-up，driver 会直接打开下一 turn。只有队列耗尽，driver 才回到 idle。</p>
<p data-tool="mdnice编辑器">第五层是长期工作流结束。</p>
<p data-tool="mdnice编辑器">Goal Driver 可以监听 idle，检查持久 Goal 状态，再调用 <code>followup()</code> 开启新 turn。Plan mode 也可能跨越多个 turn 持续生效。因此，idle 只描述当前 activity；它无法回答长期目标是否完成。</p>
<p data-tool="mdnice编辑器">Pi 的层级少一些。一次 assistant 响应与随后执行的工具批次，会对应一个 <code>turn_end</code>。工具回访模型时，Pi 再开启下一次 turn。内部队列耗尽后发出 <code>agent_end</code>，当前 invocation 至此结束。</p>
<p data-tool="mdnice编辑器">从语义上看：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>DeepSeek Harness 的 step 接近 Pi 的 turn；</section>
</li>
<li>
<section>DeepSeek Harness 的一次 driver activity 接近 Pi 的一次 Agent Loop invocation；</section>
</li>
<li>
<section>DeepSeek Harness 的 Goal Round 位于 driver activity 外层；</section>
</li>
<li>
<section>Pi 的长期 Goal 通常由宿主或扩展继续调度。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">术语相似，粒度相差一层。如果日志消费端忽略这一点，统计出的 turn 数、工具往返次数和任务完成率都会失真。</p>
<h1 data-tool="mdnice编辑器"><span class="content">驱动收敛</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的入口：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">while</span> (<span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.turn()) {
}
</code></pre>
<p data-tool="mdnice编辑器"><code>turn()</code> 返回 <code>true</code>，driver 继续处理下一 turn；返回 <code>false</code>，driver 收敛；抛出异常时，异常会被 driver 边界容纳，随后进入 idle。</p>
<p data-tool="mdnice编辑器">短代码背后有一套完整的 phase 状态机：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">idle
maintenance
running
</code></pre>
<p data-tool="mdnice编辑器"><code>running</code> 保存：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>当前 turn；</section>
</li>
<li>
<section>当前 step；</section>
</li>
<li>
<section><code>AbortController</code>；</section>
</li>
<li>
<section><code>wakeRequested</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>maintenance</code> 同样持有取消信号和唤醒锁存，只是对外状态仍显示为 idle。这种设计解决了一个经常被忽略的竞态：Agent 处于维护任务时收到新消息，新消息不能立即启动第二个 driver，也不能静默丢失。系统先将唤醒意图锁存，维护结束后检查 inbox，再决定是否重启。</p>
<p data-tool="mdnice编辑器">取消路径也有类似处理。</p>
<p data-tool="mdnice编辑器">如果一个 waking input 到达时，现有 activity 已经被 abort，这条消息会被重分类到 <code>next-turn</code>。它不能加入一个正在收敛的旧 turn。<code>send()</code> 在插入 inbox 前读取 aborted 状态，避免 inbox observer 触发重入取消后改变分类结果。</p>
<p data-tool="mdnice编辑器">这段细节很像传统并发状态机，而不是常见的聊天循环。它处理的是输入归属问题：一条消息必须属于当前 step、下一 step 或下一 turn，重入事件不能改变已经作出的边界判断。</p>
<p data-tool="mdnice编辑器"><code>whenIdle()</code> 也没有简单等待某个固定 Promise。它反复比较 <code>activityDone</code>：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">do</span> {
  <span class="hljs-keyword">await</span> (activity = <span class="hljs-keyword">this</span>.activityDone)
} <span class="hljs-keyword">while</span> (activity !== <span class="hljs-keyword">this</span>.activityDone)
</code></pre>
<p data-tool="mdnice编辑器">等待期间如果 activity 被新的 driver 替换，调用方会继续等待。否则 <code>whenIdle()</code> 可能在旧 driver 结束、新 driver 已经启动的窗口里提前返回。</p>
<p data-tool="mdnice编辑器">在多插件环境中，idle 是一个可观察状态，也是一种同步承诺。这个承诺需要覆盖重启竞态，不能只看某次异步任务是否 resolve。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Turn 状态机</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 在领取输入之前写入 <code>turn/start</code>。</p>
<p data-tool="mdnice编辑器">pre-step 可能拒绝输入，系统提示组装可能抛错，首步消息也可能被插件改写为空。无论发生哪种情况，日志中都存在一个完整 turn，并由唯一的 <code>turn/end</code> 收口。</p>
<p data-tool="mdnice编辑器">turn 内部维护两个变量：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>turnEnds</code>：当前已知的结束原因；</section>
</li>
<li>
<section><code>target</code>：本轮 pre-step 应领取 <code>next-turn</code> 还是 <code>next-step</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">第一个 step 从 <code>next-turn</code> 开始。后续 step 转向 <code>next-step</code>。普通用户 prompt 因此拥有独立 turn，工具附加上下文、运行时 context 和 steering 则可以进入当前 turn 的下一 step。</p>
<p data-tool="mdnice编辑器">一次循环大致经历以下过程：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">创建 step 编号
→ 从 inbox 领取消息
→ 组装系统提示和运行时 context
→ 执行 agent/pre-step
→ 记录 step/start
→ 写入 user/message
→ 调用模型并执行工具
→ 记录 step/end
→ 判断是否继续
</code></pre>
<p data-tool="mdnice编辑器">如果 <code>agent/pre-step</code> 返回 <code>reject</code>，turn 以 <code>blocked</code> 结束，模型不会被调用。</p>
<p data-tool="mdnice编辑器">如果首个 step 的消息为空，turn 以 <code>completed</code> 结束，也不会消耗模型请求。这覆盖了一个边缘场景：wakeup 已经打开 turn，随后消息被移除，或者 pre-step 插件把消息改写为空。边界既然已经建立，日志仍需闭合。</p>
<p data-tool="mdnice编辑器">如果某个 step 已经产生结束结果，但下一次 pre-step 没有消息，循环退出。若仍有消息，就继续执行。</p>
<p data-tool="mdnice编辑器">这里需要留意 <code>turnEnds</code> 与 <code>stepEnd</code> 的关系。<code>step()</code> 可以返回：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>completed</code>；</section>
</li>
<li>
<section><code>max-tokens</code>；</section>
</li>
<li>
<section><code>null</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>null</code> 表示工具执行完毕，模型还欠一次回复。它是继续循环的协议状态，和错误、未知结果没有关系。</p>
<p data-tool="mdnice编辑器">turn 的结束判断由「模型债务」与「消息债务」共同决定。模型债务来自 tool call，消息债务来自 inbox。只检查其中一项都会出错。</p>
<h1 data-tool="mdnice编辑器"><span class="content">停止钩子</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 在 turn 准备收口时触发 <code>agent/turn-stopping</code>。</p>
<p data-tool="mdnice编辑器">这个扩展点没有返回 <code>true</code> 或 <code>false</code>。插件如果希望 turn 继续，需要向 <code>next-step</code> 写入消息。hook 执行完成后，核心循环重新检查 inbox：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">turn 已经可以结束
→ next-step 为空
→ 触发 agent/turn-stopping
→ 再次检查 next-step
→ 为空则关闭
→ 非空则进入下一 step
</code></pre>
<p data-tool="mdnice编辑器">这是数据驱动的方式。</p>
<p data-tool="mdnice编辑器">多个插件都能监听 stopping。如果采用 <code>shouldContinue(): boolean</code>，组合规则会立刻变得棘手：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>任意插件返回 true 就继续？</section>
</li>
<li>
<section>所有插件都返回 true 才继续？</section>
</li>
<li>
<section>后执行的插件能否覆盖先执行的结果？</section>
</li>
<li>
<section>插件决定继续，却没有提供下一步输入，模型该看到什么？</section>
</li>
<li>
<section>插件决定停止，但另一个插件已经排入 steering，谁的优先级更高？</section>
</li>
</ul>
<p data-tool="mdnice编辑器">DeepSeek Harness 用 inbox 消除了大部分布尔值合并问题（这个设计和 Golang 的竞态处理逻辑类似，使用类似消息队列来处理竞态）。继续执行必须产生一条可消费的数据。最终判定依赖队列状态，而非插件调用顺序。</p>
<p data-tool="mdnice编辑器">当然，这也是需要代价的。</p>
<p data-tool="mdnice编辑器">插件作者需要理解 inbox target、turn 边界和 wakeup 语义。一个简单的「再跑一次」操作，需要构造 <code>UserMessage</code> 并写入正确队列。框架学习成本高于直接回调。</p>
<p data-tool="mdnice编辑器">这种成本在单体应用里可能显得繁琐。进入可恢复、多插件、可审计系统后，显式消息通常比隐式布尔控制更可靠。日志和调试工具能看到「为什么继续」，而不是只看到某个 hook 曾经返回 true。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Step 债务</span></h1>
<p data-tool="mdnice编辑器"><code>step()</code> 最关键的职责，是判断模型是否还欠一次回复。</p>
<p data-tool="mdnice编辑器">模型流结束后，<code>BlockAssembler</code> 给出 finish 状态。如果 finish 是 error 或 aborted，系统先触发 <code>agent/request-error</code> waterfall。插件可以选择 retry。没有 retry 动作时，代码抛出结构化 <code>LlmError</code>。</p>
<p data-tool="mdnice编辑器">正常组装出 assistant message 后，判断顺序如下：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>finish 为 <code>max-tokens</code>，返回 <code>max-tokens</code>；</section>
</li>
<li>
<section>没有 tool call，返回 <code>completed</code>；</section>
</li>
<li>
<section>存在 tool call，执行整批工具；</section>
</li>
<li>
<section>工具批次声明 concluded，返回 <code>completed</code>；</section>
</li>
<li>
<section>普通工具批次返回 <code>null</code>。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">顺序不能随便改。</p>
<p data-tool="mdnice编辑器">最大 token 状态在工具解析之前返回，因此响应中即使出现 tool call，也不会被执行。截断输出可能包含不完整的 JSON 参数。某些流式解析器会尝试补全 JSON，补全后甚至能通过 schema 验证，但语义字段可能已经缺失。执行这类调用会产生真实副作用。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的选择偏保守：整轮以 <code>max-tokens</code> 收口，等待外部策略决定是否继续。</p>
<p data-tool="mdnice编辑器">Pi 采用另一种处理。<code>stopReason === "length"</code> 且消息包含工具调用时，它会给所有工具生成错误结果，说明参数可能被截断，没有实际执行，然后把这些结果交回模型。模型有机会在下一 turn 重发完整调用。</p>
<p data-tool="mdnice编辑器">两者的恢复能力不同。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 保留了清晰的 durable turn reason，运维侧能准确识别 token ceiling。自动继续需要 Goal Driver、用户输入或其他插件参与。Pi 更倾向在当前 invocation 内自我修复，模型可能马上重发工具调用。</p>
<p data-tool="mdnice编辑器">我在具有写操作的 Agent 中会选择 DeepSeek Harness 的策略。达到输出上限本身意味着模型状态不完整，自动延续可能重复前一批操作。只读研究型 Agent 可以采用 Pi 的方式，失败工具结果能减少一次人工干预。</p>
<h1 data-tool="mdnice编辑器"><span class="content">粘性结果</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 <code>max-tokens</code> 具有粘性。</p>
<p data-tool="mdnice编辑器">某个 step 达到上限后，插件仍可能在 <code>turn-stopping</code> 阶段补入 steering，让 turn 再执行几个 step。后续 step 即使正常完成，最终 <code>turn/end</code> 仍然保留 <code>max-tokens</code>。</p>
<p data-tool="mdnice编辑器">判断逻辑是：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">if</span> (
  turnEnds === <span class="hljs-literal">null</span> ||
  turnEnds.kind !== <span class="hljs-string">'max-tokens'</span>
)
  turnEnds = stepEnd
</code></pre>
<p data-tool="mdnice编辑器">这里记录的是整个 turn 的质量，而非最后一次 step 的结果。</p>
<p data-tool="mdnice编辑器">如果不做粘性处理，可能产生如下日志：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">step 1: max-tokens
step 2: 插件要求继续
step 3: completed
turn/end: completed
</code></pre>
<p data-tool="mdnice编辑器">消费端会丢失曾经发生的截断。监控系统无法统计 token ceiling，Goal Driver 也可能把这轮当成完整成功并继续自动调度。</p>
<p data-tool="mdnice编辑器">把 turn reason 视为聚合结果后，合并规则就需要专门设计。DeepSeek Harness 当前只对 <code>max-tokens</code> 做了优先保留。错误和取消会直接离开主流程，不参与后续合并。</p>
<p data-tool="mdnice编辑器">如果未来加入诸如 <code>partial</code>、<code>policy-warning</code>、<code>budget-exhausted</code> 等状态，简单覆盖会越来越脆弱。更稳妥的方向是为 reason 建立显式优先级或累积 facts，再由投影层生成最终 reason。当前 union 规模尚小，现有实现足够清楚。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工具收口</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 允许工具通过结果上的 <code>concludesTurn</code> 宣布当前 turn 可以收口。</p>
<p data-tool="mdnice编辑器">工具批次采用 OR 聚合：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">任意一个已提交结果 concludesTurn === true
→ 整批 concluded === true
</code></pre>
<p data-tool="mdnice编辑器">它表达的是「模型默认不再欠一次工具结果回访」。</p>
<p data-tool="mdnice编辑器"><code>concludesTurn</code> 没有直接打断同批工具，也没有清空 <code>next-step</code>：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>已启动的并行工具继续运行；</section>
</li>
<li>
<section>结果按模型调用顺序提交；</section>
</li>
<li>
<section><code>additionalContexts</code> 写入 <code>next-step</code>；</section>
</li>
<li>
<section>同期到达的 steering 也写入 <code>next-step</code>；</section>
</li>
<li>
<section>只要 <code>next-step</code> 非空，turn 仍会进入下一 step。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">因此，工具结束能力属于软收口。它取消了自动回访债务，无法压过已经存在的消息债务。</p>
<p data-tool="mdnice编辑器">这套语义适合权威型工具。例如某个交互工具已经取得用户最终决定，或者某个提交工具已经完成不可逆事务，模型没有必要再生成一段中间解释。工具可以建议收口，同时保留系统上下文和用户 steering 的优先级。</p>
<p data-tool="mdnice编辑器">Pi 的工具终止采用 AND 聚合：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">return</span> finalizedCalls.length &gt; <span class="hljs-number">0</span> &amp;&amp;
  finalizedCalls.every(
    <span class="hljs-function"><span class="hljs-params">finalized</span> =&gt;</span> finalized.result.terminate === <span class="hljs-literal">true</span>
  )
</code></pre>
<p data-tool="mdnice编辑器">只有非空批次中的每个 finalized result 都带有 <code>terminate: true</code>，整批才终止。</p>
<p data-tool="mdnice编辑器">并行批次中，一个工具不应单方面吞掉其他工具结果。Pi 要求整批达成一致，语义更保守。DeepSeek Harness 允许一个权威工具宣布收口，扩展能力更强，也更依赖工具契约。</p>
<p data-tool="mdnice编辑器">选择 OR 还是 AND，取决于 <code>terminate</code> 的业务含义。</p>
<p data-tool="mdnice编辑器">如果它代表「任一工具发现全局终止条件」，OR 合理，比如用户取消、权限拒绝、事务已完成。</p>
<p data-tool="mdnice编辑器">如果它代表「当前工具不需要模型处理自己的结果」，AND 更稳妥，因为其他工具可能仍需模型解释。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 通过 <code>next-step</code> 上下文缓和了 OR 的侵略性，但无法覆盖所有情况。假设两个并行工具都成功，一个写数据库并 concludes，另一个返回一份需要模型分析的报告，又没有追加 context。模型回访会被跳过。工具注册规范需要限制哪些工具可以 conclude，不能把它当成普通便利字段。</p>
<h1 data-tool="mdnice编辑器"><span class="content">顺序提交</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的工具调度器还有一项容易被忽略的约束：执行可以并行，提交保持模型顺序。</p>
<p data-tool="mdnice编辑器">调度器维护：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>nextToStart</code>；</section>
</li>
<li>
<section><code>started</code>；</section>
</li>
<li>
<section><code>committed</code>；</section>
</li>
<li>
<section><code>inFlight</code>；</section>
</li>
<li>
<section>按调用位置排列的 <code>slots</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">并行工具完成时，只把结果放入对应 slot。<code>commitReady()</code> 从 <code>committed</code> 开始，连续提交已经就绪的结果。后面的工具先完成，也要等待前面的 slot。</p>
<p data-tool="mdnice编辑器">这会增加尾部等待时间，但能保证：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>tool/result</code> 顺序稳定；</section>
</li>
<li>
<section>additional context 顺序稳定；</section>
</li>
<li>
<section>replay 结果稳定；</section>
</li>
<li>
<section>插件观察顺序稳定；</section>
</li>
<li>
<section>同一模型响应在多次运行中的日志结构更一致。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">模型生成了 A、B、C 三个调用。若 C 最先完成，直接提交 C 会让后续上下文受网络时序影响。对于事件溯源系统，这类非确定性会扩大恢复和测试成本。</p>
<p data-tool="mdnice编辑器">Pi 的并行执行也使用 <code>Promise.all</code> 保留输入数组顺序，最终 tool result message 按原调用顺序发出。两套系统都接受并行执行带来的延迟收益，同时拒绝完成时序污染模型上下文。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 额外支持动态重分类。并行池补充新任务前，会重新读取工具当前 execution mode。前一个工具可能修改注册表，使后续工具从 parallel 变成 exclusive。调度器会停止补充，等待当前池排空，再把 exclusive call 留给下一个 barrier。</p>
<p data-tool="mdnice编辑器">这种灵活性会增加 scheduler 状态数量。适合「工具本身能改变工具环境」的 Harness。工具目录完全静态时，预先分组会更简单。</p>
<h1 data-tool="mdnice编辑器"><span class="content">取消语义</span></h1>
<p data-tool="mdnice编辑器">工具执行期间的取消，比模型流取消复杂得多。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的原则可以概括成三条：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>停止补充尚未启动的调用；</section>
</li>
<li>
<section>清空已经启动的调用；</section>
</li>
<li>
<section>为因取消而跳过的工具写入合成错误结果。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">已启动工具可能已经产生副作用，框架不能假装它没有运行。调度器等待这些调用 settle，并按模型顺序提交结果和 additional contexts。</p>
<p data-tool="mdnice编辑器">未启动工具则写入一对完整事件：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">tool/call
tool/result(error: aborted before dispatch)
</code></pre>
<p data-tool="mdnice编辑器">这样可以维持模型工具协议的配对关系。恢复和 replay 时，每个模型产生的 tool call 都有对应结果。</p>
<p data-tool="mdnice编辑器">内部 scheduler failure 的处理更克制。它停止新 dispatch，等待已经启动的调用结束，然后抛出第一个失败。它不会为剩余调用伪造结果。因为 scheduler failure 与用户取消不同，系统无法确认未提交调用处于什么语义状态。伪造恢复结果可能掩盖调度器缺陷。</p>
<p data-tool="mdnice编辑器">这里体现出事件日志的核心要求：日志不只服务 UI，它还是恢复协议。任何 synthetic event 都需要有确定语义。为了让数组长度好看而补事件，会污染后续决策。</p>
<p data-tool="mdnice编辑器">Pi 的实现更偏运行时事件流。工具执行异常会被转换成 error tool result，随后继续进入上下文。取消时顺序模式会在每次工具后检查 signal，平行模式也会停止准备后续调用。它的核心目标是把本次 invocation 的 message stream 完整交给调用方，持久日志的闭合约束没有 DeepSeek Harness 那么强。</p>
<h1 data-tool="mdnice编辑器"><span class="content">原因体系</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 <code>TurnEndReason</code> 包含六种核心结果：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>completed</code>；</section>
</li>
<li>
<section><code>blocked</code>；</section>
</li>
<li>
<section><code>max-tokens</code>；</section>
</li>
<li>
<section><code>aborted</code>；</section>
</li>
<li>
<section><code>error</code>；</section>
</li>
<li>
<section><code>interrupted</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些状态回答的是整个 turn 如何结束。</p>
<p data-tool="mdnice编辑器"><code>completed</code> 覆盖正常文本停止、工具要求收口，以及首步为空。它不承诺长期 Goal 完成。</p>
<p data-tool="mdnice编辑器"><code>blocked</code> 来自 pre-step reject。输入可能已经被 inbox 领取，但策略层拒绝进入模型调用。driver 当前会停止，其他尚未领取的普通 prompt 可以继续留在队列。</p>
<p data-tool="mdnice编辑器"><code>max-tokens</code> 表示至少一个 step 达到输出上限。它保留为 turn 级质量信号。</p>
<p data-tool="mdnice编辑器"><code>aborted</code> 携带取消原因，包括 user、parent、hook 和 disposed。持久导入还可能出现 legacy cause。</p>
<p data-tool="mdnice编辑器"><code>error</code> 保存结构化 <code>LlmFailure</code>。<code>LlmError</code> 的信息原样保留，其他异常会被压平为 <code>errorChain</code> 文本，并使用 <code>UNKNOWN</code> code。</p>
<p data-tool="mdnice编辑器"><code>interrupted</code> 由持久化恢复层产生。进程崩溃后，后端发现一个开放 turn，会用该状态闭合。live loop 不会主动写入它。</p>
<p data-tool="mdnice编辑器">把 crash repair 与 runtime abort 分开很有必要。abort 表示程序收到了取消请求，并有机会执行清理；interrupted 表示生命周期突然消失。两者对副作用审计、重试决策和用户提示都有不同含义。</p>
<p data-tool="mdnice编辑器">Pi 主要保留 assistant message 的 <code>stopReason</code>：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>stop</code>；</section>
</li>
<li>
<section><code>length</code>；</section>
</li>
<li>
<section><code>toolUse</code>；</section>
</li>
<li>
<section><code>error</code>；</section>
</li>
<li>
<section><code>aborted</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">它更接近最后一次模型调用的结束事实。DeepSeek Harness 的 reason 聚合了多 step turn 的结果。一个字段偏 provider，一个字段偏业务生命周期。</p>
<p data-tool="mdnice编辑器">从监控角度看，Pi 的 stopReason 更利于分析模型行为；DeepSeek Harness 的 turn reason 更利于分析任务执行。生产系统通常两类数据都需要。DeepSeek Harness 通过 <code>assistant/message</code>、request events 和 <code>turn/end</code> 同时保留两层事实，日志体积也会更大。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Goal 外循环</span></h1>
<p data-tool="mdnice编辑器">长期 Goal 无法靠核心 turn 的 <code>completed</code> 判断。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 采用外围 Round Driver。它监听 Agent 进入 idle，然后检查：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>Agent 实例是否仍然有效；</section>
</li>
<li>
<section>inbox 是否存在竞争中的普通 prompt；</section>
</li>
<li>
<section>Goal 是否存在；</section>
</li>
<li>
<section>Goal phase 是否为 active；</section>
</li>
<li>
<section>当前 activation 是否 armed；</section>
</li>
<li>
<section>已启动 round 数是否低于上限。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">满足条件后，Driver 构造一条来源为 goal 的 user message，并调用 <code>agent.followup()</code>。新消息进入 <code>next-turn</code>，打开全新的 turn。</p>
<p data-tool="mdnice编辑器">时序如下：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">turn/end(completed)
→ agent/status: idle
→ Goal Driver 读取持久状态
→ flush
→ followup(goal round)
→ turn/start
</code></pre>
<p data-tool="mdnice编辑器">这种结构给每个 Goal Round 一个完整、独立、可恢复的生命周期。进程在两个 Round 之间退出，恢复时可以从最后一个闭合 turn 继续判断。一个覆盖几十轮的巨大 while 会让持久化边界、用户抢占和异常恢复都更困难。</p>
<p data-tool="mdnice编辑器">Goal 还维护 phase 与 activation 两组状态。</p>
<p data-tool="mdnice编辑器">持久 phase 描述业务状态：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>active；</section>
</li>
<li>
<section>complete；</section>
</li>
<li>
<section>blocked；</section>
</li>
<li>
<section>paused。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">进程内 activation 描述自动续行授权：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>armed；</section>
</li>
<li>
<section>disarmed。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">把两者拆开可以处理 resume 和 fork。一个持久 Goal 仍然 active，不代表新进程应该自动继续消耗预算。恢复后的 active Goal 默认 disarmed，需要人类再次授权。否则某次服务重启可能让多个历史任务同时复活。</p>
<p data-tool="mdnice编辑器">达到最大 Round 数时，Goal 会进入 blocked，并记录 round-limit。<code>max-tokens</code>、Agent error、flush failure、driver failure 会 disarm。取消路径也会 pause 或 disarm，防止 idle 后被 Goal Driver 再次拉起。</p>
<p data-tool="mdnice编辑器">这里采用了 fail-closed 策略。长期自治任务遇到状态不确定时停止续行。对成本敏感、有写权限的 Agent，这是合理默认值。自动重试可以放在更高层，由持久预算和幂等策略约束。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Goal 收尾</span></h1>
<p data-tool="mdnice编辑器">Goal 状态改为 complete 或 blocked 后，当前 turn 通常还会再调用一次模型。</p>
<p data-tool="mdnice编辑器"><code>update_goal</code> 工具会修改持久状态，并通过 deferred context 注入收尾指令。工具结果与 <code>&lt;goal_complete&gt;</code> 或 <code>&lt;goal_blocked&gt;</code> context 进入下一 step，模型生成最终面向用户的说明。之后 turn 以 completed 结束。Agent 进入 idle，Goal Driver 读取到非 active phase，不再唤醒。</p>
<p data-tool="mdnice编辑器">这解决了状态完成与用户可见输出之间的时间差。</p>
<p data-tool="mdnice编辑器">如果 <code>update_goal(complete)</code> 直接 conclude turn，持久状态已经完成，用户界面可能只看到工具卡片，没有自然语言总结。若让模型先写总结再更新状态，模型或进程可能在总结期间失败，Goal 又会保持 active。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 先提交状态，再执行展示收尾。恢复时即便缺少最终文本，Goal 也不会重复工作。用户体验层可以根据 complete 状态补一条提示，或者提供重新生成总结的操作。</p>
<p data-tool="mdnice编辑器">代价是 complete 后多一次模型调用，也会增加少量 token 和延迟。我认为这笔成本合理。长期任务的最终输出属于交付结果，不能依赖 UI 猜测工具状态。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Pi 双循环</span></h1>
<p data-tool="mdnice编辑器">Pi 的 <code>runLoop()</code> 使用内外两层循环。</p>
<p data-tool="mdnice编辑器">内层条件为：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">hasMoreToolCalls || pendingMessages.length &gt; 0
</code></pre>
<p data-tool="mdnice编辑器">它处理工具回访和 steering。外层负责 Agent 本来要停止时的 follow-up。</p>
<p data-tool="mdnice编辑器">一次内层迭代包括：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">prepareNextTurn
→ 处理 pending steering
→ 调用模型
→ 执行工具
→ 发出 turn_end
→ shouldStopAfterTurn
→ 拉取新的 steering
</code></pre>
<p data-tool="mdnice编辑器">内层耗尽后，系统调用 <code>getFollowUpMessages()</code>。存在 follow-up 时，将它们设置为 pending，重新进入内层。没有 follow-up 时发出 <code>agent_end</code>。</p>
<p data-tool="mdnice编辑器">Pi 提供三个高杠杆控制点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>getSteeringMessages()</code>；</section>
</li>
<li>
<section><code>getFollowUpMessages()</code>；</section>
</li>
<li>
<section><code>shouldStopAfterTurn()</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这套接口很适合嵌入应用。</p>
<p data-tool="mdnice编辑器">steering 处理当前 activity 内的即时干预；follow-up 处理 Agent 自然收口后的补充工作；shouldStopAfterTurn 提供宿主级截停。扩展作者不需要理解持久 inbox，也不需要创建新的状态投影。</p>
<p data-tool="mdnice编辑器">复杂度被交给宿主。多个扩展共同提供 follow-up 时，需要上层决定合并方式。进程恢复后，回调内部的临时状态如何重建，也由应用实现。若不同扩展分别定义 Goal、Plan 和审批流程，结束语义会逐渐碎片化。</p>
<p data-tool="mdnice编辑器">Pi core 的目标是提供一个小而可组合的运行时。它没有承诺统一的长期任务协议。这个边界和 DeepSeek Harness 的产品定位并不相同。</p>
<h1 data-tool="mdnice编辑器"><span class="content">失败路径</span></h1>
<p data-tool="mdnice编辑器">Pi 遇到 assistant <code>error</code> 或 <code>aborted</code> 时，会发出 <code>turn_end</code> 和 <code>agent_end</code>，然后结束当前 invocation。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 会先区分流 finish error、显式 abort 和普通异常。</p>
<p data-tool="mdnice编辑器">请求错误可以进入 <code>agent/request-error</code> waterfall。插件能够依据 provider、failure、retry policy 和 signal 决定 retry。重试发生在同一个 step 内，重新创建 assembler，再发一次请求。外部 turn 和 step 边界保持不变。</p>
<p data-tool="mdnice编辑器">这种设计使日志更紧凑，但需要谨慎处理重复请求。前一次失败若已经在 provider 侧产生计费，session 中可能只有请求重试后的最终 assistant message。底层观测系统需要独立记录 attempt，不能只靠 conversation event log 统计模型调用次数。</p>
<p data-tool="mdnice编辑器">Pi 通常由 stream implementation 或上层配置承担 retry。Agent Loop 接收到的 final message 已经带有 stopReason。职责划分更轻，调用方也更容易替换模型层。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 将 request header、adapter defaults、request context 和 conversation surface 都纳入 session 语义，retry 与恢复自然更靠近 loop。架构更重，换来统一的请求重建能力。</p>
<h1 data-tool="mdnice编辑器"><span class="content">请求序列</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 每次构建请求时都会比较当前 header 与 session 中的 baseline。</p>
<p data-tool="mdnice编辑器">header 包含：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>provider 和 model；</section>
</li>
<li>
<section>reasoning effort、max tokens 等调用配置；</section>
</li>
<li>
<section>adapter materialized defaults；</section>
</li>
<li>
<section>system prompt；</section>
</li>
<li>
<section>tool schemas。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">首个 header 记录为 initial 或 resume。配置变化时记录 change。显式开始新消息序列或 surface replacement 后，会记录 series。</p>
<p data-tool="mdnice编辑器">结束判断和 request series 没有直接控制关系，但它影响「同一个上下文连续体」的解释。Goal 的自动 Round 可以标记 <code>startsRequestSeries: true</code>，让每轮长期任务拥有独立请求序列。缓存、展示和重放层因此能识别边界。</p>
<p data-tool="mdnice编辑器">如果只依靠 turn start 判断新序列，会把工具回访、用户 follow-up、Goal Round 和 compaction 后续请求全部归入一种语义。DeepSeek Harness 单独记录 series，是在为后续缓存和重建保留信息。</p>
<p data-tool="mdnice编辑器">成本是日志事件更多，header equality 与 canonicalization 也必须稳定。工具 schema 排序、空字段处理或 adapter defaults 若不规范，可能制造大量无意义 change 事件。代码使用 canonical header 和 deep freeze，正是在控制这种漂移。</p>
<h1 data-tool="mdnice编辑器"><span class="content">日志消费</span></h1>
<p data-tool="mdnice编辑器">消费 DeepSeek Harness 日志时，先问清业务方想判断哪一层结束。</p>
<p data-tool="mdnice编辑器">判断一次模型调用完成，应读取 step 内的 assistant message、stream finish 和 usage。</p>
<p data-tool="mdnice编辑器">判断 turn 是否闭合，应寻找配对的 <code>turn/start</code> 与 <code>turn/end</code>。只有 assistant message 不够。pre-step reject、空 turn、异常和 crash repair 都可能产生不同结构。</p>
<p data-tool="mdnice编辑器">判断当前 Agent 是否暂无活动，应读取 status 或等待 <code>whenIdle()</code>。最后一条 turn/end 仍不足以证明 idle，因为 driver 可能已经打开下一 turn。</p>
<p data-tool="mdnice编辑器">判断长期 Goal 是否结束，应读取 Goal phase。complete、blocked 和 paused 都会停止自动 Round，业务含义各不相同。</p>
<p data-tool="mdnice编辑器">判断 Plan 是否退出，应读取最新 <code>plan/mode.active</code>。看到 <code>exit_plan_mode</code> 调用，只能说明模型提出了退出请求。审批可能被拒绝，pending intent 也可能尚未在 pre-step 提交。</p>
<p data-tool="mdnice编辑器">判断会话以后是否还会运行，需要观察 Agent 是否 disposed、是否从 registry 移除，以及外围 scheduler 是否仍能发送消息。idle 和 completed 都没有永久性。</p>
<p data-tool="mdnice编辑器">Pi 的消费方式更简单：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>assistant <code>stopReason</code> 描述模型调用；</section>
</li>
<li>
<section><code>turn_end</code> 描述一次模型与工具批次；</section>
</li>
<li>
<section><code>agent_end</code> 描述当前 invocation；</section>
</li>
<li>
<section>Goal、Plan 和工作流完成度读取具体扩展状态。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">即使已经收到 <code>agent_end</code>，宿主仍然可以再次调用 Agent Loop。它同样不具备永久终止含义。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工程取舍</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 为结束判断支付了较高复杂度。</p>
<p data-tool="mdnice编辑器">它需要：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>phase 状态机；</section>
</li>
<li>
<section>inbox 分类；</section>
</li>
<li>
<section>wake latch；</section>
</li>
<li>
<section>平衡的 turn 和 step 事件；</section>
</li>
<li>
<section>surface projection；</section>
</li>
<li>
<section>request header 重建；</section>
</li>
<li>
<section>crash repair；</section>
</li>
<li>
<section>Goal activation；</section>
</li>
<li>
<section>Plan boundary intent；</section>
</li>
<li>
<section>有序工具提交。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些机制会增加代码量、测试矩阵和插件开发门槛。一个简单聊天产品采用全套设计，投入可能超过收益。</p>
<p data-tool="mdnice编辑器">它适合以下场景：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>会话需要跨进程恢复；</section>
</li>
<li>
<section>Agent 拥有高风险工具；</section>
</li>
<li>
<section>多个插件会同时 steering；</section>
</li>
<li>
<section>需要长期 Goal 与预算控制；</section>
</li>
<li>
<section>日志需要审计和 replay；</section>
</li>
<li>
<section>Plan、审批和执行必须跨边界保持一致；</section>
</li>
<li>
<section>子 Agent 和父 Agent 存在取消传播。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Pi 的循环更适合：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>单次 invocation 生命周期清晰；</section>
</li>
<li>
<section>宿主已经拥有自己的持久化层；</section>
</li>
<li>
<section>扩展数量有限；</section>
</li>
<li>
<section>工作流由应用统一控制；</section>
</li>
<li>
<section>希望快速接入多模型与工具；</section>
</li>
<li>
<section>对 session event vocabulary 没有强约束。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Pi 的小核心减少了框架内部状态。业务一旦开始增加 Goal、审批、自动 follow-up、并发工具策略、恢复和预算，宿主会逐步补齐类似机制。复杂度没有消失，只是落在不同层。</p>
<p data-tool="mdnice编辑器">团队选择框架时，不应只比较 Agent Loop 文件有多少行。要看系统准备把「继续运行的权力」放在哪里。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 把权力分散给模型 finish、工具结果、inbox、生命周期 hook 和外围持久状态机，并通过事件日志协调。Pi 把权力集中在当前循环和几个宿主回调中，扩展层可以自行定义更高层协议。</p>
<h1 data-tool="mdnice编辑器"><span class="content">设计建议</span></h1>
<p data-tool="mdnice编辑器">如果我们团队要自行实现 Agent Loop，我会保留以下原则。</p>
<p data-tool="mdnice编辑器">第一，区分模型结束、工具批次结束、用户 turn 结束和长期任务结束。类型和事件名也要分开，避免一个 <code>done</code> 字段横跨所有层级。</p>
<p data-tool="mdnice编辑器">第二，继续执行需要携带数据。插件要求再跑一轮时，应提交 steering 或 follow-up message。单独返回布尔值会让调试和组合越来越困难。</p>
<p data-tool="mdnice编辑器">第三，工具调用后的模型回访应被建模为债务。普通工具结果产生一次回复债务，conclude 或 terminate 可以消除债务，steering 又会重新增加消息债务。用这套视角设计状态机，比堆叠 <code>if (hasToolCalls)</code> 更稳定。</p>
<p data-tool="mdnice编辑器">第四，最大 token 需要独立结果，且不要执行疑似截断的工具参数。自动修复可以存在，但必须显式记录恢复过程。</p>
<p data-tool="mdnice编辑器">第五，并行工具允许乱序完成，持久结果应按模型顺序提交。除非协议明确支持乱序 tool result，否则不要让网络时序改变上下文。</p>
<p data-tool="mdnice编辑器">第六，取消必须区分已启动与未启动调用。已启动工具要排空或记录未知结果，未启动工具需要完整的 synthetic result，具体选择取决于日志协议。</p>
<p data-tool="mdnice编辑器">第七，idle 不能充当长期完成信号。Goal phase、workflow state 和 Agent lifecycle 应拥有独立状态。</p>
<p data-tool="mdnice编辑器">第八，模式切换要提交在请求边界。Plan、权限、模型路由和工具目录的变化，都不应在一个已开始的请求批次中途生效。</p>
<p data-tool="mdnice编辑器">第九，恢复后的自动续行默认关闭。长期 Agent 会消耗资金，也可能修改外部系统。新进程接管旧任务时，应重新取得授权或租约。</p>
<p data-tool="mdnice编辑器">第十，结束原因需要兼顾机器消费。<code>completed</code>、<code>blocked</code>、<code>max-tokens</code>、<code>aborted</code>、<code>error</code>、<code>interrupted</code> 这类结构化 union，比一段自然语言错误更适合监控、重试和审计。</p>
<h1 data-tool="mdnice编辑器"><span class="content">架构落点</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 把结束判断分布在四个控制面上。</p>
<p data-tool="mdnice编辑器">模型输出控制 step：还有没有工具回访债务。</p>
<p data-tool="mdnice编辑器">inbox 控制 turn：还有没有当前轮待处理消息。</p>
<p data-tool="mdnice编辑器">driver 控制 activity：队列耗尽后是否进入 idle。</p>
<p data-tool="mdnice编辑器">Goal 与 Plan 控制工作流：idle 后是否开启新 Round，下一请求采用哪种策略。</p>
<p data-tool="mdnice编辑器">Pi 将更多判断放在同一个运行循环里。工具调用和 pending steering 驱动内层循环，follow-up 驱动外层循环，<code>shouldStopAfterTurn</code> 提供宿主截停点，队列耗尽后发出 <code>agent_end</code>。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 更接近一个事件溯源的 Agent Runtime。Pi 更接近一个可嵌入的 Agent 执行内核。</p>
<p data-tool="mdnice编辑器">评价两者没啥意义，我们需要关注的是其系统边界。</p>
<p data-tool="mdnice编辑器">如果产品只需要一次 prompt 驱动的工具循环，Pi 的结构更容易维护。若产品已经出现跨轮 Goal、审批、恢复、多插件 steering 和高风险工具，DeepSeek Harness 的分层会减少后期协议冲突。</p>
<p data-tool="mdnice编辑器">Agent Loop 最难处理的部分，从来都不是让模型继续调用工具。真正消耗工程时间的是：它为什么继续、谁允许它继续、失败后还能不能继续、恢复后应不应该继续，以及日志能否解释它曾经做过的每一次选择。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DeepSeek Harness 的 Cordis 插件架构</title>
		<link>https://www.phppan.com/2026/08/deepseek-harness-cordis/</link>
		<comments>https://www.phppan.com/2026/08/deepseek-harness-cordis/#comments</comments>
		<pubDate>Sun, 23 Aug 2026 03:36:05 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agent Harness]]></category>
		<category><![CDATA[AIAgent架构]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>
		<category><![CDATA[harness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2531</guid>
		<description><![CDATA[DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插件。</p>
<p data-tool="mdnice编辑器">系统没有一块承载业务能力的固定内核，Cordis 只维护上下文、服务注册、事件分发、依赖激活和副作用回收；Agent 能做什么，由启动时挂入的一棵插件树决定。</p>
<p data-tool="mdnice编辑器">传统的插件有注册容易，撤销困难；初始化容易，失败回滚困难；全局单例容易，局部覆盖困难等问题，而 Cordis 把这些困难收进了运行时语义，Harness 再用 session、agent 和 preset 的业务约束补齐它。</p>
<p data-tool="mdnice编辑器">今天聊了一下 DeepSeek Harness 的核心 Cordis 架构以及 Cordis 所谓「时空可组合」，究竟怎样落到代码里，又为何适合 Agent Harness 这类持续变化的运行时。</p>
<h1 data-tool="mdnice编辑器"><span class="content">插件的历史</span></h1>
<p data-tool="mdnice编辑器">插件架构的历史并不短。Eclipse 在二十多年前已经把 IDE 拆成插件、扩展点和扩展，宿主声明可扩展位置，其他插件通过清单贡献菜单、编辑器和处理逻辑。</p>
<p data-tool="mdnice编辑器">OSGi 更进一步，给 bundle 配置安装、启动、停止、更新和卸载生命周期，再通过共享服务注册表完成发布、查找和绑定。服务离开注册表时，依赖方必须跟着处理动态变化。</p>
<p data-tool="mdnice编辑器">今天看 Cordis，能找到这些设计的清晰痕迹：动态服务、生命周期、注册表、声明式依赖、事件通知都已有成熟先例。</p>
<p data-tool="mdnice编辑器">Cordis 没有发明插件系统。</p>
<p data-tool="mdnice编辑器">它简化并重新组合了这些概念，让它们适合 TypeScript 应用内的细粒度组装。OSGi 的部署单元是 bundle，模块、生命周期、服务和安全各有一层；Cordis 的执行单元是 Fiber，一个函数或一个 <code style="color: #ef7060;">Service</code> 子类就能成为插件。Eclipse 的扩展点通常由宿主定义结构化清单，Cordis 让服务和类型化事件直接成为扩展面。Agent 运行时里的工具、提示词片段、模型适配器和审批策略变化频繁，如果每次扩展都要引入重量级模块边界，团队很快会绕过框架，重新写回几个全局数组。</p>
<p data-tool="mdnice编辑器">Cordis 官方仓库把自己定位为「Meta-Framework of Spatiotemporal Composability」。</p>
<p data-tool="mdnice编辑器">从源码看，「元框架」表示它不规定 Agent、Web 服务或机器人该有什么组件，只提供构造框架所需的基础语义。DeepSeek Harness 在其上定义 <code style="color: #ef7060;">sessions</code>、<code style="color: #ef7060;">tools</code>、<code style="color: #ef7060;">llm</code>、<code style="color: #ef7060;">agents</code> 等服务，又用这些服务组装产品。Cordis 与 Harness 的关系更接近运行时机制和领域框架，而非通用插件平台与插件集合。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 还把 Cordis 源码直接放进 <code style="color: #ef7060;">vendor/</code>，固定在明确的上游提交，并将包名重映射到 <code style="color: #ef7060;">@deepseek-ai</code> 命名空间。</p>
<p data-tool="mdnice编辑器">项目维护的本地修改包含 Fiber 重入卸载加固、配置更新事务、HMR 精确监听、延迟配置解析等。这增加了维护成本，也换来了框架层的可审计性。Agent 能执行 Shell、修改文件、访问网络，插件生命周期出错会留下进程、监听器、终端模式或权限状态。</p>
<p data-tool="mdnice编辑器">此时依赖一个本地的代码白盒，会让人放心一些。</p>
<h1 data-tool="mdnice编辑器"><span class="content">调用链路图</span></h1>
<p data-tool="mdnice编辑器">简单的调用图如下所示：</p>
<figure data-tool="mdnice编辑器"><img src="https://files.mdnice.com/user/36365/c8ef3798-50e3-40a7-810e-c68c5f71ca03.png" alt="" /></figure>
<h1 data-tool="mdnice编辑器"><span class="content">最小内核</span></h1>
<p data-tool="mdnice编辑器">Cordis 的根对象是 <code style="color: #ef7060;">Context</code>。它同时承担依赖容器、插件挂载入口和事件入口。</p>
<p data-tool="mdnice编辑器">服务通过稳定名称出现在 <code style="color: #ef7060;">ctx</code> 上，例如 <code style="color: #ef7060;">ctx.tools</code>、<code style="color: #ef7060;">ctx.llm</code>、<code style="color: #ef7060;">ctx.sessions</code>。插件声明 <code style="color: #ef7060;">inject</code> 后，Cordis 只有在所需服务可用时才激活它；服务消失，依赖插件也会进入卸载或等待状态。配置文件中条目的先后顺序因此不承担启动顺序，依赖关系才承担。</p>
<p data-tool="mdnice编辑器">这种处理解决了传统插件系统的第一个顽疾：隐含初始化顺序。</p>
<p data-tool="mdnice编辑器">常见实现会遍历插件数组，依次调用 <code style="color: #ef7060;">init</code>。当工具依赖文件系统、文件系统依赖沙箱、UI 又依赖会话时，数组顺序就变成一套没有类型、没有诊断的依赖图。后来插入一个插件，顺序约束可能跨越几十个文件。</p>
<p data-tool="mdnice编辑器">Cordis 把需求写进 <code style="color: #ef7060;">inject</code>，Fiber 会为每项依赖保存当前实现；缺少依赖时保持 <code style="color: #ef7060;">PENDING</code>，依赖齐备后进入 <code style="color: #ef7060;">LOADING</code> 和 <code style="color: #ef7060;">ACTIVE</code>。启动失败则进入 <code style="color: #ef7060;">FAILED</code>。状态机至少让故障有了准确位置。</p>
<p data-tool="mdnice编辑器"><code style="color: #ef7060;">Context</code> 还是一个代理对象。</p>
<p data-tool="mdnice编辑器">插件直接读取 <code style="color: #ef7060;">ctx.tools</code> 时，反射层会检查它是否声明过依赖，并沿 Fiber 父链解析实现。未声明就读取会抛错；声明了但当前上下文不可用，也会抛出另一类错误。这种约束防止依赖藏在任意函数深处。源码里依然提供 <code style="color: #ef7060;">ctx.get(name)</code> 读取可选服务，区别在于调用方显式接受服务可能不存在。</p>
<p data-tool="mdnice编辑器">这里的「服务定义、服务提供方、消费方」三种角色被完整建模。比如文件系统能力不能只写一个接口，也不能只挂一个本地实现。定义方稳定调用协议，提供方接入本地目录或远程沙箱，消费方把能力变成模型可见工具。Harness 把这组关系称为 capability seam。替换沙箱时，Shell、PTY、LSP 只要依赖同一能力面，就能整体迁移到新的执行环境，消费方无需知道提供方运行在本机还是远端。</p>
<p data-tool="mdnice编辑器">「一切皆插件」有一个前提是：一切产品能力皆由插件贡献，Cordis 自身仍保留插件得以存在的机制。上下文代理、Fiber 状态机、服务存储、事件总线和 Loader 属于元层。</p>
<p data-tool="mdnice编辑器">这并不是系统里没有内核，更准确的说，应该是，内核不拥有模型、工具和循环等产品特权。</p>
<h1 data-tool="mdnice编辑器"><span class="content">空间组合</span></h1>
<p data-tool="mdnice编辑器">同一个进程里，服务名称必须稳定，实例又不能全局唯一。两个 Agent 可能选择不同模型、不同工具集、不同 persona 和不同沙箱。如果 <code style="color: #ef7060;">ctx.llm</code> 永远指向一个全局对象，插件替换只发生在进程级，无法满足多会话并存。</p>
<p data-tool="mdnice编辑器">Cordis 的 <code style="color: #ef7060;">Context.isolate</code> 为指定服务名创建一个 realm 标签。服务注册和查找都以该标签定位，同名服务可以在不同子上下文里各自存在。两个 <code style="color: #ef7060;">isolate</code> 调用传入同一标签时会加入同一 realm；使用新标签时彼此隔离。子上下文通过原型继承父上下文，隔离映射按层遮蔽，因此局部配置不需要复制整棵容器。</p>
<p data-tool="mdnice编辑器">这解释了「空间可组合」的第一层：组合具有位置。插件挂在哪个上下文，决定它提供的服务、注册的监听器和持有的副作用在哪个范围可见。传统依赖注入容器也有 singleton、request、session 等 scope，Cordis 的差异在于作用域与插件树、事件过滤、资源所有权使用同一个 Context 表达。开发者无需在四套 API 之间同步身份。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 又增加了 <code style="color: #ef7060;">dsh-scope</code>。它用不透明对象作为 scope key，维护父子关系，并创建带路由身份的事件 receiver。注册视图沿父链向下继承：Agent 能看到所属 preset 的提示词和工具。事件则沿链向上接纳：preset 级监听器能收到其下 Agent 的事件，兄弟 Agent 互不串线。这是第二层空间结构，解决的是领域身份，而非单个服务名的 realm。</p>
<p data-tool="mdnice编辑器">为何需要两层？<code style="color: #ef7060;">isolate</code> 处理「哪个 <code style="color: #ef7060;">tools</code> 服务实例」，<code style="color: #ef7060;">dsh-scope</code> 处理「同一工具注册表里，哪些注册项对当前 Agent 可见」。前者适合替换提供方，后者适合对注册内容分层。把所有差异都做成独立服务实例，会放大内存和初始化成本；把所有差异都塞进一个全局注册表，又会让过滤规则散落在调用点。这两层模型把实例隔离和内容路由分开了。</p>
<p data-tool="mdnice编辑器">Agent preset 是空间组合的完整应用。一个 preset 的 Cordis 配置会挂在一个长期存在的 scope 下，Agent 创建时把自己的 scope key 绑定到该 preset。相同 preset 的并发首次使用通过 single-flight 共享一次挂载，后续 Agent 复用这份组合。文件变化后，新会话加入新一代组合，已有会话保留原来的代际。这个策略避免会话运行中途突然更换工具或提示词，代价是旧代际要保留到整棵运行时退出，配置频繁变化时内存会按代际增长。</p>
<p data-tool="mdnice编辑器">源码还专门审计 preset 子树是否把服务发布到了 root realm。发生这种泄漏时，第二个会话挂载同一 preset 会与第一个冲突，所谓会话级组合也会退化成进程全局状态。DeepSeek Harness 在发布 Agent 前拒绝这种配置。空间隔离若只靠约定，迟早会被一个没有 <code style="color: #ef7060;">isolate</code> 的 provider 穿透；运行时审计比文档警告可靠。</p>
<p data-tool="mdnice编辑器">这套空间模型存在认知成本。插件作者要同时理解 Context 父链、服务 realm、业务 scope 父链和事件过滤方向（当然，在当前 Vibe Coding 盛行的时代，也可以作者不理解，直接让 AI 来搞）。</p>
<p data-tool="mdnice编辑器">在认知不清楚的时候，常见的错误往往表现为某项能力「看不见」或意外泄漏，类型系统无法证明运行时挂载位置。DeepSeek Harness 用 preset 挂载审计、包级 invariant 和真实组合测试降低风险，却没有消除模型复杂度。团队若只需要单进程单 Agent，直接引入整套空间语义会显得过重。</p>
<h1 data-tool="mdnice编辑器"><span class="content">时间组合</span></h1>
<p data-tool="mdnice编辑器">插件能挂载只是静态组合。运行期间服务上线、配置改变、插件失败或上下文销毁，系统还要回到一致状态。Cordis 用 Fiber 和 effect 管理这条时间轴。</p>
<p data-tool="mdnice编辑器">每次插件应用都会产生一个 Fiber。Fiber 记录父上下文、原始配置、解析后配置、依赖实现快照、生命周期状态和 disposables。插件调用 <code style="color: #ef7060;">ctx.effect()</code> 注册副作用，effect 的执行结果返回 disposer。事件监听、服务提供、子插件和访问器最终都进入这套所有权体系。Fiber 卸载时按注册的逆序执行清理，并等待异步清理达到静止状态。</p>
<p data-tool="mdnice编辑器">逆序本身不是一个特别要讲的事情，因为这是必须的。</p>
<p data-tool="mdnice编辑器">若插件先启动子进程，再注册输出监听，最后暴露服务，销毁时应先撤销服务，停止新请求，再移除监听，最后结束进程。资源创建顺序的反向通常就是依赖安全的拆卸顺序。</p>
<p data-tool="mdnice编辑器">传统插件常给出一个 <code style="color: #ef7060;">deactivate()</code> 钩子，把所有清理责任推给作者；漏掉一个定时器或事件监听，热加载几次便出现重复执行。Cordis 让每次注册同时产生撤销动作，框架持有所有权。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 vendor 版本进一步处理了重入场景：effect 在执行 setup 前先登记所有者包装；插件发布事件时，观察者可能同步卸载它；异步 cleanup 已经开始后，其他调用者仍能等待同一次清理；Fiber 处于 <code style="color: #ef7060;">UNLOADING</code> 时拒绝创建新 effect。这些代码看起来全是繁琐的边界处理，但为了保证生命周期并发的安全，一个都不能少。否则插件的热卸载就只是个玩具，没法真正落地。</p>
<p data-tool="mdnice编辑器">依赖变化也属于时间组合。某个服务被提供后，反射层通知所有声明该依赖的 Fiber 重新检查；条件满足便激活。服务撤销后，依赖方会卸载并回到等待。OSGi 早已采用动态服务注册表，Cordis 的新意主要在于把动态依赖、作用域上下文和 effect 所有权压进一个很小的进程内模型。代价同样继承自 OSGi：任何持有服务引用越过生命周期的代码，都可能在提供方撤销后继续调用过期对象。Cordis 保存加载时的实现快照，却无法替业务代码管理逃逸引用。</p>
<p data-tool="mdnice编辑器">Loader 把时间组合延伸到配置。DeepSeek Harness 的 profile 由多层 bundle patch 叠加：基础 bundle、模式 bundle、profile patch、home patch、命令行 overlay。patch 按 id 定位条目，配置采用整块替换。整块替换要求用户重述保留字段，使用上稍显笨重，却避免深度合并规则在数组、表达式和删除语义上制造歧义。</p>
<p data-tool="mdnice编辑器">配置热更新时，Loader 先导入候选插件，再卸载旧实例并应用候选；候选失败会恢复旧插件或旧配置。Group 对一批子条目并发启动，收集全部结果，只要一项失败就删除新增项并重建旧配置。Include 读取候选文件、在副本上应用 patch、完成树协调后才提交缓存。用户 patch 语法错误或新插件启动失败时，最后一棵可用树继续运行。</p>
<p data-tool="mdnice编辑器">这已经接近配置事务，却不能等同于数据库事务。</p>
<p data-tool="mdnice编辑器">插件 effect 可能调用外部 API、创建远端资源、发送消息；disposer 只能做补偿，无法保证外部世界回滚。Loader 能保证自己管理的树和服务注册恢复，无法撤回所有不可逆副作用。插件开发规范仍要限制初始化期行为，把外部写入延迟到真正的业务请求，或者设计幂等键和补偿路径。</p>
<p data-tool="mdnice编辑器">时间组合还带来可观的测试面积。挂载成功只是第一条路径，还要覆盖依赖晚到、依赖撤销、初始化失败、卸载重入、异步清理、热更新失败、回滚再次失败。DeepSeek Harness 要求注册项证明 disposal，产品可见插件还要通过真实 Loader 组合测试。这个成本无法靠框架消失，只能被框架集中暴露。相比线上出现幽灵监听器和半更新状态，还是愿意支付这部分测试费用。</p>
<h1 data-tool="mdnice编辑器"><span class="content">事件契约</span></h1>
<p data-tool="mdnice编辑器">插件之间只靠服务调用，会把所有扩展都变成接口方法。宿主每增加一项策略，就要修改服务定义。Cordis 同时提供类型化事件，并区分 <code style="color: #ef7060;">emit</code>、<code style="color: #ef7060;">parallel</code>、<code style="color: #ef7060;">serial</code>、<code style="color: #ef7060;">bail</code> 和 <code style="color: #ef7060;">waterfall</code>。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 最依赖的是 waterfall。它的监听器拿到 <code style="color: #ef7060;">next</code>，调用后把控制权交给下一层，不调用就截断链条。模型请求、工具执行和轮次控制都能被插件包裹。审批策略可以在工具执行前拒绝，重试插件可以包围模型流，日志插件可以观察前后状态。它与 Koa 一类中间件链相似，但事件名和 Context 过滤让同一个分发器覆盖多个能力域。</p>
<p data-tool="mdnice编辑器">waterfall 也最容易出错。一个只想记录日志的监听器忘记调用 <code style="color: #ef7060;">next()</code>，整条能力链便被短路。DeepSeek Harness 把「必须调用 <code style="color: #ef7060;">next()</code>」写进项目级规则，并在事件文档里记录 dispatch mode。类型能保证参数，却很难保证 continuation 一定执行。代码评审和组合测试仍是主要防线。</p>
<p data-tool="mdnice编辑器">事件的持久性也被刻意分层。<code style="color: #ef7060;">agent/*</code> 和 <code style="color: #ef7060;">tools/*</code> 事件用于活跃运行时的拦截；会话事件追加到日志，承担恢复、fork、UI 回放和模型历史投影。DeepSeek Harness 规定「模型可见即已记录」：进入模型请求的输入必须能从 session log 重建。插件若偷偷修改 prompt 却不产生会话事实，重放结果会漂移，问题也无法审计。</p>
<p data-tool="mdnice编辑器">这条约束说明，一切皆插件并不意味着一切皆事件。直接能力调用放进 Service，策略和拦截放进实时事件，需要持久化的事实进入 session log。三类通信各自承担调用、扩展和历史。将它们混成一个全局 event bus，短期代码更少，长期会失去时序语义和数据权威。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Agent 适配</span></h1>
<p data-tool="mdnice编辑器">Agent Harness 比普通 Web 服务更需要动态组合。模型供应商会变，工具权限随工作区变化，子 Agent 的执行环境可能与父 Agent 不同，UI 还要消费同一份流式过程。如果主循环直接 import 每个能力，任何替换都会改循环；循环逐渐成为依赖最多、风险最高的文件。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的默认 agent loop 仍然存在，但它也是一个服务提供方。循环读取 session log 生成历史，组装插件贡献的 prompt 和 tool schema，通过 <code style="color: #ef7060;">agent/request</code> 进入模型适配器，再经过工具流水线记录结果。扩展点围绕步骤、请求、工具和停止阶段布置。新功能通常挂到这些事件或注册表，项目规则甚至要求修改 agent loop 时同步更新架构文档。</p>
<p data-tool="mdnice编辑器">我赞成这种限制。循环承担控制流，频繁承载产品策略后会迅速腐化。压缩上下文、重试、审批、工具超时、计划模式、子 Agent 调度都可以有自己的生命周期与配置。它们进入循环的接口必须少且稳定。插件化不会自动获得解耦，真正起作用的是 DeepSeek Harness 为能力选择了明确的 seam，并拒绝消费方特有逻辑污染定义层。</p>
<p data-tool="mdnice编辑器">UI 作为插件也有实际意义。Web 和 headless profile 在共享 base bundle 上增加不同条目，后者可以完全不启动服务器。UI 驱动 <code style="color: #ef7060;">ctx.agents</code>，订阅 <code style="color: #ef7060;">session/event</code> 渲染状态，没必要成为循环的一部分。服务端、CLI、ACP 和浏览器界面可以共享会话及 Agent 语义，同时保留自己的传输和展示逻辑。</p>
<p data-tool="mdnice编辑器">插件树还提供自省基础。Fiber 保留名称、状态、依赖和 effect 元数据，DeepSeek Harness 能实现查看、挂载、卸载自身插件的能力。对 Agent 而言，这比普通应用更敏感：模型可以通过工具改变运行时，错误配置可能直接扩大权限。源码中的 sandbox 和 approval 仍是独立能力，插件架构没有天然安全性。自修改必须受工具权限、配置校验和作用域审计约束。</p>
<h1 data-tool="mdnice编辑器"><span class="content">架构代价</span></h1>
<p data-tool="mdnice编辑器">第一项代价是启动与运行时开销。每个插件产生 Fiber，服务访问经过 Proxy 和反射解析，事件分发要执行作用域过滤，注册项还要保存 disposer 与诊断元数据。对于 LLM Agent，单次模型请求通常以百毫秒到秒计，这些 JavaScript 调度开销很难成为主要瓶颈；高频 token chunk、文件扫描或终端字节流若全部穿过通用事件总线，成本会被放大。DeepSeek Harness 把流式模型输出定义为能力语义，但大量数据处理仍应留在具体 provider 内，事件只承载必要扩展点。</p>
<p data-tool="mdnice编辑器">第二项代价是故障面扩大。插件初始化可以同步抛错、异步拒绝、等待缺失服务，也可能在 disposer 中失败。Group 并发启动缩短时间，却会带来多个兄弟同时失败的 AggregateError。DeepSeek Harness 的 boot 会等待 Loader 结算，审计所有启用条目是否加载与激活，失败时先 dispose 部分构造的上下文，再退出。少做任何一步，都可能让终端停留在 raw mode，或者让后台 watcher 继续运行。</p>
<p data-tool="mdnice编辑器">第三项代价是配置成为编程接口。<code style="color: #ef7060;">cordis.yml</code> 支持 <code style="color: #ef7060;">!!js</code> 表达式，条目可以按环境禁用，配置会在依赖激活后求值。灵活性很高，静态分析能力随之下降。表达式读取服务时，求值时机与上下文位置都会影响结果。DeepSeek Harness 只允许 <code style="color: #ef7060;">config</code> 和 <code style="color: #ef7060;">disabled</code> 使用插值，其他元数据保持字面量，并让错误尽早暴露。这仍要求运维人员理解插件依赖和 patch 覆盖语义，配置文件已经超出普通 YAML 参数表的复杂度。</p>
<p data-tool="mdnice编辑器">第四项代价是生态兼容。Cordis 的服务名和事件类型提供了源码级协议，插件版本升级仍可能修改配置、事件 payload 或生命周期假设。DeepSeek Harness 目前处于预发布阶段，仓库规则允许直接拒绝旧磁盘格式，也没有承诺 session 格式兼容。现在的可组合性主要服务于同一发行版内的替换和扩展，尚不能推导出跨版本插件 ABI 稳定。</p>
<p data-tool="mdnice编辑器">第五项代价是组织治理。一切都能成为插件后，团队容易把每段十几行逻辑都拆成包，形成依赖图膨胀、文档分散和测试启动缓慢。DeepSeek Harness 用 package 分组、Service Definition/Provider/Consumer 角色、每包 README、运行时 invariant 和真实组合测试控制边界。这些规范本身就是成本。小团队若没有维护扩展生态或多 profile 的需求，模块化函数加显式依赖可能更合适。</p>
<h1 data-tool="mdnice编辑器"><span class="content">历史对照</span></h1>
<p data-tool="mdnice编辑器">从 OSGi 看 Cordis，动态服务和生命周期联动已有先例。服务注册、发现、撤销后触发依赖变化，这条主线几乎一致。Cordis 值得学习的新点，是把资源清理统一成 effect，并让子插件、服务、监听器都归属于同一个 Fiber。OSGi 的 bundle 生命周期更完整，也更重；Cordis 选择应用内细粒度对象，失去了类加载隔离、标准版本解析和安全层，获得了低门槛组合。</p>
<p data-tool="mdnice编辑器">从 Eclipse 看 DeepSeek Harness，profile 与 bundle patch 很像部署时组装，Service 和事件类似扩展点。差异在于 Eclipse 扩展通常围绕稳定宿主能力，DeepSeek Harness 连 agent loop 和 UI 都可替换，核心插件与第三方插件共享同一挂载机制。这个平权减少了特权内核，风险也更集中到协议治理：循环能被替换，不代表任何替代循环都遵守 session log、权限和工具时序约束。</p>
<p data-tool="mdnice编辑器">从依赖注入容器看，Cordis 的服务解析并不陌生。新的组合来自 DI 与生命周期的绑定。普通容器负责构造对象，定时器、监听器和子进程仍由业务代码清理；Cordis 把注册动作收敛成 effect。空间 scope 与时间 ownership 同时存在，插件才能在某个 Agent 范围内挂载，并随该 Agent 完整撤销。</p>
<p data-tool="mdnice编辑器">从微内核看，DeepSeek Harness 的内核确实很轻，但其 Loader 和配置协调已经承担不少平台职责。它要解析模块、等待依赖、处理 HMR、执行事务式更新、保存最后可用树、输出诊断。这提醒我，微内核减少的是业务特权，不会减少生命周期复杂度。能力越动态，内核对失败语义的要求越高。</p>
<p data-tool="mdnice编辑器">Cordis 的贡献可以概括为一种紧凑的组合坐标：Context 决定插件位于哪里，Fiber 决定插件在何时有效，Service 负责直接能力，Event 负责横切协作，effect 负责撤销。DeepSeek Harness 在这个坐标系上加入 Agent scope、持久会话事件和配置事务，使其能承载多会话、多 profile 与热更新。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工程取舍</span></h1>
<p data-tool="mdnice编辑器">当我想要用类似架构时，会先确认三个条件，基于这三个条件判断来是否采用。</p>
<p data-tool="mdnice编辑器">其一，能力确实需要独立替换，至少存在两个生产提供方或明确的外部扩展需求。</p>
<p data-tool="mdnice编辑器">其二，同一进程需要多套组合并存，或者运行期间需要可靠更新。</p>
<p data-tool="mdnice编辑器">其三，团队愿意为卸载、回滚和真实组合测试持续付费。</p>
<p data-tool="mdnice编辑器">缺少这些条件，插件框架容易沦为复杂的工厂模式。</p>
<p data-tool="mdnice编辑器">能力切分要从消费方开始。先列出谁调用、调用时需要哪些稳定语义，再设计 Service Definition；随后实现 provider，最后把面向模型的 schema、展示和错误文本留给 consumer。接口只有一个内部调用者时，保留私有闭包，不急着升格为公共 service。可替换性必须由真实替代者证明。</p>
<p data-tool="mdnice编辑器">所有注册都要有明确所有者。注册工具、提示词片段、适配器、监听器时同步返回 disposer；卸载测试要观察注册项确实消失。涉及异步资源时，dispose 完成的定义要包含子进程退出、队列排空和 watcher 关闭，不能只发出取消信号。DeepSeek Harness 对 Fiber quiescence 的处理值得照搬。</p>
<p data-tool="mdnice编辑器">配置更新要区分内存一致性和外部副作用。Loader 可以恢复旧树，插件初始化若已经创建云资源，回滚需要业务补偿。规则可更保守一些：挂载阶段只做校验和本地注册，外部写操作放到带幂等标识的运行阶段。确实要在初始化创建资源时，必须让 disposer 能识别部分完成状态。</p>
<p data-tool="mdnice编辑器">作用域要控制在两种以内。实例隔离与注册可见性已经足够表达多数 Agent 场景，再增加租户、请求、工作区、角色四套独立 scope，调试成本会失控，需要根据实际业务需求和必要性来判断。当需要新增维度时，先判断它属于服务实例选择、注册过滤、持久数据权限还是请求参数。很多所谓新 scope，其实只是一次显式参数传递。</p>
<p data-tool="mdnice编辑器">事件也要克制。需要唯一返回值的调用用 Service，需要持久恢复的事实写 session log，需要多个插件观察或包裹的阶段才用事件。waterfall 必须把短路当成公共协议，监听器顺序也要有测试。事件名虽然能降低导入耦合，却会增加时序耦合；后者通常更难排查。</p>
<h1 data-tool="mdnice编辑器"><span class="content">小结</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 选择 Cordis，解决的主要矛盾是 Agent 能力变化速度与运行时一致性之间的冲突。模型、工具、沙箱和 UI 都可以替换并不稀奇；这些能力可以在局部作用域内共存，能随依赖变化启停，能在配置失败后保留旧树，卸载时还能回收监听器、服务和子插件，才构成可用的插件架构。</p>
<p data-tool="mdnice编辑器">它也没有抹平工程风险。Proxy 隐藏了查找路径，双层 scope 增加理解成本，热更新无法回滚任意外部副作用，预发布阶段也缺少跨版本兼容承诺。团队采用这套思路时，应复制它的生命周期纪律和能力边界，别只复制「一切皆插件」的目录结构。</p>
<p data-tool="mdnice编辑器">源码给我们一些启发，把插件设计的评审顺序倒过来：先问如何撤销，再问如何注册；先证明局部挂载不会泄漏，再讨论全局复用；先定义失败后保留哪一棵树，再讨论热更新速度。做到这些，时空可组合才是一组可以验证的运行时语义。</p>
<p data-tool="mdnice编辑器">这套架构个人理解是想往 Agent 的实时「自生长」，通过 AI 的能力在使用过程中让自己更强大。</p>
<p data-tool="mdnice编辑器">逼逼这么多，主要还是在这个过程中学习一下，说实话，有了 DeepSeek Harness，想自己开发一个专属的 Agent ，快捷方便了很多，eg且这是 MIT 协议的。</p>
<p data-tool="mdnice编辑器">以上。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">参考资料</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.zh.md">DeepSeek Harness 架构文档</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://github.com/cordiverse/cordis">Cordis：Meta-Framework of Spatiotemporal Composability</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://osgi.github.io/osgi/core/framework.service.html">OSGi Service Layer</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://www.osgi.org/resources/architecture/">OSGi Architecture</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html">Eclipse Plug-in Architecture</a></section>
</li>
</ul>
</section>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/08/deepseek-harness-cordis/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
