已发布资料基础官方资料 + 个人实践
·8 分钟0 次阅读AI Agent CLI 工程
文章/AI 与智能体

Agent CLI:从交互式终端到可验证的进程接口

AI Agent CLI 工程第 1 篇:把 Codex、Claude Code 这类终端 Agent 拆成可调度的进程合同,并用真实 Codex exec 验证工作目录、权限、JSONL、Schema、退出码、超时与输出产物。

#Codex#Claude Code#CLI#Agent#Harness Engineering
文章目录
  1. 一分钟概览
  2. 1. 先分清五层:模型并不直接运行 Shell
  3. 2. 交互模式和非交互模式不是高低级关系
  4. 3. 一次 Agent 运行要固定七部分合同
  5. 3.1 先写一张 CLI 运行卡(Run Card)
  6. 4. 实验一:先验证普通进程,不急着调用模型
  7. 5. 实验二:真实运行一次 Codex exec
  8. 6. JSONL、stderr 和最终回答应该怎样分工
  9. 7. exit 0 只是第一道门
  10. 8. 超时和取消:最容易被低估的部分
  11. 9. Claude Code 的接口怎样映射
  12. 10. 一份失败诊断顺序
  13. 收藏清单
  14. 参考资料

我以前会把“在终端里成功跑了一次 Codex”理解成 CLI 已经接好了。真正把它放进脚本后,才发现还差很远。

人盯着交互界面时,可以临时补 Prompt、批准命令、忽略一条噪声,再凭经验判断结果是否可信;调度器只看得到进程:用什么参数启动、在哪个目录运行、输出了什么、何时结束、退出码是多少。

因此这篇不再重复 Codex CLI 修 bug 的操作流程,而是回答另一个问题:

怎样把一次 Agent 对话,变成调度器可以重复调用、观察、中止和验收的进程接口?

先约定两个容易混淆的称呼:本文把启动 CLI、设置目录、控制超时并收集输出的外部程序称为“调度器”;把 CLI 内部组织上下文、循环和工具调用的部分称为“Agent 执行框架(Agent Harness)”。两者都可能被笼统叫作 Harness,但责任并不相同。

项目说明
内容类型Agent CLI 运行时与进程合同实战
适合读者已经使用 Codex 或 Claude Code,准备接入脚本、CI、定时任务或内部平台的开发者
阅读 / 跟做时间约 18 分钟 / 35-45 分钟
环境要求Python 3.10+;真实探针需要已登录的 Codex CLI
本地实测Windows 11、Python 3.11、codex-cli 0.146.0
Claude Code 边界按官方文档核对,本文未在本机安装和执行 Claude Code
代码检查点13df6f4
实验下载agent-cli-runtime-lab-0.1.0.zip
SHA-256086C04854D1A22A4FD21ADEBCE77F17CFD99ECA3843FC3121BA847C3D8675293
资料核对日期2026-08-10

一分钟概览

先记住六点:

  1. 模型不会直接“进入终端”;调度器启动 CLI,CLI 内的 Agent 执行框架再组织模型、工具、Shell 和权限。
  2. 交互 CLI 适合探索,非交互 CLI(Headless CLI)适合边界明确、可以自动结束的任务。
  3. 一个可调度 Agent 至少要固定版本、目录、输入、环境、权限、输出和终态。
  4. exit 0 只表示进程成功结束,不等于现实任务已经完成;仍要验证结构化结果和产物(Artifact)。
  5. stdout、stderr、JSONL 事件和最终回答不是同一种输出,不应混在一个文本解析器里。
  6. 超时不是“程序不再等待”这么简单,还要处理子进程、部分输出和结果未知状态。

图 1:Agent CLI 从命令经过执行框架、权限和子进程抵达一次性工作区,并留下事件、产物和退出状态图 1:Agent CLI 从命令经过执行框架、权限和子进程抵达一次性工作区,并留下事件、产物和退出状态

图 1:CLI 的工程价值不在黑色终端,而在它把一次 Agent 运行压成了可以被操作系统和调度器管理的进程边界。动画表示事件和状态流动。

