REA 逆向神器,AI 时代不再有闭源软件

闭源软件还会存在。但我们已经很难继续把「没有源码」当成一道足够可靠的理解门槛。

截至 2026 年 10 月 18 日,写这篇文章的时候,REA 的 GitHub 原仓库显示约 316K Star、80K Fork(半天增加将近 1 万 Star)。社区讨论的内容中包括如何理解一个函数,延伸到了旧软件维护、跨平台实现,以及某些商业软件是否还能维持原有优势。这些数字说明项目受到广泛关注,无法证明实际使用人数,更无法证明复杂应用已经可以被完整复现。GitHub 原仓库

当模型能够持续调用反编译器、追踪引用、检查交付包,再把结果组织成可继续查询的证据,软件的分析成本会怎样变化?技术团队此前依赖的哪些假设,需要重新评估?

这是一篇基于 REA 源码的分析文章,比读源码稍微好一些,其目的是快速了解 REA 是什么,解决了什么问题,以及如何解决这些问题的。

在开始文章之前,我们需要了解: REA 不是能独立完整逆向的工具,它只是一套模型能调用的工具(以 MCP 或 CLI 的方式),帮助 Codex 这类 Agent 调查程序。根据目标类型,调查返回模块关系、函数位置、调用线索或编译指令等信息,模型再据此解释行为、编写实现和运行检查。

逆向是什么

逆向工程从交付产物和可观察行为出发,推导程序的结构、接口与实现逻辑。输入可以是可执行文件、程序集、ASAR、APK,也可以是抓包、崩溃记录和一段运行过程。

编译会丢失部分信息。机器码通常无法直接告诉我们原始变量名、注释、项目组织和开发者意图。反编译器生成的伪代码,需要结合汇编、字符串、引用、数据布局和运行行为来解释。反编译文本足够可读,仍然可能在类型、边界和间接调用上留下问题。

因此,逆向的交付应该围绕具体问题。一个长度字段怎样编码,某个按钮怎样触发文件读写,某个版本改变了哪条分支,都能形成有限的调查范围。要求恢复一个应用的完整源码,会把目标扩大到原始工程、构建环境、资源、依赖和未观察行为,验收条件随之失去约束。

传统逆向工作流中,逆向分析者负责把这些线索连接起来:搜索字符串,定位引用,检查函数,修改假设,再设计实验。AI 参与后,大量重复查询和跨文件追踪可以交给 Agent。分析者仍需决定哪些证据足以支持结论,以及哪些未知会影响后续实现。

这里要区分「功能解释」和「复现正确」。模型能够解释一段伪代码,只完成了前者的一部分。实现通过几个样例,也只能支持已经覆盖的输入。把有限观察写成整个软件等价,会让后续开发继承一份未经验证的规格。

热度背后的需求

REA 的爆火我觉得来自两个方面:一个是逆向作为一个传统的门槛非常高的工种,当前 Agent 结合 REA 能把这个门槛降得很低;另一个是对于一个传统闭源的软件,用这套工具可以复刻一个出来,这种打破规则的禁忌也让人着迷。

REA + Agent 解决了逆向工作中的一个特别重要的问题:模型已经能读懂不少代码,工程师却仍需手动替它操作分析工具。

Ghidra、Hopper、IDA 各有自己的数据库、API 和使用方式。面对 Electron,又要处理 ASAR、模块加载、preload 和 IPC。问题一旦跨越技术层,分析者就要在多套工具之间搬运上下文,判断地址、路径和符号是否指向同一对象。

REA 把这些能力接到 CLI 和 MCP 上。人可以直接运行命令,Agent 可以调用工具,返回结果使用统一的来源和限制描述。这让调查可以围绕一个问题持续推进,减少人工整理每一次工具交互的工作。

社区期待在这里已经失控。

能够降低分析成本,与能够经济地完整复现一个大型软件,之间还有很长的距离。交互细节、异常分支、数据格式兼容和未被触发的行为,都要进入验证范围。Star 和演示很难呈现这部分成本。

GitTested 对 REA 6.2.0 的测评提供了一个约束:在四个不超过 264 KB 的去符号原生程序上,普通工具组得到 74/75 的事实检查分数,REA 组为 73/75,后者使用的 token 约为前者的 2.5 倍。这份结果针对特定版本、模型和小程序,无法外推到当前全部能力;它足以说明,增加分析工具没有自动带来更高收益。测评原文

