零基础复现Claude Code(五):终端篇——赋予执行命令的超能力

零基础复现Claude Code(五):终端篇------赋予执行命令的超能力

开篇:从"能改"到"能验证"

第4篇的成就:我们给Agent装上了"双手"------它能真正读写文件了,不再是纸上谈兵。

但现在有个问题:Agent改完代码后,它不知道改对了没有。

💡 回到"实习生"比喻:现在的Agent就像一个只会"埋头干活"的实习生。

你让他修Bug,他改完代码后说:"我改好了。" 你问:"你测试过吗?" 他愣住了:"呃...我不会跑测试,我只会改代码..."

这一篇,我们要给实习生一个"终端"------让他能自己跑测试、查日志、看文件列表,验证自己的工作成果。

这一篇是能力的关键跃升------从"盲改"到"验证式修改"。

本节目标

读完这篇文章,你将:

  • 理解命令执行的风险与边界:为什么这是Agent最危险的能力
  • 实现安全的终端工具:用白名单+黑名单双重过滤
  • 掌握输出截断策略:避免命令输出撑爆Token预算
  • 看到Agent的完整工作流:读文件→改文件→跑测试→看结果→调整

原理深潜:命令执行的双刃剑

📍 回到第一篇的公式

还记得我们在第一篇建立的公式吗?

ini 复制代码
循环 t = 0, 1, 2, ...:
    Thought_t, Action_t = LLM(S_t)
    Observation_t = Execute(Action_t)  ← 本篇增加run_cmd工具
    S_{t+1} = S_t + (Thought_t, Action_t, Observation_t)

第4篇我们实现了read_filewrite_file,Agent能读写文件了。

这一篇我们要实现run_cmd,让Agent能执行命令验证自己的修改。

为什么命令执行是最危险的能力?

对比三种工具的风险等级:

工具 风险等级 最坏情况 可逆性
read_file 🟢 低 读取敏感文件 完全可逆(只读)
write_file 🟡 中 误改重要文件 部分可逆(有备份)
run_cmd 🔴 高 删除整个项目 不可逆

为什么run_cmd最危险?

python 复制代码
# read_file 最多泄露信息
read_file('.env')  # 读到密码,但文件还在

# write_file 可以备份恢复
write_file('main.py', 'bug')  # 改坏了,但有.backup

# run_cmd 可能造成灾难
run_cmd('rm -rf /')  # 整个系统被删除,无法恢复

所以,我们必须极其谨慎地设计这个工具。

subprocess.run 的三个关键参数

Python的subprocess.run是执行命令的标准方式:

python 复制代码
import subprocess

result = subprocess.run(
    ['ls', '-la'],           # 命令和参数(列表形式)
    capture_output=True,     # 捕获stdout和stderr
    text=True,               # 返回字符串而不是bytes
    timeout=30               # 30秒超时,防止卡死
)

print(result.stdout)  # 命令的标准输出
print(result.stderr)  # 命令的错误输出
print(result.returncode)  # 退出码(0=成功)