1. 先分清五层:模型并不直接运行 Shell

一条 codex exec 背后至少有五层:

text
调度器 / CI
  -> Codex CLI 进程
    -> Agent 执行框架(Agent Harness)
      -> 权限与工具路由
        -> Shell 子进程 / 文件系统 / Git

模型负责提出下一步行动,Agent 执行框架负责把行动映射为工具调用,权限层决定是否允许,操作系统才真正启动命令。把这些都叫“模型执行了 Shell”,会掩盖三个关键事实:

  • 相同模型在不同 Agent 执行框架下可能表现不同;
  • CLI 进程结束时,仍可能留有它启动的后台进程;
  • 目录、环境变量、权限和退出码都属于模型外部的工程合同。

图 2:模型、Agent 执行框架、权限层、Shell 子进程和工作区的责任边界图 2:模型、Agent 执行框架、权限层、Shell 子进程和工作区的责任边界

图 2:Prompt 只进入 Agent 执行框架的输入面。真正决定可读写范围和进程生命周期的是外层控制面。

2. 交互模式和非交互模式不是高低级关系

Codex 的交互入口是 codex,非交互入口是 codex exec;Claude Code 对应 claudeclaude -p。两种模式共享 Agent 能力,但承担的工作不同。

判断交互模式非交互模式(Headless)
需求还在变化合适,可以补上下文不合适,输入会漂移
需要人工批准合适需要预先写好权限策略,否则会中断
要进入 CI不合适合适
输出给人阅读终端 UI 更自然应保存最终摘要
输出给程序消费很难稳定解析使用 JSONL 与 JSON Schema
失败后继续追问直接对话保存并恢复会话(Session),或重新执行幂等任务

图 3:交互式探索和非交互自动化从同一 Agent 能力分流图 3:交互式探索和非交互自动化从同一 Agent 能力分流

图 3:当任务仍需要人不断改写目标时,自动化只会把模糊放大。非交互运行的前提是任务合同已经足够清楚。

我会用一个简单门槛决定是否切到非交互模式:

text
能否在运行前写清输入、权限、停止条件和机器可检查的输出?
  能 -> 可以考虑非交互运行
  不能 -> 继续在交互模式探索

3. 一次 Agent 运行要固定七部分合同

配套实验把运行合同拆成七部分。

合同最少记录什么常见失败
可执行文件产品与确切版本更新后参数或默认行为变化
工作目录主目录、附加目录、Git 基线Agent 读错仓库或混入旧改动
输入Prompt、stdin、附件摘要调度器无法重放同一次任务
环境允许继承的变量、认证来源Secret 被子进程或日志意外继承
权限Sandbox、Approval、网络和写入边界自动化停等批准,或权限过大
输出stdout、stderr、事件流、最终消息、产物人类文本被当 JSON 解析
终态退出码、超时、取消、结果状态进程结束被误判为任务完成

图 4:可验证 Agent CLI 的七部分运行合同图 4:可验证 Agent CLI 的七部分运行合同

图 4:七项不是为了多写配置,而是为了让失败能归因。少一项,通常就会多一种“这次为什么不一样”的争论。

3.1 先写一张 CLI 运行卡(Run Card)

在接入任何 Agent CLI 前,可以先填这张卡:

yaml
run:
  executable: "codex-cli 0.146.0"
  mode: "headless"
  cwd: "一次性 Git fixture 的绝对路径"
  input: "只读检查两个固定文件"
  environment: "PATH 等最小白名单;认证由本机 Codex 管理"
  permissions:
    sandbox: "read-only"
    dangerous_bypass: false
  outputs:
    events: "stdout JSONL"
    diagnostics: "stderr"
    final: "artifacts/final.json"
  limits:
    timeout_seconds: 180
    output_bytes: 256000
  acceptance:
    exit_code: 0
    final_schema: "codex-probe.schema.json"
    workspace_unchanged: true

注意 workspace_unchangedfinal.json 并不冲突:最终消息由 CLI 调用者写入工作区之外的产物目录,Agent 的只读工作区仍保持不变。

