能跑代码的 Agent 不是 demo,是真正的生产力工具

让 Agent 自己写代码、自己运行,是 Agent 能力的一个分水岭。Agent 拿到一个 CSV 文件,不再需要你给它 parse_csvfiltergroupby 这些工具,它可以直接写 pandas 代码处理;Agent 需要画图,不需要你给它 plot_barplot_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 会:

  1. 写代码读 CSV
  2. 写代码探索数据(df.head()df.describe())
  3. 写代码做聚合(groupby("product").sum())
  4. 写代码画图
  5. 汇总输出 + 告诉用户图在哪

整个过程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/

本文最后一次更新为 天前,文章中的某些内容可能已过时!