8200 字
约 27 分钟
1
第13章 CLI能力-Agent操作本地工具

第13章 CLI能力-Agent操作本地工具

来源:https://ai-agent-guide.xiaofuge.cn/chapters/ch09-cli-capability.html 所属:第三篇-Agent的手脚


让 Agent 操控 Git、Docker、Maven……一切 CLI 工具皆为 Agent 的手脚

13.1 为什么 CLI 能力是 Agent 的关键突破

前两章我们讲了 MCP 和 Skills——它们让 Agent 能调用远程 API、搜索文档、操作数据库。但程序员日常工作中最频繁使用的工具是什么?是命令行git commitmvn clean packagedocker compose upkubectl apply……这些 CLI 工具构成了开发者的核心操作链路。

如果 Agent 只能调 HTTP API,它永远是个"远程助手";但如果 Agent 能操作本地命令行工具,它就真正成了你电脑上的智能搭档——帮你构建项目、部署服务、排查故障、管理代码仓库,而你只需要用自然语言说一句"帮我打包部署到测试环境"。 💡 CLI 能力 = Agent 从"遥控器"升级为"机械手"

MCP 和 HTTP API 像遥控器——Agent 在远端按按钮,服务在云端响应。CLI 能力则像机械手——Agent 直接握住你本地的工具,gitdockermavennpmpython……每一款 CLI 工具都是 Agent 可以操控的"手指"。这是 Agent 从"线上助手"到"本地搭档"的关键跃迁。

Agent 能操控的 CLI 工具全景

几乎所有开发者日常使用的工具都有 CLI 接口,Agent 可以像人一样"敲命令"来操控它们: | 类别 | CLI 工具 | Agent 可以做什么 | | --- | --- | --- | | 📦 构建 | mvngradlenpmpipgo build | 打包、编译、依赖管理、版本发布 | | 🔀 版本控制 | gitsvn | 提交、分支管理、合并冲突、Changelog 生成 | | 🐳 容器 | dockerkubectlhelm | 镜像构建、容器部署、集群管理、滚动更新 | | 🔧 运维 | sshscpsystemctljournalctl | 远程部署、日志排查、服务启停、配置修改 | | 📊 数据 | mysqlredis-clipsqlmongo | 数据库操作、缓存管理、数据迁移 | | 🧪 测试 | pytestjunitcurlab | 跑测试、压测、接口调试 | | 📝 文档 | swaggerdoxygenjavadoc | API 文档生成、代码文档提取 | | 🌐 网络 | pingnetstatnslookupiptables | 网络诊断、端口检查、DNS 查询 | ## 13.2 CLI 能力的三层架构

Agent 操作 CLI 工具,不是简单地把用户的话翻译成命令然后执行。它需要一个完整的三层架构:意图理解层、命令生成层、安全执行层。每一层都有独特的技术挑战。

Layer 1:意图理解层

用户说"帮我部署到测试环境",Agent 需要理解这背后的完整意图链🧠 意图理解:从一句话到多步执行计划

用户输入:"帮我部署到测试环境"

