DeepSeek Harness 的 Harness 到底做了什么

DeepSeek Harness 解决的核心问题,是多轮模型调用、工具执行与会话状态之间的协调:输入何时进入请求,工具结果如何写回历史,请求失败后从哪里重试,上下文压缩后保留哪些信息,以及任务中断后能够恢复到什么状态。

围绕这些问题,框架将运行过程拆分为插件装配、回合驱动、会话日志、工具管道和异常处理。分析其架构的关键,在于各部分的状态边界,以及这些边界提供的保证与限制。

本篇文章限定在提交 477b4f4205(2026 年 9 月 25 日)对应的实现。

1. 插件装配

插件是 DeepSeek Harness 的核心能力,插件的配置决定 Agent 的运行能力。

DeepSeek Harness 通过 Cordis 插件组织运行能力。Cordis 提供 Context、Service、Plugin 和 Event 等抽象,插件通过依赖注入与事件系统协作,并由框架管理生命周期。

LLM 服务、Session、Agent、AgentLoop 和工具注册表等基础组件,由 bundle 统一挂载。启动时,dsh 以 profile 为入口加载配置,按 id 定位配置行,并替换对应的整段 config。

这里的配置语义是整段替换,而非字段级合并。覆盖某个组件的配置时,原配置中未被保留的字段也可能随之消失。

插件装配因此直接影响 Agent 的实际行为:模型服务是否可用、哪些工具被注册、哪些事件处理器参与执行,都取决于最终加载的插件与配置。仓库包含某项能力,并不等于当前运行实例已经启用该能力。

这种设计便于替换组件和组合能力,但也让执行逻辑分布在多个插件中。分析一次请求的行为,需要同时查看核心循环、插件注册关系和最终生效的配置。

2. 核心循环

Agent 循环是 Agent 的核心逻辑,其主要区分回合、步骤与请求重试。

从实际的工作逻辑上来看,agent-loop 负责驱动模型与工具之间的循环。其主要执行路径包括:

  1. 准备模型请求,解析配置并选择适配器。
  2. 投影系统提示词,提交本次需要进入历史的用户输入。
  3. 从会话构造请求,接收并累积流式响应。
  4. 将 assistant 消息写入 Session。
  5. 执行模型提出的工具调用,并将结果交给后续模型请求。
  6. 在没有工具调用或满足结束条件时,结束本轮执行。

理解这条路径,需要分三个层次:

  • turn:由用户输入驱动的回合。
  • step:回合内部的执行边界。
  • 请求尝试:一次实际发往模型的调用,失败后可以在同一个 step 内重试。

因此,step 不能简单等同于一次网络请求。重试可能产生多次请求尝试,但不应重复消费输入或重复提交用户消息。

Agent 维护 next-turn 和 next-step 两个队列,分别承接下一回合与下一步的输入。队列修改以 agent/inbox/spliced 事件写入 Session,待处理输入可以从日志投影恢复。

这一设计区分了「系统已经收到消息」和「模型已经消费消息」。

执行过程中到达的补充要求,需要在相应边界进入后续请求。它不能自动改变已经发出的模型请求,也不能撤销已经发生的工具副作用。输入排队与执行取消因此属于不同机制。

将队列变化写入日志,还能避免恢复时只找回聊天记录,却丢失尚未处理的输入。

3. Session

Session 机制实现了运行记录与模型历史分离。

Session 被设计为只追加的事件日志,事件通过递增序列号确定顺序,以 type 区分类型,以 body 保存内容。

日志不仅记录用户与 assistant 消息,也记录请求失败、输入队列变化、工具执行和压缩过程等运行事实。

但日志中存在的事件,不一定进入模型上下文。

真正发送给模型的 messages,由 Session 的 surface 通过 deriveMessages() 重建,再冻结为不可变请求。只有进入 surface 的消息事件参与历史投影;assistant/attempt 等运行记录不会自动成为模型消息。

这一分离有两个作用:

  • 保留排障信息:请求失败与恢复过程可以完整记录,而不必全部占用上下文窗口。
  • 允许调整可见历史:压缩或替换模型上下文时,仍能保留原始事件。

Session 的 append 还会检查 JSON 可序列化性、事件规则及 surface 替换的合法性,以减少无法持久化或无法正确投影的数据进入日志。

日志恢复不等于外部动作回滚

只追加日志为审计与状态重建提供了基础,但不能单独保证任意中断点都能无损恢复。

例如,工具已经修改文件,但结果尚未写入 Session 时进程退出,外部环境与日志之间就可能出现状态差异。恢复日志可以还原已提交的记录,却不能仅凭「缺少结果」判断工具没有执行。

因此,恢复能力需要区分:

  • 已提交事件及其投影的恢复;
  • 外部副作用的确认与处理。

后者还依赖工具的幂等性、状态检查能力或额外的事务机制,不能仅由 Session 的追加式设计推导出来。

4. 工具管道

工具管道实现了从模型意图到受控执行。

工具注册表只向模型暴露 name、description、parameters 等白名单字段。执行函数、超时设置和并发属性保留在运行时。

这将模型侧的工具描述与运行时的执行控制分开:模型负责提出调用,框架负责判断调用是否可执行,以及如何执行。

一次工具调用依次经过:

  1. 解析模型生成的参数。
  2. 查找当前 Agent 可见的工具。
  3. 通过 tools/pre-execute 进行执行前检查,允许或拒绝调用。
  4. 进入 tools/execute 包装链。
  5. 通过 tools/post-execute 处理结果。
  6. 将结果写回 Session,供后续模型请求使用。

权限检查、执行包装和结果处理因而拥有独立的介入位置,不必全部写进工具函数。

并发执行与顺序提交分离

同一条 assistant 消息可能提出多个工具调用。调度器通过工具的 isConcurrencySafe 属性分类:

  • 明确返回 true 的调用进入并行池。
  • 其他调用作为独占屏障处理。

并发是显式声明的能力,未声明安全的工具不会默认并行执行。

工具本体可以并发,但准备、后处理、tool/result 写入及附加 context 入队,按模型调用顺序提交。这样可以避免工具完成时间的随机性直接改变会话中的结果顺序。