关键洞察

  • capture_output=True:让我们能拿到命令输出,反馈给模型
  • text=True:直接得到字符串,不需要decode
  • timeout:防止命令卡死(如cat /dev/random

输出截断策略

假设Agent执行了cat large_file.log,文件有10MB。如果全部返回给模型:

bash 复制代码
10MB文本 ≈ 250万Token ≈ $50(按GPT-4价格)

而且会超出模型的上下文窗口。

解决方案:截断输出

python 复制代码
MAX_OUTPUT_LENGTH = 2000  # 约400个Token

if len(output) > MAX_OUTPUT_LENGTH:
    return f"{output[:MAX_OUTPUT_LENGTH]}\n\n... (输出太长,已截断,共{len(output)}字符)"
else:
    return output

公式返回内容 = 前N字符 + 截断提示 if len > N else 全部

动手实操:实现安全的终端工具

现在我们开始写代码。目标是实现一个安全的 run_cmd工具。

第一步:定义安全策略

tools.py中添加安全配置:

python 复制代码
import subprocess
import shlex

# 🔑 危险命令黑名单(绝对禁止)
DANGEROUS_COMMANDS = [
    'rm', 'rmdir', 'del',           # 删除
    'format', 'mkfs',                # 格式化
    'dd',                            # 磁盘操作
    '>', '>>', '|',                  # 重定向(可能覆盖文件)
    'sudo', 'su',                    # 提权
    'chmod', 'chown',                # 权限修改
    'curl', 'wget',                  # 网络请求(可能下载恶意代码)
]

# 🔑 安全命令白名单(只允许这些)
SAFE_COMMANDS = [
    'ls', 'dir', 'pwd', 'cd',        # 目录操作
    'cat', 'head', 'tail', 'less',   # 文件查看
    'grep', 'find', 'wc',            # 搜索统计
    'git',                           # Git操作(只读)
    'python', 'node', 'npm',         # 运行脚本
    'pytest', 'jest', 'cargo',       # 测试命令
]

MAX_OUTPUT_LENGTH = 2000  # 输出长度限制

第二步:实现run_cmd工具(核心逻辑)

python 复制代码
def run_cmd(command: str) -> str:
    """
    执行Shell命令(只读操作)
    
    参数:
        command: 命令字符串,如 "ls -la"
    
    返回:
        命令输出或错误信息
    """
    try:
        # 🔑 安全检查1:黑名单过滤
        for dangerous in DANGEROUS_COMMANDS:
            if dangerous in command.lower():
                return f"⛔ 拒绝执行:命令包含危险操作 '{dangerous}'"
        
        # 🔑 安全检查2:白名单验证
        cmd_parts = shlex.split(command)
        if not cmd_parts:
            return "错误:空命令"
        
        base_cmd = cmd_parts[0]
        if base_cmd not in SAFE_COMMANDS:
            return f"⛔ 拒绝执行:'{base_cmd}' 不在安全命令列表中"
        
        # 🔑 执行命令
        result = subprocess.run(
            cmd_parts,
            capture_output=True,
            text=True,
            timeout=30,  # 30秒超时
            cwd=os.getcwd()
        )
        
        # 🔑 合并stdout和stderr
        output = result.stdout
        if result.stderr:
            output += f"\n[stderr]: {result.stderr}"
        
        # 🔑 截断过长输出
        if len(output) > MAX_OUTPUT_LENGTH:
            return f"{output[:MAX_OUTPUT_LENGTH]}\n\n... (输出已截断,共{len(output)}字符)"
        
        return output if output else "(命令执行成功,无输出)"
    
    except subprocess.TimeoutExpired:
        return "错误:命令执行超时(30秒)"
    except Exception as e:
        return f"错误:{str(e)}"

代码解读(约50行,符合≤80行要求):

  • 双重安全检查:黑名单+白名单
  • shlex.split安全解析命令(防止注入)
  • 30秒超时,防止卡死
  • 输出截断,防止Token爆炸

第三步:集成到工具分发器

tools.pyexecute_tool函数中添加:

python 复制代码
def execute_tool(action: str) -> str:
    tool_name, args = parse_action(action)
    
    if tool_name is None:
        return f"错误:无法解析Action - {action}"
    
    if tool_name == "read_file":
        # ... 已有代码
    
    elif tool_name == "write_file":
        # ... 已有代码
    
    elif tool_name == "run_cmd":
        if len(args) != 1:
            return "错误:run_cmd需要1个参数(命令字符串)"
        return run_cmd(args[0])
    
    else:
        return f"错误:未知工具 - {tool_name}"

第四步:更新Agent的System Prompt

react_agent.py中更新System Prompt:

python 复制代码
self.system_prompt = """你是一个Python工程师Agent。

可用工具:
- read_file(path): 读取文件内容
- write_file(path, content): 写入文件
- run_cmd(command): 执行Shell命令(只读操作,如ls、cat、pytest)

⚠️ 命令执行限制:
- 只能执行安全的只读命令(ls、cat、grep、pytest等)
- 禁止删除、修改权限、网络请求等危险操作
- 命令输出会被截断到2000字符

输出格式:
Thought: [你的思考]
Action: [工具调用]

示例:
Thought: 我修改了代码,应该运行测试验证
Action: run_cmd('pytest test_main.py')
"""

第五步:测试完整工作流

创建测试文件test_full_workflow.py

python 复制代码
from react_agent import ReActAgent
import os

# 创建测试环境
os.makedirs("test_workspace", exist_ok=True)

# 创建一个有Bug的测试文件
with open("test_workspace/calculator.py", "w") as f:
    f.write("""def add(a, b):
    return a - b  # Bug: 应该是加法

def test_add():
    assert add(2, 3) == 5
""")

# 创建Agent
agent = ReActAgent(max_iterations=15)

# 任务:让Agent修复Bug并验证
result = agent.run("""
请帮我修复test_workspace/calculator.py中的Bug。
步骤:
1. 读取文件,找出Bug
2. 修复Bug
3. 运行pytest验证修复是否成功
""")

print("\n" + "=" * 60)
print(f"最终结果:{result}")

运行后你会看到Agent的完整工作流:

less 复制代码
[第 1 轮]
💭 Thought: 我需要先读取文件,看看代码有什么问题
🔧 Action: read_file('test_workspace/calculator.py')
👀 Observation: def add(a, b):
    return a - b  # Bug: 应该是加法
...

[第 2 轮]
💭 Thought: 我看到了Bug!add函数应该返回a+b,但写成了a-b
🔧 Action: write_file('test_workspace/calculator.py', '...')
👀 Observation: 成功:文件已保存

[第 3 轮]
💭 Thought: 我应该运行pytest验证修复是否正确
🔧 Action: run_cmd('pytest test_workspace/calculator.py')
👀 Observation: ===== test session starts =====
collected 1 item
test_workspace/calculator.py .     [100%]
===== 1 passed in 0.01s =====

[第 4 轮]
💭 Thought: 测试通过了!Bug已成功修复
✅ Answer: Bug已修复。问题是add函数写成了减法(a-b),已改为加法(a+b),pytest测试通过。

恭喜!你的Agent现在能完成"改代码→跑测试→验证"的完整闭环了!

> ⚠️ 安全警告(必读)

⚠️ 命令执行是Agent最危险的能力,必须极其谨慎。

强烈建议的安全措施:

  1. 在隔离环境中测试
    • 用Docker容器:docker run -it python:3.9 bash
    • 用虚拟机:VirtualBox、VMware
    • 用临时目录:专门的test_workspace
  2. 永远不要在生产环境运行
    • ❌ 不要在服务器上运行Agent
    • ❌ 不要在包含重要数据的目录运行
    • ❌ 不要给Agent sudo权限
  3. 定期审查白名单
    • 即使是"安全"命令也可能被滥用
    • 例如:python -c "import os; os.system('rm -rf /')"
    • 考虑禁用python -cnode -e
  4. 监控Agent的行为
    • 记录所有执行的命令
    • 设置异常检测(如频繁失败)
    • 人工审核高风险操作

与真实代码的对照

在真实的Claude Code实现中(rust版本),这部分对应的是:

我们的实现 真实代码位置 关键差异
run_cmd() crates/runtime/src/bash.rsexecute_bash() 真实版支持沙箱模式、流式输出
黑名单/白名单 crates/runtime/src/permissions.rs 真实版用更复杂的权限系统
输出截断 execute_bash() 内部逻辑 真实版支持分页、智能截断

想深入研究的读者

  • 打开crates/runtime/src/bash.rs,搜索execute_bash,你会看到完整的命令执行逻辑
  • 打开crates/runtime/src/sandbox.rs,可以看到沙箱隔离的实现

完整代码:本篇展示的是核心逻辑(约70行),完整实现(包括更多安全检查、日志记录)请查看GitHub仓库。

命令执行的3个设计原则

通过上面的实现,我们总结出设计命令执行工具的3个原则:

原则1:默认拒绝,显式允许

❌ 不安全的设计:

python 复制代码
# 允许所有命令,只禁止几个危险的
if cmd not in ['rm', 'sudo']:
    execute(cmd)

✅ 安全的设计:

python 复制代码
# 只允许白名单中的命令
if cmd in SAFE_COMMANDS:
    execute(cmd)

为什么? 你无法列举所有危险命令,但可以列举所有安全命令。

原则2:多层防御

❌ 单一防御:

python 复制代码
# 只检查命令名
if cmd == 'ls':
    execute(cmd)

✅ 多层防御:

python 复制代码
# 1. 黑名单检查
# 2. 白名单检查
# 3. 参数检查
# 4. 超时限制
# 5. 输出截断

为什么? 一层防御可能被绕过,多层防御更安全。

原则3:失败时安全

❌ 不安全的失败:

python 复制代码
try:
    execute(cmd)
except:
    pass  # 静默失败,Agent不知道出错了

✅ 安全的失败:

python 复制代码
try:
    execute(cmd)
except Exception as e:
    return f"错误:{str(e)}"  # 返回错误信息,Agent能调整策略

为什么? Agent需要知道失败原因才能调整策略。

📝 自检清单(读完本篇请确认)

在进入下一篇之前,请确认你能回答以下问题:

  • 为什么run_cmdread_filewrite_file更危险?
  • 黑名单和白名单的区别是什么?哪个更安全?
  • 为什么需要截断命令输出?
  • subprocess.runtimeout参数有什么作用?
  • 你能说出3个应该禁止的危险命令吗?

如果都能回答,恭喜你,Agent的"终端"部分你已经掌握了。下一篇见!

⚠️ 新手容易踩的坑

  1. 坑1:用shell=True执行命令

    • 危险:subprocess.run(cmd, shell=True) 容易被注入
    • 安全:subprocess.run(shlex.split(cmd)) 用列表形式
  2. 坑2:忘记设置timeout

    • 后果:cat /dev/random 会让程序卡死
    • 正确做法:始终设置timeout=30
  3. 坑3:白名单太宽松

    • 错误:允许python命令
    • 问题:python -c "import os; os.system('rm -rf /')"
    • 正确做法:只允许pytest等特定测试命令
  4. 坑4:没有截断输出

    • 后果:ls -R / 输出几MB,Token预算瞬间耗尽
    • 正确做法:限制输出长度到2000字符

下一步:把所有拼图组装起来

现在你已经学会了:

  • 实现安全的命令执行工具
  • 设计黑名单+白名单双重过滤
  • 处理命令超时和输出截断
  • 看到Agent的完整工作流(读→写→测试→验证)

但有一个关键问题还没解决:

我们的Agent还是"散装"的------llm_client.pytools.pyreact_agent.py三个文件,没有统一的入口。

下一篇,我们将组装完整的Mini Claude Code

  1. 统一的命令行入口(python agent.py "帮我修Bug"
  2. 完整的错误处理和日志
  3. 配置文件支持(设置模型、Token限制等)
  4. 一个可以真正使用的Agent系统

这就是从"教学原型"到"可用工具"的关键一步。

预告一个核心问题:如何让Agent在遇到错误时自动重试,而不是直接放弃?答案在下一篇揭晓。


系列进度

  • ✅ 第1篇:总览与前置准备------Claude Code到底是什么?
  • ✅ 第2篇:地基篇------让模型开口说话(System Prompt的艺术)
  • ✅ 第3篇:灵魂篇------ReAct循环的骨架
  • ✅ 第4篇:双手篇------赋予读写文件的能力
  • ✅ 第5篇:终端篇------赋予执行命令的超能力
  • ⏭️ 第6篇:整合篇------组装Mini Claude Code
  • 第7篇:上下文篇------让Agent看懂整个文件夹
  • 第8篇:反思与展望------我们得到了什么,还缺什么?
相关推荐
Yunzenn2 小时前
零基础复现Claude Code(四):双手篇——赋予读写文件的能力
开源·github
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 日榜(2026-04-23)
人工智能·ai·大模型·github·ai教程
IT博客技术分享2 小时前
2026年4月份前端面试题及答案
面试
叹一曲当时只道是寻常2 小时前
Reference 工具安装与使用教程:一条命令管理 Git 仓库引用与知识沉淀
人工智能·git·ai·开源·github
不会编程的-程序猿2 小时前
PyCharm 直接把本地项目上传到 GitHub
ide·pycharm·github
李日灐3 小时前
<4>Linux 权限:从 Shell 核心原理 到 权限体系的底层逻辑 详解
linux·运维·服务器·开发语言·后端·面试·权限
Wect3 小时前
HTML5 原生拖拽 API 实战案例与拓展避坑
前端·面试·浏览器
knight_9___4 小时前
RAG面试篇7
java·面试·agent·rag·智能体
one_love_zfl4 小时前
java面试-微服务篇
java·微服务·面试