Agent 意图理解链:

  • 环境判断 → 测试环境 = test profile
  • 前置步骤 → 先打包(mvn clean package -Dmaven.test.skip=true
  • 构建镜像 → docker build -t system/app:1.0.0 .
  • 推送镜像 → docker push registry/app:1.0.0
  • 部署服务 → docker compose -f docker-compose-app.yml up -d
  • 验证结果 → docker ps | grep app + curl health

意图理解层的关键能力:

  • 上下文感知:知道当前目录是什么项目、用的什么语言、有什么配置文件
  • 环境推断:根据项目结构推断构建工具(有 pom.xml → Maven,有 package.json → npm)
  • 多步规划:一个意图可能需要 3~6 步命令才能完成,Agent 要自动规划

Layer 2:命令生成层(NL2Shell)

将自然语言意图翻译成精确的 Shell 命令,是 CLI 能力的核心技术。这个过程叫 NL2Shell(Natural Language to Shell)。

🔬 NL2Shell 的三种实现路径 | 路径 | 原理 | 准确率 | 适用场景 | | --- | --- | --- | --- | | 直接生成 | LLM 直接输出 Shell 命令 | 70~85% | 简单命令 | | Few-Shot 模板 | LLM + 常用命令示例库 | 85~92% | 标准场景 | | 工具 Schema | Function Calling + 命令 Schema 定义 | 92~97% | 企业级 | 推荐使用工具 Schema 方式——把每个 CLI 工具定义成 Function Calling 的 Schema,LLM 不需要"猜"命令,而是像调 API一样选择工具并填参数。这和我们第10章讲的 Function Calling 完美衔接。

// CLI 工具 Schema 定义示例:git { "name": "git_operation", "description": "执行 Git 版本控制操作", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["commit", "push", "pull", "branch", "log", "diff", "merge"], "description": "Git 操作类型" }, "message": { "type": "string", "description": "提交消息(仅 commit 需要)" }, "branch_name": { "type": "string", "description": "分支名(branch/merge 需要)" }, "count": { "type": "integer", "description": "查看最近 N 条记录(log 需要)" } }, "required": ["action"] } }

Layer 3:安全执行层

CLI 命令有系统级权限——可以删除文件、重启服务、修改配置。安全执行层是 CLI 能力最不可省略的部分。

🔒 三级安全策略 ✅ Level 1 · 白名单命令 — 自动执行

git statuslscatdocker ps……只读、无破坏性、无系统修改的命令,Agent 可直接执行。 ⚠️ Level 2 · 中风险命令 — 确认后执行

git pushmvn deploydocker restart……有修改但可控,需用户确认 "Y/N" 后执行。 ❌ Level 3 · 高风险命令 — 拒绝或需二次审批

rm -rfDROP DATABASEshutdownmkfs……不可逆操作,默认拒绝,特殊场景需管理员二次审批。

13.3 CLI 工具的 Schema 注册

要让 Agent 精准操控 CLI 工具,每个工具需要定义Schema——告诉 LLM 这个工具能做什么、有哪些参数、参数的类型和约束。这是连接"第10章 Function Calling"和"第11章 MCP"的桥梁。

五大核心工具的 Schema 设计

以下是开发者最常用的五大 CLI 工具的 Schema 注册示例��

🔀 Git — 版本控制工具

Git 是开发者最高频使用的 CLI 工具。Agent 可以帮你提交代码、管理分支、查看历史、解决冲突。

// git_schema.json { "tool_name": "git", "description": "Git 版本控制系统", "commands": [ { "name": "git_status", "risk_level": "safe", // 只读 → Level 1 "description": "查看工作区状态", "shell": "git status" }, { "name": "git_commit", "risk_level": "medium", // 写操作 → Level 2 "description": "提交代码到本地仓库", "parameters": { "message": { "type": "string", "required": true } }, "shell_template": "git commit -m '{message}'" }, { "name": "git_push", "risk_level": "medium", "description": "推送代码到远程仓库", "shell": "git push origin {branch}", "requires_confirm": true } ] } 📦 Maven — 项目构建工具

Maven 是 Java 项目的标准构建工具。Agent 可以帮你编译、打包、运行测试、管理依赖。

// maven_schema.json { "tool_name": "maven", "description": "Maven 项目构建与依赖管理", "commands": [ { "name": "maven_clean_package", "risk_level": "safe", "description": "清理并打包项目(跳过测试)", "shell": "mvn clean package -Dmaven.test.skip=true" }, { "name": "maven_deploy", "risk_level": "medium", "description": "部署到远程仓库", "shell": "mvn deploy -Dmaven.test.skip=true", "requires_confirm": true }, { "name": "maven_dependency_tree", "risk_level": "safe", "description": "查看依赖树", "shell": "mvn dependency:tree" } ] }

🐳 Docker — 容器化工具

Docker 是部署的核心工具。Agent 可以帮你构建镜像、启停容器、查看日志、管理网络。

// docker_schema.json { "tool_name": "docker", "commands": [ { "name": "docker_build", "risk_level": "safe", "shell_template": "docker build -t {image_name}:{tag} {path}" }, { "name": "docker_compose_up", "risk_level": "medium", "shell": "docker compose -f {file} up -d", "requires_confirm": true }, { "name": "docker_stop_remove", "risk_level": "high", // 停删容器 → Level 3 "shell_template": "docker stop {container} && docker rm {container}", "requires_double_confirm": true } ] }

13.4 CLI 工具的统一调度器

Agent 同时掌握 git、maven、docker、kubectl 等多个 CLI 工具,需要一个统一调度器来:

  • 路由:根据用户意图,选择正确的 CLI 工具
  • 编排:多步命令按顺序执行,前一步的输出作为后一步的输入
  • 容错:命令执行失败时自动重试或切换策略

调度器与 MCP 的关系

CLI 调度器和 MCP 服务器可以是同一套架构。每个 CLI 工具的 Schema 可以直接注册为 MCP Server 的 Tool,让 MCP 协议统一管理调度: 🔗 CLI Schema → MCP Tool 的映射 | CLI Schema | MCP Tool 定义 | 执行方式 | | --- | --- | --- | | git_commit | MCP Tool: git_commit(message) | 子进程执行 git commit -m "{msg}" | | maven_clean_package | MCP Tool: maven_build(skip_test) | 子进程执行 mvn clean package | | docker_compose_up | MCP Tool: docker_deploy(file, env) | 子进程执行 docker compose up -d | 这样,CLI 能力就和第11章 MCP、第12章 Skills 形成完整的工具链闭环🛠️ 第二篇完整工具链闭环

Function Calling → MCP 协议 → Skills 组合 → CLI 执行

Function Calling 定义调用格式 → MCP 统一通信协议 → Skills 组合多步流程 → CLI 在本地沙箱执行

13.5 实战:构建一个 CLI Agent

我们将构建一个最小但完整的 CLI Agent,它能理解自然语言、生成命令、安全执行并返回结果。

这个实战把前面所有概念串成一条可运行的链路:意图理解层解析用户想做什么,命令生成层(NL2Shell)把意图翻译成具体命令,安全执行层在沙箱中运行并捕获结果。三层各司其职又环环相扣——理解错了会生成错误的命令,生成对了但执行环境不安全则可能危及系统。我们刻意保持架构最小,让你看清 CLI Agent 的骨架,而不是被框架细节淹没。

架构代码

"""cli_agent.py — 最小可用的 CLI Agent"""

class CLIAgent:
    def __init__(self, llm_client, tool_schemas):
        self.llm = llm_client            # LLM 客户端
        self.schemas = tool_schemas       # CLI 工具 Schema 注册表
        self.sandbox = CommandSandbox()   # 安全执行沙箱
        self.risk_checker = RiskChecker() # 风险等级校验

    def execute(self, user_input: str, context: dict):
        # Step 1: 意图理解 + 命令生成(Function Calling)
        response = self.llm.chat(
            messages=[
                {"role": "system", "content": self._build_system_prompt()},
                {"role": "user", "content": user_input}
            ],
            tools=self.schemas,  # 注入 CLI Schema 作为 Function Calling 工具
            context=context      # 携带当前目录、项目信息等上下文
        )

        # Step 2: 解析 LLM 返回的工具调用
        tool_calls = response.tool_calls
        if not tool_calls:
            return response.content  # LLM 直接回答,无需执行命令

        results = []
        for call in tool_calls:
            # Step 3: 风险校验
            risk = self.risk_checker.check(call.name, call.arguments)
            if risk == "blocked":
                results.append({"error": "该操作被安全策略阻止"})
                continue
            if risk == "needs_confirm":
                if not self._ask_user_confirm(call):
                    results.append({"error": "用户取消操作"})
                    continue

            # Step 4: 沙箱执行
            shell_cmd = self._render_shell(call)  # Schema → Shell 命令
            output = self.sandbox.run(shell_cmd)
            results.append(output)

        # Step 5: 结果格式化返回
        return self.llm.summarize(results)
/** cli_agent.ts — 最小可用的 CLI Agent */

import { OpenAI } from 'openai';
import { CommandSandbox } from './sandbox';
import { RiskChecker } from './risk-checker';

interface ToolCall {
  name: string;
  arguments: Record;
}

interface LLMResponse {
  content: string;
  tool_calls?: ToolCall[];
}

class CLIAgent {
  private llm: OpenAI;
  private schemas: any[];
  private sandbox: CommandSandbox;
  private riskChecker: RiskChecker;

  constructor(llmClient: OpenAI, toolSchemas: any[]) {
    this.llm = llmClient;              // LLM 客户端
    this.schemas = toolSchemas;        // CLI 工具 Schema 注册表
    this.sandbox = new CommandSandbox();   // 安全执行沙箱
    this.riskChecker = new RiskChecker();  // 风险等级校验
  }

  async execute(userInput: string, context: Record): Promise {
    // Step 1: 意图理解 + 命令生成(Function Calling)
    const response = await this.llm.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: this.buildSystemPrompt() },
        { role: 'user', content: userInput }
      ],
      tools: this.schemas,  // 注入 CLI Schema 作为 Function Calling 工具
    });

    const message = response.choices[0].message;

    // Step 2: 解析 LLM 返回的工具调用
    const toolCalls = message.tool_calls || [];
    if (toolCalls.length === 0) {
      return message.content || '';  // LLM 直接回答,无需执行命令
    }

    const results: any[] = [];
    for (const call of toolCalls) {
      const fnName = call.function.name;
      const fnArgs = JSON.parse(call.function.arguments);

      // Step 3: 风险校验
      const risk = this.riskChecker.check(fnName, fnArgs);
      if (risk === 'blocked') {
        results.push({ error: '该操作被安全策略阻止' });
        continue;
      }
      if (risk === 'needs_confirm') {
        if (!this.askUserConfirm(call)) {
          results.push({ error: '用户取消操作' });
          continue;
        }
      }

      // Step 4: 沙箱执行
      const shellCmd = this.renderShell(fnName, fnArgs);  // Schema → Shell 命令
      const output = this.sandbox.run(shellCmd);
      results.push(output);
    }

    // Step 5: 结果格式化返回
    return this.llm.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: '请将以下命令执行结果格式化为用户友好的回复。' },
        { role: 'user', content: JSON.stringify(results) }
      ]
    }).then(r => r.choices[0].message.content || '');
  }

  private buildSystemPrompt(): string { return ''; }
  private renderShell(name: string, args: any): string { return ''; }
  private askUserConfirm(call: any): boolean { return false; }
}
package main