代价是可能出现等待:后面的工具即使先完成,也需要等待前面的调用提交。并发能够缩短部分执行时间,却不保证结果可以立即进入历史;提前完成的结果还可能需要暂存。

此外,提交顺序稳定不等于外部副作用确定。并发工具是否真正安全,仍取决于它们是否访问共享状态、是否存在执行依赖,以及并发声明是否准确。

5. 安全隔离

DSH 在实现上让审批与执行限制分别生效,以实现安全隔离。

Harness 的沙箱模式包括:

type SandboxMode =
  | 'read-only'
  | 'workspace-write'
  | 'danger-full-access'

策略涉及文件路径权限、命令执行限制和权限提升。工具可以通过 justification 与 sandbox_permissions 请求提升权限,再由 approval 机制处理审批。

基础 bundle 默认挂载 workspace-write 文件沙箱与 ask 审批策略;sandbox-policy 将当前文件操作模式解析给实际后端。

这一结构包含三个不同层次:

  • 工具可见性:模型能够提出哪些调用。
  • 执行审批:某次调用是否获得许可。
  • 后端隔离:获准执行后,实际能够访问哪些资源。

三者不能相互替代。审批通过只说明动作获得许可,不代表执行环境具有无限权限;模型看不到某个工具,也不能据此证明其他工具无法访问相同资源。

安全边界最终取决于策略配置与实际后端的共同约束,而不只是沙箱模式的名称。

6. 异常收敛

异常是应用中很常见的东西,也是 Harness 的重点处理对象。

DSH 通过重试、压缩与取消各自处理不同问题。

6.1 请求重试

通过请求重试来保留失败记录,避免重复提交输入。

模型请求失败后,系统先写入 assistant/attempt,再交给 agent/request-error 瀑布链处理。

llm-retry 根据适配器提供的 retry policy,判断错误类型、重试次数与退避方式。计划重试与实际启动分别记录为 llm/retry 和 llm/retry-started。

重试在同一个 step 中重新准备请求,不重复提交首次用户消息。这一边界避免了网络层重试演变成会话层的重复输入。

分别记录重试计划与启动,也使日志能够区分“决定重试”与“已经开始下一次尝试”。

6.2 上下文压缩

上下文压缩实现了替换可见历史,保留原始事件。

自动压缩在 agent/pre-step 检测上下文压力,并记录:

  • compaction/start
  • compaction/summary
  • compaction/end

随后,系统将选中的连续历史区间替换为一个 checkpoint 用户消息。

这里发生的是 surface replace:模型可见历史缩短,原始日志仍然保留。因此,压缩减少的是请求上下文,不等于同步减少持久化存储。

压缩也引入了信息损失的可能。原始约束即使仍在日志中,只要没有进入摘要或剩余 surface,后续模型就无法直接使用。压缩是否有效,除了看上下文长度,还取决于任务目标、限制条件和未完成事项是否得到保留。

6.3 取消

通过取消来传递停止信号,而非撤销既有结果。

AbortSignal 贯穿回合、请求和工具,使取消信号能够沿执行链传递。

但信号传递不等于立即停止所有动作。工具是否及时退出,取决于其是否检查并响应取消;已经完成的文件写入或远端操作,也不会因 AbortSignal 自动回滚。

取消机制提供的是停止继续执行的控制路径,外部副作用的撤销仍需要独立实现。

7. 小结

DSH 的 Harness 实际上是一种架构的取舍,其取舍考量的是可追溯性与运行成本。

如下的机制共同形成了 DeepSeek Harness 的主要工程取舍:

机制 提供的能力 成本或边界
Cordis 插件装配 组件替换与能力组合 行为分布在配置和多个处理器中
turn / step 与输入队列 明确输入归属和执行边界 增加队列状态与事件维护
追加式 Session 审计与已提交状态重建 日志持续增长,不保证外部动作恰好执行一次
surface 投影 分离运行记录与模型上下文 需要维护投影一致性与替换规则
并发执行、顺序提交 利用并发,同时稳定历史顺序 可能产生提交等待与结果暂存
沙箱与审批 分层控制动作权限 实际保证依赖配置和执行后端
自动压缩 降低上下文窗口压力 增加摘要成本,并可能丢失任务信息
AbortSignal 统一传递取消意图 依赖工具配合,不能自动回滚副作用

DeepSeek Harness 的核心价值,是将 Agent 执行中的关键状态显式化:输入有队列,执行有边界,请求有可见历史,工具有调度与审批,失败有独立的处理路径。

这些机制使多轮任务能够被追踪、检查和恢复,同时也明确了框架的能力边界:日志恢复不能替代副作用确认,顺序提交不能替代并发安全,权限审批不能替代环境隔离,上下文压缩也不能保证信息无损。

以上。

带团队的进阶:从解决问题到发现问题

好久没有写团队管理的文章了。今天写一下最近在思考到的一个问题。

我有一个习惯是会经常检查自己的日程,看看时间花在哪里了。

我会发现很多的时间都是在解决问题,不管是架构的问题,还是需求的问题,还是人的问题。你会发现自己很忙,也很充实,觉得自己发挥了作用。

我们一直讲 XXX左移,如测试左移,需求左移。问题也需要左移。

如果我们正在忙的这些事情下个月还会以相似的形式发生,思考一下我今天到底解决了什么问题?

从接到一个明确任务开始,延伸到问题尚未被准确描述的时候。

题目从哪里来

工程师接到一个任务,通常会先考虑约束。

吞吐量要达到多少,延迟要控制到什么范围,需要兼容哪些调用方,什么时候上线。约束越清楚,技术讨论越容易收敛。方案可以比较,工作可以拆分,结果也有验收标准。

进入管理岗位以后,麻烦在于:这些约束本身,也需要被检查。

假设业务提出,要把某个接口的响应时间减半。团队可以研究索引、缓存、并发和计算过程,也可以重新分配机器资源。这些工作都有工程价值。

但我会先追问:用户究竟在等待什么?

如果用户完成任务要经过多个环节,这个接口只占很小一部分时间,那么局部优化可能无法改变使用体验。如果等待主要发生在人工审核阶段,继续压缩服务端耗时,投入方向就需要调整。如果响应已经足够快,用户仍然反复提交,我们还需要检查状态反馈和操作流程。

