smolagents (HuggingFace)

一句话定位

Hugging Face 出品的极简(核心库约 12.8K 行代码)单进程、同步、无持久化的 ReAct agent 框架;唯一的架构决策是一个共享的 MultiStepAgent 主循环,配两种可互换的动作表达方式——写 Python 代码(CodeAgent)或发 JSON/native tool call(ToolCallingAgent)——其余维度(长期记忆、路由编排、自进化、可观测性、轨迹训练反哺)基本都刻意留白,交给外部生态(OpenTelemetry、MCP、E2B/Docker/Modal/Blaxel)去补。

核心架构总览(目录结构关键路径 + 引用的 commit)

  • repo: https://github.com/huggingface/smolagents,分析基准 commit 526069c1ead958b36d9fd09a6b1ef37f68ed6ade(2026-06-16),2026-07-07 shallow clone 检出并抽取关键文件。
  • 核心库全部在 src/smolagents/ 下,无子包拆分,按文件划分职责:
    • agents.py(1813 行)——MultiStepAgent 抽象基类、ToolCallingAgentCodeAgentRunResult、planning-step 逻辑、agent 树可视化、save/push_to_hub、AGENT_REGISTRY
    • memory.py(316 行)——MemoryStep 层级(ActionStep/PlanningStep/TaskStep/SystemPromptStep/FinalAnswerStep)、AgentMemoryCallbackRegistry
    • monitoring.py(273 行)——AgentLogger(Rich 控制台日志)、Monitor(token/耗时指标)、TokenUsageTiming
    • local_python_executor.py(1768 行)——自研 AST 遍历式 Python 沙箱解释器。
    • remote_executors.py(1076 行)——RemotePythonExecutor 抽象基类 + E2B/Docker/Modal/Blaxel 四个真沙箱后端。
    • tools.py(1422 行)——Tool 基类、@tool 装饰器、ToolCollection.from_hub/from_mcp
    • mcp_client.py(171 行)——基于第三方 mcpadapt 库的 MCP 客户端包装。
    • cli.py(294 行)——smolagent CLI 入口。
    • prompts/{code_agent,toolcalling_agent,structured_code_agent}.yaml ——三套完整 Jinja2 系统提示模板。
  • 无独立的 router/orchestrator 模块、无独立的 memory-store 模块、无训练/评测反馈模块——这些维度在源码里都不存在对应文件,是有意留白(见下文各章节)。

Agent Loop(主循环 / 何时继续何时停)

  • 唯一抽象:MultiStepAgentagents.py:268),官方文档自称”an abstraction of ReAct framework”(docs/source/en/conceptual_guides/react.md)。
  • 循环驱动是生成器 _run_streamagents.py:540-611):while not returned_final_answer and self.step_number <= max_steps。每轮迭代:
    1. 若设置了 planning_interval 且步数命中间隔,先跑一次规划步(agents.py:549-567,委托给 _generate_planning_stepagents.py:639-747)。
    2. 构造 ActionStep,调用 _step_stream(action_step)(抽象方法,由子类实现)。
    3. 通过 ActionOutput.is_final_answer 判定是否终止;若有 final_answer_checks 校验器则执行(agents.py:589-593_validate_final_answer 在 613 行)。
    4. 遇到 AgentError(非生成错误)时把错误写入 step 并继续循环(agents.py:597-599)——模型可恢复的错误会作为 Observation:/Error: 消息反馈回去;AgentGenerationError(实现层错误)总是重新抛出并退出循环。
    5. 停止条件:(a) 生成了最终答案;(b) step_number > max_steps_handle_max_steps_reachedagents.py:625-637)强制再发一次 LLM 请求索要 best-effort 答案,并打上 AgentMaxStepsError 标记;(c) 通过 agent.interrupt() 置位的 self.interrupt_switchagents.py:754-756)会在下一轮循环开头抛 AgentError("Agent interrupted.")——这是手动取消机制,没有异步 cancellation token。
  • 两种具体的动作执行策略共享该循环:
    • ToolCallingAgentagents.py:1215-1502)——_step_stream 调用 model.generate(...,tools_to_call_from=...),拿到 chat_message.tool_calls(原生 tool-calling API 或经 model.parse_tool_calls 解析),经 process_tool_callsagents.py:1361-1442)执行——当一条消息里返回多个 tool call 时,用 ThreadPoolExecutor 并行执行agents.py:1417-1434,受 max_tool_threads 配置控制)。
    • CodeAgentagents.py:1505-1804)——_step_stream 调用 model.generate(...),用 parse_code_blobscode_block_tags(默认 <code>...</code>,或 Jinja 可配为 markdown 代码块)提取 Python 代码片段,交给 PythonExecutorself.python_executor(code_action))执行,若 code_output.is_final_answer 则将 code_output.output 作为最终答案返回。
  • RunResultagents.py:196-253)——return_full_result=True 时的对外返回对象:聚合 outputstate"success"|"max_steps_error")、steps(完整 memory dump)、token_usagetiming
  • 流式:run(stream=True) 直接返回生成器本体(agents.py:494-496);非流式 run() 内部耗尽生成器,返回最后一个 FinalAnswerStep