// cli_agent.go — 最小可用的 CLI Agent

import (
	"context"
	"encoding/json"
	"fmt"

	openai "github.com/sashabaranov/go-openai"
)

type CLIAgent struct {
	LLM         *openai.Client
	Schemas     []interface{}
	Sandbox     *CommandSandbox
	RiskChecker *RiskChecker
}

func NewCLIAgent(llmClient *openai.Client, toolSchemas []interface{}) *CLIAgent {
	return &CLIAgent{
		LLM:         llmClient,              // LLM 客户端
		Schemas:     toolSchemas,            // CLI 工具 Schema 注册表
		Sandbox:     NewCommandSandbox(),    // 安全执行沙箱
		RiskChecker: NewRiskChecker(),       // 风险等级校验
	}
}

func (a *CLIAgent) Execute(ctx context.Context, userInput string, contextData map[string]interface{}) (string, error) {
	// Step 1: 意图理解 + 命令生成(Function Calling)
	resp, err := a.LLM.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
		Model: openai.GPT4o,
		Messages: []openai.ChatCompletionMessage{
			{Role: openai.ChatMessageRoleSystem, Content: a.buildSystemPrompt()},
			{Role: openai.ChatMessageRoleUser, Content: userInput},
		},
		Tools: a.schemasToTools(), // 注入 CLI Schema 作为 Function Calling 工具
	})
	if err != nil {
		return "", err
	}

	msg := resp.Choices[0].Message

	// Step 2: 解析 LLM 返回的工具调用
	if len(msg.ToolCalls) == 0 {
		return msg.Content, nil // LLM 直接回答,无需执行命令
	}

	var results []map[string]interface{}
	for _, call := range msg.ToolCalls {
		var args map[string]interface{}
		json.Unmarshal([]byte(call.Function.Arguments), &args)

		// Step 3: 风险校验
		risk := a.RiskChecker.Check(call.Function.Name, args)
		if risk == "blocked" {
			results = append(results, map[string]interface{}{"error": "该操作被安全策略阻止"})
			continue
		}
		if risk == "needs_confirm" {
			if !a.askUserConfirm(call) {
				results = append(results, map[string]interface{}{"error": "用户取消操作"})
				continue
			}
		}

		// Step 4: 沙箱执行
		shellCmd := a.renderShell(call.Function.Name, args) // Schema → Shell 命令
		output := a.Sandbox.Run(shellCmd)
		results = append(results, output)
	}

	// Step 5: 结果格式化返回
	resultsJSON, _ := json.Marshal(results)
	summaryResp, err := a.LLM.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
		Model: openai.GPT4o,
		Messages: []openai.ChatCompletionMessage{
			{Role: openai.ChatMessageRoleSystem, Content: "请将以下命令执行结果格式化为用户友好的回复。"},
			{Role: openai.ChatMessageRoleUser, Content: string(resultsJSON)},
		},
	})
	if err != nil {
		return fmt.Sprintf("%v", results), nil
	}
	return summaryResp.Choices[0].Message.Content, nil
}
import com.openai.client.OpenAIClient;
import com.openai.models.*;
import java.util.*;