这里没有贬低性能优化的重要性。问题在于,团队拿到的技术指标,是否足以代表想要改善的结果。

我经常和业务同学、商务同学以及其它提需求的人,你讲需求,不要讲方案。 因为很多人来把自己的方案当需求。

要求他们描述:谁遇到了什么困难,发生在什么条件下,造成了什么损失,现有证据是什么。

先不要写方案。

管理者需要对任务的来源负责。 方案评审只能检查给定题目下的解法,无法自动纠正题目本身。

扁鹊的难题

《鹖冠子·世贤》里,扁鹊评价兄弟三人的医术,将长兄排在最前,中兄其次,自己居后。长兄在疾病尚未显形时处理,中兄在病情轻微时处理,扁鹊的治疗动作更显著,名声也传得更远。

引用这个故事,我想表达的是其中的评价问题。

问题严重以后,损失是可见的,处理动作也是可见的。系统恢复了,业务继续运行了,参与者很容易理解这项工作的价值。

提前处理则有一个问题:我们无法直接观察那个没有发生的结果。

一个团队说,经过架构调整,避免了一次严重故障。这个说法可能成立,也可能只是把一种担忧包装成了成绩。没有发生故障,可能与改造有关,也可能因为流量没有达到预期,或者相关路径根本没有被触发。

「预防很重要」,但不会接受任何预防性投入。

假设有人提出,某项依赖存在单点风险,需要增加冗余。我希望看到的是:这个依赖失效后会影响哪些功能,现有替代路径是否可用,恢复需要什么条件,团队通过什么方式验证过。

随后才讨论增加冗余的成本,包括建设、切换、演练和持续维护。

故事中的医者似乎能够准确识别尚未显形的疾病。技术管理没有这种确定性。我们面对的是不完整的信息、会变化的业务,以及随时可能被新证据推翻的判断。

因此,观察到了什么,推测了什么,准备如何验证,判断错误时损失有多大。

发现得早,只有在判断质量和处理成本都合适的时候,才值得投入。

把症状写清

我们常常看到当一个问题刚被提出时,责任归属就已经确定了。这是不对的。

项目延期,于是执行力不足;线上出错,于是质量意识不够;评审反复,于是技术能力欠缺。这些解释很容易进入管理讨论,因为它们足够简短,也容易对应到熟悉的动作:催进度、加审核、做培训。

但它们经常缺少可以验证的内容。

以延期为例,我会先把工作过程拆开:需求什么时候达到可开发状态,编码用了多久,等待联调用了多久,返工发生在哪个阶段,验收条件是否改变过。

假设一个任务总共经历了两周,其中多数时间在等待依赖方确认接口。此时要求开发人员提高编码效率,无法覆盖主要损失。继续增加进度会议,还会占用原本可以用于协调和实现的时间。

不过,看到等待时间很长,也不能立即宣布「跨团队协作有问题」。

等待可能来自接口定义不清,也可能来自对方没有资源,还可能因为我们过早启动了一个条件尚未具备的任务。几种情况需要不同的处理方式。统一加一个协调角色,可能只会多出一次信息转述。

可以使用如下的逻辑:已经观察到的事实,基于事实形成的解释,以及准备采取的动作。

例如,几个任务都在同一个依赖环节停留较久,这是事实;对方资源不足,这是待验证的解释;调整双方排期,是一种候选动作。把它们写在一起,会让后面的讨论默认接受前面的推测。

问题描述越接近可观察的行为,团队越容易讨论具体改动。把大量精力花在解释一个人「为什么不够负责」上,通常得不到能够验收的方案。

寻找提前信号

等到事故发生再分析,证据往往比较集中。寻找早期问题,面对的信号要模糊得多。

我会优先关注那些暂时没有形成损失,却持续依赖额外劳动才能维持的环节。

例如,告警频繁发生需要开发介入排查;每次发布都需要某个人手工确认;某类数据每天都要补一次;一个模块只有固定的人敢修改;项目能按时交付,但前提是临时取消测试或压缩验收。

这些现象还不足以证明系统即将失控,但值得进一步检查。因为当前结果里,包含了没有被正式记录的人工投入。

如果只看交付是否完成、服务是否可用,我就无法知道团队为维持这些结果付出了多少补偿性劳动。

我会让团队用较低成本记录几类信息:重复的人工操作、等待外部决定的时间、计划外的工作,以及必须依赖特定人员才能完成的事项。记录的目的,是找到重复出现的位置,不追求把每个人的一天切成几十个时间片。

粒度过细,记录会变成负担,数据也容易围绕考核要求失真。

技术系统的观察也需要类似的取舍。我不会因为担心遗漏,就要求所有路径全量记录详细日志。采样比例、保存时间、字段数量和查询需求,都需要对应到具体诊断问题,并给采集开销和存储费用设预算。

如果新增一组数据无法回答任何决策问题,我倾向于先不采集。

告警同样需要说明后续动作。某个指标越界以后,谁要介入,要判断什么,允许等待多久?回答不了这些问题的告警,我会先把它放进观察范围,避免直接打断值班人员。

提前发现需要足够的信息,却不能靠无限增加信息量完成。可以从几个高损失场景倒推:在结果恶化之前,我们有没有机会看到变化?现有信息能否区分不同原因?缺少的那部分,怎样以最小代价补上?

追问到哪一层

发现重复问题以后,很容易进入另一个极端:不停追问原因,直到得到一个足够宏大的解释。

一次发布失败,可以追到测试不足;测试不足,可以追到排期紧张;排期紧张,可以追到业务压力;最后写成「组织需要加强质量文化」。

讨论完成了,下一次发布怎么做,仍然没有答案。

建议把分析停在当前能够改变、能够验证的层次,同时记录更上层的约束。

假设某次故障涉及配置变更。恢复服务后,我希望继续弄清楚:配置是否经过校验,发布前能否发现异常,变更是否可以独立回退,为什么已有检查没有覆盖这条路径。

如果缺少校验,就讨论怎样增加校验。如果具备校验能力,却因为发布时间紧张被跳过,就需要检查绕过条件和批准权限。再往上,若每次承诺交付时都默认压缩验证时间,排期方式也要进入改进范围。