记忆与上下文管理(压缩、长期记忆、会话持久化)

  • 核心库没有跨运行/长期记忆或持久化层AgentMemorymemory.py:214-277)纯粹是进程内、单次运行范围的:system_promptSystemPromptStep)+ steps: list[TaskStep|ActionStep|PlanningStep]agent.memory.reset() 只是清空 stepsmemory.py:232-234);run() 开始时默认自动调用,除非传 reset=Falseagents.py:478-480)——这是跨 run() 调用延续对话的唯一机制(状态完全保存在 Python 对象里,不会自动序列化落盘)。
  • 每次 LLM 调用前的上下文组装在 write_memory_to_messages()agents.py:758-770):拼接 system_prompt.to_messages() + 每个 step 的 to_messages()。每个 MemoryStep 子类实现自己的 to_messages(summary_mode: bool)memory.py:92-206),渲染成带具体 MessageRoleASSISTANT/TOOL_CALL/TOOL_RESPONSE/USER/SYSTEM)的一条或多条 ChatMessage
  • 压缩机制 = summary_mode 标志位,不是基于 token 预算的逐条截断。summary_mode=True(仅在重新规划时使用,agents.py:684)会丢弃:ActionStep.to_messages 里模型的原始 model_output 文本(memory.py:94)、系统提示(SystemPromptStep.to_messages 返回 []memory.py:204-205)、以及 PlanningStep.to_messages 也返回 []memory.py:175-176)。代码注释原文:“Summary mode removes the system prompt and previous planning messages output by the model. Removing previous planning messages avoids influencing too much the new plan.”(agents.py:682-683)。agents.py/memory.py 中找不到任何基于滑动窗口/token 计数的旧 ActionStep 淘汰逻辑。
  • 输出截断:truncate_content()(从 utils.py 引入)截断工具调用观测字符串(agents.py:1401, 1752-1753)与代码执行器的 print 缓冲区(local_python_executor.py:59 定义 DEFAULT_MAX_LEN_OUTPUT = 50_000 字符,可经 max_print_outputs_length 配置)——这是输出体积截断,不算真正的记忆管理。
  • 检视/回放用的持久化(不会反馈给 agent 本身):agent.memory.get_full_steps()/get_succinct_steps()memory.py:236-246)和 agent.replay()memory.py:248-271,内部调用 agents.py:859-866)——纯人类可读打印,不会作为模型输入回灌。
  • save()/push_to_hub()/from_folder()/from_hub()agents.py:892-1213, 1064-1158)持久化的是agent 定义本身(工具代码、prompt YAML、agent.json 配置)到文件夹/HF Hub Space,用于重新实例化——这是 agent 配置持久化,不是会话记忆持久化。
  • 官方文档(docs/.../tutorials/memory.md)确认设计意图:自定义记忆管理的官方推荐做法是用 step_callbacks 原地修改 agent.memory.steps(例如示例中把早期 ActionStep.observations_images 里的旧截图剔除以省 token)——即用户需要自己写 hook;框架本身不提供基于向量的长期/RAG 记忆。