/** cli_agent.java — 最小可用的 CLI Agent */
public class CLIAgent {
    private OpenAIClient llm;              // LLM 客户端
    private List schemas;         // CLI 工具 Schema 注册表
    private CommandSandbox sandbox;        // 安全执行沙箱
    private RiskChecker riskChecker;       // 风险等级校验

    public CLIAgent(OpenAIClient llmClient, List toolSchemas) {
        this.llm = llmClient;
        this.schemas = toolSchemas;
        this.sandbox = new CommandSandbox();
        this.riskChecker = new RiskChecker();
    }

    public String execute(String userInput, Map context) {
        // Step 1: 意图理解 + 命令生成(Function Calling)
        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
            .model("gpt-4o")
            .addSystemMessage(buildSystemPrompt())
            .addUserMessage(userInput)
            .tools(schemas)  // 注入 CLI Schema 作为 Function Calling 工具
            .build();
        ChatCompletion response = llm.chat().completions().create(params);
        ChatCompletionMessage msg = response.choices().get(0).message();

        // Step 2: 解析 LLM 返回的工具调用
        List toolCalls = msg.toolCalls();
        if (toolCalls == null || toolCalls.isEmpty()) {
            return msg.content(); // LLM 直接回答,无需执行命令
        }

        List> results = new ArrayList<>();
        for (ToolCall call : toolCalls) {
            String fnName = call.function().name();
            Map fnArgs = parseArgs(call.function().arguments());

            // Step 3: 风险校验
            String risk = riskChecker.check(fnName, fnArgs);
            if ("blocked".equals(risk)) {
                results.add(Map.of("error", "该操作被安全策略阻止"));
                continue;
            }
            if ("needs_confirm".equals(risk)) {
                if (!askUserConfirm(call)) {
                    results.add(Map.of("error", "用户取消操作"));
                    continue;
                }
            }

            // Step 4: 沙箱执行
            String shellCmd = renderShell(fnName, fnArgs); // Schema → Shell 命令
            Map output = sandbox.run(shellCmd);
            results.add(output);
        }

        // Step 5: 结果格式化返回
        return llm.chat().completions().create(ChatCompletionCreateParams.builder()
            .model("gpt-4o")
            .addSystemMessage("请将以下命令执行结果格式化为用户友好的回复。")
            .addUserMessage(results.toString())
            .build()
        ).choices().get(0).message().content();
    }
}

安全沙箱实现

import subprocess

class CommandSandbox:
    """安全执行 Shell 命令的沙箱"""

    ALLOWED_COMMANDS = {  # Level 1 白名单
        "git status", "git log", "git diff",
        "ls", "cat", "pwd", "echo",
        "docker ps", "docker images",
        "mvn dependency:tree", "mvn help",
    }

    BLOCKED_PATTERNS = [  # Level 3 黑名单
        "rm -rf", "mkfs", "dd",
        "shutdown", "reboot", ":(){ :|:& };:",
    ]

    def run(self, command: str, timeout=30) -> dict:
        # 检查黑名单
        for pattern in self.BLOCKED_PATTERNS:
            if pattern in command:
                return {"error": f"危险命令已被拦截: {pattern}"}

        # 子进程隔离执行,限制超时和资源
        proc = subprocess.run(
            command, shell=True,
            capture_output=True, text=True,
            timeout=timeout,
            cwd=self.working_dir
        )
        return {
            "exit_code": proc.returncode,
            "stdout": proc.stdout[:5000],   # 截断过长输出
            "stderr": proc.stderr[:2000],
        }
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

class CommandSandbox {
  // Level 1 白名单
  static ALLOWED_COMMANDS = new Set([
    'git status', 'git log', 'git diff',
    'ls', 'cat', 'pwd', 'echo',
    'docker ps', 'docker images',
    'mvn dependency:tree', 'mvn help',
  ]);

  // Level 3 黑名单
  static BLOCKED_PATTERNS = [
    'rm -rf', 'mkfs', 'dd',
    'shutdown', 'reboot', ':(){ :|:& };:',
  ];

  workingDir: string;

  constructor(workingDir: string) {
    this.workingDir = workingDir;
  }

  async run(command: string, timeout = 30): Promise> {
    // 检查黑名单
    for (const pattern of CommandSandbox.BLOCKED_PATTERNS) {
      if (command.includes(pattern)) {
        return { error: `危险命令已被拦截: ${pattern}` };
      }
    }

    // 子进程隔离执行,限制超时和资源
    try {
      const { stdout, stderr } = await execAsync(command, {
        cwd: this.workingDir,
        timeout: timeout * 1000,
        maxBuffer: 1024 * 1024,
      });
      return {
        exit_code: 0,
        stdout: stdout.slice(0, 5000),
        stderr: stderr.slice(0, 2000),
      };
    } catch (e: any) {
      return {
        exit_code: e.code || 1,
        stdout: (e.stdout || '').slice(0, 5000),
        stderr: (e.stderr || '').slice(0, 2000),
      };
    }
  }
}
package main

import (
	"os/exec"
	"strings"
	"time"
)

// CommandSandbox 安全执行 Shell 命令的沙箱
type CommandSandbox struct {
	workingDir string
}

// Level 1 白名单
var allowedCommands = map[string]bool{
	"git status": true, "git log": true, "git diff": true,
	"ls": true, "cat": true, "pwd": true, "echo": true,
	"docker ps": true, "docker images": true,
	"mvn dependency:tree": true, "mvn help": true,
}

