2104 字
约 7 分钟
6
CLI智能体
无标签

系列:ToyAgent 智能体知识库 25/26

CLI 智能体:把骨架装进终端

一句话核心:形态变了,灵魂不变——Claude Code、Aider、Gemini CLI 这类终端智能体剥掉外壳后仍是同一个 Agent.chat(input);REPL 只是 chat 的 while 循环,工具侧换成沙箱执行器,骨架零改动。

为什么需要

智能体跑起来之后,自然会遇到"它以什么形态面对用户"的问题:Web 页面、IDE 插件、终端……很多工程师的第一反应是"CLI 是不是另一套架构"。答案是:形态即壳。CLI Agent 的共同特征是"以终端为界面、以命令为手脚、以对话为交互",其中前一条是壳,后两条是早已实现的骨架能力:

  • 对话交互 → 就是 chat 方法被反复调用;
  • 命令为手脚 → 就是工具调用,只是工具恰好是 shell 命令,套上沙箱执行。

如果此前的骨架真的做到了"复杂度只藏在一个 chat() 方法里",那么换形态就应该是零重构、只加壳。CLI 形态是对整个架构的一次检验——它验证骨架的可复用性是否名副其实。

核心机制

CLI 版新增的东西只有三样,每一样都能找到对应的来处:

要点 实现 呼应
shell 执行器 run_command 工具:ProcessBuilder 按空白拆分命令,不经 shell 工具调用
四层沙箱 拒绝名单 → 路径边界 → 命令白名单 → 受限工作区 cwd 复用沙箱纵深防御
工作区上下文 cwd 写进系统提示词,模型"知道自己在哪" 系统提示词分层
// 终端形态 = Agent.chat 的 while 循环
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
    System.out.print("you› ");
    String line = in.readLine();
    if (line == null || line.equalsIgnoreCase("exit")) break;
    if (line.isBlank()) continue;
    System.out.println("agent› " + agent.chat(line));   // 灵魂不变
}

// 手脚换成沙箱执行器:ProcessBuilder 直接拆参,不经 /bin/sh
// 没有 shell 就没有管道、重定向、命令替换这些注入面
ProcessBuilder pb = new ProcessBuilder(command.split("\\s+"));
pb.directory(workdir);      // 受限工作区 = 沙箱的空间边界
Process proc = pb.start();  // 读取 stdout/stderr + 退出码回传给模型

"不经 shell"是一个容易被低估的安全决策。如果用 sh -c "整体命令字符串" 的方式执行,模型输出的字符串会被 shell 再解释一遍——cat a; rm -rf b、cat $(whoami)、管道与重定向全部成为攻击面。ProcessBuilder 把命令拆成参数数组直接交给操作系统,;、|、$() 都退化成普通字符,注入面被结构性消除。代价是丢掉了 shell 的便利(管道、通配符),这正是"安全与能力做交换"的典型取舍——需要时应该用受控的方式加回来(如白名单内的管道支持),而不是默认全开。

cwd 进系统提示词是体验上很妙的一笔:终端用户天然理解"当前目录"的概念,模型拿到 cwd 后能说出"你当前在 project-x 下,里面有这些文件",并且对相对路径的解释都有了锚点。工具的运行环境成为提示词的一部分——这是环境感知(context awareness)最朴素也最有效的实现。

graph TD
    U["终端输入 stdin"] --> R{"REPL while 循环"}
    R -->|exit 或 EOF| X["退出进程"]
    R -->|非空一行| C["Agent.chat 复用同一骨架"]
    C --> D{"模型决策"}
    D -->|需要执行命令| T["调用 run_command 工具"]
    T --> S["沙箱四层校验"]
    S -->|通过| P["ProcessBuilder 参数数组直执行 不经 shell"]
    S -->|拒绝名单命中| E["回传拒绝原因给模型"]
    P --> O["stdout 与退出码回传"]
    O --> C
    E --> C
    D -->|可直接回答| A["agent 输出 stdout 打回终端"]
    C --> A

CLI 形态数据流:REPL 只是 chat 的 while 循环,换壳的两个可替换点在终端界面与沙箱执行器,骨架零改动。

