
当真正开始把多个 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: Recordstring, 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: Recordstring, string; timeoutMs: number; } export interface ExecResult { agentName: string; exitCode: number; stdout: string; stderr: string; timedOut: boolean; durationMs: number; } export function runSubagent(options: ExecOptions): PromiseExecResult { 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): Promisestring { 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优先于 PATHargs启动参数[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-coderbinary 是否被有效解析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再强行执行一个会失败的 subagentherdr run 重构一个不存在的文件 --force此时如果 codex 子进程返回非 0运行器应保留 stderr 并给出exitCode为 2 的结果。失败场景的验证和成功场景同样重要因为多 Agent 协作的编排器最终要能被报警系统和 CI 准确识别“这次任务挂了”。6. 常见问题与排查链路多 Agent CLI 的部署链路上节点越少问题越少但即便是一个轻量 CLI也有几个反复出现的故障点。下面按问题现象、可能原因、检查命令和处理建议整理成表。问题现象常见原因检查命令处理建议unable to locate the codex cli binaryPATH 未继承或 binary 未安装which codex node -e console.log(process.env.PATH)在配置里显式设置binaryPathspawn 报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 的命令调用抽象成插件协议。例如定义三类 adaptercommand-adapter调用任意本地 CLI 命令http-adapter调用远程 Agent APIcustom-adapter套用用户自己的函数。目前主从模式下的 adapter 只需要实现三件事接收任务上下文、启动执行、返回结果对象。Herdr 的核心代码不需要关心 adapter 内部是 codex 还是自研脚本。想验证这套设计是否合理最好从一个小数据集开始两个 subagent、三个任务、一个隔离目录。跑通之后再加入并发、重试和共享记忆。“多 Agent 协作”听着抽象但它的工程形态其实很具体就是进程、文件、配置和结构化结果的管理。只要把子任务的输入输出看成协议把每一个 subagent 当成一个可被调度的执行节点编排器的复杂度就能一直保持在可控范围内。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。