工具体系(定义/调用协议/注册/权限)

  • Tool 基类(tools.py:106-...)要求类属性 name: strdescription: strinputs: dict(简化版 JSON-schema,type 取值集合 AUTHORIZED_TYPES = ["string","boolean","integer","number","image","audio","array","object","any","null"],及 description)、output_type: str。通过 __init_subclass__validate_after_init 装饰器(tools.py:70-77, 140-142)在子类初始化时强制调用 self.validate_arguments()
  • validate_arguments()tools.py:144-226)检查:必需属性的类型存在性/正确性、工具名是合法 Python 标识符(is_valid_name)、每个 input 的 typeAUTHORIZED_TYPES 内,以及(除非 skip_forward_signature_validationforward() 的参数名与 inputs.keys() 完全一致、且 inputs 字典与函数签名的 nullable 标记通过 _convert_type_hints_to_json_schema 保持一致。
  • 两种调用面:
    • CodeAgent:工具作为字面 Python 可调用对象暴露在沙箱解释器里——Tool.to_code_prompt()tools.py:258-287)把每个工具渲染成 def toolname(args) -> type: """docstring""" 桩函数注入系统提示的代码块,运行时 LocalPythonExecutor.send_tools()local_python_executor.py:1763-1765)把 {**tools, **BASE_PYTHON_TOOLS, **additional_functions} 合并进 static_tools,LLM 字面写 tool_name(arg=val) 作为 Python 代码。
    • ToolCallingAgent:工具经 to_tool_calling_prompt()tools.py:289-290)和模型原生 function-calling schema(model.generate(tools_to_call_from=...))暴露;分发在 MultiStepAgent.execute_tool_call()agents.py:1453-1502)中完成——按名称在 {**self.tools, **self.managed_agents} 里查找,用 _substitute_state_variablesagents.py:1444-1451)把匹配 self.state 键的参数替换(让图片等工具输出可跨步骤按名引用),执行前调用 validate_tool_arguments(),异常包装成 AgentToolCallError/AgentToolExecutionError 并附带重试提示(“Please try again or use another tool”)。
  • 工具注册/创建路径:@tool 装饰器(tools.py:1061+)把普通函数动态转成 Tool 子类实例;Tool.from_space()/from_hub()/from_gradio()/from_langchain()(仅 grep 确认存在,未深读)包装外部可调用对象;ToolCollection.from_hub()tools.py:909-947)拉取 HF collection 里所有 Space 工具;ToolCollection.from_mcp()tools.py:949-1058)经第三方 mcpadapt.MCPAdapt + SmolAgentsAdapter 把 MCP 服务器的工具包装进来,受 trust_remote_code=True 门控(否则抛 ValueErrortools.py:1052-1056)——这是本框架里最接近”工具加载权限门”的机制。
  • 内置工具注册表 TOOL_MAPPINGdefault_tools.py):python_interpreterfinal_answer(总是自动添加,agents.py:402)、user_inputduckduckgo_searchgoogle_searchweb_searchApiWebSearchTool/WebSearchTool)、visit_webpagewikipedia_searchspeech_to_text(包装 HF transformers pipeline 的 PipelineTool)。
  • 结构化输出支持(output_schema 属性,tools.py:161-164, 258-287):工具可声明返回值的 JSON schema,to_code_prompt() 会把它渲染进工具 docstring,让 LLM 知道可以直接取字段而不需中间 print()

Prompt 设计(系统提示结构、动态组装)

  • Prompt 完全外置为 Jinja2 模板化的 YAML,不是硬编码字符串:prompts/code_agent.yamlprompts/toolcalling_agent.yamlprompts/structured_code_agent.yaml(CodeAgent 的结构化输出变体),若调用方不提供自定义 prompt_templates,在 agent 构造时经 importlib.resources 加载(agents.py:1241-1243 for ToolCallingAgent,1548-1554 for CodeAgent)。
  • PromptTemplates TypedDict(agents.py:166-192)有四个必需顶层字段:system_promptplanninginitial_plan/update_plan_pre_messages/update_plan_post_messages)、managed_agenttask/report)、final_answerpre_messages/post_messages)。构造函数会断言(agents.py:316-326)任何自定义 prompt_templates 必须包含全部这些 key——不允许静默的部分覆盖。
  • 动态组装经 populate_template()agents.py:102-107,是 jinja2.Template(..., undefined=StrictUndefined).render(**variables) 的薄封装)——StrictUndefined 意味着任何模板中引用但缺失的变量会立即报错(快速失败,不会静默留空)。
  • code_agent.yaml 系统提示结构(全文已保存):角色设定 → 6 个 few-shot 完整示例(覆盖多工具链式调用、数学、图像+翻译、迭代式网页搜索、比较任务、双信源确认)→ 动态注入的工具桩({% for tool in tools.values() %}{{ tool.to_code_prompt() }}{% endfor %})→ 条件注入的 managed-agent 桩(仅当 {%- if managed_agents and managed_agents.values() | list %} 时渲染)→ 11 条编号硬规则(如”9. only import from: {{authorized_imports}}”、“6. never re-do a tool call with the exact same parameters”)→ 条件注入的 {{custom_instructions}}(构造函数 instructions 参数)→ “Now Begin!”。
  • toolcalling_agent.yaml(经 diff 核实)在动作表达部分有实质差异:指示模型输出 Action:\n{"name": ..., "arguments": {...}} JSON 块而非代码,其余工具桩/managed-agent 桩/规则尾结构共享。
  • 规划提示(code_agent.yaml:176-287)是独立的迷你协议:“Facts survey”(已知事实/待查事实/待推导事实)然后是 “Plan”,以字面 <end_plan> 停止序列标签结束(同时作为 stop_sequences=["<end_plan>"] 传给 model.generateagents.py:660,672,718,730)。
  • managed_agent.task 模板(code_agent.yaml:289-304)是管理者 agent 把子任务包给受管 agent 时的包装(MultiStepAgent.__call__agents.py:868-890)——强制受管 agent 的 final_answer 变成三段式结构化报告(简短结论/详细结论/额外背景),专门避免管理者只拿到一句话答案。
  • 代码块分隔符本身可按 agent 配置(code_block_tags 参数,agents.py:1523,1556-1564):默认 ("<code>","</code>"),或 "markdown" 简写为三反引号 python 代码块——这会改变代入 Jinja 模板的字面标签字符串({{code_block_opening_tag}}/{{code_block_closing_tag}}),即 prompt 结构会按输出格式动态重参数化,不只是按工具集变化。