我们做选型时,应当保留普通工具基线。复杂度较低的任务,如果文件读取、搜索和 objdump 已经能回答,额外加载大型工具目录和完整图结果,可能只是在增加任务成本。

REA 与当前的工具如何协作

REA 的生产主链没有一个必须内置的通用大模型规划器。模型通常来自 Codex、DeepSeek Harness 等调用端,负责提出下一次查询;REA 提供具体分析操作,管理目标、结果和相关资源。

当前的 6.5.0 版本总共声明了 207 个工具合约、6 个引导式 Prompt。原生路径包含函数档案、伪代码、汇编、字符串、交叉引用、调用图,以及字节和类型检查。反编译主要由外部引擎承担,REA 负责把结果转换为统一表达。

这个统一不是一模一样。不同引擎能提供的事实不同,不能为了得到整齐的返回格式,给缺失能力填上空数组。REA 会保留不可用原因和覆盖信息。调用者需要知道某个侧面没有观察到,或者该 Provider 根本不支持。

引擎选择也没有被藏起来。自动模式只有在存在唯一可用分析配置时才建立深度绑定;多个候选均可用,会报告歧义。显式选择的引擎失败,也不会悄悄换成另一个,再继续沿用原来的来源说明。

这种设计牺牲了一点使用上的顺滑,换来了可解释的结果。如果分析过程中发生了引擎切换,类型恢复、函数边界和调用关系都可能改变。让调用者作出选择,比把差异隐藏在成功响应里更适合需要复查的调查。

CLI 与 MCP 共用应用工作流,生命周期有所区别。CLI 通常执行一次便结束,MCP 可以保留目标和调查状态。连续研究同一个大目标时,会话复用与快照有价值;只有一次明确查询时,单次命令更容易控制输入和输出。

跨层分析难点

以 JavaScript 和 Electron 路径为例,其尤其能体现 REA 自身的分析工作。

一个文件导入功能可能从 renderer 进入 preload,经 contextBridge 和 IPC 到达主进程,最后调用原生扩展。搜索按钮文本只能得到入口,import 图也无法独立解释跨进程关系。

REA 会解析 AST,建立作用域、绑定、引用和模块来源,再投影为应用图与语义图。别名、解构、成员变更以及部分打包结构,都要参与判断。代码里出现同名函数,不能证明调用来自同一个模块;局部变量遮蔽之后,相同文本甚至可能对应完全不同的行为。

IPC 配对采用较保守的条件:字面量 channel 与通信模式相容,且存在唯一候选,才生成确定配对。多个候选保留歧义,动态 channel 无法证明时保持未配对。这里的静态关联也没有自动升级为运行时事实。

这种策略适合回答「应该继续查哪里」。遇到运行时计算、动态加载或不透明影响,静态图会留下缺口。继续扩大扫描范围未必能填补缺口,可能需要转向指定动作的运行观察。

源码文本进入 AST,再变成语义 IR、应用图和 Evidence,中间表示会扩大内存需求。REA 把解析分析放在独立受控进程中,使用堆限制、超时和传输预算;部分遍历、校验和摘要计算通过协作检查点响应取消。

独立进程增加了启动、序列化和数据交接成本,却能为大 bundle 提供更清楚的资源边界。面对少量文本,普通搜索更轻;面对跨模块来源和 Electron 边界,结构化分析才可能抵消这笔开销。

能力各有边界

REA 的其他模块也遵循专题分工。

.NET 路径自己读取 PE/CLI 元数据、类型成员、IL 方法体和托管原生边界,不需要运行程序集。它适合结构调查,不能因此承诺恢复完整 C# 工程。Android 又拆成 JADX 静态代码、Apktool 资源解码、ADB 设备操作和 JEB 接入,避免把看代码与安装、启动应用混成一个权限层次。

包与资源模块负责目录、ZIP、ASAR、DMG 和架构切片的清单、摘要及提取。Apple 专题进一步处理 plist、Interface Builder、动态库解析和相关元数据。输入身份从这里建立,后面的分析才能确认读取的是同一份内容。

固件依靠 Binwalk、Unblob 等外部工具识别区域和提取内容;Flutter 路径检查构建、ABI 与 Dart AOT 线索;WASM 借助 WABT 读取验证结果、结构和 WAT;EVM 借助 EVMole 提取 selector 和推断接口。它们的输出范围各不相同,统一称为「还原源码」会抹去重要限制。

