能跑代码的 Agent 不是 demo,是真正的生产力工具
让 Agent 自己写代码、自己运行,是 Agent 能力的一个分水岭。Agent 拿到一个 CSV 文件,不再需要你给它 parse_csv、filter、groupby 这些工具,它可以直接写 pandas 代码处理;Agent 需要画图,不需要你给它 plot_bar、plot_line 工具,它写 matplotlib 就行。
代码执行把 Agent 从"调固定工具"提升到"自主造工具"。这是为什么 Claude 的 Artifacts、ChatGPT 的 Code Interpreter、Cursor 的 Composer、以及所有数据分析类 Agent 的核心都是这个能力。
但它有一个不能妥协的前置条件:沙箱。让 LLM 生成的代码在你的主机上直接跑,无异于打开了后门。这一篇讲清楚几种主流沙箱方案的取舍,以及做一个生产可用的 Code Execution Agent 的完整骨架。
为什么沙箱必须是硬约束
LLM 生成的代码可能包含:
rm -rf /级别的破坏性命令(无论是"故意"还是"不小心")- 对外网发请求(exfiltration 风险)
- 读写主机敏感文件(
/etc/passwd、SSH key) - 消耗全部 CPU / 内存造成主机不可用
这些不一定是模型"恶意",更多是没有边界就什么都会被试。LLM 在探索性任务里会尝试各种办法——它不知道 /tmp/../etc/passwd 不该读。沙箱的作用就是:模型能尝试任何东西,但边界是硬的,尝试不成功的代码不会造成伤害。
沙箱方案全景
主流的五种,按"隔离强度"和"使用门槛"排序:
1. Pyodide(浏览器里的 Python)
把 Python 编译成 WebAssembly,跑在浏览器里。零服务端依赖,天然隔离(浏览器沙箱)。Claude Artifacts 的 Python 和 ChatGPT Canvas 用的就是这个。
适合:前端场景、需要用户本地跑代码的场景、不涉及文件系统操作的任务。局限:不能安装任意 Python 包(需要预先编译 Pyodide 兼容包);没有真正的文件系统;执行速度比原生 Python 慢 3~5 倍。
2. E2B (e2b.dev)
商业化的托管代码沙箱。调一个 API 就有一个完整的 Ubuntu 虚拟机,跑完销毁。
from e2b_code_interpreter import Sandbox
sbx = Sandbox() # 几秒内启动
result = sbx.run_code("import pandas as pd; print(pd.__version__)")
print(result.logs.stdout)
sbx.close()
优点:启动快(~300ms)、全功能(pip install 任意包、完整文件系统、GPU 可选)、隔离强(每个 sandbox 是独立虚拟机)。缺点:要付费(免费额度很小)、需要网络(你的服务连 E2B 的 API)。
2026 年的主流 Code Interpreter Agent 大多跑在 E2B 或类似服务上。它是"省心生产级"的选择。
3. 本地 Docker 容器
自己维护一个 Docker 镜像,每次创建容器跑代码。
import docker
client = docker.from_env()
def run_in_sandbox(code: str, timeout: int = 30):
container = client.containers.run(
image="python:3.12-slim",
command=["python", "-c", code],
detach=True,
mem_limit="512m",
network_mode="none", # 禁网
read_only=True, # 根文件系统只读
tmpfs={"/tmp": ""}, # 只在 /tmp 可写
)
try:
container.wait(timeout=timeout)
return container.logs().decode()
finally:
container.remove(force=True)
优点:免费、私有、可控。缺点:启动慢(2~5 秒)、维护繁琐(镜像、资源限制、清理)。适合企业内部 Agent,不想把数据送出公司。
4. Jupyter Kernel(本地或远程)
和前三种不同,Jupyter 有一个关键优势——状态持久化。代码一块跑,变量留在内核里,下一块代码可以用。
from jupyter_client import KernelManager
km = KernelManager(kernel_name="python3")
km.start_kernel()
client = km.client()
def execute(code: str):
msg_id = client.execute(code)
outputs = []
while True:
msg = client.get_iopub_msg(timeout=10)
if msg["msg_type"] == "stream":
outputs.append(msg["content"]["text"])
if msg["msg_type"] == "status" and msg["content"]["execution_state"] == "idle":
break
return "".join(outputs)
execute("x = 42")
execute("print(x * 2)") # 84 —— x 还在
状态持久化对数据分析 Agent 至关重要。你让 Agent "读入这个 CSV,做清洗,再画图,再按列分组统计",这是三次代码执行。如果每次都是新 Python 进程,每次都要重新读 CSV。Jupyter 模式里 DataFrame 留着,每一步只加工一点。
缺点:隔离弱(默认没沙箱,要自己加 Docker 或 gVisor);需要多次执行的交互式场景才值;kernel 崩溃时状态全丢。
5. Code Sandbox as Tool (Anthropic Computer Use 方式)
Claude 的 Computer Use(第 14 篇会讲)把整个虚拟机都给 Agent,代码执行只是其中一个能力。适合需要"Agent 看屏幕、点鼠标、开 Jupyter"这种超全能场景,但做纯代码执行是大材小用。
选择建议
如果你在做的是:
- 浏览器里的 Agent——Pyodide
- 通用的 Code Interpreter 或 Data Agent——E2B(省心)或 Docker(自控)
- 数据分析、多步处理,状态要留——Jupyter kernel + Docker 隔离
- 企业内部,高合规——Docker + network_mode=none + read_only
工具设计:把代码执行包装成一个工具
无论底层是哪种沙箱,Agent 看到的就是一个 run_python 工具:
def run_python(code: str) -> str:
"""执行 Python 代码,返回 stdout 和 stderr"""
result = sandbox.run_code(code)
return f"STDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
TOOL_SCHEMA = {
"type": "function",
"function": {
"name": "run_python",
"description": "在沙箱中执行 Python 代码。可以安装包、读写 /tmp 文件、进行数据处理。每次调用保留前一次的变量(Jupyter 风格)。",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的 Python 代码"},
},
"required": ["code"],
},
},
}
description 里要明确告诉模型两件事:状态是否保留、允许做什么(能否读写文件、能否联网)。这决定模型会怎么写代码——如果知道状态保留,它会把中间结果存在变量里;如果知道能联网,它会直接 pip install 缺的包。
文件系统:让 Agent 能处理真实数据
纯代码执行不够,Agent 还经常要处理用户上传的文件。设计模式:
class DataAgent:
def __init__(self, sandbox, workdir="/workspace"):
self.sbx = sandbox
self.workdir = workdir
def upload_file(self, local_path: str, name: str):
"""把用户文件传到沙箱"""
with open(local_path, "rb") as f:
self.sbx.files.write(f"{self.workdir}/{name}", f.read())
def download_file(self, name: str, local_path: str):
"""把沙箱里的文件(比如 Agent 生成的图表)下载回来"""
data = self.sbx.files.read(f"{self.workdir}/{name}")
with open(local_path, "wb") as f:
f.write(data)
def run(self, task: str, input_files: list[str]):
# 1. 上传文件
for f in input_files:
self.upload_file(f, Path(f).name)
# 2. 跑 Agent 循环
messages = [
{"role": "system", "content": f"你有一个 Python 沙箱。工作目录是 {self.workdir}。用户上传了这些文件: {[Path(f).name for f in input_files]}"},
{"role": "user", "content": task},
]
# ... agent loop using run_python tool ...
# 3. Agent 完成后,扫描沙箱里的新文件
new_files = self.sbx.files.list(self.workdir)
return new_files
这个模式在生产里非常常见:用户上传 CSV → Agent 分析并生成图表 → 用户下载图表。整个过程用户只看到"提问 → 拿到结果",沙箱、文件传输、Agent 循环都对用户透明。
错误反馈:让 Agent 从崩溃中学到东西
Code Agent 一定会写错代码。错误信息怎么回给模型,决定它能不能自己修好。
差的反馈:"Error"
勉强的反馈:"NameError: name 'pd' is not defined"
好的反馈:
执行失败:
NameError: name 'pd' is not defined
Traceback:
File "<code>", line 3
df = pd.read_csv('data.csv')
提示:pandas 没被 import。试试 `import pandas as pd` 在第一行。
前两行是 Python 的原生 traceback,第三块是你的系统根据错误类型添加的提示。对常见错误(NameError、ModuleNotFoundError、FileNotFoundError)配几个简单规则就能让 Agent 恢复率显著提升。
ModuleNotFoundError 的处理尤其值得特殊对待。Agent 常常忘了 pip install。你的系统可以检测到这个错误,给出明确提示:"Module X 不存在。你可以用 !pip install X 安装"。Agent 看到就会修好。
超时和资源限制
代码执行永远要有超时保护。while True 是 LLM 很容易生成的代码——不是故意,只是逻辑没想清楚。
def run_python(code: str, timeout: int = 30):
try:
result = sandbox.run_code(code, timeout=timeout)
return result.stdout
except TimeoutError:
return f"执行超时({timeout}s)。代码可能有死循环或耗时过长。"
30 秒是大部分数据分析任务的上限。超过就让 Agent 看到超时提示,让它考虑优化代码(分批处理、采样、改用向量化操作)。内存上限也类似,512MB~2GB 对一般任务够用,超了给明确提示让 Agent 缩小数据。
一个完整的 Data Analysis Agent
把上面所有东西拼起来——一个可以接收 CSV、分析、画图的 Agent:
from e2b_code_interpreter import Sandbox
from openai import OpenAI
import json
client = OpenAI()
class DataAgent:
def __init__(self):
self.sbx = Sandbox()
def run_code(self, code: str) -> str:
exec_result = self.sbx.run_code(code, timeout=30)
out = []
if exec_result.logs.stdout:
out.append("STDOUT:\n" + "".join(exec_result.logs.stdout))
if exec_result.logs.stderr:
out.append("STDERR:\n" + "".join(exec_result.logs.stderr))
if exec_result.error:
out.append(f"ERROR: {exec_result.error.name}: {exec_result.error.value}")
if exec_result.results:
for r in exec_result.results:
if r.png:
out.append(f"[生成图片] 保存为 /tmp/plot_{id(r)}.png")
return "\n\n".join(out)
def analyze(self, task: str, csv_path: str):
self.sbx.files.write("/workspace/data.csv", open(csv_path, "rb").read())
messages = [
{"role": "system", "content": "你是数据分析助手,有一个 Python 沙箱。数据文件在 /workspace/data.csv。状态在多次 run_python 调用间保留。"},
{"role": "user", "content": task},
]
for _ in range(15):
resp = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[{
"type": "function",
"function": {
"name": "run_python",
"description": "执行 Python 代码,状态在调用间保留",
"parameters": {
"type": "object",
"properties": {"code": {"type": "string"}},
"required": ["code"],
},
},
}],
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return msg.content
for tc in msg.tool_calls:
code = json.loads(tc.function.arguments)["code"]
result = self.run_code(code)
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
return "超出最大步数"
agent = DataAgent()
print(agent.analyze(
"分析这份销售数据,找出销售额最高的 5 个产品,画个柱状图",
"sales.csv"
))
这段代码跑起来,Agent 会:
- 写代码读 CSV
- 写代码探索数据(
df.head()、df.describe()) - 写代码做聚合(
groupby("product").sum()) - 写代码画图
- 汇总输出 + 告诉用户图在哪
整个过程Agent 自己决定每一步写什么代码、遇到错误自己修。这是 Code Execution Agent 的核心价值。
小结
代码执行让 Agent 突破"工具边界"。一个 run_python 工具的灵活性等同于几百个专门工具的组合。但它的前置条件是沙箱——沙箱选得合适,是"生产级工具";选不好,是"开往灾难的高铁"。E2B 给商业场景省心,Docker 给私有场景自控,Jupyter 给需要状态的场景解决根本问题。
下一篇切到另一个前沿形态——Browser & Computer Use。Agent 不再只处理文本,而是看屏幕、点鼠标、填表单。Playwright + Vision 的组合,Claude Computer Use 和 OpenAI Operator 的对比,以及为什么这一代 Browser Agent 还是脆弱但已经能用在部分任务上。
相关阅读
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:13. Code Execution Agent:让 Agent 真的能写代码、跑代码
本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/13-Code-Execution-Agent/
本文最后一次更新为 天前,文章中的某些内容可能已过时!