// Level 3 黑名单
var blockedPatterns = []string{
	"rm -rf", "mkfs", "dd",
	"shutdown", "reboot", ":(){ :|:& };:",
}

func (s *CommandSandbox) Run(command string, timeout int) map[string]interface{} {
	// 检查黑名单
	for _, pattern := range blockedPatterns {
		if strings.Contains(command, pattern) {
			return map[string]interface{}{"error": "危险命令已被拦截: " + pattern}
		}
	}

	// 子进程隔离执行,限制超时和资源
	cmd := exec.Command("sh", "-c", command)
	cmd.Dir = s.workingDir

	timer := time.AfterFunc(time.Duration(timeout)*time.Second, func() {
		cmd.Process.Kill()
	})
	defer timer.Stop()

	stdout, stderr := "", ""
	if out, err := cmd.Output(); err == nil {
		stdout = string(out)
	} else if ee, ok := err.(*exec.ExitError); ok {
		stdout = string(ee.Stdout)
		stderr = string(ee.Stderr)
	}

	// 截断过长输出
	if len(stdout) > 5000 {
		stdout = stdout[:5000]
	}
	if len(stderr) > 2000 {
		stderr = stderr[:2000]
	}

	return map[string]interface{}{
		"exit_code": cmd.ProcessState.ExitCode(),
		"stdout":    stdout,
		"stderr":    stderr,
	}
}
import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class CommandSandbox {
    private String workingDir;

    // Level 1 白名单
    private static final Set ALLOWED_COMMANDS = Set.of(
        "git status", "git log", "git diff",
        "ls", "cat", "pwd", "echo",
        "docker ps", "docker images",
        "mvn dependency:tree", "mvn help"
    );

    // Level 3 黑名单
    private static final List BLOCKED_PATTERNS = List.of(
        "rm -rf", "mkfs", "dd",
        "shutdown", "reboot", ":(){ :|:& };:"
    );

    public CommandSandbox(String workingDir) {
        this.workingDir = workingDir;
    }

    public Map run(String command, int timeout) {
        // 检查黑名单
        for (String pattern : BLOCKED_PATTERNS) {
            if (command.contains(pattern)) {
                return Map.of("error", "危险命令已被拦截: " + pattern);
            }
        }

        // 子进程隔离执行,限制超时和资源
        try {
            ProcessBuilder pb = new ProcessBuilder("sh", "-c", command);
            pb.directory(new File(workingDir));
            pb.redirectErrorStream(false);

            Process process = pb.start();

            // 超时控制
            if (!process.waitFor(timeout, TimeUnit.SECONDS)) {
                process.destroyForcibly();
                return Map.of("error", "命令执行超时");
            }

            String stdout = readStream(process.getInputStream(), 5000);
            String stderr = readStream(process.getErrorStream(), 2000);

            Map result = new LinkedHashMap<>();
            result.put("exit_code", process.exitValue());
            result.put("stdout", stdout);
            result.put("stderr", stderr);
            return result;
        } catch (Exception e) {
            return Map.of("error", e.getMessage());
        }
    }

    private String readStream(InputStream is, int maxLen) throws IOException {
        String content = new String(is.readAllBytes());
        if (content.length() > maxLen) {
            content = content.substring(0, maxLen);
        }
        return content;
    }
}

13.6 CLI Agent 的错误自愈

命令执行失败是常态——参数错误、环境缺失、权限不足。好的 CLI Agent 不是简单报错,而是自动诊断并修复

错误自愈是 CLI Agent 区别于"一次性命令生成器"的关键能力。真实环境中,命令失败的概率远高于理想情况:目标路径不存在、依赖工具未安装、权限被拒、网络超时……如果每次失败都要人工介入,Agent 就失去了"自主"的意义。自愈的核心思路是把错误信息本身作为新的上下文反馈给 LL让它像人类运维一样"看报错→想原因→换方案重试"。这个"执行→观察错误→修正→再执行"的小循环,正是第 5 章 ReAct 模式在 CLI 场景的具体落地。 🔄 错误自愈:四种典型场景 | 错误类型 | 示例 | 自愈策略 | | --- | --- | --- | | 命令不存在 | mvn: command not found | 检测 Maven 是否安装,建议安装命令 | | 参数错误 | git push: no upstream set | 自动加 --set-upstream origin branch 重试 | | 权限不足 | Permission denied | 提示需要 sudo,请求用户确认后重试 | | 依赖缺失 | ModuleNotFoundError | 先执行 pip install,再重新运行 | 错误自愈的核心思路:把 stderr 反馈给 LLM,让它分析原因并生成修正命令,最多重试 3 次:

def self_heal_loop(agent, user_input, max_retries=3): for attempt in range(max_retries): result = agent.execute(user_input) if result["exit_code"] == 0: return result # 成功!

失败 → 把错误信息反馈给 LLM 重新规划

error_msg = result["stderr"] user_input = f"上次执行失败,错误信息:{error_msg}\n请修正命令重新执行。"

return {"error": "重试3次仍然失败,请人工介入"}

13.7 CLI Agent 与 MCP + Skills 的组合

单独的 CLI 能力只是"一双手"。但配合 MCP(通信协议)和 Skills(组合复用),CLI Agent 就成了一个完整的智能体🧩 CLI × MCP × Skills 组合实战

场景:用户说 "帮我部署到测试环境"

  • Skills 编排:识别意图 → 加载 "deploy-to-test" Skill → 规划多步流程
  • MCP 通信:Skill 调用 MCP Tool → MCP Client 路由到 CLI MCP Server
  • CLI 执行:MCP Server 把 Tool 调用翻译成 Shell 命令 → 沙箱执行 mvn packagedocker builddocker compose up
  • 结果返回:CLI 输出 → MCP 响应 → Skills 判断是否成功 → LLM 格式化回复

