做 Agent 会用到的 Node API(2):子进程
本系列讲实现 Agent harness 时会反复碰到的 Node.js API。默认读者会一点 JS但还没系统用过 Node。上一篇1路径与文件示例仓库react-agent-mini相关前作Agent 终于有终端了 · 主循环先弄清什么是「进程」什么是「子进程」你双击打开一个程序操作系统就会为它创建一个进程process一块独立的内存空间 一串正在跑的代码。你现在跑的 Node / Bun 程序本身就是一个进程。这个进程再去「拉起」另一个程序比如git、cmd.exe、一次测试被拉起的那个叫子进程child process。父子关系可以简化成你的 Agent 进程父 └── spawn 出来的 shell / git / 测试 等子为什么 Agent 需要子进程因为读文件可以用上一篇的fs跑一条终端命令git status、bun test、npm run build操作系统只认「另起一个程序」Node 里对应模块就是node:child_process。本篇核心 APIimport{spawn}fromnode:child_process;对照示例仓库里的Bash 工具模型说「执行这条命令」工具就spawn一个系统 shell把命令交给它再把输出收回来给模型。1. 三个管子stdin / stdout / stderr每个进程通常带三条「标准流」可以想成三根水管名字方向对子进程而言日常含义stdin流进子进程标准输入你「喂」给程序的数据stdout从子进程流出标准输出程序正常打印的内容stderr从子进程流出标准错误警告、报错信息在终端里你敲git status屏幕上看到的字多半就是它的stdout有时错误在stderr。Agent 的 Bash 工具要做的事本质是起子进程去跑命令监听stdout / stderr把文字攒起来等进程结束连同「退出码」一起塞进tool_result一般不往 stdin 写东西——命令参数已经通过 shell 传进去了后面讲到「把 JSON 写进子进程」时用的就是stdin本篇先把 Bash 这条主线讲透。2. 退出码命令成功还是失败子进程结束时操作系统会给一个整数退出码exit code约定常见含义0成功非0失败具体含义因程序而异另外还可能因为信号被杀掉比如超时主动杀进程这时不一定是普通的「跑完返回码」。Agent 工具不要把「命令失败」当成「整个 Agent 崩溃」应把非零退出整理成带is_error的tool_result让模型自己改策略。主循环继续转就行。3.spawn和exec为什么 Bash 选 spawnchild_process里有好几种起子进程的方式初学先分清这两个spawnexec输出怎么拿一点点通过事件推过来流式默认先攒在内存结束后一次给你长输出可以边收边截断、边决定是否杀掉容易一把撑爆内存Agent 里更合适是测试日志、find可能很大短命令图省事时可以用示意先建立直觉还不是仓库原码import{spawn}fromnode:child_process;constchildspawn(echo,[hello]);// 起名为 echo 的程序参数 hellochild.stdout.on(data,(chunk){// chunk 是 Buffer字节块常转成字符串console.log(chunk.toString(utf-8));});child.on(close,(code){console.log(结束退出码,code);});要点spawn(程序名, 参数数组, 选项)输出不是函数返回值而是data事件一段段来close表示进程结束管道也关了4. Bash 工具为什么要「先起 shell再执行整句」模型给的常常是一整句用户习惯的写法例如gitstatusbuntest这里有上一句成功才跑下一句。如果直接spawn(git,[status,,bun,test]);// 错git 不认识 是shell 的语法不是git的参数。所以正确做法是先启动系统自带的shellWindows 上常见cmd.exeUnix 上常见bash告诉 shell「请执行后面这一整串命令」由 shell 去解析、管道、重定向等示例仓库里的写法function runCommand(command: string, timeoutMs: number): PromiseBashRun { return new Promise(resolve { const isWin process.platform win32 const shell isWin ? process.env.ComSpec || cmd.exe : process.env.SHELL || /bin/bash const shellArgs isWin ? [/d, /s, /c, command] : [-c, command] const child spawn(shell, shellArgs, { cwd: process.cwd(), windowsHide: true, })逐项拆开代码 / 选项含义process.platform win32当前是不是 WindowsComSpec/SHELL系统环境变量里配置的默认 shell没有就用常见默认值Windows/c 命令cmd 的意思「执行后面这串然后退出」Unix-c 命令bash 等同语义cwd: process.cwd()子进程的「当前目录」锁在 Agent 工作区避免命令跑到奇怪路径windowsHide: trueWindows 上尽量不弹出黑色控制台窗口不用shell: true偷懒自己显式选 shell 二进制行为更可控、好调试process.env就是进程的环境变量表PATH、SHELL等子进程默认会继承除非你改env选项。5. 收齐输出监听 data最后在 close 里收尾骨架可以记成letstdout;letstderr;constcap200_000;// 最多攒多少字符防止撑爆内存 / 上下文child.stdout?.on(data,(chunk:Buffer){if(stdout.lengthcap){stdoutchunk.toString(utf-8);}});child.stderr?.on(data,(chunk:Buffer){if(stderr.lengthcap){stderrchunk.toString(utf-8);}});child.on(close,(code,signal){// code退出码signal被信号杀掉时可能有值// 在这里把 stdout/stderr 合并、截断再 resolve Promise});child.on(error,(err){// spawn 阶段就失败比如找不到 shell 可执行文件});几个初学易混点Buffer一块原始字节。toString(utf-8)才变成可读字符串。?.有的配置下 stdout 可能是null不配管时可选链避免报错。边收边限长输出可能无限刷不能无盘接收。stderr 不要扔编译失败、测试报错经常在 stderr给模型排错很重要。示例里常把 stdout/stderr合并成一段再截断并注明截断。结束时通常要汇报三件事给上层退出码code是否因信号结束signal是否超时timedOut对应到模型侧就是「成功 / 失败 / 超时」三种文案的tool_result。6. 超时必须杀而且尽量杀掉整棵进程树如果模型让 Agent 跑一条死循环或极慢的命令不能干等。做法setTimeout到点 → 标记timedOut true杀掉子进程带着「超时」结果结束 Promise让工具返回主循环继续为什么「只 kill 一下」往往不够你 spawn 的是 shellshell 又可能再拉起bun test、再拉起一堆 worker。只杀 shell孙子进程可能还在跑继续占 CPU、写文件。示例仓库在 Windows 上用taskkill /T /F按进程树强杀在 Unix 上对子进程发SIGKILLfunction killTree(child: ReturnTypetypeof spawn): void { if (process.platform win32 child.pid ! null) { try { spawn(taskkill, [/pid, String(child.pid), /T, /F], { windowsHide: true, }) } catch { child.kill(SIGKILL) } return } child.kill(SIGKILL) }配合定时器逻辑示意consttimersetTimeout((){timedOuttrue;killTree(child);// 再 resolve 超时结果……},timeoutMs);另外超时上限要有硬顶。模型若传入天文数字的 timeout不能照单全收。示例 Bash 默认约 120 秒、上限约 600 秒——具体数字以源码为准原则是「可配置但不能无限」。SIGKILL是 Unix 上「强制杀」的信号名在 Node 里写成child.kill(SIGKILL)。Windows 走taskkill分支。7. 跨平台同一套 Bash 工具两套壳点WindowsmacOS / Linux常用 shellcmd.exe看ComSpecbash/sh看SHELL「执行整句」/d /s /c 命令-c 命令杀进程树taskkill /pid … /T /Fkill(SIGKILL)仍可能有漏网情况黑框窗口用windowsHide: true尽量隐藏一般没有这个问题写 Agent 命令工具时不要假设用户一定在 bash 里用process.platform分支选壳再谈命令内容。命令内容本身也要注意模型在 Windows 上生成的 bash 专属语法有时会跑不通——那是产品层提示词 / 工具说明的问题Node API 层至少要把「怎么起壳」做对。8. 同一套子进程另外两种常见用法知道即可本篇主线是 Bash。同一套「起进程 管道」心智还会出现在场景和 Bash 的差别直觉命令型钩子脚本除了看退出码有时还要把一段 JSON写进 stdin脚本读完再决定放行/拒绝MCP 本地 stdio server子进程长时间活着stdin/stdout 上跑协议报文不是「跑完一句就退出」细节分别在各自专题文里这里只要记住都是子进程 标准流差别在生命周期和「往 stdin 写不写东西」。常见坑坑建议用exec跑可能巨量输出的命令长任务用spawn自己限长、可中途杀超时只kill父 shellWindows 用带/T的杀树文档写清「尽力」忘记设cwd命令可能跑在意外目录Agent 应锁工作区丢掉 stderr模型排错缺信息宜保留或与 stdout 合并非零退出当成「工具代码崩了」应变成失败向的tool_result别炸主循环把spawn的返回值当「命令输出」返回的是 ChildProcess输出在data事件里和主循环的关系query → 模型返回 tool_use: Bash →可选权限门卫决定是否允许跑 → spawn(系统 shell, [执行整句命令]) → 收 stdout/stderr、超时杀树、截断 → tool_result 回模型 → 主循环继续主循环仍然不直接操作进程Bash 这类工具才调用spawn。学子进程 API是在学 Agent 的「终端手脚」。本系列下一篇预告3异步与流ReAct 怎么转起来——async/await、async function*、for await、流式调用模型时输出怎么一段段出来。你可以带走什么子进程 父进程再拉起的另一个程序跑终端命令几乎总要它。stdout / stderr 用事件收退出码表示成败失败应回传给模型而不是拖垮 Agent。整句命令经系统 shell 执行/c或-c等语法才生效。优先spawn可流式收、可限长、可超时杀。超时要尽量杀整棵进程树输出要截断并注意 Windows / Unix 差异。仓库与延伸GitHubreact-agent-mini上一篇1路径与文件相关前作Bash 工具篇源码BashTool.ts欢迎 Star、Issue 和 PR。本文为「做 Agent 会用到的 Node API」系列第 2 篇示例基于 react-agent-mini。

相关新闻