文章 · Agent 工具链

让 agent 一次写完整段代码:heredoc 为什么比 REPL 更合适

给 agent 一个终端,它几乎什么都能做。但如果它像人一样一条命令一条命令地试,每一步都是一次模型调用。建联系统跑了几个月之后我越来越确定:把整段任务一次交给运行时,比让它在 REPL 里边走边看要省得多。

作者 Nick2026-10-168 分钟阅读

这篇回应的是 ego lite 官网的 《Why heredoc-style execution is a better fit for AI agents than a REPL》。观点是我自己的,例子来自我搭的 KOL 建联系统。

先说结论
  1. CLI 本身不是问题,交互方式才是:命令一条条敲,模型和工具之间的来回次数会随任务长度成倍增长,每一次来回都在烧 token 和时间。
  2. heredoc 把一整段 JavaScript 当作一次工具调用提交,运行时跑完再返回;REPL 让解释器一直活着,模型就倾向于走一步看一步。
  3. ego lite 公开的实验里,同样的浏览器任务 heredoc 比 REPL 少 44% 的执行轮次、少 35.5% 的工具调用、便宜 21.6%。我自己的建联脚本也是同一个方向:能写进代码的判断,就别留给模型。

从「CLI is all you need」说起

早期的编程 agent 给了所有人一个启发:只要能用 Bash,agent 就能写代码、查资料、跑测试、管 Git。于是出现了一个很顺口的口号:CLI is all you need。把软件做成命令行,agent 就能用。

这句话有一个隐藏成本。agent 用终端的方式和人很像:敲一条短命令,看结果,再决定下一条。任务一复杂,这种来回就会迅速膨胀。每一次来回都是一次完整的模型调用,上下文越拖越长,等待时间也越来越久。

我第一版建联系统就是这么搭的。让 Claude Code 用一串命令操作浏览器:打开达人库、按条件筛、点进一条记录、读邮件、再决定要不要催。一个二十来人的催信任务,光是模型和工具之间的往返就要几十个回合,大部分时间花在「等结果」上。

CLI 只是入口,代码才是接口

问题不在命令行,而在「用命令表达任务」。对 Git、Docker、FFmpeg 这类老工具,模型在训练时见过无数命令和报错,闭着眼都会用。但一个为 agent 新造的 CLI,对模型来说就是一门新的小语言:子命令、参数、输出格式、错误语义都要重新学,还得通过反复调用才摸清它们怎么拼在一起。

把同样的能力做成一个 JavaScript API,模型要学的只剩领域概念。循环、条件、异常处理、数据加工,用的全是它早就会的东西。所以更好的架构是:保留 CLI 作为统一入口,但用代码作为和软件真正交互的方式。agent 提交一段代码,里面装着操作、判断和数据处理,本地运行时负责执行。

拿一个最简单的任务对比:打开一个页面,读它的主标题。命令式接口大概要三步:

# 第一步:打开页面
ego-browser open https://example.com
# 第二步:找标题
ego-browser find --role heading
# 第三步:读出来
ego-browser read --role heading

每一步后面都藏着一次「等工具返回、模型再想」。换成代码接口,整件事一次写完:

const task = await taskSpace("read example page");
const page = task.page("p1");
await page.goto("https://example.com");
const snap = await page.snapshot();
console.log(snap.match(/heading.*\n\s+text "([^"]+)"/)?.[1]);

两种写法做的是同一件事,差别在于前者把过程拆成好几轮模型与工具的对话,后者让模型把完整过程写一遍,交给本地跑。

heredoc 和 REPL:真正的差别是进程生命周期

一旦 CLI 开始接受代码,下一个问题就是:agent 怎么把这段代码递给执行环境?严格说 heredoc 是 shell 的输入语法,REPL 是解释器的运行模式,两者不在一个层面。这篇文章比较的其实是「通过 heredoc 一次性执行一段代码」和「基于 REPL 的持久交互会话」。下面为了省事就直接叫 heredoc 和 REPL。

heredoc 的用法是把一整段 JavaScript 一次塞给命令:

ego-browser nodejs <<'EOF'
const task = await taskSpace("weekly follow-ups");
const page = task.page("p1");
await page.goto("https://kol-system.example/outreach?due=1");
const rows = await page.evaluate(() => [...document.querySelectorAll("tr.clickable")].length);
console.log({ due: rows });
EOF

shell 把代码交给进程,等它跑完,拿到结果,进程退出。对 agent 来说这和一次普通的工具调用没有区别:提交命令,等待进程,收到结果。