Go、Rust 模块主要读取嵌入的构建或依赖信息。SQLite 检查稳定数据库及 WAL 的私有副本。抓包和录制信号模块解释已有记录。元数据缺失,不能推出依赖不存在;某个窗口没有观察到请求,也不能推出功能从不访问网络。

这些模块的价值取决于问题。需要了解依赖时,读取元数据比启动反编译器更合适;需要解释某条数据路径时,元数据又不足以代替函数和运行观察。选工具之前,先确定哪种事实能够改变当前判断。

证据决定结论

REA 的 Evidence 记录了目标摘要、Provider、分析配置、操作参数、原始与标准化结果、位置、限制及关联证据。它让模型的解释可以回到具体来源,而不只剩一段听起来合理的文字。

其中 observed、derived、inferred 区分观察、推导和推断,表达的是知识来源,不能当成概率。观察到反编译器输出某段伪代码,与证明这段伪代码准确描述所有行为,仍然是两件事。

证据 ID 根据语义内容生成。目标的展示名称和本地路径被排除,其他参数、结果或位置中的路径依然可能影响摘要。这种身份设计方便保存和交换,也要求我们理解它证明了什么:摘要匹配可以检查内容一致性,无法替代引擎正确性和解释正确性。

快照进一步绑定目标、Provider、分析配置、操作和参数。读取实时状态的能力不能被历史缓存伪装成当前观察,依赖隐含光标或文档的查询也需要额外约束。缓存命中率只能在这些条件成立之后讨论。

未知问题有独立的版本和状态。一个问题可以被验证解决,也可以被撤回或排除在范围外。后两种状态并不提供事实已经成立的证明。对需要交付分析结论的团队,这种区分应当进入评审,而不只停留在模型提示词里。

运行才有观察

静态分析留下的关键疑问,需要通过实验减少。REA 的浏览器、Inspector、PTY、LLDB、GDB 和 Frida 等模块提供了不同观察路径,各自有明确的影响范围。

被动附着页面读取信息,与启动应用、点击按钮或加载插桩脚本,涉及不同状态变化。一次动作完成后,还要分别确认截图、DOM、网络、存储等采集项是否成功,不能用动作成功代替采集完整。

程序退出、输出关闭和清理完成,也有各自的观察条件。客户端超时可能只结束等待;后台工作仍然执行。进程 leader 退出之后,后代进程可能继续存在。PID 还可能被复用,仅凭一个数字无法确认终止权限。

REA 使用运行 token、进程身份和资源 owner 管理这些边界。共享启动过程中,一个等待者取消,不应打断其他调用者;最后一个等待者离开时,又要等生产者收尾。清理失败则保留原 owner,以便后续对尚未释放的资源重试。

这部分代码增加了状态复杂度。并发、取消和最终收尾的组合很难靠正常路径测试覆盖。但逆向任务的单次执行成本可能很高,错误地重启分析器或留下后台进程,会让下一轮调查继承无法解释的环境。长会话系统需要承担这笔工程成本。

如何使用 REA

Codex、DeepSeek Harness 与 REA 可以通过 MCP 组合。我们没有必要为了接入逆向能力,再实现一套 Agent loop。

Harness 负责模型、工具执行、上下文、会话和插件生命周期;REA 负责分析目标、引擎及证据。两边都有会话,含义不同。模型会话保存了工具结果,不代表 REA 的进程和目标状态也已经被完整保存。

在 Codex 中,可以使用 REA 的 setup 先查看注册计划,再确认应用。手动方式则通过 Codex MCP 配置启动 rea mcp,并为原生分析设置合适的调用超时。

其实也可以直接把 github 的地址给 Codex,让它给你安装。

效率必须实测

分析规模越大,越需要区分计算完成与结果可用。

完整应用图和 Evidence 可能很大。MCP 同时承载文本和结构化内容,还受到传输预算、JSON 编码以及字符串表示的限制。让模型直接接收全部结果,可能把大量成本花在无关模块上。

REA 提供概要、受支持的分页视图和证据导出。超过传输预算时,部分路径会保留完整结果,再返回可继续读取的引用。分析没有被压缩成较小的事实集合,交付方式发生了变化。