Router / 编排(任务分解、多 agent、子 agent)

  • 没有独立的”路由”模块——多 agent 编排是把下级 agent 当作一个可调用工具来实现的:MultiStepAgent._setup_managed_agents()agents.py:369-387)要求每个受管 agent 都有 namedescription,然后强制设置 agent.inputs = {"task":..., "additional_args":...}agent.output_type = "string" 使其满足类 Tool 调用契约,存入 self.managed_agents: dict[name, agent]
  • 分发方式:ToolCallingAgent.execute_tool_call()agents.py:1453-1502)里查找字典是 {**self.tools, **self.managed_agents}——受管 agent 与工具调用方式完全相同,只是走 agent(**arguments) 而非 tool(**arguments, sanitize_inputs_outputs=True)agents.py:1486 处分支)。在 CodeAgent 里,受管 agent 与工具一样以可导入函数形式暴露在沙箱内(self.python_executor.send_tools({**self.tools, **self.managed_agents})agents.py:492)。
  • 管理者调用受管 agent 的约定:MultiStepAgent.__call__()agents.py:868-890)用 managed_agent.task Jinja 模板包装原始任务字符串(增加框架性描述:“You’re a helpful agent named X… your manager…“),运行受管 agent 自己完整的 run() 循环,再用 managed_agent.report 模板包装受管 agent 的最终答案;若受管 agent 的 provide_run_summary=True,会附加该受管 agent memory 中所有消息的截断转储(write_memory_to_messages(summary_mode=True))作为 <summary_of_work> 块供管理者查看。
  • 除了单 agent 内可选的 PlanningStep(facts+plan)外,没有内置的任务分解/规划算法——跨多 agent 的分解完全交给 LLM 自己判断调用哪个受管 agent/工具;没有调度器、没有 DAG 执行器,也没有跨受管 agent 的自动并行分发(并行仅存在于单个 ToolCallingAgent 单步内的同时多工具调用,agents.py:1417-1434,经 ThreadPoolExecutor)。
  • 官方文档示例拓扑(docs/.../examples/multiagents.md):一个 CodeAgent”manager” 持有类 python_interpreter 代码执行工具,加一个受其管理的 ToolCallingAgent”Web Search agent”(持有 WebSearchTool+VisitWebpageTool)——明确是两层树,没有证据支持更深层的原生递归(除了每个子 agent 各自独立的 max_steps)。
  • 官方设计理由(guided_tour.md,仅 grep 摘录未单独存档):把受管 agent 的记忆隔离开是刻意的上下文隔离手段(原文大意:“为什么要把代码生成 agent 的记忆塞满网页搜索 agent 访问过的所有网页内容?分开更好”)。
  • 远程执行下的限制(来自 secure_code_execution.md):E2B 单代码片段沙箱明确不支持受管 agent(“since any call to a managed agent would require model calls… this solution does not work (yet) with more complicated multi-agent setups”)——代码里也确认:CodeAgent.create_python_executor()agents.py:1598-1618)在 self.managed_agents 非空且执行器非本地类型时会抛 Exception("Managed agents are not yet supported with remote code execution.")

