当真正开始把多个 Agent 放进同一条工作流时,最先遇到的问题往往不是模型能力,而是编排方式。Herdr 这类“多 Agent 协作轻量 CLI”所处理的,正是本机或小团队场景下的 Agent 任务分发问题:发起方拿到一个任务,判断该交给哪个 subagent,执行后取回结果,再决定是否进入下一轮。围绕 Herdr 这个名字,很多尝试过自建 Agent 工作流的开发者会自然想到一种设计判断:最新的多 Agent 架构里,主从模式越来越常见,本质上是把 subagent 当成另一种形式的 tool 调用来处理,而不是让多个 Agent 自由对话。
这篇文章按一个最小可运行的 Herdr 编排器来展开,覆盖项目结构、任务路由、subagent 的 Tool Context 封装、本地 Agent CLI 的二进制定位,以及最常见的unable to locate the codex cli binary类错误排查。适合正在尝试把 Codex CLI、Claude Code、OpenCode 或自研 Agent 脚本接入统一工作流的开发者,也适合刚接触多 Agent 编排、想先搭一个轻量原型的读者。整篇文章会带着你从命令入口一直调试到 subagent 输出结果,并解释每一步为什么这样设计。
1. 先理解主从模式为什么更适合作轻量编排
多 Agent 协作并不是只有一种组织方式。在工程上,常见的做法可以分为两类:一类是多个 Agent 处于平等关系,通过互相发消息完成任务;另一类是主从模式,由主 Agent 负责拆解、调度和汇总,subagent 只执行被分配的子任务。Herdr 这类 CLI 工具通常采用后者,并且会把 subagent 的调用方式设计得和调用一个工具函数非常接近。
1.1 从“Agent 对话”到“Agent 任务分发”的转变
多个 Agent 如果采用自由对话,最大的问题是流程不可控。A Agent 抛出问题,B Agent 可能会反问,C Agent 可能半路插话,主任务上下文很快就会被无关信息淹没。解决这个问题不是靠更好的提示词,而是靠改变数据流方向。
主从模式把协作变成一种树状调用:主 Agent 保留最终目标,每个 subagent 只接收一个边界明确的子任务,执行完后把结构化结果返回给主 Agent。这样设计的好处很直接:
- 每个 subagent 的输入输出都可以被校验;
- 单步失败可以被捕获,不影响整条流程;
- 日志里能看到任务从哪个节点分发到哪个 subagent;
- 撤掉或替换某个 subagent,不需要改动其他 Agent 的逻辑。
Herdr 作为 CLI 工具,天然适合这种结构。CLI 进程是按命令触发、按退出码上报、按标准输出反馈结果的,这比在 WebSocket 上维护一段多 Agent 长连接要容易得多。
1.2 subagent 本质上是一种特殊的 tool
很多第一次接触多 Agent 编排的人会问:subagent 和 tool 有什么区别?如果只是按“被调用、执行、返回结果”这个链路看,它们几乎一样。区别只在于调用成本。tool 通常是一个函数或一个 API,执行时间短、参数固定、返回结构固定;subagent 则是一个完整的大模型推理过程,可能还要操作文件、运行命令、读取环境信息,执行时间更长,结果也更不确定。
但作为编排器,你可以把两者统一成一件事:配置一个可执行入口,传入一段任务描述,拿到一个结果对象。
下面是 Herdr 内部要维护的一个核心抽象:
interface ToolContext { type: 'tool' | 'subagent'; name: string; command: string; args?: string[]; maxTurns?: number; cwd: string; } interface SubagentResult { status: 'ok' | 'failed' | 'timeout' | 'skip'; summary: string; detail: string; durationMs: number; exitCode: number; }主 Agent 不关心codex到底是一个模型助手还是一个命令行脚本。它只需要知道:这个可执行入口叫什么、在哪个目录工作、能接收什么命令、返回什么格式。这样做的收益是,后续接入一个本地 shell 脚本 Agent 和接入 Codex CLI 的方式完全一致。
1.3 轻量 CLI 与重框架的分界线
如果项目里需要灰度权限、多人同时使用、长任务断点续跑、模型统一网关,那么选择 Dify、LangGraph、CrewAI 这类完整框架是合理的。但如果你只是在一个仓库里跑代码分析、在 CI 里生成 commit message、在开发机上批量重构文件,引入一个需要部署服务端的框架会明显过重。
Herdr 的定位是:
- 单进程启动;
- 不依赖外部数据库;
- 读写本机文件和工作目录;
- 用 YAML 或 JSON 描述 Agent 能力;
- 通过 spawn 进程调用各种 CLI Agent。
满足这些条件的场景,使用轻量 CLI 会更直接。它不需要额外暴露 HTTP 服务,也没有 Agent 间消息路由的延迟,任务走完一个进程就结束。对于本机工具链来说,简单本身就是可靠性。
2. 把编排模型落成目录和配置文件
主从模式的轻量编排可以拆成三部分:Agent 注册表、任务路由、subagent 执行协议。注册表告诉 Herdr“有哪些 Agent 可用、怎么启动、擅长什么”;路由负责把主任务切成子任务并找到合适的 Agent;执行协议则规定输入输出的组织方式。
2.1 最小编排模型
为了让一个编排器能在多台机器上复用,需要先确定配置分层。Herdr 里可以设计三层:
- 全局配置:日志级别、默认工作目录、默认输出格式;
- Agent 注册表:哪些 subagent 可被调用,命令是什么;
- 任务定义:一个主任务如何被拆分为多个 subagent 调用。
三层互不混淆。修改某个 Agent 的命令不需要修改任务代码,增加任务也不需要改动 Agent 注册表。
全局配置使用config.yaml或环境变量均可,示例配置文件如下:
# herdr.yaml logLevel: info defaultWorkdir: ./workspace output: structured subagentTimeoutMs: 120000 # 编排失败后是否允许主 Agent 基于已有结果自行重试 allowRecovery: false这里需要解释两个参数:subagentTimeoutMs控制 spawn 出去的 subagent 进程最长运行时间。时间过短会导致模型还没跑完就被杀掉,过长会让问题任务挂住整个 CLI。allowRecovery控制失败后是否触发第二轮主 Agent 决策。学习环境可以打开,生产环境建议关闭,否则容易产生无意义的费用和运行时间。
2.2 Agent 注册表结构
每一个可调用的 subagent 在注册表中都是一条记录,包含名称、命令、能力标签、工作目录和可选的环境变量。一个最简单的agents.json如下:
{ "agents": [ { "name": "codex-coder", "binary": "codex", "args": ["exec", "--json"], "capabilities": ["code-refactor", "code-review", "unit-test"], "workdir": "${task.workdir}", "env": { "CODEX_MODEL": "gpt-5-codex", "RUN_MODE": "agent" } }, { "name": "doc-writer", "binary": "doc-agent", "args": ["run"], "capabilities": ["write-doc", "changelog"], "workdir": "${task.workdir}", "env": {} } ] }这里要特别小心一个陷阱:不要把工作目录写死。多 Agent 协作最常见的问题就是多个 subagent 共用一个目录,后写入的内容覆盖前一个 Agent 的结果。更稳妥的做法是按任务 ID 生成隔离目录:
const taskWorkdir = path.join( process.cwd(), 'workspace', taskId, agentName );注册表中用${task.workdir}只是占位,实际启动前需要替换成任务实例的真实路径。
2.3 任务分发方式:对话式与 tool 式
同样是“让 subagent 写一份变更日志”,两种设计有本质差别。
对话式分发会把主 Agent 的完整上下文甚至一大堆历史记录发给 subagent。这虽然让 subagent“更懂背景”,但也引入了两个问题:多余的上下文占用模型输入窗口,且不同 subagent 可能基于同一段历史得出互相矛盾的判断。tool 式分发则只传入必要字段:
{ "taskId": "task_20250415_001", "agent": "doc-writer", "input": { "goal": "根据下列 commit 信息生成 CHANGELOG.md", "scope": "./src/commands", "constraints": ["不要修改非 markdown 文件"] } }subagent 拿到的信息是收窄过的,正好够执行任务。主 Agent 在汇总时会把多个 subagent 的输出拼接成结构化报告。这种模式牺牲了部分灵活性,但换来了可观测性和可恢复性。
在实际 CLI 里,主 Agent 可以按两种方式决定路由:一是根据能力标签精确选择,二是通过大模型做一次轻量意图判断。下面是一份对比表:
| 路由方式 | 适用条件 | 优点 | 风险 |
|---|---|---|---|
| 标签匹配 | 子任务可以直接映射到 Agent 能力 | 执行快、可确定、易排查 | 需要上游有拆解步骤 |
| 模型判断 | 任务边界不清晰,需要判断意图 | 适应自然语言描述 | 选错 Agent、多一轮模型消耗 |
| 规则+模型结合 | 先按 workflow 配置顺序执行 | 可回退、可解释 | 配置复杂度升高 |
Herdr 的最小版本建议先实现标签匹配,把模型判断留到第二版。
3. 用 Node.js 实现 Herdr 的 CLI 核心
考虑到 spawn 子进程的便利性,以及 JSON 配置解析的原生支持,Herdr 可以用 Node.js 的 TypeScript 编写。下面按命令入口、注册表加载、任务执行器、结果汇总四步来实现。
3.1 搭建项目结构和命令入口
项目结构如下:
herdr/ ├── package.json ├── tsconfig.json ├── herdr.yaml ├── agents.json └── src/ ├── index.ts ├── registry.ts ├── runner.ts ├── resolver.ts └── logger.tspackage.json至少需要这些依赖:
{ "name": "herdr-cli", "version": "0.1.0", "type": "module", "bin": { "herdr": "./dist/index.js" }, "dependencies": { "commander": "^12.0.0", "yaml": "^2.4.0", "zod": "^3.22.0" }, "devDependencies": { "typescript": "^5.4.0", "@types/node": "^20.11.0" } }入口index.ts注册两个命令,一个用于直接执行任务,一个用于检查注册表:
#!/usr/bin/env node import { Command } from 'commander'; import { registry } from './registry.js'; import { runTask } from './runner.js'; const program = new Command(); program .name('herdr') .description('多 Agent 协作轻量 CLI') .version('0.1.0'); program .command('run') .description('运行一个主任务') .argument('<task>', '任务描述') .option('-a, --agents <agents>', '仅使用指定的 subagent,逗号分隔') .option('-w, --workdir <dir>', '任务工作目录', './workspace') .action(async (task, options) => { const agents = await registry.load(); await runTask(task, agents, options); }); program .command('list') .description('列出可用的 subagent') .action(async () => { const agents = await registry.load(); for (const agent of agents) { console.log(`${agent.name}\t${agent.capabilities.join(',')}`); } }); program.parseAsync(process.argv);这样设计命令入口的目的是把“任务描述”和“Agent 能力注册”解耦。herdr run是实际干活的人,herdr list是做运维检查的人,两者只共享同一个agents.json。
3.2 加载并校验注册表
不能信任配置文件一定是合法的。加载注册表时至少要校验二进制名、能力列表和工作目录是否存在。
import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { z } from 'zod'; const AgentSchema = z.object({ name: z.string(), binary: z.string().min(1), args: z.array(z.string()).default([]), capabilities: z.array(z.string()).default([]), workdir: z.string().default('.'), env: z.record(z.string()).default({}) }); const ConfigSchema = z.object({ agents: z.array(AgentSchema) }); export async function loadRegistry() { const raw = await readFile(path.resolve('agents.json'), 'utf-8'); const parsed = ConfigSchema.parse(JSON.parse(raw)); return parsed.agents; }使用 Zod 校验在项目早期可能显得多余,但当 subagent 数量超过 5 个、配置由不同人维护时,字段缺失会直接导致模糊错误。提前校验比在 spawn 阶段报错更容易定位。
3.3 按任务描述选择 subagent
最朴素的路由是根据关键词与 capability 做包含匹配。例如任务里出现“重构”就命中code-refactor,出现“文档”“CHANGELOG”就命中write-doc。关键词映射可以放在单独路由文件里:
const taskAgentKeywords: Record<string, string[]> = { 'code-refactor': ['重构', 'refactor', '优化代码', '拆分文件'], 'code-review': ['审查', 'review', '检查代码', '找 bug'], 'write-doc': ['文档', 'changelog', 'readme', '使用说明'] };匹配时按顺序执行:
- 把任务描述拆成小写字符串;
- 遍历关键词表,记录所有命中的能力;
- 如果命中多个能力,返回多个 Agent 候选;
- 如果没有任何命中,抛出“未找到匹配 Agent”的错误。
例如命令:
herdr run "请重构 src/commands/index.ts,并补充 README 文档"命中两个能力,调度结果可能如下:
{ "plan": [ { "agent": "codex-coder", "goal": "重构 src/commands/index.ts" }, { "agent": "doc-writer", "goal": "补充 README 文档" } ] }主 Agent 会把这两个调用按序执行还是并行执行,取决于配置。若两个任务没有依赖关系,可以并发;若文档需要参考重构后的代码,则必须串行。
3.4 以 Tool Context 方式启动 subagent 进程
执行器是整个 Herdr 最核心的文件。它负责把注册表中的 Agent 描述翻译成 Node.js 的child_process.spawn调用,并收集 stdout、stderr 和退出码。
import { spawn } from 'node:child_process'; import { EventEmitter } from 'node:events'; import path from 'node:path'; export interface ExecOptions { agentName: string; binaryPath: string; args: string[]; cwd: string; env: Record<string, string>; timeoutMs: number; } export interface ExecResult { agentName: string; exitCode: number; stdout: string; stderr: string; timedOut: boolean; durationMs: number; } export function runSubagent(options: ExecOptions): Promise<ExecResult> { return new Promise((resolve, reject) => { const start = Date.now(); const child = spawn( options.binaryPath, options.args, { cwd: options.cwd, env: { ...process.env, ...options.env }, shell: false, stdio: ['pipe', 'pipe', 'pipe'] } ); let stdout = ''; let stderr = ''; let settled = false; const timer = setTimeout(() => { if (!settled) { child.kill('SIGKILL'); resolve({ agentName: options.agentName, exitCode: -1, stdout, stderr: `${stderr}\n[herdr] subagent timed out after ${options.timeoutMs}ms`, timedOut: true, durationMs: Date.now() - start }); settled = true; } }, options.timeoutMs); child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', (err) => { if (!settled) { clearTimeout(timer); settled = true; reject(new Error(`${options.agentName} 启动失败: ${err.message}`)); } }); child.on('close', (code) => { if (!settled) { clearTimeout(timer); settled = true; resolve({ agentName: options.agentName, exitCode: code ?? -1, stdout, stderr, timedOut: false, durationMs: Date.now() - start }); } }); }); }这里有一个容易被忽略的细节:spawn的shell参数必须保持false。如果设置为true,二进制名里一旦包含空格或特殊字符,整个命令会被拼进 shell 再执行,容易出现注入和转义问题。生产环境建议显式传入绝对路径的 binary,而不是仅仅传一个名字。
但传入绝对路径前,还需要处理“在 PATH 里找不到 CLI binary”的问题。这部分单独在下一章展开。
3.5 整合最小运行主函数
把加载配置、解析任务、执行 subagent 串起来的 runner 如下:
import path from 'node:path'; import { runSubagent } from './runner.js'; import { loadRegistry } from './registry.js'; import { planTask } from './router.js'; import fs from 'node:fs/promises'; export async function runTask(task: string, agents: AgentConfig[], options: any) { const plan = planTask(task, agents); if (plan.length === 0) { console.error('[herdr] 未找到能处理该任务的 subagent'); process.exit(1); } const taskId = `task_${Date.now()}`; const workdir = path.resolve(options.workdir); await fs.mkdir(workdir, { recursive: true }); const results = []; for (const step of plan) { const agent = agents.find((a) => a.name === step.agent)!; const stepDir = path.join(workdir, taskId, agent.name); await fs.mkdir(stepDir, { recursive: true }); console.log(`[herdr] 启动 subagent: ${agent.name}`); const result = await runSubagent({ agentName: agent.name, binaryPath: await resolveBinary(agent.binary), args: [...agent.args, step.goal], cwd: stepDir, env: agent.env, timeoutMs: options.timeoutMs ?? 120000 }); results.push({ step, result }); if (result.exitCode !== 0 && !options.force) { console.error(`[herdr] subagent ${agent.name} 执行失败`); console.error(result.stderr); process.exit(2); } } console.log(JSON.stringify({ taskId, results }, null, 2)); }这个版本是单线程串行执行,便于理解。真实项目可以把没有依赖关系的 step 合并并发执行,但并发会带来输出写冲突和资源占用,通常需要额外设计。
4. 本地 Agent CLI 找不到 binary 的问题与解决路径
当 Herdr 去调用 Codex CLI、Claude Code、OpenCode 这类本地 Agent 时,最常见也最让人困惑的错误是一串相似文案:
failed to start. unable to locate the codex cli binary. set codex_cli_path or ensure the electron resources include bin/codex.很多人第一反应是“明明终端里能运行codex,为什么程序里找不到?”这背后本质上是进程环境的 PATH 与终端环境不一致。GUI 应用或某些 Electron 宿主启动外部 CLI 时,不会完整继承用户 shell 里配置的 PATH,导致它找不到codex可执行文件。这类框架把 Agent CLI 作为子进程 spawn 时,就必须自己做二进制路径解析。
4.1 常见调用模式中的 PATH 丢失
终端里执行命令时,Shell 启动文件(如.zshrc、.bashrc)会先把 Node、Homebrew、CLI 安装目录写入 PATH。由 GUI 应用、后台服务、任务调度器或某些运行宿主启动的进程则不经过这些启动文件,process.env.PATH里可能只有系统默认路径。
下面两种模式经常触发该问题:
- 在桌面应用或 Electron 宿主里点按钮调用
codex exec; - 在 systemd 服务、launchd、CI Runner 里以非交互登录方式执行 Agent 编排。
现象一致:程序进程能启动,但 spawncodex时子上报ENOENT,或者 Agent 客户端只提示无法定位 CLI binary。
4.2 三级二进制解析顺序
在设计 Herdr 的resolveBinary时,推荐按以下优先级解析:
- 配置文件或环境变量中显式指定的完整路径;
- 项目目录下的
.bin或内置目录; - 操作系统的 PATH 环境变量。
import { existsSync } from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; function isExecutable(filePath: string): boolean { try { return existsSync(filePath); } catch { return false; } } export async function resolveBinary(agentBinary: string, explicitPath?: string): Promise<string> { if (explicitPath) { const p = path.resolve(explicitPath); if (isExecutable(p)) { return p; } throw new Error(`显式指定的 binary 路径不存在: ${p}`); } if (path.isAbsolute(agentBinary)) { if (isExecutable(agentBinary)) { return agentBinary; } throw new Error(`binary 不是绝对路径且不存在: ${agentBinary}`); } const envPath = process.env.PATH ?? ''; const suffixes = process.platform === 'win32' ? ['', '.cmd', '.exe'] : ['']; for (const dir of envPath.split(path.delimiter)) { for (const suffix of suffixes) { const candidate = path.join(dir, agentBinary + suffix); if (isExecutable(candidate)) { return candidate; } } } throw new Error( `无法定位 ${agentBinary} 可执行文件。请在配置中设置 binaryPath,或将其安装目录加入 PATH。` ); }如果你在 Electron 或宿主程序里复现这个问题,还需要补充一个步骤:定位当前用户 shell 的真实登录路径,例如在 macOS/Linux 上执行:
$(which zsh || which bash) -lic 'echo $PATH'然后把输出的 PATH 合并进 subagent 的进程环境。这个方案能解决“终端能跑、程序找不到”的很大一部分问题。
4.3 在配置中显式指定 Agent 路径
与其依赖运行时猜路径,更稳妥的兼容做法是在agents.json里按环境显式写入完整路径:
{ "agents": [ { "name": "codex-coder", "binary": "codex", "binaryPath": { "darwin": "/opt/homebrew/bin/codex", "linux": "/usr/local/bin/codex", "win32": "C:\\Users\\dev\\AppData\\Roaming\\npm\\codex.cmd" } } ] }配置文件里没有给定时,resolveBinary才回退到 PATH 搜索。这个顺序的优先级需要写清楚,否则用户在配置里设置了错误路径后,会发现无论如何修改 PATH 都不生效。
下面是一张常见配置项速查表:
| 配置项 | 含义 | 常见值 | 修改后影响 |
|---|---|---|---|
binary | 逻辑命令名 | codex | 影响搜索命令 |
binaryPath | 绝对路径覆盖 | /opt/homebrew/bin/codex | 优先于 PATH |
args | 启动参数 | ["exec", "--json"] | 影响调用协议 |
env | 追加环境变量 | {"CODEX_MODEL":"gpt-5-codex"} | 影响模型和运行模式 |
workdir | 子进程工作目录 | ./workspace/taskId | 决定文件读写范围 |
timeoutMs | 超时上限 | 120000 | 影响长任务稳定性 |
这些参数相互独立,排查时不要一起改。先固定 binary 路径,确认能正常启动,再调整 args 和 env。
4.4 在 Electron 宿主中集成外部 CLI 时的注意点
搜索材料里反复出现的chatgpt failed to start. unable to locate the codex cli binary一类错误,很多时候并不是 Herdr 这类编排器的问题,而是宿主应用在打包时没有把 CLI 二进制打进electron resources/bin/,或者桌面应用启动时无法继承开发终端的 PATH。
从编排器的角度,处理这类外部依赖要遵守一个原则:不要把某个 CLI Agent 的安装目录假设成一定存在。每启动一个 subagent 前都先执行一次二进制探测,探测失败时给出明确的配置引导,而不是等到 stdout 输出 undefined 或直接抛 ENOENT。
try { const bin = await resolveBinary('codex', config.binaryPath?.[process.platform]); console.log(`[herdr] 使用 codex binary: ${bin}`); } catch (err) { console.error('[herdr] Codex CLI 未找到,请安装 codex 或在 agents.json 中配置 binaryPath'); process.exit(1); }把探测和真实任务执行拆开,是很多 CLI 编排工具容易忽略的健壮性细节。
5. 运行验证与结果观察
只写代码不验证,多 Agent 协作的可靠性无从谈起。下面用三个场景验证 Herdr 是否真的按照“主 Agent 路由 -> subagent 执行 -> 结果汇总”的路径工作。
5.1 场景一:单 Agent 执行代码重构
假设注册表里只有一个codex-coder,执行:
herdr run "请重构 src/commands/index.ts" --workdir ./demo预期输出类似:
[herdr] 启动 subagent: codex-coder [herdr] 使用 codex binary: /opt/homebrew/bin/codex [herdr] subagent codex-coder 退出码: 0 { "taskId": "task_1710000000000", "results": [ { "step": { "agent": "codex-coder" }, "result": { "exitCode": 0, "stdout": "reconstruction complete", "stderr": "", "durationMs": 8300 } } ] }这一步验证的不只是“能跑”,还包括:
- 任务是否被正确路由到 codex-coder;
- binary 是否被有效解析;
- subagent 的退出码是否为 0;
- 结果是否以 JSON 形式汇总。
如果目标是文件写入,还需要检查demo/task_.../codex-coder/目录下是否生成了新文件。不要用“终端没有报错”代替结果检查。
5.2 场景二:多个 subagent 按顺序协作
执行一个需要两个 Agent 的任务:
herdr run "重构 src/commands/index.ts,并基于新代码补充 README 文档" --workdir ./demo如果顺序设计是“先重构,再写文档”,那么第二个 Agent 必须能看到第一个 Agent 的输出。实际工程里,主 Agent 可以把重构结果传给文档 Agent,例如在输入中指定新的 API 名称或函数签名。顺序执行时要注意:
- 两个 Agent 不能同时写同一个文件;
- 第二个 Agent 的工作目录应该独立;
- 第一个 Agent 的 stdout 需要被截断或摘要后再作为上下文,避免上下文膨胀。
5.3 场景三:subagent 失败时的表现
让一个不存在的任务流入:
herdr run "这个任务没有对应 agent"如果路由没命中,进程应该明确退出,并返回非 0 退出码:
[herdr] 未找到能处理该任务的 subagent再强行执行一个会失败的 subagent:
herdr run "重构一个不存在的文件" --force此时如果 codex 子进程返回非 0,运行器应保留 stderr 并给出exitCode为 2 的结果。失败场景的验证和成功场景同样重要,因为多 Agent 协作的编排器最终要能被报警系统和 CI 准确识别“这次任务挂了”。
6. 常见问题与排查链路
多 Agent CLI 的部署链路上,节点越少,问题越少,但即便是一个轻量 CLI,也有几个反复出现的故障点。下面按问题现象、可能原因、检查命令和处理建议整理成表。
| 问题现象 | 常见原因 | 检查命令 | 处理建议 |
|---|---|---|---|
unable to locate the codex cli binary | PATH 未继承或 binary 未安装 | which codex && node -e "console.log(process.env.PATH)" | 在配置里显式设置binaryPath |
spawn 报ENOENT | 二进制路径不存在或没有执行权限 | ls -l $(which codex) | 确认安装目录,检查文件权限 |
| subagent 输出为空但退出码为 0 | 交互式命令在非 TTY 环境下未执行 | 查看完整 stderr | 换用exec等非交互子命令 |
| 多个 subagent 写入相同文件 | 工作目录未隔离 | 查看工作目录的目录层级 | 按 taskId/agentName 隔离工作目录 |
| 配置修改后不生效 | 缓存或加载了错误的配置文件 | herdr list查看注册内容 | 清掉缓存并确认加载路径 |
| 超时但模型仍在运行 | timeoutMs设置过短 | 检查 durationMs | 调大超时并评估是否需要并行 |
6.1 排查“unable to locate codex cli binary”的正确顺序
当错误来自某个宿主应用而不是 Herdr 时,先不要急着改全局 PATH。按下面顺序排查:
- 在终端里执行
which codex,确认 codex 已安装; - 执行
codex --version,确认能正常运行; - 执行
echo $PATH,记录 codex 所在目录是否在 PATH 中; - 检查宿主应用的环境变量,确认进程是否继承 PATH;
- 若宿主应用无法继承 PATH,把 codex 所在目录写入应用的启动环境或设置
binaryPath/codex_cli_path; - 重启应用,重新触发任务。
这个顺序是从“输入是否正确”到“工具本身是否可用”的完整链路。跳过步骤 1 直接去改配置,很可能白改。
6.2 subagent 输出不可解析的常见原因
很多 Agent CLI 会区分“输出型文案”和“过程日志”。如果 stdout 里既有滚动日志又有最终 JSON,进程退出码可能是 0,但结果难以解析。有两种处理方式:
- 在 subagent 启动参数中要求机器可读输出,如
--json、--output-format json; - 在主 Agent 汇总前,用正则或边界标识从 stdout 中截取最终结果块。
不要在代码里假定 stdout 一定是纯 JSON。真实进程几乎不会只输出一个 JSON,除非你在 subagent 端自己封装一层只透传 JSON 的适配器。最稳妥的方法是让 subagent 把最终结果写入独立文件,例如output.json,主 Agent 读取该文件而不是解析 stdout。
6.3 关于“某些 Agent 桌面版无法粘贴文字”之类的现象
如果是在桌面版宿主中人工使用某个 Agent CLI,且出现无法粘贴文字、无法定位 CLI binary,这些往往不是编排器代码问题,而是宿主应用把外部 CLI 作为打包资源时的路径配置问题。虽然这些报错看起来像开发问题,但成因与上面 PATH 丢失一致。工程上建议所有 CLI Agent 都安装到统一管理目录,如~/.local/bin、/opt/homebrew/bin,并在编排配置里统一指向该目录,避免每个 Agent 安装位置不同导致排查范围发散。
注意:不要在非交互式子进程中模拟人工粘贴操作。CLI Agent 如果提供
exec、run、--prompt这类非交互参数,应优选用它们,而不是通过标准输入模拟键盘事件。模拟粘贴在 CI 环境往往会失败。
7. 生产环境使用建议与扩展方向
从最小可运行原型到生产可用,中间还需要补齐安全、日志、权限、可观测性和失败恢复机制。这一节给出几条可执行的建议,以及 Herdr 后续可以扩展的方向。
7.1 生产环境发布前检查清单
- [ ] 所有 subagent 在配置中用绝对路径解析 binary,或至少探测失败能给出明确提示;
- [ ] 每个 subagent 使用独立工作目录,目录按 taskId 和 agentName 隔离;
- [ ]
timeoutMs按任务类型分别配置,不统一套用默认值; - [ ] 日志记录每次启动、退出码、耗时、stdout 长度,不记录完整 API key 和环境变量;
- [ ] 执行前对 agents.json 做 schema 校验,避免字段缺失导致运行时崩溃;
- [ ] 对 subagent 的
env做白名单管理,不把宿主的全部敏感环境变量透传出去; - [ ] 准备一个失败回调,退出码非 0 时能推送通知或生成报告;
- [ ] 确定是否允许主 Agent 自动重试,生产环境默认关闭;
- [ ] 明确文件写入边界,避免 subagent 修改仓库之外的目录。
7.2 环境变量与敏感信息隔离
subagent 也是 Agent 进程。给它传入过多权限和环境变量,等于把你的全部密钥暴露给一段长文本模型控制的工具链。推荐做法是在 agents.json 中只透传执行所需的最小集合:
{ "env": { "OPENAI_API_KEY": "${env.OPENAI_API_KEY}" } }启动时只从宿主读取白名单变量,其他变量不要合并进子进程环境。
7.3 从 Tool Context 到共享记忆
热词里反复出现“多 Agent 共享记忆”,这是一个自然的下一步演进方向。Herdr 可以先实现最小共享记忆:所有 subagent 的结果统一写入指定目录,下一个 subagent 通过读目录得到前序 Agent 的结论文件,而不是重新执行一次。这种方式比把全部历史塞进 Prompt 更可靠,也更节省模型输入窗口。
workspace/ └── task_1710000000000/ ├── shared/ │ ├── decisions.json │ └── final-report.md ├── codex-coder/ │ └── output.json └── doc-writer/ └── output.json主 Agent 只负责在合适时机把shared/中的结论与 subagent 的输出做对比,判断是否进入下一轮。这比“所有 Agent 自由读同一份大上下文”更接近工程可维护的边界。
7.4 扩展为插件协议
如果后续要支持更多 Agent 类型,可以把每个 subagent 的命令调用抽象成插件协议。例如定义三类 adapter:
command-adapter:调用任意本地 CLI 命令;http-adapter:调用远程 Agent API;custom-adapter:套用用户自己的函数。
目前主从模式下的 adapter 只需要实现三件事:接收任务上下文、启动执行、返回结果对象。Herdr 的核心代码不需要关心 adapter 内部是 codex 还是自研脚本。
想验证这套设计是否合理,最好从一个小数据集开始:两个 subagent、三个任务、一个隔离目录。跑通之后再加入并发、重试和共享记忆。“多 Agent 协作”听着抽象,但它的工程形态其实很具体,就是进程、文件、配置和结构化结果的管理。只要把子任务的输入输出看成协议,把每一个 subagent 当成一个可被调度的执行节点,编排器的复杂度就能一直保持在可控范围内。