这就是第二篇四个章节的完整闭环📖 第二篇知识脉络总结 | 章节 | 核心问题 | 解决什么 | | --- | --- | --- | | 第10章 Function Calling | LLM 如何调用外部工具? | 定义调用格式和参数 Schema | | 第11章 MCP | 工具调用如何标准化? | 统一通信协议,解耦 Client/Server | | 第12章 Skills | 工具如何组合与复用? | 多工具编排、懒加载、沉淀机制 | | 第13章 CLI 能力 | Agent 如何操控本地工具? | 本地命令行执行 + 安全沙箱 + 错误自愈 | 📋 八股总结 — 面试高频考点

Q1 CLI 能力和 MCP 有什么区别?什么时候用 CLI,什么时候用 MCP?

MCP通信协议,解决"怎么调工具"的问题(格式标准化、解耦 Client/Server)。CLI 能力执行方式,解决"怎么在本地执行命令"的问题(Shell 生成、安全沙箱、错误自愈)。

两者互补而非替代:CLI 工具可以通过 MCP Server 注册为 Tool,MCP Client 调用后由 CLI 沙箱执行。远程 API 用 MCP 直接调,本地工具用 CLI 执行。

Q2 NL2Shell 的三种实现路径各有什么优缺点?企业级场景推荐哪种?

直接生成:LLM 直接输出 Shell 命令,简单但准确率 70~85%,容易产生幻觉命令。

Few-Shot 模板:加常用命令示例库提升准确率到 85~92%,但模板库维护成本高。

工具 Schema:用 Function Calling 定义命令 Schema,准确率 92~97%,参数可控、安全可审计。企业级推荐此方案

Q3 CLI Agent 的安全策略如何设计?三级分类的依据是什么?

Level 1 白名单(自动执行):只读命令,无修改、无破坏性,如 git statuslsdocker ps

Level 2 确认执行:有修改但可控,如 git pushdocker restart,需用户 Y/N 确认。

Level 3 拒绝/二次审批:不可逆操作,如 rm -rfshutdown,默认拒绝,特殊场景管理员审批。

分类依据:是否可逆 + 影响范围 + 数据丢失风险

Q4 CLI Agent 的错误自愈机制怎么实现?有什么局限性?

核心思路:把 stderr 反馈给 LLM,让它分析原因并生成修正命令,最多重试 3 次。

四种典型场景:命令不存在→建议安装;参数错误→自动修正;权限不足→加 sudo 重试;依赖缺失→先安装再执行。

局限性:① 循环重试可能产生副作用;② 某些错误 LLM 无法判断原因;③ sudo 确认不能自动绕过。企业级需设重试上限和回滚机制。

Q5 为什么 CLI 能力要单独成章,而不是合并到 MCP 章节?

因为 CLI 能力的技术栈和挑战与 MCP 完全不同

① MCP 是协议层问题(JSON-RPC、握手、能力协商);CLI 是执行层问题(Shell 生成、子进程隔离、资源限制)。

② MCP 处理的是远程 API 调用;CLI 处理的是本地系统级操作,安全模型完全不同。

③ CLI 有独特的错误自愈NL2Shell沙箱隔离问题,这些在 MCP 章节无法深入覆盖。

13.8 NL2Shell 实战:Few-Shot 提升准确率

NL2Shell(自然语言转 Shell 命令)是 CLI Agent 的核心能力。纯零样本(zero-shot)生成的命令准确率约 60-70%,通过 Few-Shot 示例 + 工具 Schema 可提升至 90%+。

零样本之所以不准,是因为自然语言存在大量歧义:"找出大文件"是找 >100MB 还是 >1GB?"最近修改"是 24 小时内还是一周内?LLM 在没有任何参照时会自行猜测,猜错就生成错误命令。Few-Shot 的作用是给模型提供带标准答案的范例,把模糊的意图锚定到具体的命令模式上。实验表明,精心设计的 3-5 个示例就能覆盖 80% 的常见运维场景,让准确率产生质的飞跃。这也是"提示词工程"在 CLI 领域最直接的收益点。

Python TypeScript

# nl2shell.py — 自然语言转 Shell 命令引擎
from dataclasses import dataclass
from typing import List, Optional

# Few-Shot 示例库(按意图分类)
FEW_SHOT_EXAMPLES = {
    "查看进程": [
        {"input": "查看占用 8080 端口的进程", "command": "lsof -i :8080"},
        {"input": "查看 CPU 占用最高的 5 个进程", "command": "ps aux --sort=-%cpu | head -5"},
        {"input": "查看内存使用情况", "command": "free -h"},
    ],
    "文件操作": [
        {"input": "找出 /var/log 下大于 100MB 的日志文件", "command": "find /var/log -type f -size +100M"},
        {"input": "统计当前目录下各类型文件数量", "command": "find . -type f | sed 's/.*\\.//' | sort | uniq -c | sort -rn"},
        {"input": "批量将 .txt 文件重命名为 .md", "command": "for f in *.txt; do mv \"$f\" \"${f%.txt}.md\"; done"},
    ],
    "网络诊断": [
        {"input": "检查到 github.com 的网络连通性", "command": "ping -c 3 github.com"},
        {"input": "查看当前网络连接数", "command": "ss -s"},
        {"input": "抓取 80 端口的 HTTP 请求", "command": "tcpdump -i any port 80 -A -s 0"},
    ],
    "Docker运维": [
        {"input": "查看所有运行中的容器", "command": "docker ps"},
        {"input": "清理已停止的容器和悬空镜像", "command": "docker container prune -f && docker image prune -f"},
        {"input": "查看 nginx 容器最近 50 行日志", "command": "docker logs --tail 50 nginx"},
    ],
}

