零基础复现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_file和write_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:直接得到字符串,不需要decodetimeout:防止命令卡死(如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.py的execute_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最危险的能力,必须极其谨慎。
强烈建议的安全措施:
- 在隔离环境中测试
- 用Docker容器:
docker run -it python:3.9 bash- 用虚拟机:VirtualBox、VMware
- 用临时目录:专门的test_workspace
- 永远不要在生产环境运行
- ❌ 不要在服务器上运行Agent
- ❌ 不要在包含重要数据的目录运行
- ❌ 不要给Agent sudo权限
- 定期审查白名单
- 即使是"安全"命令也可能被滥用
- 例如:
python -c "import os; os.system('rm -rf /')"- 考虑禁用
python -c、node -e等- 监控Agent的行为
- 记录所有执行的命令
- 设置异常检测(如频繁失败)
- 人工审核高风险操作
与真实代码的对照
在真实的Claude Code实现中(rust版本),这部分对应的是:
| 我们的实现 | 真实代码位置 | 关键差异 |
|---|---|---|
run_cmd() |
crates/runtime/src/bash.rs 的 execute_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_cmd比read_file和write_file更危险? - 黑名单和白名单的区别是什么?哪个更安全?
- 为什么需要截断命令输出?
-
subprocess.run的timeout参数有什么作用? - 你能说出3个应该禁止的危险命令吗?
如果都能回答,恭喜你,Agent的"终端"部分你已经掌握了。下一篇见!
⚠️ 新手容易踩的坑
-
坑1:用
shell=True执行命令- 危险:
subprocess.run(cmd, shell=True)容易被注入 - 安全:
subprocess.run(shlex.split(cmd))用列表形式
- 危险:
-
坑2:忘记设置timeout
- 后果:
cat /dev/random会让程序卡死 - 正确做法:始终设置
timeout=30
- 后果:
-
坑3:白名单太宽松
- 错误:允许
python命令 - 问题:
python -c "import os; os.system('rm -rf /')" - 正确做法:只允许
pytest等特定测试命令
- 错误:允许
-
坑4:没有截断输出
- 后果:
ls -R /输出几MB,Token预算瞬间耗尽 - 正确做法:限制输出长度到2000字符
- 后果:
下一步:把所有拼图组装起来
现在你已经学会了:
- 实现安全的命令执行工具
- 设计黑名单+白名单双重过滤
- 处理命令超时和输出截断
- 看到Agent的完整工作流(读→写→测试→验证)
但有一个关键问题还没解决:
我们的Agent还是"散装"的------llm_client.py、tools.py、react_agent.py三个文件,没有统一的入口。
下一篇,我们将组装完整的Mini Claude Code:
- 统一的命令行入口(
python agent.py "帮我修Bug") - 完整的错误处理和日志
- 配置文件支持(设置模型、Token限制等)
- 一个可以真正使用的Agent系统
这就是从"教学原型"到"可用工具"的关键一步。
预告一个核心问题:如何让Agent在遇到错误时自动重试,而不是直接放弃?答案在下一篇揭晓。
系列进度
- ✅ 第1篇:总览与前置准备------Claude Code到底是什么?
- ✅ 第2篇:地基篇------让模型开口说话(System Prompt的艺术)
- ✅ 第3篇:灵魂篇------ReAct循环的骨架
- ✅ 第4篇:双手篇------赋予读写文件的能力
- ✅ 第5篇:终端篇------赋予执行命令的超能力
- ⏭️ 第6篇:整合篇------组装Mini Claude Code
- 第7篇:上下文篇------让Agent看懂整个文件夹
- 第8篇:反思与展望------我们得到了什么,还缺什么?