但分页也有代价。模型可能先发现工具,再读概要,再取数页,再因选择错误重试。每次回复更小,整项任务的调用次数、上下文和延迟却可能增加。REA 自己的实验记录也保留了这种混合结果,回复字节减少没有稳定证明总 token 和耗时减少。

因此,性能评估应当以同一个问题的完整过程为单位:普通工具能否回答,使用 REA 后正确性是否改善,失败和重试多少次,总 token、时间与进程族内存分别是多少。冷启动和会话复用也要分开。没有这组对照,压缩输出只能算一次局部优化。

闭源边界变化

标题中的判断需要落到可讨论的范围。闭源作为分发和商业形态会继续存在;用户能否拿到原始源码,与能否推导部分实现逻辑,正在逐渐分离。

交付到本地的字节、资源、接口和运行行为,提供了调查输入。Agent 能持续利用这些输入,减少搜索、查询、整理和局部实现的人工成本。原来因为耗时而很少被分析的功能,可能因此进入更多人的调查范围。

这种变化会削弱一部分依赖信息不对称的优势。对于可以被独立描述、输入输出有限的功能,我们需要重新估计其被理解和重写的成本。仅凭实现复杂或代码不可见,已经很难为这个估计提供足够依据。

完整产品仍有不同的成本结构。兼容性、异常处理、版本演进和持续验证不会随着函数解释一同完成。远端执行、未提供的数据和未观察路径,也不会从本地文件中自动出现。分析工具可以报告边界,无法凭空补足缺失输入。

从技术管理角度,我们应当同时调整两端:研究外部软件时,把调查问题和验收范围收紧;评估自身产品时,减少对实现隐蔽性的依赖,把可验证的行为质量和持续维护能力纳入投入判断。

REA 已经把逆向工具、结构化证据和 Agent 调用连接起来,也在代码中保留了失败、歧义和未知。我们可以利用这套能力缩短调查过程,同时要求每一次功能解释都能追溯来源,每一次复现结论都能说明覆盖范围。缺少来源或覆盖声明的结论,应继续留在调查阶段,避免直接进入实现规格和验收报告。

但是,最后逆向商业软件是有可能触发法律风险的,以及当前社区已经有人反馈,因为逆向导致 Codex 封号。

以上。

开发同学在 vibe coding 后,面对代码黑盒应该做什么

最近半年,我们在代码库中看到越来越多这样的提交:一个功能完整的 PR,包含数千行代码,单测覆盖率达到 85% 以上,本地集成测试全绿。然而在提测演示或做方案复盘时,提交代码的工程师无法在不借助模型的情况下,准确描述出某条核心业务链路在异常分支下的具体状态扭转过程。

代码由大模型直接生成,工程师负责提供提示词、粘贴报错、运行命令以及验收功能。这种被称为 vibe coding 的开发模式正在研发团队中快速蔓延。开发人员正在迅速丧失对代码内部实现细节的掌握。

如果一个开发人员完全不需要知道代码内部发生了什么,只要在外部通过输入输出来验证系统,这项工作换成一个不懂技术的产品经理或业务人员,结果并没有本质区别。 当编写代码的动作被模型接管之后,技术人员本身的专业壁垒就会受到直接冲击。

在实际工程推进中,不懂底层原理的使用者在与模型交互时,会迅速遇到一道无法逾越的墙。当系统行为偏离预期,或者出现跨多个子系统的复合型偶发故障时,缺乏系统内部认知的人甚至无法组织出有效的排查提示词。他们只能把控制台的错误堆栈反复扔给模型,陷入尝试、失败、再尝试的低效死循环。

当然,这种情况在模型和 Harness 越来越强的情况下,越来越少了。

当前,判断开发工程师价值的关键指标,在于具体业务子领域是否还需要我们深入理解系统的内部实现,以及需要理解到何种程度。

复杂系统的黑盒沉降

我们原本设想黑盒开发只会停留在低风险的边缘地带。那些生命周期以周计的营销脚本、孤立的数据管道、或者一次性的内部报表工具,被开发者直接当成黑盒丢给模型演化。只要输入和输出符合预期,内部堆叠了多少冗余逻辑,人类完全不需要过问。

这种边界在过去半年里被全面打破。

在真实的研发场景中,复杂的长周期核心系统同样正在被迅速黑盒化。系统的实现逻辑已经开始脱离了我们的掌控。

有些系统已经没有任何一个工程师完整读过底层实现,所有人都在依靠行为验收来驱动迭代。