这几个层次可以同时存在。没有必要强行选出唯一的「根因」。

我更关心哪一项改动能切断已知的失败路径,哪一项能缩短恢复时间,以及哪些约束暂时只能接受。

增加发布审核,是一种常见选择,但我会谨慎使用。它需要持续占用审核者时间,也会让发布等待另一个人的判断。如果某类错误可以通过确定性检查识别,我会优先评估自动校验;如果必须依赖业务背景判断,人工审核才有对应价值。

自动化是一种常见的策略,但是要考虑成本。一个发生频率很低、每次处理只需少量时间的例外,未必值得建设复杂流程。反过来,高频且规则稳定的操作,长期依赖人工检查,我会要求说明理由。

分析问题的深度,应当服务于干预选择。继续追问已经无法改变行动时,我会停止扩展讨论,把精力转向验证。

问题也要排期

发现问题的能力提高以后,待办事项很可能先变多。

代码有债,流程有缺口,人员有依赖,业务假设也有风险。如果每一个发现都必须立即解决,团队会同时启动大量改善工作,原本的交付计划也失去可信度。

管理者需要对「暂时不处理」承担责任。

判断一个问题是否进入排期,我会看几件事:已经造成了什么损失,未来损失可能如何扩大,判断依据有多可靠,处理成本是多少,以及推迟以后是否会失去调整空间。

简单来说就问一个问题:不做什么怎么样,做要多少成本? 这也算是两个问题吧。

假设两项改造都能减少一定维护工作。其中一项随时可以做,另一项涉及即将被多个团队依赖的接口约定。后者一旦扩散,迁移就需要更多参与者配合。我可能优先处理后者,即使它眼前节省的时间更少。

但我不会把这些因素全部塞进一个精确分数,然后按小数点排序。概率、影响和工作量里包含大量估计,算式不能消除不确定性。

对于信息不足的问题,我会单独安排验证工作。

例如,先花一个有限时间窗口检查实际调用情况、做一次容量实验,或者回放一批历史任务。验证结束后,再决定是否启动完整改造。验证任务需要有结束条件,不能以「持续调研」的形式长期占用人力。

我也会区分两种决定:允许风险暂时存在,以及承诺在某个时间解决。前者需要记录接受风险的人、接受范围和重新检查的条件。不能把所有暂缓事项都放进一个没有期限的技术债列表,等下一次出事再说「早就提过」。

有限的资源只能处理有限的问题。管理者的工作包含排序、延后和拒绝,不能只提供一份越来越长的风险清单。

先做有限验证

提前发现的问题,往往还没有足够证据支持大规模改造。我们需要优先寻找能改变判断的小实验。

假设团队认为,交付慢主要来自公共组件不好用。直接重建组件库,投入很大,验证周期也长。可以先选一类重复任务,调整其中一个高频接口,再比较接入步骤、返工次数和总耗时。

这个实验不需要证明新方案在所有场景都更好。它需要回答:当前识别出的障碍,是否确实影响了交付;移除障碍以后,预期变化有没有出现。

实验设计里,我们可以要特别检查比较条件。

两次任务的复杂度是否接近,参与者是否不同,是否有额外辅导,新方案是否获得了旧方案没有的资源。如果这些条件发生变化,就要缩小结论的范围。

如果测试任务都是规则清楚、信息完整的样本,结论也只能覆盖这部分任务。遇到资料缺失、规则冲突或无法判断的情况,系统怎样退出,谁来接手,都要进入验证范围。

有限实验也有局限。它可能遗漏低频问题,额外受到团队关注,或者无法覆盖规模扩大后的协作成本。因此,通过一次实验以后,我会继续限制推广范围,保留回退条件。

我愿意为这些步骤支付一些时间。相较于一次性更换完整流程,分阶段验证让我有机会在投入扩大之前修正判断。

授权与介入

发现问题容易让管理者产生一种冲动:既然我已经看到了,亲自处理应该最快。

在紧急情况下,这可能是合适的选择。但如果每一次复杂问题最后都回到我这里,我就需要检查自己的介入方式。

我会区分三个环节:识别风险、作出决定、执行处理。它们可以由不同的人负责。

管理者发现某个发布环节存在风险,可以要求补充证据、确定负责人和完成期限。除非风险已经超过团队当前能力,或者需要跨越现有权限,否则没有必要接管全部实现。

授权时,我需要交代的是目标、约束、可使用的资源,以及什么情况下必须升级。对于实现细节,我会留出空间。

这里存在实际代价。团队选出的方案可能与我的习惯不同,局部实现也可能没有我亲自处理得快。如果每次出现这种差异,我就收回任务,成员很难积累完整判断的经验。

但授权也不能变成不检查。

我会根据风险安排检查点。可以轻易撤销的局部改动,允许负责人快速推进;涉及数据破坏、跨团队承诺或难以回退的迁移,需要更早检查方案和前提。检查频率随风险变化,避免所有任务都接受相同强度的管理。

我也需要亲自参与一部分现场工作,例如阅读一次故障时间线,跟进一个长期受阻的任务,或者检查一次改造的实际使用情况。

如果信息只来自层层整理后的汇报,我很难判断哪些成本被省略了。亲自接触现场,是为了校准判断;后续仍然要让明确的负责人完成处理和验收。

发现问题之后,我还欠团队一个决定:谁负责、投入多少、何时检查,以及哪些事情因此不做。

让风险能上来

如果我希望团队提前报告问题,就需要检查自己怎样回应坏消息。

成员提出风险以后,立刻被要求证明一定会出事;承认进度存在不确定性以后,被评价为缺乏担当;发现历史缺陷以后,首先被追问为什么之前没有发现。面对这些回应,一个人有理由等证据更充分以后再开口。

等到证据充分,调整空间可能已经缩小。

我会允许风险报告保留不确定性,但要求表达具体:观察到了什么,可能影响什么,还缺少哪些信息,希望获得什么支持。

「这个项目肯定要延期」和「关键依赖尚未确认,如果本周无法完成,我们需要调整后续联调安排」,包含的信息量不同。后一种表达让管理者可以作出决定,也允许后续事实修正判断。

