研究日期:2026-07-23(Asia/Shanghai)
仓库:huggingface/smolagents
源码快照:e3a5b8994b301983b91c0325546e9dc82eab8cf0
main 版本:1.27.0.dev0
最新稳定版:1.26.0
Python:>=3.10
许可证:Apache-2.0

版本与实时校准

本文研究的是 2026-07-23 从 main 快进后固定的源码:

e3a5b8994b301983b91c0325546e9dc82eab8cf0
2026-07-11T12:44:42+02:00
Update security policy (#2495)

当日 GitHub API 快照:

  • 28,504 stars;
  • 2,804 forks;
  • 719 个 open issues + PRs;
  • Apache-2.0;
  • 默认分支 main

GitHub Release 与 PyPI 的最新稳定版都是 1.26.0,发布 / 上传于 2026-05-29;当前 mainpyproject.toml 已进入 1.27.0.dev0。因此本文 讲解的是固定源码快照,而不是承诺所有细节都已进入 PyPI 稳定包。

README 仍把 agent 逻辑描述为“约 1,000 行”,但快照中的 src/smolagents/agents.py 已有 1,813 行。这个变化不影响项目追求小核心的 方向,却提醒我们:产品边界、executor 类型、streaming 与 serialization 已经 比早期口号丰富,不能再只靠 README 的极简叙述理解现在的 runtime。

结论先行

smolagents CodeAgent 的关键抽象不是“LLM 会写 Python”,而是把 Python 作为 一轮 action policy 的中间语言:

MODEL-VISIBLE LANE
TaskStep / PlanningStep / ActionStep / error
→ to_messages()
→ model
→ one Python program
→ synthetic ToolCall("python_interpreter", code)

EXECUTOR LANE
persistent Python namespace + tools + imports
→ execute program
→ logs + last value / final_answer
→ one observation returns to transcript

这形成两套相关但不等价的状态:

  1. typed transcript:模型可见、可转成 messages、可记录错误和输出;
  2. executor heap:变量、对象、imports 与 tool references 在 Python namespace 中继续存活。

run(reset=True) 只清空前者的 memory.steps 与 monitor,不重建 python_executor,也不清空 executor namespace。错误和 timeout 会进入 transcript,却不会回滚 heap 或外部副作用。save() 也只保存 agent 配置、 模型、tools 与 prompts,不保存这两套 runtime state。

所以 Python 不只是一种动作格式,也是 agent 的第二块、目前不可 checkpoint 的记忆。

对旧稿最重要的修正

  1. Python 不只是比 JSON 更能写循环。
    变量和对象会跨 step 存活,甚至在 reset=True 的新 run 里继续存在;这会 直接改变 reset、resume、replay 和测试隔离的语义。

  2. ActionStep 并不逐个记录 Python 内部的 tool call。
    CodeAgent._step_stream() 只记录一个合成调用: ToolCall(name="python_interpreter", arguments=code_action)。代码内部可能 调用 search()fetch()save() 多次,但 transcript 的顶层事件仍只有 一次 interpreter call。ToolCallingAgent 才会逐次记录工具调用与结果。

  3. 错误反馈不是事务回滚。
    evaluator 在代码出错前对 namespace 或外部系统造成的修改依然存在。下一轮 模型看见错误,不代表系统已恢复到 action 前状态。

  4. 本地 timeout 不是可抢占的 deadline。
    当前 decorator 用 ThreadPoolExecutorfuture.result(timeout=...); 抛出 timeout 后,context manager 退出仍会等待 worker。worker 也可能继续 修改共享 state。开放 issue #2197 与 #2263 正在讨论这个问题。

  5. restricted evaluator 不是 security sandbox。
    import、dunder、operation、loop 与 print 限制能减少意外,却不能隔离不可信 代码。源码和官方安全教程都要求高风险执行使用 remote executor。

  6. remote isolation 不等于 durable replay。
    E2B、Modal、Blaxel、Docker 提供更强的进程 / 容器边界,但 kernel 仍是 stateful 的;框架没有把每步 heap 做成确定性 checkpoint。

  7. 序列化边界本身是设计。
    remote executor 默认通过 SafeSerializer 只接受 JSON-safe 数据; allow_pickle=False 是安全默认。启用 pickle 能传更多对象,也会引入任意 代码执行风险。

  8. 最终答案检查发生在 action 之后。
    final_answer_checks 验证的是 executor 已运行之后的答案;callbacks 也在 _finalize_step() 中 action 后执行。它们不是 CodeAgent 的 pre-tool approval hook。

  9. 持久化缺口是项目当前已知问题。
    issue #1216 仍在请求保存 / 恢复 agent memory;#2176、#2172 与 #1883 分别 讨论 tool governance、audit trail 与 lifecycle hooks。它们说明治理面还在 生长,不能把 callback 等同于完整审计或人工批准系统。

一次真实请求怎样运行

场景:

查询五个城市的天气,把摄氏温度归一化,找出异常值,再给出最终结果。

0. 构造阶段

CodeAgent(...)

  • 继承 MultiStepAgent,持有 model、tools、managed agents、memory、monitor;
  • 根据 executor_type 创建 local / E2B / Modal / Blaxel / Docker executor;
  • local executor 得到 authorized imports、operation / loop / print / timeout 限制;
  • remote executor 得到额外 imports、logger 与 serialization 设置;
  • 把 tool 描述、authorized imports 和 code tags 填进 system prompt。

1. run() 只重置对话 lane

MultiStepAgent.run(task, reset=True)

  1. 更新 self.task
  2. additional_args 写入 self.state
  3. 重新生成 system prompt step;
  4. reset=True,调用 memory.reset()monitor.reset()
  5. 追加新的 TaskStep
  6. self.state 和 tools 发送到已存在的 python_executor
  7. 进入 _run_stream()

源码没有在这里调用 python_executor.cleanup()、重建 executor,或清空其 state

2. typed memory 被编译成 messages

_run_stream() 可按 planning_interval 插入 PlanningStep,随后为每轮创建 ActionStepCodeAgent._step_stream() 调用 write_memory_to_messages(),把 system、task、planning、model output、 tool call、observation 与 error 转成 provider-neutral messages。

ActionStep 还能保存:

  • model input / output;
  • parsed code;
  • synthetic tool calls;
  • observations / images;
  • error;
  • action output;
  • token usage 与 timing。

它是可观测的 event record,但不是 executor heap snapshot。

3. 模型返回一段小程序

模型可能返回:

cities = ["北京", "上海", "成都", "深圳", "哈尔滨"]
rows = [get_weather(city) for city in cities]
avg = sum(row["temp_c"] for row in rows) / len(rows)
outliers = [row for row in rows if abs(row["temp_c"] - avg) > 8]
final_answer({"average": avg, "outliers": outliers})

parse_code_blobs() 解析 code,fix_final_answer_code() 做兼容修正。框架随后 只追加一个:

ToolCall(
    name="python_interpreter",
    arguments=code_action,
)

get_weather() 被调用五次的事实只能从 code、executor logs 或下游 tool 自己 的 telemetry 复原,不会自动变成五个标准化 ToolCall events。

4. executor 执行并保留 heap

LocalPythonExecutor.__call__() 把代码交给 AST evaluator,后者在 self.state namespace 中解释执行。执行后返回:

CodeOutput(
  output = 最后一个表达式或 final answer,
  logs = captured print output,
  is_final_answer = bool
)

如果没有 final answer,logs 与 last output 被写进 ActionStep.observations, 下一轮模型据此继续;variables 仍在 executor state。若调用 final_answer(),local executor 通过专用 final-answer mechanism 结束;remote executor 则把特殊异常序列化回 host,再恢复值。

5. error 成为下一轮上下文

parse、import、execution 等 agent errors 会落到 ActionStep.error 并进入 后续 messages。这个反馈机制适合自我修正,但它只追加事实,不撤销已经发生的 写入、网络调用或 tool 副作用。

6. action 后才做 checks 与 callbacks

executor 返回 final answer 后,_run_stream() 才运行 final_answer_checksfinally_finalize_step() 再计算 timing、监控并 调用 step callbacks,随后把 step 追加进 memory。它们适合观测和 post-check, 不是“执行每个 tool 前必须批准”的边界。

三个可复现实验

实验都在固定源码快照、临时 uv 环境中运行,没有网络工具,也没有外部副作用。

实验 A:reset=True 后 executor 仍记得旧变量

用一个确定性 scripted model 分两次返回:

# run 1
x = 41
final_answer(x)

# run 2, reset=True
final_answer(x + 1)

结果:

run_1 = 41
executor_has_x = 41
run_2 = 42
memory_after_run_2 = [TaskStep, ActionStep]
memory_contains_first_code = false
executor_x_after_run_2 = 41

第二次 transcript 中已经没有 x = 41,模型却能通过 executor heap 读到它。 这不是“conversation continuation”,而是两套 reset 语义不一致。

实验 B:异常不会回滚 state

执行:

ledger = []
ledger.append("charged")
1 / 0

结果:

exception = InterpreterError
ledger_after_error = ["charged"]

这说明即使错误被记录,action 前半段造成的 state mutation 仍然生效。若 ledger.append() 换成支付、发邮件或数据库写入,恢复责任必须由 tool / 业务层 负责。

实验 C:timeout 抛出后仍等待并可晚写 state

构造 timeout_seconds=0.2、授权 time 的 local executor,执行:

import time
time.sleep(1.2)
y = 7

结果:

exception = ExecutionTimeoutError
wall_time = 1.21s
state_y = 7

名义 timeout 是 0.2 秒,调用却约 1.21 秒后返回,worker 还写入了 y。官方 test_local_executor_custom_timeout 同样通过,但 sleep(2) / timeout_seconds=1 的测试用时约 2.01 秒;该测试只验证异常类型,没有验证 deadline 响应时间。

验证记录

Agent / memory 定向测试

uv run --with pytest python -m pytest -v \
  tests/test_memory.py tests/test_agents.py \
  -k 'fake_code_agent or reset_conversations or fails_max_steps or
      error_saves_previous_print_outputs or full_result'

8 passed, 101 deselected

Local / remote executor 定向测试

第一次收集因临时环境缺少 numpydocker 依赖失败;补齐测试声明依赖后:

uv run --with pytest --with numpy --with pandas --with docker \
  --with websocket-client python -m pytest -v \
  tests/test_memory.py tests/test_local_python_executor.py \
  tests/test_remote_executors.py \
  -k 'not Integration and (memory or state_name or
      local_executor_custom_timeout or send_variables or
      deserialize_final_answer or serialization)'