这一变化的驱动力来自工程吞吐量的极端失衡。过去由一个六人资深团队耗时三个月才能搭建完的复杂状态机,现在借助高阶推理模型,两天内就能生成数万行代码,并配套数千个通过的单元测试与端到端用例。当代码生成的速率超越人类视网膜与大脑工作记忆的物理极限时,人工逐行审查机制就失去了事实上的防御能力。

我们开始被迫接受这种现实。 在面对极其庞大的调用拓扑和复杂的上下文依赖时,我们已经放弃了对抽象语法树的微观控制,转而把整个复杂系统视作一个自组织的、不透明的动力学网络。

这种转变直接剥夺了传统工程学给我们带来的安全感。当不懂内部机理的团队开始掌控这种复杂黑盒系统时,表面上的研发效率提升与潜伏的系统性崩溃风险就绑定在了一起。

演化哲学的工程代价

把软件代码视同生物 DNA 序列,认为系统可以在目标函数的约束下自然演化,是黑盒派的核心立论。生物演化积累了大量的无用突变、历史包袱与内含子序列,依然能在残酷的自然选择中维持机体运转。在很多开发者的设想里,只要外部的行为约束足够严格,内部的代码哪怕堆积成山,系统也能通过模型的自我修剪持续向后演化。

在复杂系统中使用演化哲学,会遭遇物理层面的硬约束。

软件系统与生物体存在本质区别。生物体拥有物理世界施加的绝对法则限制,而软件的目标函数是由人类通过测试套件和契约规范手工定义的。人类工程师编写的测试用例,无论规模多么庞大,都只能覆盖有限的状态空间。

当模型在复杂的业务系统里自我演化时,它会不断寻找使测试用例全绿的阻力最小路径。在处理一个涉及分布式两阶段提交的业务分支时,模型为了修复一个并发竞争的偶发报错,可能会在关键路径上引入一段极其隐蔽的全局读写锁,或者擅自放宽事务隔离级别。从行为验收的角度看,那十几个报错的测试用例确实顺利通过了。

这种微小的局部优化在多轮提示词和版本迭代后,会引发全局架构的雪崩。并发瓶颈从数据库行锁被硬生生搬运到了应用层内存,系统的吞吐量在特定流量脉冲下出现断崖式下跌。由于团队没有人理解这段被模型演化出来的内部调度机制,监控指标只能呈现出 CPU 软中断升高与连接池耗尽,排查人员根本无法把这种宏观症状与某个被模型悄悄修改的同步原语对应起来。

生物演化历经数十亿年,其代价是无数个体的死亡与物种灭绝。在商业软件工程中,任何一次演化走入死胡同,换来的都是真实的资损、核心数据损坏与长达数小时的服务不可用。

重写的大坑与暗知识

当代码产出如此快速时,以前不敢做的重构操作,现在频频出现在现实之中。

并且对于黑盒的代码,当无法维护时,会有人开始直接让 AI 来重写了。

对于极度依赖隐性规则的复杂系统,重写会是一个大坑。

复杂系统的内部实现中,沉淀了海量的暗知识。这些暗知识由线上历史事故、冷门协议的缺陷规避、上下游陈旧系统的怪异行为,以及特定硬件环境下的性能妥协交织而成。在漫长的迭代周期里,这些细节几乎不可能被完整记录在需求文档或提示词工程的上下文中。

当一个承载核心业务的复杂黑盒系统遭遇架构死锁时,工程师试图命令模型从零重写一套全新的系统。模型根据现有的规范文档和接口契约,迅速生成了一个结构干净、抽象完美的新架构。但在接入真实生产流量的瞬间,系统会被各种意想不到的边缘异常彻底击穿。

老系统中那些看似丑陋的防重试逻辑、硬编码的延时等待以及反常的类型转换,恰恰是系统在生产环境存活多年的抗体。在黑盒演化过程中,这些抗体没有被人类工程师转化为显性的设计原则,而是散落在无法辨识的代码废墟中。

当模型完全接管了代码,人类不再阅读实现细节,这些暗知识就随着上下文窗口的更迭永久失传了。重写一个复杂黑盒系统,意味着要重新把过去五年踩过的所有生产故障从头经历一遍。这种重写代价根本不是代码生成速度能够弥补的。

工程师的剩余价值

