Agent Engineering 课程阅读
首页/课程七 · 智能体前沿/CodeAct 手写:代码是最通用的工具

在 GitHub 查看原文

本页目录

Lesson 06 — CodeAct 手写:代码是最通用的工具

本课目标:理解「让 Agent 写代码执行」为什么比「调预定义工具」强,手写最小 CodeAct loop + 进程级沙箱(subprocess + 超时 + import 白名单 + 输出截断),演示一个工具调用做不动、代码一次搞定的任务。

学完你能回答:「Code Agent 怎么回事?为什么写代码比调工具强?」——代码是行动空间不是又一个工具,组合与循环免费获得。


0. 从"点菜"到"进厨房"

工具调用 = 从菜单点菜

LangChain function calling:你预定义了一组工具(searchcalculatesend_email),Agent 从中选一个调用。就像去餐厅从菜单点菜——能点什么取决于菜单上有什么。

Agent: 我要算 20 条数据的年份分组统计
工具菜单: search / calculate / send_email
Agent: calculate(数据) → 只能算一个表达式,不能分组循环
       🚫 做不到——calculate 不是为分组统计设计的

CodeAct = 进厨房自己做

CodeAct(Wang et al. 2024):Agent 直接写 Python 代码,沙箱执行,结果回注。就像进厨房自己做——只要食材(标准库)够,想做什么做什么。

Agent: 我要算 20 条数据的年份分组统计
CodeAct: 
  from collections import Counter
  years = [extract_year(d) for d in data]
  stats = Counter(years)
  for y, c in sorted(stats.items()): print(f"{y}: {'█'*c} {c}")
  → ✅ 一次搞定——循环、分组、统计、可视化全在代码里

🎯 核心认知:代码是行动空间(action space)不是又一个工具。工具是"预定义的动作",代码是"任意可组合的动作"。写代码意味着 Agent 获得了循环、条件、中间变量、函数组合——这些在工具调用里要每个都单独定义。