18 passed, 434 deselected
1 FutureWarning(insecure pickle fallback)

合计 26 个聚焦测试通过。此结果不等于全仓测试通过,也没有启动真实的 E2B / Modal / Blaxel / Docker 服务。

关键源码入口

  1. src/smolagents/agents.py::MultiStepAgent
  2. src/smolagents/agents.py::MultiStepAgent.run
  3. src/smolagents/agents.py::MultiStepAgent._run_stream
  4. src/smolagents/agents.py::MultiStepAgent._finalize_step
  5. src/smolagents/agents.py::MultiStepAgent.write_memory_to_messages
  6. src/smolagents/agents.py::ToolCallingAgent._step_stream
  7. src/smolagents/agents.py::CodeAgent
  8. src/smolagents/agents.py::CodeAgent.create_python_executor
  9. src/smolagents/agents.py::CodeAgent._step_stream
  10. src/smolagents/memory.py::ActionStep
  11. src/smolagents/memory.py::AgentMemory
  12. src/smolagents/local_python_executor.py::evaluate_python_code
  13. src/smolagents/local_python_executor.py::timeout
  14. src/smolagents/local_python_executor.py::LocalPythonExecutor
  15. src/smolagents/remote_executors.py::RemotePythonExecutor
  16. src/smolagents/remote_executors.py::RemotePythonExecutor.send_variables
  17. src/smolagents/remote_executors.py::_patch_final_answer_with_exception
  18. src/smolagents/serialization.py::SafeSerializer
  19. src/smolagents/models.py::Model
  20. src/smolagents/tools.py::Tool