举例:两种入口 + 一条沙箱轨迹

CLI 的经典双入口——交互式 REPL 与非交互单命令(后者是 Claude Code -p 习惯的同款):

# 交互式 REPL:多轮对话,记忆延续
java -cp target/classes com.example.agent.CliAgent

# 非交互单命令:调一次 chat 就退出,适合脚本管道 / CI
java -cp target/classes com.example.agent.CliAgent -p "看看工作区里有什么"

交互模式下的一条完整轨迹:

you› 运行:ls
  模型决策:调用 run_command("ls")
  沙箱四层:
    ① 白名单:ls 在允许列表            → PASS
    ② 拒绝名单:未命中                 → PASS
    ③ 路径边界:无路径参数              → PASS
    ④ 受限工作区:cwd=/app/workspace   → PASS
  ProcessBuilder 执行 → stdout: hello.txt notes.md
  agent› 工作区里有两个文件:hello.txt 和 notes.md

you› 运行:cat hello.txt
  四层全过 → 执行 → 回传文件内容

you› 运行:rm -rf /
  ① 命令在拒绝名单 → DENY,工具未执行
  agent› 这个命令被沙箱拒绝执行了,原因:rm -rf 属于破坏性命令

you› 列出工作区文件
  注意:这里没有写死任何命令。模型自己决策"该调 run_command 跑 ls",
  和上一轮"运行:ls"殊途同归 —— 区别只在触发方式(人指定 vs 模型决策)

最后两条轨迹的对比点出了一个本质:"运行:X"是人替模型做了工具决策,"列出文件"才是完整的 Agent 形态——模型自主理解意图、选择工具、组织参数。CLI 只是把这套循环搬到了终端上。

举一反三:GUI Agent 的做法完全同构——把 run_command 换成浏览器执行器(click / type / screenshot),沙箱换成页面白名单与操作域限制,骨架依旧不动。理解了"形态即壳",从 CLI 到 GUI 到 IDE 插件到聊天机器人,只是换执行器和换界面两个可替换点。

教学版 vs 生产版

维度 教学版 生产版
命令拆分 split("\\s+") 简单拆 shlex 级解析(引号、转义),或结构化参数协议
白名单 硬编码命令列表 权限配置文件,按命令/参数细粒度授权
输出 原样回传 截断、编码处理、二进制检测
执行控制 无超时 超时 + 输出上限 + 资源限制
高危操作 直接 DENY 接入审批门禁,高危命令转人工确认

面试考点

  1. "CLI Agent 和 Web Agent 架构上有什么区别?"——骨架完全相同(chat 循环 + 工具 + 记忆 + 运行时),区别只在两个可替换点:交互界面(终端 REPL/单命令)与执行器(沙箱 shell);"形态即壳"。
  2. "为什么执行命令不用 sh -c 而用参数数组?"——不经 shell 就消除了管道、重定向、命令替换等注入面,;/|/$() 退化为普通字符;这是用功能换安全的结构性决策。
  3. "怎么让 CLI Agent 知道自己'在哪'?"——把 cwd 等运行环境写进系统提示词,工具环境成为上下文的一部分;这是环境感知最朴素的实现,也让相对路径语义有一致锚点。

生产化路径

  • 命令拆分升级为 shlex 级解析(正确处理引号与转义),或更进一步让模型输出结构化参数(命令 + 参数数组),由宿主拼装;
  • 白名单升级为权限配置体系:按命令、参数模式、目录范围细粒度授权,支持按项目/租户差异化;
  • 输出治理:超长输出截断(带截断提示)、二进制内容检测、stderr 与退出码结构化回传;
  • 执行控制:超时杀进程、输出字节上限、CPU/内存/进程数配额,防止单命令拖垮宿主;
  • 审批接入:拒绝名单命中的命令不直接丢弃,转审批门禁由人工放行,形成"拦截-审批-执行"闭环;交互模式与会话管理(事件溯源持久化)打通,终端断开后会话可恢复。
CLI智能体
https://clxhxhhr.top/posts/4725/
作者
clxstart
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。