最近身边的团队聊 Coding Agent 的越来越多,从 OpenAI 的 Codex 到各类开源方案,大家都在试着把写代码这件事交给“一个能自己动手的助手”。但真把 Agent 用起来之后你会发现,单个 Agent 好用不代表多个 Agent 好用,尤其在同一个项目里同时跑两三个 Agent,它们可能改同一个文件、抢同一份环境变量、往同一个终端里吐日志,整个状态非常混乱。于是就有了 Herdr 这类专门做“多 Coding Agent 管理”的工具。
Herdr 的定位可以理解成一个调度器加运行时管理器:给每个 Coding Agent 分配独立的工作空间和终端会话,通过 PTY(伪终端)做底层状态管理,让多个 Agent 能并行干活又不互相踩脚。这篇文章我会顺着“为什么要管多个 Agent → PTY 在其中扮演什么角色 → 终端运行时怎么落实 → 实际配置与排障”这条线,把动手折腾 Herdr 的过程和踩坑记录都写出来。适合已经在用 Codex、pi coding agent 这类工具、希望在项目中并行引入多个 Agent 的开发者,也适合还没用上 Herdr、但被多 Agent 并发问题困扰的朋友做个参考。
1. 多 Agent 协作到底难在哪——为什么需要 Herdr 这种工具
1.1 Coding Agent 的形态演变:从单助手到多协作
早期大家用的 Coding Agent,本质上是把一个 LLM 包装成能操作代码库的工具链。你给它一个 issue,它自己读代码、改文件、跑测试、提 PR,整个过程像是一个“远程实习生”。这个阶段,你只需要关心一个 Agent 的表现,给它喂任务、等结果、检查输出就完了。
但真实工程项目里,单 Agent 的瓶颈很明显。一个稍微大点的需求往往要跨模块修改,Agent 在单个上下文窗口里能看到的内容有限,改到后面经常出现“前面改了 A 文件,后面又按旧逻辑改 B 文件”的情况。于是大家开始尝试拆分任务:前端模块一个 Agent,后端模块一个 Agent,测试补全再雇一个 Agent,希望它们并行推进。想法很好,可一旦真这么干,问题马上就来了。
这时候就轮到 Herdr 这类工具登场。它做的事情不是“再做一个更强的 Agent”,而是把多个 Agent 当成多个可编排的“员工”来管理——给每个员工单独的工位、单独的工具箱、单独的任务清单,再通过一个统一的控制台观察它们各自在干什么。这个定位听起来简单,但真要在工程上落地,涉及的东西比想象中多得多。
1.2 多个 Agent 同时干活时的四大混乱现场
我在实际项目里踩过的坑,基本可以归成下面四类:
文件冲突。两个 Agent 同时 clone 同一个仓库,各自基于自己的理解去改同一个文件。等两边都提交的时候,后提交的那一个直接覆盖掉前面 Agent 的修改,代码丢失连 git diff 都救不回来。即使在同一个分支上,前面 Agent 刚重构完的接口,后面 Agent 还在用旧接口调,编译直接挂。
环境变量与端口冲突。多个 Agent 要跑测试,全默认监听 8000 端口,第二个 Agent 一起来就报 Address already in use。更隐蔽的是环境变量污染,一个 Agent 改了
PYTHONPATH,另一个 Agent 的子进程继承了改后的值,明明代码没问题,跑出来的结果就是不对。终端输出互相污染。多个 Agent 的日志同时刷在同一个终端里,命令的回显和 Agent 的思考过程交叉混在一起,根本分不清哪段输出是哪个 Agent 产生的。排查问题的时候,对着混杂的日志发呆半小时是常有的事。
资源竞争与编排丢失。模型推理本身吃 CPU/GPU,多个 Agent 同时推理会导致互相变慢;更麻烦的是任务编排,手动在两个 Agent 之间分配任务,一个 Agent 完成了,另一个 Agent 还在等它的产出,没有人做协调,整个流水线就卡住了。
这四个问题单独看都不难解决,但合在一起就需要一个系统性的方案。Herdr 的答案很直接:既然问题出在“共享”上,那就把每个 Agent 的终端会话、工作目录、进程空间彻底隔离,同时提供一个统一调度层来做管理和观察。而这个隔离方案的技术核心,就是 PTY。
2. PTY 为什么是多 Agent 管理的关键底座
2.1 先搞懂 PTY 到底是什么
PTY 全称是 pseudo-terminal,伪终端。普通终端是一个硬件设备(比如你面前的显示器加键盘),而 PTY 是一对由内核提供的虚拟字符设备,分为 master 端和 slave 端。程序连接到 slave 端时,它看到的是一个“标准终端设备”,可以读输入、写输出、控制光标、配置行列数。而 master 端由另一个程序控制,可以往 slave 端写入数据模拟用户输入,也可以读取 slave 端输出的所有内容。
生活化类比:可以把 PTY 理解成“一间为程序准备的虚拟办公室”。程序(Agent)走进这间办公室,看到的是正常的办公桌、电话、白板——它以为自己在一个真实的环境里工作。但实际上,房间的另一面是单向玻璃,外面的人(Herdr)能看到它的一举一动,还能通过内线电话向房间里喊话,房间里的程序却完全不知道外面坐着谁。
这套机制的核心奥秘在于:很多程序的行为取决于“自己连接的是不是终端”,而 PTY 让 Agent 连接到了一个“看起来是终端但不是真实终端”的地方。这就是为什么 PTY 成了 Agent 运行时的关键底座,而不是简单地用管道把输入输出接起来。
2.2 为什么管理 Coding Agent 离不开 PTY
你可能会有疑问:不用 PTY,直接把 Agent 的标准输出重定向到一个文件,再通过管道喂命令进去,不也能实现“观察和操控”吗?
理论上可以,但实际会碰一鼻子灰。一个很现实的问题是:大量 CLI 工具一旦发现自己不是 TTY,就会切换成非交互模式。比如docker compose up,在非 TTY 下跑,不会输出进度条和颜色;再比如cd、export这类 shell 内建命令,在非交互 shell 里行为也不一样;更麻烦的是那些需要交互确认的工具,在非 TTY 下往往直接报错退出,或者默默以默认选项跑下去。
Coding Agent 的本质恰恰是“一边看命令输出,一边决定下一步做什么”。它需要命令输出尽量完整、格式尽量友好、交互尽量真实。如果输出被截断、进度信息丢失、颜色代码被剥离,Agent 的判断依据就不完整,最后生成的代码质量一定会打折扣。Herdr 选择用 PTY 而不是普通管道,就是为了让每个 Agent 看到的命令行为,和人类开发者在真实终端里看到的几乎一致。
还有一个更关键的点:PTY 天然支持终端尺寸调整(resize)。Agent 在渲染长输出的时候,会根据终端宽度决定是否换行、是否截断。通过 PTY,Herdr 可以主动设置窗口行列数,让 Agent 拿到一个合理的展示空间,这也避免了“输出被折行折得稀碎,Agent 理解错上下文”的情况。实测下来,同样的任务,用 PTY 跑和用管道跑,Agent 的出代码质量确实有明显差别,尤其是涉及多步骤命令、需要观察中间输出的时候。
3. 终端运行时:Herdr 怎么把 Agent 变成可运行、可管理的进程
3.1 终端运行时的三层结构
知道了 PTY 是关键,接下来要理解 Herdr 的“终端运行时”到底是怎么组织的。我在实际使用中把它拆成了三层来看,这样排查问题的时候思路就清晰很多。
第一层是进程管理层。每个 Agent 在 Herdr 里对应一个主进程,以及由它派生的所有子进程。Herdr 会为这个主进程分配独立的进程组和会话,让它的生命周期可以被单独控制。启动、停止、重启,都是针对这一整棵进程树操作的,而不是只杀掉顶层进程。这一点极其重要,后面我会专门讲。
第二层是 PTY 会话层。每个 Agent 的进程连接到一个独立的 PTY slave 端,Herdr 持有对应的 PTY master 端。这一层负责三件事:把键盘输入(或者 Agent 的输入请求)写入 master 端;把 slave 端的所有输出读出来并存储;实时感知终端尺寸变化并同步给 PTY。这一层做得够稳,Agent 的输入输出才能真正“像一块终端”。
第三层是会话恢复层。Herdr 会把每个 PTY 的输出流持久化为日志,同时记录 Agent 的执行状态、工作目录、环境变量快照。这样一来,即使 Agent 进程崩溃了,或者整个 Herdr 重启了,也能根据会话记录恢复现场,重新拉起 Agent 而不丢失上下文。这层对长任务非常有用,跑着跑着断网、机器重启,恢复后还能接着干。
这三层结构合在一起,就是“终端运行时”的含义:它不是为了跑某一个命令,而是为每一个 Agent 提供一个完整的、可观察的、可恢复的运行环境。
3.2 信号、会话和进程组:运行时怎么做到“关掉一个不连累所有”
在普通终端里同时跑多个命令时,按 Ctrl+C 会把 SIGINT 信号发给整个前台进程组。如果两个 Agent 共享一个终端会话,一个 Ctrl+C 会把另一个 Agent 的执行直接打断,代码改到一半进程就没了,非常打击士气。
Herdr 的做法是给每个 Agent 独立的会话和进程组。会话(session)是进程组的上层集合,一个会话里可以有多个进程组,但一个会话通常关联着一个控制终端。通过setsid这个系统调用,Herdr 让每个 Agent 变成一个新的会话首领(session leader),彻底脱离原来那个“共享终端”的进程组。这样一来,外部信号就可以精确投递到目标 Agent,而不会误伤其他 Agent。
实际配置时,Herdr 还可以设置每个 Agent 的停止策略:优雅停止(先发 SIGTERM,等几秒再发 SIGKILL)还是强制停止(直接 SIGKILL)。对 Coding Agent 来说,优雅停止很重要,因为 Agent 可能正在写文件,突然被 SIGKILL 会留下半写状态的临时文件,下次启动直接报错。我会把重启机制配合上,max_restarts设成两三次,防止因为一次偶发信号就挂掉整个任务。
这就解释了为什么 Herdr 叫 “terminal runtime” 而不叫 “terminal launcher”。它管理的不是一个终端命令,而是一整套进程生命周期、信号流转、会话隔离的运行时环境。理解了这一层,再看它的配置项,每个字段的作用就很清楚了。
4. Herdr 的实际使用流程与配置要点
4.1 安装与快速初始化
Herdr 目前官方提供的是二进制分发包,安装方式比较简单:下载对应平台的压缩包,解压后把可执行文件放到PATH里就行,不需要额外的运行时依赖(比如不需要单独装 Node 或 Python 环境,这一点对部署环境比较友好)。Linux 和 macOS 下基本是同样的操作流程,Windows 下的兼容性目前还没完全覆盖,建议在容器或者 WSL 里跑。
安装完先执行一次初始化命令,Herdr 会在用户目录下生成一个默认的配置文件目录,里面包含配置模板和日志目录结构。我建议把配置模板打开看一遍,每个字段都有注释,清楚标明了作用,比自己盲写配置强得多。
一个最小可用的配置模板大概长这样(基于 YAML 格式):
# herdr.yaml runtime: default_shell: /bin/bash log_dir: ~/.herdr/logs agents: - name: codex-worker-1 model: codex command: codex exec cwd: /workspace/project-a env: OPENAI_API_KEY: ${OPENAI_API_KEY} pty: enabled: true rows: 40 cols: 120 restart_policy: max_restarts: 3 - name: pi-agent-runner model: pi command: pi coding cwd: /workspace/project-b env: PI_API_KEY: ${PI_API_KEY}这里有几个字段值得展开讲:
command指定 Agent 启动时的实际命令。如果你是拿 Herdr 管理 Codex,就填codex exec这类子命令;如果你是管理 pi coding agent,就填对应的pi coding。这个命令会在 PTY 里以交互模式启动,所以确保 Agent 支持交互式 shell 的调用方式。cwd是每个 Agent 的工作目录,强烈建议不同 Agent 用不同目录,避免文件冲突。pty.enabled打开后,Herdr 才为这个 Agent 分配伪终端;关闭了就退化成普通管道模式,一般不建议关闭。rows和cols是 PTY 的行列数,默认 40x120 比较接近真实终端,如果你的 Agent 经常输出超长日志,可以把 cols 加大,减少折行。
配置完成后,启动 Herdr 通常是一个前台命令加一个--daemon参数,把运行时放到后台。此时 Herdr 会读取配置,逐个拉起 Agent,并为每个 Agent 创建独立的 PTY 会话。任何 Agent 异常退出,Herdr 都会在日志里记录退出码和最近一段输出,方便排查。
4.2 任务调度与资源控制的实操建议
配置只是第一步,真正用好 Herdr 要从调度和资源控制上下手。
先说并发数。默认情况下,Herdr 会尽量把配置里的所有 Agent 都拉起来,但你的机器可能并不具备同时推理多个模型的条件。我自己的做法是:先看机器配置,如果只有一块普通消费级 GPU,同时跑两个 Agent 的推理已经比较吃力,三个以上基本会互相拖累,响应速度肉眼可见地下降。所以建议在配置里显式设置并发上限,让 Herdr 按队列调度,而不是一次性全拉起。
再说日志策略。每个 Agent 的 PTY 输出都会写到独立日志文件,这个文件如果不做滚动,跑一次长任务可能就几百 MB。Herdr 提供了日志滚动配置,可以按大小或按时间轮转。我会设置max_size: 50MB和max_files: 3,保证磁盘占用可控,同时保留最近几次任务的完整输出用于排查。
最后是任务编排。Herdr 本身不是工作流引擎,它不负责“Agent A 做完后自动通知 Agent B”这种逻辑。但它提供了 API 和事件钩子(webhook),可以在 Agent 状态变化时向外发送通知。我在项目里就是配合外部脚本轮询 Herdr 的状态接口,一旦检测到某个 Agent 完成,再触发下一个 Agent 的任务。这个“外置编排”的做法,比把编排逻辑塞进 Herdr 配置里更灵活,因为任务之间的依赖关系往往和项目本身强相关,放外面更好维护。
5. 常见问题与排查技巧实录
5.1 典型问题的表现与根因分析
用 Herdr 跑了两三个月的多 Agent 并行任务,我积累了一份高频问题清单,每次遇到相似症状,都能快速定位到根因。
| 问题表现 | 可能根因 | 排查思路与解法 |
|---|---|---|
| Agent 启动后立刻退出,日志里只有一段空白 | command配置的 Agent 路径不存在,或者 Agent 检测到非交互模式直接报错退出 | 先手动在终端里跑一遍该命令,确认能正常进入交互模式;再检查pty.enabled是否为 true |
| 两个 Agent 同时改同一个文件,代码互相覆盖 | 工作目录未隔离,多个 Agent 共享cwd | 给每个 Agent 配置独立的cwd,必要时先 git 分支隔离,再让 Agent 各自工作 |
| 端口冲突导致 Agent 测试失败 | 多个 Agent 的测试服务都监听同一默认端口 | 在env里为每个 Agent 注入不同的PORT变量,或者让 Agent 从配置文件读取端口 |
| 一个 Agent 卡死,Ctrl+C 无法停止 | PTY 会话里的进程没有正确响应 SIGTERM,可能有子进程阻塞 | 在配置里调成强制停止策略,先发 SIGTERM 等 5 秒,超时再发 SIGKILL;观察日志确认子进程被清理 |
| 日志文件膨胀,磁盘被占满 | 没有配置日志滚动策略 | 开启max_size和max_files,并对单次长任务的输出量做预估 |
| Agent 的输出和另一个 Agent 的日志混在一起 | 多个 Agent 使用了同一个日志文件或输出流 | 检查 Herdr 配置中每个 Agent 的log_dir,确保独立;不要在命令里强制 `2>&1 |
这个问题清单里,最常见也最隐蔽的是端口冲突。Agent 跑测试时默认监听端口来自框架约定,写死在代码里的情况很多,而 Agent 又不会主动去读其他 Agent 的配置,所以必须靠 Herdr 注入环境变量来强制区分。我在配置里会给每个 Agent 分配一段端口区间,比如codex-worker-1用 8100-8200,pi-agent-runner用 8201-8300,能让冲突概率降低很多。
5.2 实操中的几条独家经验
最后分享几个我在实际使用中摸索出来的小技巧,一般文档里不会写。
第一,给 Agent 单独的 workspace 之后,还要把源码先 clone 好,而不是让 Agent 自己 clone。Agent 在 clone 大仓库时容易因为输出过多、上下文过长而“迷失方向”,而且 clone 时间完全不可控。我现在都是提前在cwd目录里准备好代码,让 Agent 从“修改已有代码”这个步骤开始工作,效率高很多。
第二,注意 PTY resize 带来的输出截断。Agent 拿到一个 40x120 的终端,遇到长度超过 120 字符的行,会按终端宽度折行。人类在终端里看到折行没问题,但 Agent 在解析自己读到的输出时,折行后的内容会多出换行符,可能干扰它的判断。如果发现 Agent 总是对某些输出理解偏差,试试把cols调大到 200 以上,让输出尽量保持单行完整。
第三,定期查看 Herdr 的会话日志,不要只在出错时看。每个 Agent 的状态变化都会记录在独立日志里,平时养成抽空扫一眼的习惯,能提前发现 Agent 在循环重试、反复执行同一条命令之类的异常。这类问题早期发现就是一分钟的事,拖到后期往往已经污染了代码库,清理成本高得多。
第四,尽量用 git 分支隔离每个 Agent 的工作。Herdr 只管进程和会话,不管代码版本。两个 Agent 即使是同一个需求,也建议从同一个基线分支各自切分支工作,最终由人来合并。这样即使 Agent 改错了,也不会影响主线,回滚成本很低。我自己项目里的标准流程是:每个 Agent 一个分支,Agent 工作完成后由我 review 并合并,Herdr 负责保证 Agent 们并行跑得稳。
这些经验和教训,都是我一遍一遍试出来的。Herdr 这个工具本身不算复杂,真正的复杂度来自多 Agent 并行工作带来的系统性问题。工具只负责把底层环境管好,怎么编排任务、怎么隔离风险,还是得靠使用者的工程经验。希望这篇文章能让你在走这条路的时候,少踩几个我已经踩过的坑。