对于善意提出、最终没有发生的风险,我不会仅凭结果认定提出者判断失误。我会回看当时的信息是否足以支持担忧,验证动作是否合理,以及是否因为提前处理才没有继续恶化。

同样,我也不会奖励没有证据的持续悲观。风险提出以后,需要有人补充信息、缩小范围,必要时撤回判断。只负责提醒所有可能性,却不参与验证,会把筛选成本转移给整个团队。

预防性工作的评价也需要证据。

我会看风险路径有没有被验证,改造是否覆盖了它,回退或恢复是否经过检查,后续维护责任是否落实。至于「避免了多少损失」,没有足够依据就不写一个夸张数字。

对于重复的人工劳动,可以比较前后的处理次数和时间;对于恢复能力,可以记录演练中实际完成的操作与耗时;对于协作改善,可以检查等待和返工有没有变化。评价应当尽量落在能够复查的记录上。

这样做会增加一些记录成本。我愿意保留与决策和验收有关的部分,不要求团队为每项改善制作完整汇报材料。

检查已经完成的

还有一类问题,很容易被技术管理忽略:任务完成了,但投入没有产生预期结果。

系统按时上线,功能通过验收,接口符合设计,团队也没有犯明显错误。几个月以后,使用量很低,原有人工流程仍在继续,新系统还多了一份维护责任。

如果我的检查到上线就结束,这类问题不会进入视野。

我会在立项时约定一次投入后的复查。具体检查什么,取决于项目要解决的问题:人工步骤是否减少,用户是否完成了原来难以完成的任务,业务是否真的采用了新流程,旧系统是否按计划退出。

不能用「已经上线」替代这些问题的回答。

如果结果没有出现,我需要重新检查原来的假设。也许我们识别错了障碍,也许方案只处理了一小部分,也许业务条件已经发生变化。继续增加功能之前,先确认追加投入还有没有依据。

停止项目在这里也是一种需要认真评估的选择。它涉及已有投入、人员安排和承诺调整,处理起来往往比批准下一期更麻烦。但维护一个缺少使用理由的系统,同样会持续消耗资源。

发现问题因此也包括检查团队正在解决的问题是否仍然存在,解决方式是否仍然适用,以及目标是否已经改变。

这会改变我安排时间的方式。

我仍然需要参与故障处理,理解技术细节,对交付结果负责。同时,我会固定留出时间检查重复劳动、尚未验证的判断和上线后的实际结果。对于已经暴露的问题,要求处理闭环;对于只有迹象的问题,安排有限验证;对于暂时接受的问题,留下重新检查的条件。

我也会定期回看自己提出的问题。有多少得到了证据支持,有多少被否定,哪些因为迟迟没有决定而继续消耗团队,哪些处理动作带来了新的负担。

否则,所谓发现问题,很容易变成管理者不断向团队追加任务。

从解决问题到发现问题,对我意味着工作范围扩大了:需要参与定义,需要作出取舍,还需要在结果出现以后检查当初的判断。问题提前进入视野,只是给了我们更多处理空间。这个空间有没有被用好,最终仍然要回到具体的行动和结果上。

以上。

DeepSeek Harness 的上下文管理、记忆和知识库剖析

DeepSeek Harness 里,上下文、记忆和知识库并没有被设计成三个彼此独立的重型系统。它们围绕 Session Log 协同工作。

用三句话概括:

  • 上下文管理负责决定当前这一轮模型能看到什么。系统提示词、动态环境、工具定义和会话消息,会在模型调用前完成装配。
  • 记忆机制负责在上下文窗口不足时折叠旧历史。原始会话日志继续保留,模型当前可见的历史被替换成结构化摘要。
  • 知识获取负责把仓库文件和外部信息送入模型。文件通过路径引用和读取工具进入,网络信息通过搜索、抓取工具进入,所有结果最终写回 Session Log。

三个模块共享一条约束:

模型看到的内容,需要拥有可追踪的来源,并且能够从会话记录中重新构造。

这条约束决定了 DeepSeek Harness 的整体形态。上下文不能由控制器随手拼接,工具结果不能停留在进程内存,压缩不能覆盖原始记录,检索结果也不能绕过会话日志直接塞给模型。

它由此形成了一条完整链路:

SystemPrompt.assemble() 组装当前上下文,ReactLoopAgent.preStep() 决定动态内容是否进入会话,Session.append() 保存事件,Session.deriveMessages() 生成模型历史,buildRequest() 构建最终请求,llm.stream() 完成模型调用。

理解了这条链路,就大概能了解 DeepSeek Harness 的知识和记忆相关的逻辑了。

上下文管理

上下文管理解决的是一个具体问题:每次调用模型时,系统应该选择、组织并发送哪些内容。

现在大多数 Agent 系统不会简单维护一个持续增长的字符串,而是使用结构化消息列表管理上下文。较成熟的系统还会根据任务状态,动态选择系统提示词、历史消息、工具定义、运行环境和检索结果。

真正的难的不是 「是否使用消息列表」,而在于:

  • 哪些内容应该进入本轮请求;
  • 不同内容由哪个模块提供;
  • 动态状态发生变化后如何更新;
  • 重复信息是否需要再次发送;
  • 长会话中哪些历史应该保留或压缩;
  • 服务重启后能否恢复模型当时看到的内容;
  • 最终请求能否被追踪、审计和重放。

DeepSeek Harness 同样以结构化消息为基础,但它没有让 AgentLoop 直接维护所有上下文,而是将上下文拆分为不同来源,在每个 step 开始前统一装配。

1. 分层组织上下文

DeepSeek Harness 中的上下文主要分为四类:

  • 系统规则:角色设定、行为约束和工具使用规范;
  • 动态状态:当前工作目录、运行环境和终端状态;
  • 会话历史:用户消息、模型回复和工具调用结果;
  • 工具定义:当前允许模型调用的工具及其参数结构。

这些内容分别由 SystemPrompt、Runtime Context、Session 和 ToolRuntime 管理,最后在模型调用前合并。

这种设计的重点不是改变消息格式,而是明确上下文的来源和职责。AgentLoop 只负责控制执行流程,不直接承担提示词拼接、历史管理和工具注册等工作。

2. 动态装配本轮内容