这张运行卡使用本机已登录的 Codex 凭证,只用于本地探针。进入 CI 时,不要把 ~/.codex/auth.json 复制进公共 Runner,也不要把 API Key 作为整个作业都可见的环境变量暴露给会执行仓库代码的步骤。OpenAI 当前文档建议 GitHub Actions 优先使用 Codex GitHub Action;其他环境则把 CODEX_API_KEY 只注入单次 codex exec 进程。

4. 实验一:先验证普通进程,不急着调用模型

下载并解压实验后:

bash
cd agent-cli-runtime-lab-0.1.0/phase-8-agent-cli-engineering
python -m unittest discover -s tests -v
python run_lab.py runtime-demo --output reports/runtime-demo.json

runner.py 使用 Python 标准库完成几件事:

  • 检查工作目录存在;
  • 使用参数数组而不是拼接 Shell 字符串;
  • 只继承显式环境变量白名单;
  • 分开捕获 stdout 和 stderr;
  • 记录退出码、耗时和超时状态;
  • 给输出设置上限并标记截断。

生成的 run-record.json 结构接近:

json
{
  "argv": ["python", "-c", "..."],
  "cwd": ".../agent-cli-runtime-xxxx",
  "status": "completed",
  "exit_code": 0,
  "duration_ms": 31,
  "stdout": "{\"status\": \"ok\", \"count\": 3}\n",
  "stderr": "",
  "stdout_truncated": false,
  "stderr_truncated": false
}

先用普通子进程验证调度器,是因为模型调用昂贵且不确定。若目录检查、超时和输出截断在普通命令上都不可靠,换成 Agent 只会更难调试。

5. 实验二:真实运行一次 Codex exec

确认下面命令可用:

bash
codex --version
python run_lab.py codex-probe --mode read-only --output reports/codex-probe.json

实验实际构造的核心命令为:

text
codex exec \
  -C <disposable-workspace> \
  --sandbox read-only \
  --ephemeral \
  --ignore-user-config \
  --strict-config \
  --output-schema schemas/codex-probe.schema.json \
  --json \
  --output-last-message <artifact-dir>/final.json \
  "Inspect this disposable repository without changing files..."

这段命令包含路径占位符,只用于解释参数结构。Windows 读者直接运行前面的 run_lab.py codex-probe 即可,脚本会用参数数组构造命令,不需要把反斜杠续行复制到 PowerShell。

这些参数分别解决不同问题:

  • -C 固定 Agent 的工作根目录;
  • --sandbox read-only 限制模型发起的工具操作;
  • --ephemeral 不持久化会话记录;
  • --ignore-user-config 不读取用户 config.toml,但不应把它误解成操作系统级“纯净容器”;
  • --strict-config 遇到当前版本不认识的配置就失败;
  • --json 把过程事件写成 JSONL;
  • --output-schema 约束最终回答;
  • --output-last-message 把最终结果单独保存,避免从事件流里猜哪一行是结论。

本机成功探针得到:

text
codex-cli 0.146.0
exit_code: 0
status: completed
final_message: {"status":"ok","file_count":2,...}
workspace_files_after: [README.md, data.txt]

第一次运行并没有成功。Schema 写成了:

json
"status": { "const": "ok" }

API 返回 invalid_json_schema,指出该属性缺少 type。修正为下面这样后才通过:

json
"status": { "type": "string", "const": "ok" }

这个失败比一张成功截图更有用:结构化输出不是“Prompt 里叫它返回 JSON”,而是调用链上的真实协议合同。

6. JSONL、stderr 和最终回答应该怎样分工

一次非交互运行通常有四类输出:

输出面面向谁适合保存什么
stdout JSONL调度器线程、Turn、工具调用和完成事件
stderr运维与开发者连接、配置、重试和运行时诊断
final message下游任务通过 Schema 验证的最终结论
产物文件人与后续步骤报告、diff、数据导出等较大结果

不要把 stderr 合并进 stdout 后再逐行 json.loads。一条诊断信息就足以破坏整个事件流。也不要只看 JSONL 中出现过一条漂亮的中间回答;本文实测事件里,模型在真正检查文件前曾产生临时结构化消息,最终应以 turn.completed 和保存的最终消息为准。