REPL 不一样。解释器一直活着,agent 可以反复输入代码,变量和会话状态都保留着。从表达能力看两者几乎等价:REPL 也能一次跑一个带循环和异常处理的完整程序,heredoc 也可以只提交一行。真正的差别是进程生命周期:heredoc 跑完就退出,REPL 一直等着下一条输入。

这也意味着两者对 agent 工具的要求不同。heredoc 走的是最普通的请求响应模型;REPL 要求工具支持持久进程、会话管理、持续输入、中断和恢复。今天大多数 agent 自带的 Bash 工具没有这些能力,主流产品里只有 Codex 支持得比较完整。而且就算工具支持了,模型会不会好好用它,是另一回事。

模型在两种环境里写出来的代码不一样

理论上模型在两种环境里的编程能力相同。实际观察到的行为却很一致:在 REPL 里,模型倾向于一点点往里敲;在 heredoc 里,它更可能一次生成完整程序。

原因不难猜,是训练数据的分布。REPL 的样本大多来自教程、调试过程和问答,典型模式是探索式的:拿到页面,返回信息;找个元素,返回信息;读内容,返回文本。heredoc 代码块则更像脚本和源文件,有明确的开头和结尾,天然鼓励模型写出一条连贯的工作流。

给人用的软件要讲人体工学,给 agent 用的软件也需要类似的纪律,可以叫它「模型体验工程」:接口应该顺着模型在训练中养成的行为模式走,而不是逆着来。

这种倾向还决定了控制流住在哪里。在 REPL 里,模型更可能把任务拆成几段,每拿到一个结果就重新决策一次;在 heredoc 里,循环、条件、过滤和数据处理会被直接写进程序,交给本地运行时去跑。前者是「执行、观察、继续」,后者是「先组织、再执行」。

数字:ego lite 的实验,和我自己的体感

ego lite 团队在 Codex SDK 上搭了一个自动化基准:同一个 agent 用 REPL 和 heredoc 两种方式完成四类真实的浏览器任务,重复多轮取平均。结果是 heredoc 少了 44% 的执行轮次、少了 35.5% 的工具调用,成本低了 21.6%。这几个数字是他们公开的,不是我测的。

我自己没有做过这么严格的对照,但建联系统的日常给了我足够的体感。系统每天做三件事:该催谁、同步回复、出报表。每件事都是一段 heredoc 脚本。「沉默超过三天且没催过」「这是第几轮话术」「哪些回复要进待审核」这些判断全部写在代码里,模型只负责两件事:决定要不要跑,以及看最后那几行汇总。一次任务通常只有一两次工具调用,而不是几十次。

还有一个意外的好处:脚本是可以审阅的。出了问题我可以把那段 heredoc 原样再跑一遍,或者改一行再跑;REPL 里的对话记录做不到这一点,因为它的状态散落在几十个回合里。

heredoc 不是永久的答案

它也有明显的短板。一段长脚本在中途失败,就得从头再来,没有状态可以接着用。页面出现意料之外的变化时,调试也更费劲,因为你看到的是一次性的输出,不是一步一步的回放。

我的折中是两段式:先用一段很短的 heredoc 拿一次页面快照,确认结构没变;再提交真正干活的那段。长任务则在代码里写检查点,每完成一批就把结果落到文件,失败后从检查点继续。

随着 agent 工具开始支持长生命周期的会话,REPL 的优势会慢慢回来。但原则不会变:尽量减少模型和工具之间的来回,确定性的判断留在代码里,模型只在真正需要判断的地方出手。

如果你要给 agent 做一个 CLI

  • 入口保持简单,一个命令接一段代码;不要把能力拆成几十个子命令让模型去背。
  • 用模型已经会的语言暴露 API,领域概念越少越好,命名跟着它熟悉的习惯走。
  • 每次执行都返回一份能直接当下一步输入的输出,比如页面快照、带 ref 的元素列表,省得模型再去要一次。
  • 把「谁该被处理」这类规则放进脚本或配置,而不是提示词;提示词每次都要付 token,代码只写一次。

常见问题

不是。heredoc 是所有 POSIX shell 都支持的输入语法,任何接受标准输入的程序都能用。ego-browser 只是围绕这个思路设计了它的 CLI:一个命令接一段 JavaScript。

探索一个陌生页面、调试一条失败的步骤,REPL 仍然更顺手,因为状态还在。这篇文章说的是把任务交给 agent 自动执行的场景,这时候来回次数直接等于成本。

MCP 把每个能力做成一个工具,每次调用也是一次来回,而且工具的 schema 和快照都会进上下文。它和命令式 CLI 的成本结构很像,所以同一个结论基本适用:步骤越多,差距越大。

想看这些想法落地的样子?

KOL 建联系统就是用这套方法搭出来的。