每个 step 开始前,ReactLoopAgent.preStep() 会调用 SystemPrompt.assemble(),收集本轮所需的系统提示、动态上下文和工具定义。

其中:

  • sections 保存相对稳定的系统提示;
  • contexts 提供会随运行状态变化的环境信息;
  • tools 描述当前可用的工具;
  • variables 提供提示词渲染需要的变量。

各插件只负责注册自己的内容,最终由 SystemPrompt 统一生成本轮快照。

这意味着上下文并不是固定不变的。例如,插件启停、工作目录变化或工具权限调整后,下一次模型调用可以获得最新状态,不需要由 AgentLoop 编写额外的业务分支。

3. 避免重复注入动态状态

动态上下文并不需要每个 step 都重复写入。

RuntimeContextProjection 会比较本轮快照和上一轮快照:

  • 如果内容没有变化,就继续沿用已有信息;
  • 如果内容发生变化,就生成新的上下文消息并写入 Session。

这种机制有两个作用。

第一,减少重复内容带来的 token 消耗。工作目录和运行环境可能连续多个 step 保持不变,没有必要反复加入会话历史。

第二,保留状态变化轨迹。当环境发生变化时,新快照会进入 Session,后续可以追踪模型从哪一轮开始看到了新的状态。

投影状态还可以从已有 Session 中恢复。因此,即使服务重启,系统也能判断某段动态上下文是否已经注入,避免因为进程内状态丢失而重复写入。

4. 统一派生会话历史

用户消息、模型回复和工具结果不会由控制器临时拼入请求,而是先写入 Session Log,再由 Session.deriveMessages() 派生为模型协议需要的消息列表。

这使系统能够区分三个层次:

  • Log:完整保存原始会话事件;
  • Surface:决定当前哪些事件参与模型上下文;
  • Messages:最终发送给模型的结构化消息。

例如,工具执行结果先作为 tool/result 写入 Session,下一轮再由 deriveMessages() 转换成模型可以读取的消息。上下文压缩也只调整 Surface,不直接删除原始事件。

因此,模型输入不是由多个模块临时修改的消息数组,而是由统一的会话记录派生出来的。

5. 构建并记录最终请求

上下文装配完成后,buildRequest() 会生成最终的模型请求,主要包括:

  • system:系统提示;
  • messages:会话历史和动态上下文;
  • tools:当前可用的工具定义;
  • sessionId:当前会话标识;
  • signal:调用控制信号。

同时,系统会记录 request/header 和 request/context,保存本次调用使用的关键上下文信息。

这样可以回答几个重要问题:

  • 模型当时看到了哪些历史;
  • 使用了哪一版系统提示;
  • 当时有哪些可用工具;
  • 动态环境是否已经发生变化;
  • 某次异常是模型推理问题,还是输入上下文问题。

DeepSeek Harness 的上下文管理并不是简单地将内容放进消息列表,而是围绕分层管理、动态装配、变化检测和统一派生建立完整流程。

其核心链路可以概括为:

各模块提供上下文 → SystemPrompt 动态装配 → RuntimeContextProjection 检测变化 → Session 派生历史消息 → buildRequest 构建并记录最终请求。

这样,每段上下文都有明确来源,动态信息不会被无意义地重复发送,模型输入可以从 Session 中恢复和追踪,AgentLoop 也不需要承载复杂的提示词与历史管理逻辑。

记忆压缩

DeepSeek Harness 当前的记忆能力主要由 Compaction 提供。

这里需要控制术语。Compaction 服务于当前会话的连续运行,它会把旧历史折叠成结构化检查点。跨 Session 的用户偏好、项目经验和长期事实存储,目前没有形成完整系统。

因此,我更愿意把它称为「会话记忆」。

1. 压缩对象

Session 内部存在三个层次:

  • Log:所有原始事件组成的追加日志;
  • Surface:当前参与模型消息派生的事件视图;
  • Messages:发送给模型的协议消息。

Compaction 修改的是 surface。

原始事件仍然保留在 Log 中,模型当前看到的历史则由摘要节点替代。这个结构同时满足了两个要求:

  • 模型输入得到缩短;
  • 原始会话记录没有被覆盖。

它比直接删除前 N 条消息安全很多。

删除历史以后,调试系统无法知道模型之前看过什么。线上出现问题时,只剩下截断后的残缺记录。Compaction 保留原始事件,后续仍可审计压缩范围和摘要来源。

2. 触发条件

BasicCompactionEngine 会通过 tokenMeter.measure(session) 估算当前会话的 token 压力,再与模型的 contextWindow 比较。

自动压缩拥有两个主要触发入口:

  • agent/pre-step 阶段的常规压力检查;
  • 模型返回上下文溢出错误后的重试处理。

常规检查负责提前处理窗口压力,错误重试负责兜底。

压缩区域由 selectCompactableRange() 选择。它会优先折叠较旧历史,并保留近期上下文。近期消息通常包含当前修改进度、最新工具结果和下一步操作,保留它们可以减少摘要对短期推理的影响。

区域选择还需要保护工具调用链。

Assistant 发出的 tool-call 和对应的 tool-result 属于一组完整语义。若压缩边界将它们切开,后续模型可能看到孤立的调用或孤立的结果。部分模型适配器甚至会直接拒绝这种消息结构。

validateSurfaceRegion() 会检查压缩区域,避免破坏工具调用与结果的配对关系。

这类结构校验比简单的「保留最近 N 条消息」可靠。Agent 历史已经超出普通聊天记录的范畴,消息之间存在协议级关联,切割时必须理解这些关联。

3. 摘要生成

压缩区域选定后,buildSummarizationInput() 会恢复对应的模型消息,并取回当时的 system 和 tools。summarizeWithLlm() 随后调用 LLM 生成摘要。

摘要受到固定模板约束,主要包含:

  • Primary Request and Intent
  • Files and Code
  • Errors and Fixes
  • Next Step
  • Critical Context

结构化模板可以防止摘要退化成普通的对话概述。

编程 Agent 的历史里,有用的信息通常集中在几类内容:

  • 用户最初要解决的问题;
  • 已经读取或修改的文件;
  • 执行过的命令;
  • 失败原因和修复过程;
  • 当前未完成步骤;
  • 不能违反的环境约束。