官方资料与外部比较

  1. 固定源码快照
  2. 固定快照 README
  3. 1.26.0 Release
  4. PyPI smolagents
  5. 官方文档首页
  6. Agent API
  7. Python executor API
  8. 官方 Secure code execution 教程
  9. CodeAct 论文
  10. DynaSaur 论文
  11. Code as Agent Harness 论文
  12. Issue #2197:local timeout 等待 worker 完成
  13. PR #2263:避免 timeout 后继续等待
  14. Issue #1216:保存 / 加载 agent memory
  15. Issue #2176:tool execution governance
  16. Issue #2172:audit trail / governance callback
  17. Issue #1883:CodeAgent lifecycle hooks
  18. PR #2442:callback 持久化 memory 示例
  19. LangGraph persistence
  20. LangGraph interrupts

写作边界

  • CodeAct 论文报告的“最高 20% success rate 提升”属于论文任务集结果,不应 写成 smolagents 在所有生产任务上的普遍收益。
  • 本文没有宣称 remote executor 完全安全;隔离能力仍取决于 provider、镜像、 network / filesystem policy、secrets 与 cleanup。
  • 本文没有运行真实外部工具,不声称验证了远端 executor 的完整生命周期。
  • GitHub stars、issue 状态、main 版本和代码路径都可能继续变化;引用固定 commit 是为了让结论可复查。