在复杂的长周期系统也逐步被黑盒代码吞噬的周期里,技术团队内充斥着工程师技能贬值的焦虑。天天面对自己看不懂或者懒得去看的自动化生成代码,开发人员的专业尊严受到了直接侵蚀。

开发人员的剩余价值非但没有消失,反而在系统复杂度失控的悬崖边缘被急速放大。只不过,这种价值的落点发生转移。

编写算法、拼装框架、修补语法的低阶体力劳动被彻底剥离。工程师的核心价值,收敛为以下几项不可替代的底层能力:

第一,是定义系统物理边界与失败容忍度的系统论设计能力。当实现细节彻底黑盒化,整个系统的生存完全取决于外部边界是否足够坚固。如何设计熔断逻辑,如何划分数据强一致性与最终一致性的边界,如何定义灾难恢复时的降级降速策略,这些决策直接关乎企业的商业生死,不可能托付给缺乏全局上下文责任感的模型。

第二,是穿透软件抽象层、理解物理基础设施底层的能力。不论上层代码被模型演化得多像一个不可名状的黑盒,当它最终被编译成汇编指令落到物理 CPU、内存条、网卡与固态硬盘上时,它依然必须服从物理世界的客观定律。

在跨机房专线延迟突增、底层存储硬件出现坏块、操作系统内核调度产生微秒级锁死等极端场景下,缺乏真实物理世界感知的黑盒系统会瞬间丧失全部应对能力。此时能够挽救全局的,永远是那些清楚 Linux 内核参数配置、深刻理解 TCP 拥塞控制算法、明白数据库 B+ 树与 LSM-Tree 存储引擎底层权衡的工程师。

第三,是决定何处必须保留白盒的战略决断力。在复杂的业务全景图中,我们必须划出一条绝对的红线。在这条红线之外,允许模型疯狂试错、野蛮生长、黑盒演化,以换取交付速度;而在红线之内,在涉及资产结算、核心密码学协议、权限鉴权核心以及数据持久化原子性的基石模块上,我们必须死守白盒阵地,每一个状态位、每一行锁逻辑、每一个并发屏障,都必须由人类大脑完全理解、推演与严格审计。

拥抱黑盒演化是应对代码生产力爆发的必然妥协,但盲目迷信黑盒则是工程理性的自杀。

白盒边界的落地

不过,把红线画出来,还远远不够。

如果白盒意味着所有代码都要由人逐行理解,那么几万行的关键模块很快就会耗尽团队的审查能力。如果白盒只意味着安排一位资深工程师签字,它又会退化成一种形式上的责任分配:代码已经合入,签字的人却无法解释失败路径。

从实际出发,白盒要求可以落实到几个可以检查的问题上:

这个模块维护哪些状态?哪些状态转换绝对不允许发生?一次操作在什么位置产生不可撤销的影响?执行中断以后,如何判断它究竟完成到了哪里?修改这里,会影响哪些调用方的既有假设?

负责的工程师需要能够独立回答这些问题,并且指出对应的实现位置。

这里仍然允许模型生成代码。我们限制的是未经理解的关键变更进入系统。生成与理解可以由不同的过程完成,但理解不能被一份自动生成的说明替代。

边界还需要沿着依赖关系检查。

假设一个关键模块本身经过了严格审查,但它依赖的公共组件被模型修改了失败处理方式,原有结论就可能失效。白盒边界如果只按目录划分,很容易漏掉这种变化。把关键模块依赖的行为约定一起纳入审查范围,尤其是超时、重试、错误返回和状态写入的语义。

这样做会拖慢部分公共组件的修改速度。我们需要接受这笔成本,也需要控制它的规模。如果一个普通改动总要召集半个团队评审,说明关键模块依赖了过多外部细节,应该考虑收缩接口和状态共享范围。

白盒区域也不必永久固定。一个模块的状态被拆出、接口变得稳定、替换方式得到验证之后,可以降低内部审查强度。一个原本独立的工具开始承接共享数据写入,就应该重新评估。

「核心业务」四个字无法覆盖整个代码库。范围画得太大,最后往往只能整体降低执行标准。

验证独立

黑盒能接受到什么程度,取决于我们能够从外部验证什么。

这里最容易出现的问题,是实现、测试和验收结论都来自同一条生成链路。模型先理解需求,再生成代码,随后根据代码补齐测试,最后告诉工程师所有测试已经通过。