7. exit 0 只是第一道门

至少把完成判断拆成三层:

text
进程完成:exit_code == 0,未超时
协议完成:出现完成事件,最终 JSON 通过 Schema
任务完成:产物和现实状态满足验收条件

反过来,非零退出也要分类:

  • 参数错误属于调用者合同问题;
  • 权限拒绝可能是策略正确工作;
  • Schema 错误属于接口版本问题;
  • 网络失败可能可以重试;
  • 工具执行失败不一定意味着整个任务不可恢复。

把所有失败都交给模型“再试一次”,会让确定性错误白白消耗预算。

8. 超时和取消:最容易被低估的部分

Python 的 subprocess.run(timeout=...) 能停止等待,但真实 Agent 可能已经启动测试服务器、浏览器或构建进程。可靠取消至少要回答:

  1. 向主进程发送什么信号?
  2. 子进程是否处于同一进程组?
  3. 优雅停止多久后升级为强制终止?
  4. 已产生的产物标记为失败、部分完成还是结果未知?
  5. 写操作是否能通过回执或幂等键判断实际状态?

本实验验证主进程的超时、终止和部分输出回收,但没有宣称覆盖 Windows Job Object、容器 PID namespace 或任意后台进程树。生产系统应在容器、任务运行器或平台级隔离里再做一层生命周期管理。

9. Claude Code 的接口怎样映射

按 2026-08-10 的 Claude Code 官方文档,非交互入口使用:

bash
claude -p "What does the auth module do?"

它也提供 textjsonstream-json 输出,--json-schema--max-turns--max-budget-usd--no-session-persistence--allowedTools 和权限模式等控制。官方还推荐脚本调用考虑 --bare,以跳过本机 Hooks、Skills、Plugins、MCP、Memory 与 CLAUDE.md 的自动发现;此模式需要显式认证配置。

概念映射如下:

进程合同CodexClaude Code
非交互入口codex execclaude -p
事件输出--json--output-format stream-json
最终结构--output-schema--json-schema
临时运行--ephemeral--no-session-persistence
工作目录-C / --cd在目标目录启动,或使用 --add-dir 扩展
工具权限Sandbox、Approval、Rules--tools--allowedTools、Permission Mode
预算控制由调度器和调用环境组合约束--max-turns--max-budget-usd

这张表只比较公开接口,不比较模型能力。Claude Code 命令在本文环境中未本地执行。

10. 一份失败诊断顺序

遇到“Agent CLI 卡住了”,按这个顺序检查比盯着最终回答更快:

  1. 启动前:可执行文件和版本是否正确?工作目录存在吗?
  2. 配置层:是否意外继承用户配置、Hooks、Skills 或 MCP?
  3. 权限层:非交互任务是否停在无法显示的批准点?
  4. 传输层:stdout 是否仍有事件?stderr 是否在重连?
  5. 工具层:最后一个开始但未完成的工具是什么?
  6. 进程层:主进程、子进程和超时状态分别怎样?
  7. 结果层:最终消息是否存在并通过 Schema?
  8. 现实层:目标文件、测试、PR 或外部系统是否真的变化?

收藏清单

  • 固定 CLI 版本并把版本写进运行记录。
  • 使用参数数组启动进程,不拼接未经验证的 Shell 字符串。
  • 显式设置工作目录、权限、环境变量白名单和超时。
  • stdout、stderr、事件、最终消息和产物分开保存。
  • 给自动消费的结果使用 JSON Schema。
  • 同时检查进程完成、协议完成和现实任务完成。
  • 把输出产物放在工作区之外的受控目录。
  • 超时后记录部分结果,并处理子进程生命周期。
  • 不在没有外部隔离时使用危险权限绕过。

下一篇会把视角反过来:这篇是“用 CLI 运行 Agent”,下一篇讨论“让 Agent 调用一个 CLI”,并把同一组能力同时暴露成 CLI 和 MCP,看看“万物皆 CLI”到底在哪些地方成立。

参考资料

继续阅读