1. 从一行报错说起:harness 和 runtime 为什么值得较真
如果你最近在折腾 Agent 开发,很可能见过这么一行报错:
error: agent harness runtime "codex" is unavailable because its plugin registry...我第一次看到这行报错的时候,第一反应是“某个依赖坏了,重装一遍就行”。但折腾了半个小时,重装了所有能重装的东西之后,我才意识到这行字远不是一个简单的环境问题——它直接把 Agent 体系内部的结构层次摆在了我面前:报错的主语是 agent harness,报错的对象是 runtime,而且 harness 是通过 plugin registry 去寻找 runtime 的。
这三个名词出现在同一句话里,说明它们是三个不同的东西。但问题来了:市面上讲 Agent 开发的文章,大多把“运行时”“框架”“编排层”混着说,今天叫 runtime,明天叫 harness,后天叫 agent framework,再加上 LangChain、CrewAI、OpenAI Agents SDK、Claude Code、Codex CLI 这些项目各自的叫法,新手很容易被绕晕。
而且这不是一个纯粹的理论问题——它直接关系到你怎么定位报错、怎么选型技术栈、怎么给团队解释“我们的 Agent 架构到底长什么样”。如果你把 harness 当成 runtime 去排查问题,或者选型时买了一个“runtime 很弱但 harness 很强”的方案,后面会踩很多冤枉坑。
这篇文章我打算用一整篇的篇幅,把这两个概念从职责、边界、交互方式、排错方法到选型思路全部拆开来讲。不绕弯子,直接从架构分层开始。
2. 先建立共识:Agent 从配置到执行,中间到底隔了几层
要理解 harness 和 runtime 的区别,得先退一步看一个 Agent 应用的整体架构。很多人觉得 Agent = 大模型 + 工具调用,这没错,但太粗了。从代码真正跑起来的角度看,一个完整的 Agent 应用至少应该拆出下面几层:
| 层级 | 职责 | 典型组件举例 |
|---|---|---|
| 模型层 | 提供推理能力,接收消息并生成文本/工具调用 | GPT、Claude、Gemini、本地开源模型 |
| Runtime 层 | 实际执行模型推理循环、调用工具的沙箱、管理进程与资源 | 代码解释器沙箱、容器运行时、浏览器自动化环境 |
| Harness 层 | 装配提示词、注册工具、注入策略、协调观测与插件发现 | 各类 Agent SDK 的编排内核、自定义的 tool registry |
| 应用层 | 面向用户的界面与业务逻辑,调用 harness 的入口 | CLI、Web 应用、客服机器人的后端服务 |
注意我的措辞:harness 在上、runtime 在下。这不是我发明的分层,而是现代 Agent 框架在工程上实际形成的结构。模型层是最底层的“大脑”,runtime 是“手脚和肌肉”,harness 是“神经中枢和控制系统”,应用层才是“对外表现出的那个人”。
你可以用另一个类比来感受:把 runtime 想成一台通用机床,它擅长切削,但不知道今天要削一个齿轮还是一个法兰。harness 是机床旁边的控制台和工艺卡,它知道零件的设计图、加工顺序、什么时候该换刀具、什么时候该停下来等人确认。至于应用层,就是车间主任,它只负责说“我要一个齿轮”,剩下的交给控制台和机床。
这个类比最关键的一点是:机床(runtime)可以被不同控制台(harness)驱动。同一个 Python 沙箱,你可以用 A 框架去编排它,也可以用 B 框架去编排它。反过来,同一个控制台也可以驱动不同型号的机床。这就是为什么我们有必要把这两个概念分开——它们本来在工程上就是可替换的两个独立组件。
但为什么很多人还是混着说?因为市面上很多 Agent 框架把这两层揉在了一起。框架提供一个Agent类,你传入模型、工具、系统提示词,它内部既帮你管理工具调用的循环(这是 runtime 的活),也帮你装配系统提示词和策略(这是 harness 的活)。框架的设计者为了简化用户心智,故意不暴露这个分层。这在快速原型阶段没问题,可一旦你开始排查诡异的线上问题,或者想把底层沙箱换掉,这个被隐藏的分层就会突然跳出来找你麻烦。
所以接下来的两章,我把 runtime 和 harness 分别拎出来仔细讲。
3. 逐层拆解 Agent Runtime:执行能力从哪里来
3.1 Runtime 的本质是“执行循环”而不是“运行环境”
很多人把 runtime 理解成“代码运行的环境”,这是一个常见的误解。Python runtime、Node.js runtime 确实是这个意思,但 Agent Runtime 不太一样。
Agent Runtime 的核心是一个循环(loop):模型生成回复 → 如果回复里包含工具调用请求 → runtime 去执行这个工具 → 把执行结果返回给模型 → 模型继续生成 → 直到模型不再请求调用任何工具。这个循环在行业里通常被称为 agent loop 或者 tool-calling loop。
伪代码示意 while model_wants_to_call_tools: response = model.generate(messages) if response.has_tool_call: result = runtime.execute_tool(response.tool_call) messages.append(role="tool", content=result) else: return response如果你把这个循环写出来,就会发现它其实不复杂。复杂的部分是循环里的每一个环节在真实世界里会遇到的问题:工具执行超时了怎么办?沙箱内存爆了怎么办?模型连续调用同一个工具 50 次形成了死循环怎么办?工具执行产生的外部副作用是否需要回滚?
所以,runtime 的实际职责比“跑代码”要广得多。一套完整的 Agent Runtime 至少要处理这些事:
- 模型推理调用:封装对模型 API 的请求,处理流式输出、重试、超时。
- 工具协议解析:把模型输出的 JSON 形式的工具调用(OpenAI function calling 格式或其他格式)转换成真实的函数调用。
- 沙箱执行:工具代码在一个受限环境里跑,限制网络、文件系统、CPU 和内存。
- 上下文管理:维护消息历史,处理超过上下文窗口的截断与摘要。
- 状态保持:多轮对话之间保持会话状态,同一个 runtime 实例或不同的实例。
- 安全策略执行:敏感操作前需要人工确认,这通常由 runtime 提供 hook,但策略内容由上层 harness 决定。
3.2 Runtime 的隔离级别决定了你的工具能疯到什么程度
工具的执行环境是 runtime 里最要命的决策点。你让 Agent 调用一个 Python 函数算个数学题,那直接在进程里调就行了。但如果你的 Agent 要执行任意用户输入的代码,或者要跑一个可能删除文件的 shell 命令,那隔离级别直接决定了出事后你还能不能睡得着觉。
我实际接触过的隔离方案大致有四档:
| 隔离级别 | 实现方式 | 适合场景 | 风险 |
|---|---|---|---|
| 进程级 | 直接在 Agent 主进程里调函数 | 工具都是自研可信代码 | 代码写崩了就全崩 |
| 子进程级 | 用 subprocess 或 node worker 跑工具 | 工具偶尔需要独立进程 | 资源隔离弱,容易互相影响 |
| 容器级 | Docker 或 gVisor 起一个临时容器 | 要执行不可信代码 | 性能开销大,镜像管理复杂 |
| 解释器/VM 级 | 沙箱化 Python 解释器、Wasm VM | 轻量代码执行、多租户场景 | 兼容性有限,有些库跑不了 |
选哪种隔离不只是安全团队说了算,它直接影响 runtime 的性能和可用性。我见过一个做数据分析 Agent 的团队,为了绝对安全把每个工具调用都塞进一个新起的 Docker 容器,结果是单轮对话的工具调用延迟从 200ms 涨到了 3 秒,用户根本等不起。后来他们改成容器常驻池 + 容器内进程级隔离,延迟才降回来。
3.3 Runtime 的边界:它不管“为什么”,只管“怎么安全地执行”
这是 runtime 和 harness 最本质的分界线。
Runtime 不关心 Agent 当前的任务目标是什么,不关心你用哪个系统提示词,不关心这个工具该不该调用、调用的权限审批流程是什么。它只关心:既然上层决定要调用这个工具,我怎么把它跑得又快又稳又安全。
这个边界划分在工程上很有价值。因为“怎么执行”和“要不要执行、怎么组织执行计划”是两类完全不同的复杂度,把它们分开,你才能独立地升级每一侧。比如你的模型从 OpenAI 换成了 Claude,工具调用协议从 function calling 换成了 tool use,那大概率只需要动 harness 的适配层,runtime 的执行引擎不用变。反过来,你想把沙箱从 Docker 换成 Firecracker,只要保持工具调用接口不变,harness 完全不受影响。
理解了 runtime 的边界之后,我们来看到它头顶上的那一层。
4. 再看 Agent Harness:编排与治理的“外壳”
4.1 Harness 最核心的一件事:把模型、工具、策略“装配”成一个可用的 Agent
如果说 runtime 是执行循环,那 harness 就是装配与治理层。它的读者不是工具代码,而是“整个 Agent”这个对象。harness 回答的问题是:这个 Agent 由哪些工具组成?它的系统提示词是什么?遇到敏感操作时怎么打断?调用失败了几次就放弃?整个过程怎么被观测和审计?
具体拆开,harness 的职责可以列得很长,但核心逃不出这五块:
- 工具注册与 schema 管理:你要给 Agent 暴露哪些工具?每个工具的 JSON Schema 是什么?哪些工具只在特定场景下可见?这个清单是动态的,harness 负责在每一轮推理前把它转换成模型能理解的函数定义。
- 提示词装配:系统提示词、few-shot 示例、工具使用说明、当前会话的目标描述,这些内容按什么模板拼到一起?哪些是固定的,哪些是动态注入的?
- 策略注入与审批流程:读文件可以自动执行,删除文件必须人工确认,调外部 API 需要额外审计——这些策略的实体是 harness 管理的,运行时由 harness 根据策略决定是直接放行、挂起等待审批、还是直接拒绝。
- 观测与追踪:每一轮推理的输入输出、工具调用的耗时和 token 消耗、失败重试的记录——这些观测数据由 harness 统一采集并送往日志系统或 tracing 系统。
- Runtime 的发现与加载:这就是 plugin registry 发挥作用的地方。harness 在启动时需要知道“我要驱动哪个 runtime”,它去 plugin registry 里查找已注册的 runtime 插件,加载对应的适配器,然后才开始跑任务。
4.2 Plugin registry 在 harness 里扮演什么角色
回到开头那行报错。agent harness runtime "codex" is unavailable because its plugin registry...这句话翻成人话就是:harness 在启动时,试图从它的插件注册表里找一个叫 codex 的 runtime,结果没找到。
为什么会找不到?最常见的几个原因:
- 对应的 runtime 插件没安装:某些 Agent 框架把运行时作为插件发布,主程序只包含 harness,runtime 需要单独安装。
- 版本不匹配:插件注册表里记录了旧版本的 runtime,新版本改了注册名或命名空间,harness 按老名字找不到。
- 配置路径问题:插件注册表文件存在某个自定义路径下,而当前环境的配置指向了另一个路径。
- 环境变量缺失:runtime 插件依赖某些环境变量(比如 API Key、沙箱根目录),环境变量缺失时插件在加载阶段就静默失败了。
我为什么说这个报错是个很好的教学案例?因为它精准地暴露了分层结构:报错的发出方是 harness(我在找 runtime),报错的内容是 plugin registry(我通过插件注册表找),报错的对象是 runtime(我要找的东西)。你要是把这三层混成一个概念,那你连这个报错的排查方向都会找错——比如你会去重新安装大模型的 SDK,但这个问题跟模型 SDK 一点关系都没有。
4.3 Harness 与 Runtime 的一次完整交互
为了把这两层的配合讲清楚,我描述一次真实的交互过程,假设我们用的是一个典型的 Agent 框架:
1. 用户输入:"帮我看看当前目录下有哪些 Python 文件" 2. Harness 装配提示词:系统提示词(你是一个文件管理助手)+ 用户消息 + 工具清单(list_files, read_file) 3. Harness 把装配好的请求发给模型(通过 runtime 的模型调用能力) 4. 模型返回一个工具调用:list_files(directory=".") 5. Runtime 解析这个工具调用,在沙箱里执行 list_files 6. Runtime 把执行结果(["a.py", "b.py"])作为 tool 角色的消息返回给模型 7. 模型基于结果生成最终回复:"当前目录下有 a.py 和 b.py 两个文件" 8. Harness 把最终回复返回给应用层,同时记录整轮的 trace 数据在这个流程里,第 2 步和第 3 步主要是 harness 的活,第 4 到第 6 步是 runtime 的活,第 7 步又回到了模型推理(通过 runtime 调用),第 8 步是 harness 的收尾工作。
注意一个细节:Harness 在启动时就去 plugin registry 找到了 runtime,所以在第 7 步之前,runtime 早就被加载到内存里了。这就是那个报错发生在“启动阶段”而不是“执行阶段”的原因——harness 根本走不到第 3 步,在第 0 步就卡住了。
5. 不是竞争关系:一张对比表看懂两者的分工
5.1 核心维度对比
很多人在网上搜“harness 和 agent 的区别”,潜台词其实是“我都用 Agent 框架了,为什么还要关心这些底层概念”。我先直接回答:因为你不关心,报错来了就抓瞎。
| 维度 | Agent Harness | Agent Runtime |
|---|---|---|
| 一句话定位 | 编排与治理层 | 执行与循环层 |
| 核心问题 | 这个 Agent 怎么做对、做得可控 | 这个任务怎么跑起来、跑得稳 |
| 主要职责 | 提示词装配、工具注册、策略审批、观测采集、插件发现 | 推理循环、工具执行、沙箱隔离、上下文管理 |
| 交互方向 | 调用 runtime 提供的执行能力 | 被 harness 调度,向上返回执行结果 |
| 故障典型表现 | 配置错误、工具不可见、策略不生效、找不到 runtime | 执行超时、沙箱崩溃、上下文溢出、OOM |
| 开发者接触方式 | 配置文件、策略代码、工具定义 | API 接口、沙箱参数、环境变量 |
| 类比 | 驾驶舱、控制台、工艺卡 | 发动机、机床、虚拟机 |
这个表里的“故障典型表现”值得仔细看,因为实际排错时,你通常不是直接从概念出发,而是从一个异常现象出发反向定位。
5.2 两层循环:Runtime 管单步工具的循环,Harness 管整个任务的循环
还有一个理解这两层关系的好视角——循环嵌套。
Runtime 内部的 agent loop 解决的是“当前这一步要不要调用工具、调用哪个、结果如何反馈给模型”。它循环的粒度是单步决策。
Harness 层面的循环解决的是“整个任务分几步完成、每步需要哪些工具、中途要不要切换策略、要不要让用户确认”。它循环的粒度是任务执行。
我举个实际场景:你让 Agent“把这个 CSV 文件里的数据清洗一遍,然后画一张分布图”。Harness 可能会把任务拆成:先检查文件结构,再确认清洗规则,然后执行清洗,最后画图。每一步 Harness 都会根据当前状态动态调整工具清单——第一步只需要“读文件”类工具,最后一步需要“绘图”类工具。而 Runtime 只在 Harness 拆好的某一步内做它的工具调用循环,比如在“执行清洗”这一步,Runtime 循环 3 次:调用清洗函数 → 发现数据缺失 → 再调用填充函数 → 重跑清洗。
这种两层循环的设计不是什么新东西,但大部分 Agent 框架的文档不会直接告诉你。你是从“这个 Agent 为什么做了这么多多余的推理轮次”这种性能问题里慢慢悟出来的。
6. 回到报错现场:codex runtime unavailable 的完整排查链路
6.1 逐字拆解报错信息
我还是第一次看到这行报错时的反应:“好像跟 codex 有关系,是不是 Codex CLI 没装好?”但如果你把报错拆开看,信息量其实非常大:
error: agent harness runtime "codex" is unavailable because its plugin registry...agent harness:报错的来源——这是一个 Agent 编排层在报错。runtime "codex":抱怨的对象——一个叫 codex 的执行运行时。is unavailable:状态——不可用。because its plugin registry...:原因——插件注册表层面出问题了。
注意:“因为插件注册表不可用”和“插件注册表里没有 codex”是两种不同的原因,后者更常见。后缀被截断了,但我们做排错的时候不能只盯着一截内存,要把完整报错打出来看。
6.2 根因分类:我遇到的和别人遇到的
根据我在几个不同 Agent 框架里遇到的情况,这类“runtime unavailable because plugin registry”的根因大概分四类:
| 根因类别 | 具体表现 | 解决方向 |
|---|---|---|
| 插件未安装 | 重新部署了环境,只装了主程序,runtime 插件没装上 | 检查插件安装命令,单独安装 runtime |
| 插件已安装但未注册 | 插件文件存在,但注册表配置里没有对应条目 | 执行插件注册命令,或修改配置文件 |
| 版本不匹配 | 框架升级后,runtime 的注册名改了,旧名字找不到新插件 | 升级 runtime 插件,或调整配置里的名称 |
| 加载失败被跳过 | 插件加载时抛异常,harness 把它标记为“不可用” | 看完整日志,找到插件内部异常 |
第四类最坑。它表面上看起来是 “registry 的问题”,实际上插件在启动加载阶段就因为缺依赖或者环境变量不对而崩了,harness 只能把它的状态标记成不可用。如果你只盯着 registry 改配置,永远解决不了问题。
6.3 一步一步怎么查
我建议按下面的顺序来排查,而不是从网上随手抄一条命令:
确认框架和插件的版本对应关系:很多 runtime 插件对框架主版本有强依赖,大版本之间注册名、配置 schema 都可能变。先查一下你当前安装的框架版本对应的 runtime 插件版本是什么。
查看插件注册表内容:一般插件的安装命令都会往注册表里写条目,注册表可能是一个配置文件,也可能是一个本地存储目录。找到它,确认里面有没有 codex 这个名称。没有 → 插件没注册;有但状态是 disabled → 可能是升级时被禁用了。
寻找完整的加载日志:如果注册表里有条目但状态异常,去看启动日志里插件加载阶段的详细信息。重点看有没有异常堆栈——很多时候真正的错误信息藏在这一段,比如“import error: module x not found”或者“invalid config field: y”。
检查 runtime 插件依赖的环境和路径:比如代码沙箱依赖某个特定的目录是否存在、Python 版本是否满足要求、环境变量是否被正确注入。这些配置在 CI 环境里尤其容易丢失。
重装插件,而不是重装主程序:我见过很多人一上来就
npm install或者pip install重装整个框架,其实框架的 harness 代码完全没问题,重装它一点用都没有。把 runtime 插件单独重装一次,让它重新执行注册逻辑,往往就能解决。验证修复:重装后,用框架自带的命令查看已注册的 runtime 列表,确认 codex 出现在列表里且状态为 available,再跑一遍启动流程。
6.4 修复之后,这堂课才算真正上完
修好了报错之后,我强烈建议你把这次的排查过程记下来,尤其是你最后定位到的根因。因为同一个“runtime unavailable”报错,在不同环境下可能是完全不同的问题——在一个人这里是因为插件没装,另一个人那里可能因为版本不匹配,第三个人那里是插件加载阶段静默失败。你把根因记下来,下次团队里有人遇到同样的报错,你就能在五分钟内给出方向,而不是重新走一遍整条链路。
这恰恰解释了“为什么要分清 harness 和 runtime”——排查这个报错的每一步,本质上都是在“harness 的配置表现”和“runtime 插件的内部状态”之间来回切换视角。你脑子里没有这两层的模型,就容易被“codex”这个名字带偏,以为问题出在 Codex 本身上。
7. 落到选型上:你的项目该先关心 runtime 还是先关心 harness
7.1 绝大多数团队的误区:先选框架,再被框架绑架
每次有朋友问我“我要做个 Agent 应用,该用哪个框架”,我一般会先反问一句:你的工具代码准备在哪跑?是直接在自己服务端进程里调,还是要执行用户上传的不可信代码?这个问题才是真正的分岔口。
很多人的选型路径是这样的:看到一个 Agent 框架社区活跃、例子多、一键 demo 很惊艳,就直接入了。做了两周后,发现内置的代码执行沙箱性能不行,想换一个,结果框架的 runtime 和 harness 深度耦合,工具注册、消息格式、提示词模板全绑在一起,一换等于重写。这就是典型的“被框架绑架”。
反过来,如果你一开始就明白 runtime 和 harness 是两个独立模块,你会在选型时多问一句:这个框架的 runtime 能不能单独替换?它有没有暴露标准的工具执行接口?它的 plugin registry 是开放的吗?
7.2 Runtime 选型:三个问题定方向
给项目选 runtime,我建议先回答这三个问题:
你的工具是可信代码还是不可信代码?如果工具都是你自己团队写的,一个子进程级 runtime 就够了,没必要为沙箱隔离付出太多性能代价。如果要执行外部输入,直接往容器级隔离走。
你的工具主要是函数调用还是自由代码执行?函数调用(比如调 Slack API、查数据库)对 runtime 的要求很轻,重点在协议解析和错误处理。自由代码执行(比如用户上传一段 Python 脚本让 Agent 跑)对 runtime 的要求重得多,重点在隔离、资源限制和超时控制。
你的延迟敏感度有多高?每一步工具调用多花 2 秒,在离线分析场景里没人管,在客服对话场景里用户早就流失了。先测一下 runtime 的典型工具调用延迟,再决定要不要上重隔离方案。
7.3 Harness 选型:也看三个问题
select harness 的时候也问自己三个问题:
策略的复杂度有多高?如果只是“列出工具清单,全自动执行”,随便一个轻量 harness 都够。但如果你需要分级审批、敏感操作拦截、审计日志、按用户维度给不同的工具可见性,那 harness 的策略扩展能力就是最重要的选型指标。
观测系统是不是已有的基建?Harness 把 trace 往哪里送?是只能送它自家的仪表盘,还是能对接你现有的日志和监控体系?在你已经有一套成熟的可观测平台的情况下,接不通永远是硬伤。
社区和插件生态活跃度怎么样?Runtime 像是标准零件,Harness 像是整个驾驶舱的设计。驾驶舱好不好用,很大程度上看你有没有足够多的“仪表”可以选——预置工具、连接器、模板。生态活跃的 harness 能帮你省掉大量从零写装配逻辑的时间。
7.4 一个终极自检方法
你选完技术栈之后,可以用这个方法来验证你的分层是否合理:如果明天我要把 runtime 从 A 换成 B,我的 harness 层代码需要改多少?如果答案是“需要改工具注册格式、改消息协议、改提示词模板”,说明框架把两层揉得太紧了,你的可替换性堪忧。如果答案是“只需要改一个适配器配置”,说明你选对了。
反过来也一样:如果明天要换一个 harness 实现,我的 runtime 能否继续在用?能,说明你的 runtime 层足够通用和标准。
这两个问题看着简单,但很少有人在项目启动前问自己。我见过太多团队在项目中期想换执行环境,结果发现 harness 层写满了对特定 runtime 的强依赖,最后只能推倒重来。
最后说几句题外话
回到开头那行报错。如果你现在再看到类似的信息,应该能条件反射地意识到:这是一个 harness 在启动时找不到或加载不了 runtime 的典型信号,不是“框架坏了”,也不是“大模型配置错了”,而是位于中间这一层插件发现机制出了问题。顺着这个思路去查,多半能快速定位。
我自己踩过几次这个坑之后,养成了一个习惯:拿到任何 Agent 相关的报错,先问一句“这行报错是哪个组件发出的?”如果报错里带了组件名,就先搞清楚这个组件在整个链路里的位置——它在 harness 上面,在 harness 里面,还是在 runtime 里。很多时候,答案就藏在报错信息的主语里。