如果最初的理解遗漏了一个条件,这个条件就可能同时从实现与测试中消失。

覆盖率无法自动发现这种遗漏。某一行代码被执行过,只能证明测试经过了那里。异常分支执行以后留下的状态是否正确,仍然需要另外检查。

因此,关键验收条件在实现之前确定。至少把正常结果、禁止出现的结果,以及失败后允许留下的状态写清楚。

尤其要检查那些跨越多个动作的过程:前一个动作已经完成,后一个动作失败,系统应该停在哪里?用户重新发起请求时,允许重新执行哪些部分?超时之后,调用方能否把结果当成失败?

这些问题没有答案时,继续增加测试数量并不能补上设计缺口。

模型可以帮助枚举场景、生成数据、执行验证。涉及业务承诺的条件,需要由了解系统的人确认。验收阶段还应保留独立于当前实现的依据,避免模型通过修改预期结果,让实现重新获得通过。

测试本身的变更也要审查。

删除一个失败用例,有时是因为旧需求确实失效,有时却只是当前实现暂时无法满足它。二者在测试报告上没有区别。对稳定约束的删除、放宽和跳过,我会要求单独解释,不能夹在几千行功能改动里一起合入。

验证强度当然有成本。复杂故障场景需要准备环境,大规模数据验证消耗资源,长时间运行的检查会增加反馈延迟。我的做法是分开执行频率:局部检查跟随每次修改,影响面较大的验证放在合入和发布环节,昂贵的故障验证集中覆盖关键路径。

目标是让不同风险都有对应的检查位置,同时避免每次修改都等待一套庞大的验证流程。

限制单次变化

代码生成速度上来以后,团队很容易把更大的任务直接交给 agent。

一个需求里同时包含接口调整、状态机修改、数据结构迁移和历史代码整理。模型可能一次完成,最终差异也显得相当统一。但人工审查时,很难再把每一处变化与最初的动机对应起来。

发生回归以后,定位范围同样会扩大。

我们要控制单次变更所包含的独立决策数量。功能增加、结构整理和历史兼容性清理,尽量分开进行。每一部分都需要有能够单独解释的目的,以及对应的验证结果。

这里不适合机械地规定 PR 行数。一次自动生成的重复性修改可能涉及很多文件,实际语义变化很少;一个几行的条件调整,也可能改变整个系统的状态约束。

这次只改变了哪些行为,哪些证据支持这些变化,以及如何撤销?

让 agent 先提交修改计划,也有帮助。人可以在生成大量代码之前检查它准备触及哪些模块,是否引入新的共享状态,是否扩大已有接口的职责。

当然,计划不能被当成执行事实。模型在修复过程中可能偏离原计划,最终仍然需要核对实际改动。尤其要检查那些为了让测试通过而新增的兼容分支、默认值和异常处理。

另外,权限应该跟着风险划分。修改实现、调整验收标准、操作生产数据,是三个不同等级的动作。不能因为 agent 已经获得代码仓库的写权限,就顺带允许它自行决定另外两件事。

保留接管能力

即便验证做得足够认真,团队仍然需要准备一种情况:系统出了问题,agent 连续几轮都没有找到原因。

这时工程师需要接管什么?

首先要接管的是故障判断和损失控制。当前还有哪些请求正在写入?哪些动作可以暂停?哪些状态已经无法直接撤销?继续重试是否会重复产生副作用?

如果这些问题无法回答,贸然修复代码可能让后续处理更困难。

因此,关键路径上的可观测信息需要围绕状态变化设计。只记录异常堆栈,通常不足以判断一个业务动作完成到了哪里。需要能够把一次操作的开始、关键写入和最终结果关联起来,同时区分「没有执行」与「执行完成但没有返回」。

信息越多,记录与检索的成本也越高。涉及数据内容时,还需要限制记录范围。我不会要求把所有局部变量都写进日志,而会优先保留能够判断执行阶段和副作用的信息。

恢复方案也要区分代码与数据。

代码版本退回以后,新版本已经写入的数据仍然存在。旧实现能否读取这些数据,正在执行的任务能否继续,外部已经发生的动作如何处理,都需要提前确认。一个发布平台上存在的回滚按钮,不能证明业务状态能够恢复。

团队可以让模型生成恢复方案,但需要通过演练检查其中的假设。演练时暴露出一个无法判断完成状态的步骤,就应该补充相应的记录或操作方式。