@dataclass
class NL2ShellResult:
    command: str
    confidence: float
    risk_level: str  # safe / medium / high
    explanation: str

class NL2ShellEngine:
    """NL2Shell 引擎:Few-Shot + 工具 Schema + 安全分级"""

    # 高危命令模式
    DANGEROUS_PATTERNS = [
        r'rm\s+-rf\s+/',       # 递归删除根目录
        r'mkfs\.\w+\s+/dev',   # 格式化磁盘
        r'dd\s+.*of=/dev/sd',  # 直接写磁盘
        r':\(\)\{.*\};:',      # Fork bomb
        r'chmod\s+-R\s+777\s+/',  # 递归 777
    ]

    def generate(self, user_input: str, context: str = "") -> NL2ShellResult:
        """将自然语言转为 Shell 命令"""
        # 1. 意图分类 → 选择 Few-Shot 示例
        category = self._classify_intent(user_input)
        examples = FEW_SHOT_EXAMPLES.get(category, [])

        # 2. 构建 Prompt(Few-Shot + 工具 Schema)
        prompt = self._build_prompt(user_input, examples, context)

        # 3. 调用 LLM 生成命令
        raw_command = self._call_llm(prompt)

        # 4. 安全分级
        risk = self._assess_risk(raw_command)

        # 5. 清理(去掉多余的解释文字)
        command = self._extract_command(raw_command)

        return NL2ShellResult(
            command=command,
            confidence=0.92 if examples else 0.65,
            risk_level=risk,
            explanation=f"分类: {category}, Few-Shot: {len(examples)} examples"
        )

    def _classify_intent(self, text: str) -> str:
        """简单意图分类"""
        if any(w in text for w in ["进程", "端口", "CPU", "内存"]):
            return "查看进程"
        if any(w in text for w in ["文件", "查找", "重命名", "日志"]):
            return "文件操作"
        if any(w in text for w in ["网络", "ping", "连通", "抓包"]):
            return "网络诊断"
        if any(w in text for w in ["docker", "容器", "镜像"]):
            return "Docker运维"
        return "general"

    def _assess_risk(self, command: str) -> str:
        import re
        for pattern in self.DANGEROUS_PATTERNS:
            if re.search(pattern, command):
                return "high"
        if any(w in command for w in ["rm ", "kill ", "drop ", "delete "]):
            return "medium"
        return "safe"

# === 使用示例 ===
engine = NL2ShellEngine()

# 测试几个例子
test_cases = [
    "找出 /tmp 下 7 天前的临时文件",
    "查看 nginx 容器的资源使用",
    "批量压缩当前目录的 .log 文件",
]

for tc in test_cases:
    result = engine.generate(tc)
    print(f"📝 {tc}")
    print(f"   → {result.command}")
    print(f"   风险: {result.risk_level}, 置信度: {result.confidence:.0%}")
    print()
# 输出:
# 📝 找出 /tmp 下 7 天前的临时文件
#    → find /tmp -type f -mtime +7
#    风险: safe, 置信度: 92%
#
# 📝 查看 nginx 容器的资源使用
#    → docker stats nginx --no-stream
#    风险: safe, 置信度: 92%
#
# 📝 批量压缩当前目录的 .log 文件
#    → for f in *.log; do gzip "$f"; done
#    风险: medium, 置信度: 92%
// nl2shell.ts
interface NL2ShellResult {
  command: string;
  confidence: number;
  riskLevel: 'safe' | 'medium' | 'high';
}

const FEW_SHOT_EXAMPLES: Record> = {
  '查看进程': [
    { input: '查看占用 8080 端口的进程', command: 'lsof -i :8080' },
    { input: '查看 CPU 占用最高的进程', command: 'ps aux --sort=-%cpu | head -5' },
  ],
  '文件操作': [
    { input: '找出大于 100MB 的文件', command: 'find . -type f -size +100M' },
    { input: '统计文件类型', command: 'find . -type f | sed \'s/.*\\.//\' | sort | uniq -c' },
  ],
};

export function generateCommand(userInput: string): NL2ShellResult {
  // 意图分类 → Few-Shot → LLM → 安全分级
  const category = classifyIntent(userInput);
  const examples = FEW_SHOT_EXAMPLES[category] || [];

  return {
    command: '', // LLM 生成
    confidence: examples.length > 0 ? 0.92 : 0.65,
    riskLevel: 'safe',
  };
}

13.9 SSH Agent:远程运维场景

企业级 Agent 不仅要操控本地环境,还需要通过 SSH 远程操控服务器。这是 DevOps 场景的核心能力,也是企业招聘面试的高频话题。以 WaLiCode 为例,其 SSH DevOps 模式允许 Agent 通过 SSH 连接到远程服务器执行命令、管理文件、部署应用。

13.9.1 SSH Agent 架构

SSH Agent 三层架构

① 连接管理层:SSH 连接池(保活 + 自动重连 + 密钥管理)

② 命令执行层:远程命令执行 + 流式输出 + 超时控制

③ 安全沙箱层:命令预检 + 权限分级 + 审计日志

WaLiCode 使用 Rust 的 russh 库实现纯 Rust SSH 客户端,避免了对系统 ssh 命令的依赖,跨平台兼容性更好。

Python (paramiko) TypeScript (ssh2)

# ssh_agent.py — SSH 远程执行 Agent
import paramiko
from dataclasses import dataclass
from typing import Optional
import re

@dataclass
class SSHConfig:
    host: str
    port: int = 22
    username: str = "root"
    key_path: str = "~/.ssh/id_rsa"

@dataclass
class CommandResult:
    stdout: str
    stderr: str
    exit_code: int
    duration_ms: int