如果只要求模型「概括以上对话」,输出往往会省略文件名、错误信息和失败尝试。摘要读起来流畅,后续执行却接不上。

固定结构会提高这些信息的保留概率,但无法保证语义完整。

4. 替换事务

摘要生成以后,会被包装进 <compacted-summary>,随后作为新的 user/message 写入 Session,并通过 surfaceOp: replace 替换旧区域。

参考实现中的主流程如下:

// compaction-basic/region.ts 的逻辑简化
session.append('compaction/start', { ... })

const summary = await summarizeWithLlm(ctx, config, input, agent, signal)
const framed = frameSummary(summary.summary)

const summaryEvent = session.append('compaction/summary', { ... })

session.append(
  'user/message',
  createUserMessage({
    content: framed,
    source: compactCheckpointSource(...),
  }),
  {
    surfaceOp: { op: 'replace', start, end },
    sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
  },
)

session.append('compaction/end', { ... })

整个过程会记录:

  • 压缩开始;
  • 摘要内容;
  • replacement message;
  • 被覆盖的事件序列;
  • 压缩结束。

sourceEventSeqs 建立了检查点与原始历史之间的关联。排查错误摘要时,开发者可以反查它覆盖了哪些事件。

Session.deriveMessages() 检测到 replaceGeneration 发生变化后,会重建消息缓存。后续模型看到的是新检查点和保留下来的近期历史。

当前的设计保证了事务的完整性。压缩失败时,旧 surface 仍然可以继续使用。摘要生成成功且提交完成后,模型视图才发生变化。

5. 有损风险

Compaction 无法绕开有损问题。

假设旧历史中出现过一条约束:测试环境使用特定环境变量,变量为空时必须跳过某项操作。摘要模型漏掉这条信息以后,后续 Agent 可能执行错误命令。

原始事件虽然还在 Log 中,当前模型无法自动访问。对推理过程而言,这条信息已经离开热上下文。

因此,Session Log 的完整性解决了审计问题,没有完全解决信息恢复问题。

我会在现有 Compaction 上增加一层历史召回能力。摘要中保留关键事件引用,模型缺少细节时,可以调用类似 recall_history 的工具读取旧历史。

召回参数可以采用结构化维度:

  • turn 范围;
  • step 范围;
  • event seq;
  • tool call id;
  • 文件路径;
  • compaction id。

这种方式比单纯的语义搜索更稳定一些。Session 已经拥有完整事件顺序和来源关系,应优先利用现有结构。

召回结果也要写成 tool/result。这样系统可以知道模型恢复了哪段历史,以及这段历史怎样影响后续决策。

6. 调度延迟

当前压缩会调用一次 LLM。压缩发生在主流程上时,用户需要等待摘要完成。

会话较短时,这个延迟可以接受。长会话的摘要输入很大,调用时间会明显增加。编程 Agent 还可能连续执行多个工具,压缩恰好卡在下一步之前,交互体验会出现停顿。

可以引入两级水位:

  • 接近窗口上限时,后台生成候选 checkpoint;
  • 到达硬上限时,提交已有 checkpoint;
  • 候选结果过期时放弃;
  • 没有可用结果时回退到同步压缩。

异步压缩的难点集中在一致性。

生成摘要期间,Session 还会继续追加事件。提交前必须校验它对应的 surface generation,确保被压缩区域没有发生冲突。检查点过期后不能强行替换,否则可能覆盖新的工具结果或用户输入。

当前已有的压缩重入保护、surface generation 和事务事件,为异步化提供了基础。早期保持同步更稳,长任务比例上升以后,再逐步引入后台 checkpoint。

知识获取

DeepSeek Harness 没有传统意义上的统一向量知识库。

它的知识入口由三部分组成:

  • 文件路径引用;
  • Web 搜索与抓取;
  • 工具结果写入 Session。

这种组合适合编程 Agent。代码仓库里的信息具有路径、符号、定义和引用关系。统一切块并写入向量数据库,会损失一部分结构信息,还要承担索引更新成本。

1. 文件引用

file-reference-local 提供工作区文件和目录候选。

它会根据 session.header.cwd 创建 WorkspaceFileSearch,然后扫描当前工作区。结果只包含:

  • path
  • kind

用户在前端选择文件后,输入框里会插入 @path 或 @"path with spaces"。

文件内容不会在这个阶段进入模型。

系统提示会告诉模型,@ token 表示工作区路径。模型需要读取内容时,应调用 read 工具。

这个分层处理了两个不同问题:

  • file-reference-local 负责找到路径;
  • read 工具负责读取内容。

如果选择路径时就把整个文件自动塞进消息,系统会遇到几类麻烦。

第一,大文件会快速占满上下文。

第二,用户无法知道系统展开了多少内容。

第三,读取行为缺少独立记录。

第四,文件权限和读取范围可能绕过工具 guard。

第五,文件修改以后,很难判断模型当时看到的是哪个版本。

路径引用保留了用户意图,工具调用保留了实际读取行为。两类信息都会进入 Session,审计链更完整。

WorkspaceFileSearch 还会控制目录边界、排除路径、最大条目和候选数量,并拒绝通过 .. 或符号链接跳出 workspace。

这些限制属于安全边界。文件路径来自用户输入,也可能由模型生成,不能默认可信。

2. 精确检索

文件路径补全只能解决「大致知道文件在哪里」的问题。

大型仓库中,开发者和模型经常需要回答另一类问题:

  • 接口定义位于哪个文件;
  • 某个符号有哪些引用;
  • 哪些实现依赖当前类型;
  • 一次修改会影响哪些调用点;
  • 编译诊断关联到哪些符号。

这些查询适合交给 LSP。

项目已经存在 packages/lsp/,可以继续扩展为代码检索能力。优先提供:

  • Go to Definition;
  • Find References;
  • Workspace Symbols;
  • Diagnostics;
  • 调用关系。

向量检索依赖语义相似度。两个函数命名和注释很接近,并不能证明它们具有依赖关系。LSP 返回的是语言服务器维护的定义和引用关系,适合代码修改场景。