论文:CodeAct: Execute Code Actions to Enhance LLM Agents(Wang et al. 2024, arXiv:2402.01030

一句话说它证明了什么:用代码作为 Agent 的行动空间(而非 JSON 工具调用),在多个 benchmark 上成功率更高——因为代码天然支持组合、循环、中间状态,而 JSON 工具调用每次都是独立的无状态请求。


1. 流派对比

问题:Agent 怎么执行复杂计算/数据处理?

流派 做法 取舍
① JSON function calling 预定义工具,Agent 选一个调 ✅ 安全可控;🚫 组合弱、每个新需求要加工具
② CodeAct(本课选它) Agent 写代码,沙箱执行 ✅ 强、组合/循环免费;🚫 需沙箱、安全风险
③ 混合 复杂计算走代码,外部副作用走工具 ✅ 各取所长;🚫 两套机制

选 ②+③ 的理由:研究助手需要"数值对比、去重统计、表格生成"这类计算——这些用预定义工具做不动(每个都要新工具),用代码一次搞定。但"联网搜索""发邮件"这类有外部副作用的还是走工具(L07 的混合模式)。本课先手写 CodeAct loop + 沙箱,L07 落地时和现有工具混合。

沙箱层级

层级 做法 安全性 成本 适用
进程级(本课) subprocess + 超时 + import 白名单 ⚠️ 中(同 OS 用户) 教学够用
容器级 Docker 容器隔离 ✅ 高 生产推荐
微 VM Firecracker 等轻量 VM ✅✅ 很高 高安全场景

⚠️ 诚实标注:本课用进程级沙箱(subprocess + 白名单),教学够用但生产不够。进程级沙箱仍是同一 OS 用户权限——恶意代码可能通过未覆盖的攻击面逃逸。生产环境要上容器隔离(Docker)或微 VM。本课的白名单覆盖了主要危险 import,但不保证 100% 安全。


2. 手写沙箱

安全设计:四道防线

┌─────────────────── 进程级沙箱 ───────────────────┐
│                                                   │
│  ① import 白名单:只允许安全标准库                 │
│     ✅ json/statistics/collections/math/re        │
│     🚫 os/sys/subprocess/socket/urllib/open      │
│                                                   │
│  ② 超时杀进程:subprocess timeout                 │
│     超时 → kill → 返回"超时"提示                  │
│                                                   │
│  ③ 输出截断:防止内存爆炸                         │
│     stdout 超过 N 字符只保留前 N                   │
│                                                   │
│  ④ 无网络无文件:白名单不放网络/文件库             │
│     (进程级做不到完全隔离,但白名单堵主要风险)    │
│                                                   │
└───────────────────────────────────────────────────┘

白名单 vs 黑名单

# 白名单(本课选它):只允许安全的,其余全拒
ALLOWED_IMPORTS = {"json", "statistics", "collections", "math", "re", "datetime", "itertools"}
# 检查代码里所有 import,不在白名单里的拒绝执行

# 黑名单(不选):拒绝危险的,其余放行
BLOCKED_IMPORTS = {"os", "sys", "subprocess", "socket"}
# 问题:漏掉一个危险库就完蛋(如 ctypes、multiprocessing)

🎯 白名单比黑名单安全:白名单默认拒绝,只有明确安全的才放行;黑名单默认放行,漏一个就完蛋。安全设计永远选白名单。

import 检测

def check_imports(code: str) -> list[str]:
    """提取代码里的 import,返回不在白名单里的。"""
    violations = []
    for line in code.split("\n"):
        line = line.strip()
        if line.startswith("import "):
            mod = line.split()[1].split(".")[0]
            if mod not in ALLOWED_IMPORTS:
                violations.append(mod)
        elif line.startswith("from "):
            mod = line.split()[1].split(".")[0]
            if mod not in ALLOWED_IMPORTS:
                violations.append(mod)
    return violations

3. 手写 CodeAct loop

核心流程

def codeact_loop(task, llm, max_rounds=3):
    history = ""
    for round in range(max_rounds):
        # 1. LLM 生成代码(基于任务 + 历史结果)
        code = llm.generate_code(task, history)
        # 2. 沙箱检查 import
        violations = check_imports(code)
        if violations:
            history += f"代码被拒:import {violations} 不在白名单\n"
            continue
        # 3. 沙箱执行
        result = sandbox_exec(code, timeout=10)
        # 4. 结果回注
        history += f"代码:\n{code}\n结果:\n{result}\n"
        # 5. LLM 判断是否完成
        if llm.is_done(result, task):
            return result
    return history

Mock LLM 设计

code.py 用 Mock LLM 演示: - 给定任务,生成预设的 Python 代码(如分组统计 + ASCII 柱状图) - 演示越权 import 被拒(import os → 拒绝) - 演示超时被杀(while True → 超时)

对比实验

任务:对 20 条搜索结果按年份分组统计并画 ASCII 柱状图

工具调用方式:
  calculate(数据) → 只能算一个表达式,不能分组循环
  🚫 做不到——需要预定义一个"分组统计"工具

CodeAct 方式:
  from collections import Counter
  data = [...]  # 20 条
  years = [...]
  stats = Counter(years)
  for y, c in sorted(stats.items()): print(f"{y}: {'█'*c} {c}")
  → ✅ 一次搞定

4. 安全红线演示

code.py 演示三道安全防线的拦截:

① import os → 🚫 拒绝(不在白名单)
② import socket → 🚫 拒绝
③ while True: pass → ⏱ 超时杀进程
④ from collections import Counter → ✅ 允许
⑤ print("hello") → ✅ 正常执行

5. 落地清单

本课是纯手写原理课,无落地改动(不改 research-assistant 任何文件)。落地在 L07(代码解释器接入)。

如何验证

cd frontier-lessons/06_codeact
PYTHONIOENCODING=utf-8 ../../.venv/Scripts/python.exe code.py
# 预期:
# - 演示 CodeAct loop(生成代码→沙箱检查→执行→回注)
# - 拦截越权 import(os/socket 被拒)
# - 超时杀进程(while True 被杀)
# - 统计任务跑通(分组 + ASCII 柱状图)

6. 本课在两条主线上的位置

  • 评估主线:本课引入了"代码执行成功率"和"沙箱安全拦截率"两个可量化点——代码能不能跑通、越权尝试能不能拦住。L08 的 TrajectoryEvaluator 会把它们纳入指标。
  • 上下文工程主线:CodeAct 是上下文工程的动态生成——不是从外部调回信息(记忆/RAG/skills),是让 Agent 生成代码、执行、把结果放进上下文。这扩展了上下文工程的边界:上下文不只是"调回已有信息",还能"生成新信息"(计算结果)。

🎯 面试话术

「CodeAct 我手写过 loop 和沙箱。核心认知:代码是行动空间不是又一个工具——组合与循环免费获得,不用每个新需求都加工具。沙箱我用进程级:subprocess + 超时 + import 白名单 + 输出截断,四道防线。白名单比黑名单安全——默认拒绝只有明确安全的放行。我也清楚进程级沙箱的边界:同 OS 用户权限,生产要上容器隔离。我的研究助手数值结论不靠 LLM 口算,路由到沙箱代码解释器。」