class SSHAgent:
    """SSH 远程执行 Agent"""

    # 远程高危命令模式
    REMOTE_DANGEROUS = [
        r'rm\s+-rf\s+/',
        r'shutdown',
        r'reboot',
        r'mkfs',
        r'iptables\s+-F',  # 清空防火墙规则
    ]

    def __init__(self, config: SSHConfig):
        self.config = config
        self.client: Optional[paramiko.SSHClient] = None

    def connect(self):
        """建立 SSH 连接"""
        self.client = paramiko.SSHClient()
        self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.client.connect(
            hostname=self.config.host,
            port=self.config.port,
            username=self.config.username,
            key_filename=self.config.key_path,
            timeout=10
        )
        print(f"✅ SSH 连接成功: {self.config.host}")

    def execute(self, command: str, timeout: int = 30) -> CommandResult:
        """远程执行命令(带安全检查)"""
        # 安全预检
        risk = self._check_command(command)
        if risk == "high":
            return CommandResult("", "命令被安全策略拒绝", -1, 0)

        if not self.client:
            raise RuntimeError("SSH 未连接,请先调用 connect()")

        import time
        start = time.time()

        stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
        exit_code = stdout.channel.recv_exit_status()

        duration_ms = int((time.time() - start) * 1000)

        return CommandResult(
            stdout=stdout.read().decode('utf-8'),
            stderr=stderr.read().decode('utf-8'),
            exit_code=exit_code,
            duration_ms=duration_ms
        )

    def execute_streaming(self, command: str, on_line=None):
        """流式执行命令(实时输出每行)"""
        if not self.client:
            raise RuntimeError("SSH 未连接")

        stdin, stdout, stderr = self.client.exec_command(command)
        for line in stdout:
            if on_line:
                on_line(line.strip())
            else:
                print(line.strip())

    def upload(self, local_path: str, remote_path: str):
        """上传文件"""
        sftp = self.client.open_sftp()
        sftp.put(local_path, remote_path)
        sftp.close()
        print(f"✅ 上传: {local_path} → {remote_path}")

    def _check_command(self, command: str) -> str:
        """命令安全分级"""
        for pattern in self.REMOTE_DANGEROUS:
            if re.search(pattern, command):
                return "high"
        if any(w in command for w in ["rm ", "kill ", "chmod 777"]):
            return "medium"
        return "safe"

    def close(self):
        if self.client:
            self.client.close()
            print("SSH 连接已关闭")

# === 使用示例:远程部署 ===
agent = SSHAgent(SSHConfig(
    host="192.168.1.100",
    username="deploy",
    key_path="~/.ssh/deploy_key"
))

agent.connect()

# 1. 检查远程环境
result = agent.execute("uname -a && docker --version")
print(f"系统信息:\n{result.stdout}")

# 2. 拉取最新代码
result = agent.execute("cd /opt/myapp && git pull origin main")
print(f"部署结果: exit={result.exit_code}")

# 3. 重启服务(流式输出日志)
agent.execute_streaming(
    "cd /opt/myapp && docker compose up -d --build",
    on_line=lambda line: print(f"  {line}")
)

agent.close()
# 输出:
# ✅ SSH 连接成功: 192.168.1.100
# 系统信息: Linux server1 5.15.0 ... x86_64 GNU/Linux
# Docker version 24.0.7
# 部署结果: exit=0
#   Building myapp
#   Container myapp  Starting
#   Container myapp  Started
# SSH 连接已关闭
// ssh-agent.ts — 基于 ssh2 的 TypeScript 实现
import { Client, ConnectConfig } from 'ssh2';

interface CommandResult {
  stdout: string;
  stderr: string;
  exitCode: number;
  durationMs: number;
}

export class SSHAgent {
  private conn: Client;

  constructor(private config: ConnectConfig) {
    this.conn = new Client();
  }

  connect(): Promise {
    return new Promise((resolve, reject) => {
      this.conn.on('ready', () => resolve());
      this.conn.on('error', reject);
      this.conn.connect(this.config);
    });
  }

  execute(command: string, timeout = 30): Promise {
    return new Promise((resolve, reject) => {
      const start = Date.now();
      this.conn.exec(command, (err, stream) => {
        if (err) return reject(err);
        let stdout = '', stderr = '';
        stream.on('close', (code) => {
          resolve({
            stdout, stderr,
            exitCode: code ?? -1,
            durationMs: Date.now() - start,
          });
        });
        stream.on('data', (data) => stdout += data);
        stream.stderr.on('data', (data) => stderr += data);
      });
    });
  }

  close() {
    this.conn.end();
  }
}

// 使用示例
const agent = new SSHAgent({
  host: '192.168.1.100',
  port: 22,
  username: 'deploy',
  privateKey: require('fs').readFileSync('/home/user/.ssh/id_rsa'),
});

await agent.connect();
const result = await agent.execute('docker ps');
console.log(result.stdout);
agent.close();

13.9.2 SSH Agent 安全要点 | 安全要点 | 实现策略 | 面试要点 | | --- | --- | --- | | 密钥管理 | AES-GCM 加密存储,不硬编码 | "密钥怎么存?"→ 加密 + 系统钥匙链 | | 命令预检 | 远端命令也要做危险模式匹配 | "远程执行和本地有什么区别?"→ 安全策略相同,但执行环境不可控 | | 审计日志 | 记录谁在什么时候对哪台服务器执行了什么命令 | "合规怎么保证?"→ 全量审计 + 告警 | | 连接隔离 | 不同用户/项目的 SSH 连接隔离 | "多租户怎么隔离?"→ 连接池 + 会话绑定 | | 超时控制 | 命令执行超时自动断开 | "Agent 挂住怎么办?"→ 超时 + 心跳 + 自动重连 |

第13章 CLI能力-Agent操作本地工具
http://www.clxhxhhr.top/posts/716/
作者
clxstart
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。