可以把仓库检索分成四层:

  1. 已知路径,直接读取文件;
  2. 已知文本,使用 grep;
  3. 已知符号,使用 LSP;
  4. 只有概念描述时,再考虑语义检索。

这个逻辑会优先消耗确定性信息。代码仓库已经提供路径和符号结构,没有必要先把问题转换成向量相似度。

LSP 也有运行成本。语言服务器需要启动和预热,多语言仓库要维护多个进程,大型 monorepo 的全局引用查询可能很慢。它适合作为工具按需调用,不适合把所有结果常驻上下文。

查询结果仍然要通过 ToolRuntime 写入 Session。这样模型读取过哪些定义、哪些引用,可以在后续回放中找到。

3. Web 检索

外部知识通过 web_search 和 web_fetch 进入模型。

整体分为三层:

  • WebRuntime 定义统一能力;
  • search/fetch provider 负责具体请求;
  • tool-web 把能力注册成模型可见工具。

这种分层使工具 schema 与供应商解耦。模型只需要理解 web_search 和 web_fetch。底层可以选择 DeepSeek、Exa、Perplexity 或 HTTP fetch provider。

Provider 选择规则保持严格:

  • 配置了 provider id,使用指定 provider;
  • 未配置时,系统要求只有一个可用 provider;
  • 多个 provider 同时可用时直接报错。

这可以避免插件加载顺序影响线上行为。

Web 搜索结果会被格式化成模型可见文本,并带有外部内容不可信和引用 URL 的提示。Web fetch 会把 HTML 转换成 Markdown,普通文本则直接进入结果。

提示只能影响模型行为,网络边界还要由 provider 控制。HTTP fetch provider 已经包含多项限制:

  • 只允许 HTTP(S);
  • 拒绝 URL credentials;
  • 拒绝私网和非公网地址;
  • 控制同源重定向;
  • 限制重定向次数;
  • 限制响应字节数;
  • 限制正文长度;
  • 固定经过校验的 DNS 地址;
  • timeout 由部署配置管理。

模型生成的 URL也属于不可信输入。网页中的 Prompt Injection 可能诱导模型请求内网地址、云元数据地址或带有凭证的 URL。网络策略需要在模型之外执行。

4. 工具入库

文件读取、Web 搜索和 Web 抓取获得的信息,都通过同一条工具链进入模型。

流程可以概括为:

  1. ToolRuntime 把工具 schema 注册到 SystemPrompt;
  2. 模型返回 tool-call;
  3. AgentLoop 调用 executeToolCalls();
  4. Session 写入 tool/call;
  5. ToolRuntime 执行工具;
  6. Session 写入 tool/result;
  7. 下一轮 deriveMessages() 把结果投影给模型。

参考实现中的调用路径如下:

// 模型返回 tool-call
const toolCalls = message.content.filter(block => block.type === 'tool-call')

// Agent 执行工具
const { concluded } = await executeToolCalls(
  this.loopCtx,
  turn,
  step,
  toolCalls,
  signal,
  context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]),
)

// 工具结果进入 session log,下一 step 被 deriveMessages() 投影给模型

工具结果不会只停留在当前执行栈,也不会直接修改临时 messages 数组。

这对 Web 信息尤其关键。搜索结果和网页内容会随时间变化。如果系统只记录搜索参数,重放时重新请求一次,模型得到的内容可能已经不同。

文件读取也一样。Agent 可能在读取后修改文件。历史推理需要保留当时真正发送给模型的内容。

当然,完整保存所有工具输出会增加 Session 体积。工程上可以保存模型实际看到的渲染结果,同时在 meta 中记录:

  • 是否截断;
  • 原始内容长度;
  • 来源路径或 URL;
  • content type;
  • 工具调用参数;
  • provider 信息。

模型没有看到的正文,无须全部伪装成上下文历史。回放能力关注的是当时的模型输入。

三者关系

上下文、记忆和知识获取分别控制模型输入的三个阶段。

  • 上下文决定当前输入: SystemPrompt 收集系统约束、动态状态和工具 schema。preStep() 判断动态内容是否变化,buildRequest() 构建最终模型请求。它解决的是「这一轮应该携带什么」。
  • 记忆控制历史体积: Compaction 观察 token 压力,选择旧历史区域,生成结构化摘要,再替换当前 surface。它解决的是「历史太长以后保留什么」。
  • 知识补充外部信息:文件引用帮助定位路径,读取工具获得仓库内容,Web 工具获取外部信息,未来还可以用 LSP 提供符号级查询。它解决的是「当前会话缺少的信息从哪里获得」。

三者最终都回到 Session。

动态上下文通过 user/message 进入日志,工具信息通过 tool/result 进入日志,压缩通过 compaction/* 事件和 replacement message 改写 surface。

所以,DeepSeek Harness 的数据主线可以压缩为:

装配上下文,记录事件,派生消息,调用模型,执行工具,写回结果,必要时折叠 surface。

插件可以增加新的上下文来源、新的工具和新的压缩策略,但不能绕过这条主线。

直接向 GenerateOptions.messages 塞内容,会产生无法回放的隐形上下文。

检索模块绕过 ToolRuntime,会失去权限、审计和结果记录。

压缩模块覆盖原始事件,会破坏历史追踪。

这几条边界比具体使用哪种模型、搜索服务或向量数据库更影响系统的维护成本。

小结

DeepSeek Harness 的三个模块可以归纳为三句话:

  • 上下文管理负责装配当前输入。
  • Compaction 负责压缩当前会话历史。
  • 文件和 Web 工具负责补充当前缺失的信息。

三者共享 Session Log:

  • 模型看到的动态状态要进入日志;
  • 模型获得的工具结果要进入日志;
  • 压缩只调整 surface,原始事件继续保留;
  • 后续模型输入由 Session.deriveMessages() 统一派生。

现有设计的优势集中在可回放、可审计和模块边界。主要缺口也很具体:Compaction 存在语义损失,压缩调用会阻塞主流程,文件路径检索缺少符号级能力,跨 Session 长期记忆尚未形成独立体系。

Session Log 继续保存事实,surface 控制模型当前看到的历史,工具负责按需获取信息。三个模块沿着这条数据链协作,系统才能在上下文窗口、运行延迟和信息完整性之间保持可控。

以上。