Skill / 插件体系

  • 没有独立于”工具”的一等公民”skill”抽象。最接近插件式扩展点的是:
    • MCP(Model Context Protocol)集成——MCPClientmcp_client.py,全文读过)和 ToolCollection.from_mcp()tools.py:949-1058),包装第三方 mcpadapt 库把任意 MCP 服务器的工具转成 smolagents.Tool 实例。支持 stdio、streamable-HTTP、以及旧版 SSE 传输。需要显式 trust_remote_code=True
    • HF Hub 工具/agent 共享——Tool.from_hub()/push_to_hub()ToolCollection.from_hub()(拉取 HF Hub “collection” 中所有 Space 托管的工具),以及整个 agent 的 Agent.from_hub()/push_to_hub()/from_folder()/save()agents.py:892-1213)——把一个 agent(prompts.yaml + agent.json + tools/*.py + managed_agents/ + 自动生成的 Gradio app.py)序列化到可移植的文件夹/HF Space——这是本框架里最接近”可发布插件包”的机制,同样受 trust_remote_code=True 门控。
    • LangChain / Gradio 工具包装——Tool.from_langchain()Tool.from_gradio()(类名仅 grep 确认,未深读)让外部生态工具可以被适配进来。
  • 没有沙箱化/带权限的”技能市场”概念,也没有除了从每个工具检测到的 import 自动生成 requirements.txtTool.to_dict()tools.py:357-359)之外的技能版本/依赖管理体系。

自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)

  • 未实现。 src/smolagents/ 中没有任何代码实现自我改进、学习型/自适应记忆,或 agent 自身 prompt/policy 的 eval 驱动纠错。曾在 src/docs/examples/ 中搜索 self.improvefine-tun*RLHFrewardtraining databenchmark 等关键词——命中的只是(a)不相关的文档描述(RAG 相对微调的优势、教程系统提示里一句玩笑性的”$1,000,000 reward”、关于代码动作为何好用的”训练语料”泛泛评论),以及(b)examples/smolagents_benchmark/examples/open_deep_research/——这些是外部评测工具(GAIA benchmark runner,run_gaia.py),运行冻结的 agent 打分,消费 agent 而不反哺 agent。
  • 运行内确实存在的”自我纠正”只是标准 ReAct 重试循环:工具/代码错误变成 Observation:/Error: 消息追加到 memory(memory.py:138-148,原文有”take care not to repeat previous errors! If you have retried several times, try a completely different approach”),同一个冻结模型下一步再试——这是提示词层面的重试,不是学习/权重更新/持久化记忆适应。
  • 结论:该维度对本框架(当前形态)不适用。

可观测性(日志 / trace 格式)

  • 核心库不自带结构化 trace/日志文件格式;可观测性故事完全委托给 OpenTelemetry,经由外部插桩包 openinference-instrumentation-smolagents(在 pyproject.tomltelemetry extras group 里,与 arize-phoenixopentelemetry-sdkopentelemetry-exporter-otlp 并列)。经 docs/.../tutorials/inspect_runs.md(全文保存)确认:SmolagentsInstrumentor().instrument() 自动把所有 agent 运行插桩成 OTel span,可发送到 Arize Phoenix、MLflow(mlflow.smolagents.autolog())或 Langfuse。
  • 核心库自带的是面向人类的 Rich 控制台日志器 AgentLoggermonitoring.py:130-273):LogLevel 枚举(OFF/ERROR/INFO/DEBUG)、log_task/log_rule/log_code/log_markdown/log_messages/visualize_agent_tree——这是终端 UX,不是机器可解析的 trace 格式。
  • Monitor 类(monitoring.py:81-117)跟踪单次运行的聚合指标(step_durationstotal_input_token_counttotal_output_token_count),以 ActionStep 回调形式挂载(agents.py:434self.step_callbacks.register(ActionStep, self.monitor.update_metrics))。
  • 不依赖外部工具的程序化内省:agent.memory.get_full_steps()/get_succinct_steps()(每个 step 的字典转储,含原始 model_input_messagesmodel_output_messagetool_callstoken_usagetiming)和 agent.replay(detailed=bool)memory.py:248-271,打印完整运行过程到控制台)——如果不接 OTel,这就是”trace 格式”:一份可经 make_json_serializable 序列化的、派生自 dataclass 的普通 Python 字典列表。
  • step_callbacksCallbackRegistrymemory.py:280-317)是其他日志/追踪集成挂载的通用 hook 点——回调按 MemoryStep 子类注册,在每步结束后触发(agents.py:620-623)。

安全与权限(审批门、密钥管理)

  • 核心库没有 API-key/密钥保险库或凭证管理子系统——模型 API key 直接经构造函数参数/环境变量传入各 Model 子类(models.py,如 InferenceClientModel(token=...)),没有集中式密钥存储。
  • 本代码库中主要的”权限门”模式是对任何执行不可信/远程代码的地方使用显式 opt-in 标志,且始终默认走安全/拒绝一侧:
    • Agent.from_hub(..., trust_remote_code: bool=False)agents.py:1065-1101)——除非调用方传 trust_remote_code=True 否则抛 ValueError;docstring 明确警告”ALWAYS inspect the tool you’re downloading before loading it within your runtime, as you would do when installing a package using pip/npm/apt.”。
    • ToolCollection.from_hub(..., trust_remote_code: bool=False).from_mcp(..., trust_remote_code: bool=False)tools.py:913-914, 954)——同样模式;from_mcp 若未设置会抛 ValueError("Loading tools from MCP requires you to acknowledge you trust the MCP server...")tools.py:1052-1056)。
    • MCPClient(structured_output: bool|None)ToolCollection.from_mcp(structured_output: bool|None) 在调用方未显式选择时发出 FutureWarning,当前默认走保守的 False 行为。
    • RemotePythonExecutor(allow_pickle: bool=False)remote_executors.py:70-78)——pickle 反序列化仅 opt-in,docstring 明确内联警告”Security Warning: Pickle deserialization can execute arbitrary code”;默认路径使用 SafeSerializer(仅 JSON)(serialization.py,未深读但在 remote_executors.py:37, 116-131 引用/导入)。
    • Agent(反)序列化安全:AGENT_REGISTRY = {"ToolCallingAgent": ToolCallingAgent, "CodeAgent": CodeAgent}agents.py:1806-1813)是显式白名单,from_dict/from_folder 用它来”prevent arbitrary code execution via importlib-based dynamic loading”(agents.py:1807-1809 注释)——即反序列化 agent 配置无法按字符串类名实例化任意 Python 类。
  • agents.py/tools.py 中没有实现内置的交互式”审批门”(例如高风险工具调用前请求人类确认)——CLI(cli.py)一旦启动就是自动执行(interactive_mode() 只在运行前询问设置选项,运行中不会逐步请求审批)。
  • final_answer_checksagents.py:287-291, 335, 589-593, 613-618)是一个校验门,但针对输出正确性而非安全——一组用户提供的谓词函数在最终答案被接受前运行,任一失败会抛 AgentError 迫使 agent 重试,不是安全控制。

沙箱与执行隔离

两层设计,官方文档(secure_code_execution.md,全文保存)与代码中均明确:

  1. LocalPythonExecutorlocal_python_executor.py:1688-1768)——不是真正的沙箱(docstring 明确写道”It is not a security sandbox: for isolated execution of untrusted code, use a remote executor”)。它是一个自研 AST 解释器(evaluate_python_code + 约 40 个 evaluate_* 节点处理函数,如 evaluate_callevaluate_importevaluate_forevaluate_while),只实现明确识别的操作——其余一律抛 InterpreterError
    • Import 白名单:authorized_imports 参数,经 check_import_authorized()local_python_executor.py:372)和 get_safe_module():1271,递归剥离未授权的子模块访问,支持通配符 numpy.*)检查。
    • 显式黑名单模块 DANGEROUS_MODULES(即使理论上可达也拒绝):builtins, io, multiprocessing, os, pathlib, pty, shutil, socket, subprocess, sys:130-141)。
    • 按限定名黑名单的 DANGEROUS_FUNCTIONS(无论 import 路径如何):builtins.compile/eval/exec/globals/locals/__import__os.popenos.systemposix.system:143-153),由每个 AST-eval 函数返回值上的 check_safer_result()/safer_eval 装饰器强制执行(:156-208)。
    • 默认阻断 dunder 属性/方法访问(ALLOWED_DUNDER_METHODS = ["__init__","__str__","__repr__"]nodunder_getattr():68-71evaluate_attribute:383-391)和 evaluate_call:917)里再次检查)。
    • 资源上限:MAX_OPERATIONS = 10_000_000MAX_WHILE_ITERATIONS = 1_000_000MAX_EXECUTION_TIME_SECONDS = 30 秒的墙钟 timeout() 装饰器(:57-60, 285-...),包裹整个 _execute_code() 调用(:1663-1667)。
    • 官方文档明确记载的残余风险:一个被攻陷的授权包(如 Pillow 生成巨型文件占满磁盘)仍可造成危害;教程原话”no local python sandbox can ever be completely secure.”。
  2. 远程执行器remote_executors.py)提供真正的 OS 级隔离,均实现共享的 RemotePythonExecutor ABC(:53-135,定义 send_tools/send_variables/__call__run_code_raise_errors):
    • E2BExecutor:335-448)——E2B 云沙箱(e2b_code_interpreter.Sandbox),同时支持 v1(Sandbox(**kwargs))和 v2(Sandbox.create(**kwargs))SDK 形态;.cleanup() 调用 sandbox.kill()
    • DockerExecutor:551-724,全文读过)——起一个本地 Docker 容器跑 Jupyter Kernel Gateway(从内联默认 Dockerfile 自动构建镜像:python:3.12-bullseye + jupyter_kernel_gateway),经带认证(KG_AUTH_TOKEN)的 WebSocket 按代码 cell 通信,.cleanup() 停止并移除容器。文档的”advanced Docker usage”章节额外建议 USER nobodymem_limitcpu_quotapids_limitsecurity_opt=["no-new-privileges"]cap_drop=["ALL"]——但这些加固参数是文档层面给用户自建 DockerSandbox helper 类的建议,不是 DockerExecutor 类本身的默认值(该类自己的 container_run_kwargs 起始为空,需调用方自行 opt-in)。
    • ModalExecutor:726-...)——modal.Sandbox.create(...),隧道转发端口做 kernel 通信,.cleanup() 调用 sandbox.terminate()
    • BlaxelExecutor:859-...)——Blaxel 云沙箱(文档宣称亚 25ms 的休眠恢复),创建具名沙箱(smolagent-executor-<uuid>),经 WS 封装的类 Jupyter-kernel API 通信,显式 _delete_sandbox/.cleanup()
    • 所有远程执行器都经 SafeSerializerserialization.py)做序列化,allow_pickle 仅 opt-in(对应安全与权限章节)。
    • 所有远程单片段执行器共享的架构性限制:多 agent(受管 agent)不受支持,因为 LLM/API-key 留在本地,只有代码片段跨越信任边界(对应 Router/编排章节);CodeAgent.create_python_executor()agents.py:1607-1609)在非本地执行器与 managed_agents 组合时会硬性抛错。
    • 文档记载的”Approach 2”(把整个 agentic 系统连模型调用一起放进沙箱运行)是一种文档化模式(E2B 多 agent 示例,secure_code_execution.md 约 198-253 行),但需要用户自己手动构造并把 agent 代码打包送进沙箱——没有对应的一等 executor_type,本质就是”在 Sandbox() 里装 smolagents 跑一个脚本”。

与模型的协同设计

  • Model 是抽象基类(models.py,仅列过类名未深读),按后端有具体子类:VLLMModelMLXModelTransformersModel(本地推理),ApiModelLiteLLMModel/LiteLLMRouterModel/InferenceClientModel/OpenAIModelAzureOpenAIModel/AmazonBedrockModel(API 类)。未逐行深读,但 agents.py 中的调用点显示了契约:model.generate(messages, stop_sequences=..., tools_to_call_from=...) -> ChatMessage,可选的 model.generate_stream(...) -> Generator[ChatMessageStreamDelta](仅当后端存在该属性时使用,经 hasattr(self.model, "generate_stream") 判断,agents.py:658, 714, 1253-1256, 1292, 1574-1577, 1659),以及 model.parse_tool_calls(chat_message) 作为后端不原生区分 tool-call 与文本时的兜底(agents.py:1327-1331)。
  • 存在两种相互竞争的动作表达方式,正是因为不同模型类别/规模在其中一种上表现更好:code-blob(CodeAgent)vs JSON tool-call(ToolCallingAgent)——secure_code_execution.md 中的官方理由引用了 3 篇论文(arXiv 2402.01030《Executable Code Actions Elicit Better LLM Agents》、2411.01747、2401.00812)说明代码即动作为何优于 JSON 工具调用格式;react.md 则指出对于需要”在每次页面交互后等待”的 agent(如浏览器 agent),JSON 仍可能更合适。
  • use_structured_outputs_internally 标志(agents.py:1520,1539,1546-1554,1656-1658)把 CodeAgent 切换到专用的 structured_code_agent.yaml 提示 + response_format=CODEAGENT_RESPONSE_FORMAT(从 models.py 导入),强制利用 LLM 的结构化输出/JSON 模式能力保证输出 {"code": "..."} 信封格式——代码注释明确说明这是为提升”许多支持结构化解码的模型”的表现。
  • 工具 docstring 的渲染是模型规模感知的:Tool.to_code_prompt()tools.py:258-287)会追加一条额外说明(“Important: This tool returns structured output!… NO print() statements needed”),专门”for smaller models (e.g. Mistral Small, Gemma 3, etc.) to properly handle structured output”——即 prompt 措辞会针对已知模型能力档位显式调优。
  • 停止序列协调很紧密:agent 只在代码块闭合标签不会与开启标签混淆时才把它追加进停止序列列表(agents.py:1651-1654),并在 "Observation:"/"Calling tools:" 处停止工具调用生成(agents.py:1295,1311,1662),防止模型幻觉出自己的假观测——这是直接与 write_memory_to_messages 下一轮如何原样回灌这些 token 绑定的 prompt/解码协同设计。

轨迹利用(session/trajectory 是否反哺训练/评测)

  • 没有内置的轨迹→训练管线。 src/smolagents/ 中没有任何代码把运行轨迹写成训练数据格式,或触发任何微调/RL 步骤。
  • 存在的只是纯评测用途,且完全位于 examples/ 下,在可安装包之外:
    • examples/smolagents_benchmark/run.py + score.ipynb——对(冻结的)agent 跑某个 benchmark 集并在 notebook 里打分;没有反馈回 agent 代码的环节。
    • examples/open_deep_research/run_gaia.py(仅 grep)——针对 GAIA benchmark 跑 agent(“Failure or ‘I cannot answer’… will not be tolerated, success will be rewarded” 是展示给 agent 的提示文本,不是接到任何训练器的 RL 奖励信号)。
  • 唯一的结构化”轨迹导出”能力是通用的 memory 转储(agent.memory.get_full_steps(),见可观测性章节),下游用户可以把它接入自己的离线训练/评测管线,但 smolagents 本身不提供这样的消费者——完全交给外部工具(可观测性章节中的 OTel/Phoenix/MLflow/Langfuse 集成是最接近”轨迹留存供后续使用”的东西,但那些也是给人类调试/监控用的,本仓库中没有任何地方把它们文档化为训练数据来源)。

与同类 harness 的关键差异(1-3 条,可以先留一句概述,后续 synthesis 阶段会做跨 harness 对比)

  • 相比多数”重”agent 框架,smolagents 的架构赌注极其单一——只有一个 MultiStepAgent ReAct 循环 + 两种动作表达(代码 vs JSON tool-call),没有独立的路由器/调度器/长期记忆模块,多 agent 编排直接退化为”把 agent 当工具调用”,这是刻意的极简主义而非能力缺失的遮掩(README/文档反复强调”smol”)。
  • 安全模型采取”数据平面沙箱 + 显式信任声明”而非”运行时人工审批”:trust_remote_code=True 式的一次性 opt-in 网关覆盖了所有加载外部代码的路径(Hub agent/tool、MCP server),但没有逐工具调用的人在环确认,这与一些强调”审批门”的 harness(如需要人工确认高危操作)形成对比。
  • 可观测性和轨迹利用两个维度都不自建,彻底外包给生态(OpenTelemetry/Phoenix/MLflow/Langfuse 做 trace,什么都不做训练反馈)——这与一些自带完整 trace schema 或训练数据导出管线的 harness 是明显的设计取舍差异,后续 synthesis 阶段可展开对比。

原始源码定位

  • repo: https://github.com/huggingface/smolagents
  • commit/version analyzed: 526069c1ead958b36d9fd09a6b1ef37f68ed6ade(2026-06-16 提交,2026-07-07 shallow clone 检出)
  • 关键文件列表(相对 repo 根路径):
    • src/smolagents/agents.py
    • src/smolagents/memory.py
    • src/smolagents/monitoring.py
    • src/smolagents/local_python_executor.py
    • src/smolagents/remote_executors.py
    • src/smolagents/tools.py
    • src/smolagents/mcp_client.py
    • src/smolagents/cli.py
    • src/smolagents/default_tools.py(仅 grep 类名列表)
    • src/smolagents/models.py(仅 grep 类名列表)
    • src/smolagents/prompts/code_agent.yaml
    • src/smolagents/prompts/toolcalling_agent.yaml
    • src/smolagents/prompts/structured_code_agent.yaml
    • docs/source/en/conceptual_guides/react.md
    • docs/source/en/tutorials/secure_code_execution.md
    • docs/source/en/tutorials/inspect_runs.md
    • docs/source/en/tutorials/memory.md
    • docs/source/en/examples/multiagents.md
    • AGENTS.md(repo 根,仅 4 行贡献者风格指南)
    • pyproject.toml(仅 grep telemetry extras)
    • examples/smolagents_benchmark/examples/open_deep_research/(仅目录列表,外部评测脚本)

一手源存档(sources/)

/Users/zhao/projects/self-wiki/ai-research/sources/harness/smolagents/ 下保存的文件:

  • NOTES.md(第一阶段调研员的完整逐维度笔记,182 行)
  • key-files/agents.py
  • key-files/cli.py
  • key-files/local_python_executor.py
  • key-files/mcp_client.py
  • key-files/memory.py
  • key-files/monitoring.py
  • key-files/remote_executors.py
  • key-files/tools.py
  • key-files/docs/inspect_runs.md
  • key-files/docs/memory.md
  • key-files/docs/multiagents.md
  • key-files/docs/react.md
  • key-files/docs/secure_code_execution.md
  • key-files/prompts/code_agent.yaml
  • key-files/prompts/structured_code_agent.yaml
  • key-files/prompts/toolcalling_agent.yaml