这些工作会占用原本可以拿来交付功能的时间。我们要优先安排给那些失败后难以恢复、影响范围又大的模块。对于可以直接丢弃并重新生成的结果,没有必要采用同样的投入标准。

我们也不应该假设所有故障最终都需要人手工解决。agent 能完成的排查可以继续交给它。人需要保留的是判断它是否仍在有效推进的能力,以及必要时改变排查方向、停止危险操作的权限。

把暗知识留下

关于暗知识,还有一个地方需要说得更准确。

只要代码、历史记录和运行证据仍然存在,很多知识就有机会被重新找回来。但随着版本累积,重新发现它们需要的时间会增加。故障发生时,团队未必有这个时间。

在每次关键修改中,顺手留下三类信息:这段特殊处理保护了什么约束,当初在什么条件下出现过问题,以及什么证据能够证明它将来可以被删除。

单独写一句「兼容历史逻辑」,对后来的维护者帮助很有限。模型也无法据此判断,这段逻辑究竟承担了必要的保护,还是早已失效的补丁。

记录需要贴近决策发生的位置。约束可以进入验收条件,历史问题可以转化为回归场景,难以自动验证的条件则保留在模块说明和变更记录里。没有必要把所有知识都塞进一份不断膨胀的架构文档。

尤其要防止另一种浪费:模型生成了大量说明,团队却没有检查其中哪些是事实,哪些只是根据代码推测出来的意图。

「当前代码这样执行」和「系统必须这样执行」需要分开记录。前者描述实现,后者约束未来修改。混在一起,模型可能把历史偶然行为永久固化,也可能把必须保留的规则当成可整理的细节删除。

重写之前,这些记录应当成为核对清单。找不到来源的特殊逻辑,需要进一步追踪,不能仅凭实现难看就判定它没有价值。

同样,也不能把所有历史补丁都当作不可触碰的要求。长期保留失效约束,会让新系统继续背负旧系统的复杂度。删除它们需要证据,保留它们也应该能够说明理由。

重新分配时间

这会改变工程师日常工作的时间分配。

一部分原本用于编写常规实现的时间,可以转移到约束定义、关键路径审查、失败恢复和演化设计上。另一部分确实可以节省下来,用于交付更多需求。我们没有必要为了证明专业价值,把节省的时间全部重新填满。

不要求所有工程师都深入内核、网络协议和存储引擎。基础设施知识在某些问题上非常关键,但大量故障仍然来自业务状态、依赖关系和错误的边界假设。团队需要根据系统的风险分布建立能力,不能用一套底层知识清单替代具体判断。

对于个人:能否识别模型没有回答的问题,能否发现验证结论依赖了未经确认的假设,能否在陌生实现中定位关键状态,以及能否解释一次技术选择会给后续修改增加什么成本。

这些能力也需要通过实际接触代码培养。如果年轻工程师长期只负责转发需求和粘贴错误,却没有机会追踪实现、分析失败、参与关键决策,团队几年后可能会出现知识传承的问题。

因此,我们需要让工程师对完整的局部问题负责:提出约束、使用模型实现、解释关键行为、验证异常路径,并参与上线后的问题处理。模型可以承担其中大量工作,但负责的人需要能够检查结果。

团队层面的评价也要变化。生成了多少代码、完成了多少轮对话,都不适合作为主要指标。我们需要观察一次需求从提出到稳定运行花了多久,评审与返工占用了多少时间,线上问题是否反复出现,以及一个模块是否越来越难由其他人接手。

如果编码时间缩短了,排查和返工时间却持续增长,就需要调整当前流程。交付数量暂时增加,也不能掩盖恢复能力和知识覆盖的下降。

回到最初那个数千行代码、测试全绿的 PR,我们不会因为提交人无法背出全部实现就否定它。

但如果他无法解释核心异常分支的状态变化,我们要要求补上这部分理解:追踪关键写入,核对失败后的状态,检查重复执行的后果,再决定是否需要增加验证或修改实现。

这个 PR 可以继续由模型完成修改。合入之前,负责的工程师需要知道哪些结论已经得到验证,哪些风险仍然存在,以及出问题以后应该从哪里开始处理。

以上。

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 执行中的关键状态显式化:输入有队列,执行有边界,请求有可见历史,工具有调度与审批,失败有独立的处理路径。

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

以上。