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,分析基准 commit526069c1ead958b36d9fd09a6b1ef37f68ed6ade(2026-06-16),2026-07-07 shallow clone 检出并抽取关键文件。 - 核心库全部在
src/smolagents/下,无子包拆分,按文件划分职责:agents.py(1813 行)——MultiStepAgent抽象基类、ToolCallingAgent、CodeAgent、RunResult、planning-step 逻辑、agent 树可视化、save/push_to_hub、AGENT_REGISTRY。memory.py(316 行)——MemoryStep层级(ActionStep/PlanningStep/TaskStep/SystemPromptStep/FinalAnswerStep)、AgentMemory、CallbackRegistry。monitoring.py(273 行)——AgentLogger(Rich 控制台日志)、Monitor(token/耗时指标)、TokenUsage、Timing。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 行)——smolagentCLI 入口。prompts/{code_agent,toolcalling_agent,structured_code_agent}.yaml——三套完整 Jinja2 系统提示模板。
- 无独立的 router/orchestrator 模块、无独立的 memory-store 模块、无训练/评测反馈模块——这些维度在源码里都不存在对应文件,是有意留白(见下文各章节)。
Agent Loop(主循环 / 何时继续何时停)
- 唯一抽象:
MultiStepAgent(agents.py:268),官方文档自称”an abstraction of ReAct framework”(docs/source/en/conceptual_guides/react.md)。 - 循环驱动是生成器
_run_stream(agents.py:540-611):while not returned_final_answer and self.step_number <= max_steps。每轮迭代:- 若设置了
planning_interval且步数命中间隔,先跑一次规划步(agents.py:549-567,委托给_generate_planning_step,agents.py:639-747)。 - 构造
ActionStep,调用_step_stream(action_step)(抽象方法,由子类实现)。 - 通过
ActionOutput.is_final_answer判定是否终止;若有final_answer_checks校验器则执行(agents.py:589-593,_validate_final_answer在 613 行)。 - 遇到
AgentError(非生成错误)时把错误写入 step 并继续循环(agents.py:597-599)——模型可恢复的错误会作为Observation:/Error:消息反馈回去;AgentGenerationError(实现层错误)总是重新抛出并退出循环。 - 停止条件:(a) 生成了最终答案;(b)
step_number > max_steps时_handle_max_steps_reached(agents.py:625-637)强制再发一次 LLM 请求索要 best-effort 答案,并打上AgentMaxStepsError标记;(c) 通过agent.interrupt()置位的self.interrupt_switch(agents.py:754-756)会在下一轮循环开头抛AgentError("Agent interrupted.")——这是手动取消机制,没有异步 cancellation token。
- 若设置了
- 两种具体的动作执行策略共享该循环:
ToolCallingAgent(agents.py:1215-1502)——_step_stream调用model.generate(...,tools_to_call_from=...),拿到chat_message.tool_calls(原生 tool-calling API 或经model.parse_tool_calls解析),经process_tool_calls(agents.py:1361-1442)执行——当一条消息里返回多个 tool call 时,用ThreadPoolExecutor并行执行(agents.py:1417-1434,受max_tool_threads配置控制)。CodeAgent(agents.py:1505-1804)——_step_stream调用model.generate(...),用parse_code_blobs按code_block_tags(默认<code>...</code>,或 Jinja 可配为 markdown 代码块)提取 Python 代码片段,交给PythonExecutor(self.python_executor(code_action))执行,若code_output.is_final_answer则将code_output.output作为最终答案返回。
RunResult(agents.py:196-253)——return_full_result=True时的对外返回对象:聚合output、state("success"|"max_steps_error")、steps(完整 memory dump)、token_usage、timing。- 流式:
run(stream=True)直接返回生成器本体(agents.py:494-496);非流式run()内部耗尽生成器,返回最后一个FinalAnswerStep。
记忆与上下文管理(压缩、长期记忆、会话持久化)
- 核心库没有跨运行/长期记忆或持久化层。
AgentMemory(memory.py:214-277)纯粹是进程内、单次运行范围的:system_prompt(SystemPromptStep)+steps: list[TaskStep|ActionStep|PlanningStep]。agent.memory.reset()只是清空steps(memory.py:232-234);run()开始时默认自动调用,除非传reset=False(agents.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),渲染成带具体MessageRole(ASSISTANT/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: str、description: str、inputs: 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 的type在AUTHORIZED_TYPES内,以及(除非skip_forward_signature_validation)forward()的参数名与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_variables(agents.py:1444-1451)把匹配self.state键的参数替换(让图片等工具输出可跨步骤按名引用),执行前调用validate_tool_arguments(),异常包装成AgentToolCallError/AgentToolExecutionError并附带重试提示(“Please try again or use another tool”)。
- CodeAgent:工具作为字面 Python 可调用对象暴露在沙箱解释器里——
- 工具注册/创建路径:
@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门控(否则抛ValueError,tools.py:1052-1056)——这是本框架里最接近”工具加载权限门”的机制。 - 内置工具注册表
TOOL_MAPPING(default_tools.py):python_interpreter、final_answer(总是自动添加,agents.py:402)、user_input、duckduckgo_search、google_search、web_search(ApiWebSearchTool/WebSearchTool)、visit_webpage、wikipedia_search、speech_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.yaml、prompts/toolcalling_agent.yaml、prompts/structured_code_agent.yaml(CodeAgent 的结构化输出变体),若调用方不提供自定义prompt_templates,在 agent 构造时经importlib.resources加载(agents.py:1241-1243for ToolCallingAgent,1548-1554for CodeAgent)。 PromptTemplatesTypedDict(agents.py:166-192)有四个必需顶层字段:system_prompt、planning(initial_plan/update_plan_pre_messages/update_plan_post_messages)、managed_agent(task/report)、final_answer(pre_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.generate,agents.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 都有name和description,然后强制设置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.taskJinja 模板包装原始任务字符串(增加框架性描述:“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)集成——
MCPClient(mcp_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/ + 自动生成的 Gradioapp.py)序列化到可移植的文件夹/HF Space——这是本框架里最接近”可发布插件包”的机制,同样受trust_remote_code=True门控。 - LangChain / Gradio 工具包装——
Tool.from_langchain()、Tool.from_gradio()(类名仅 grep 确认,未深读)让外部生态工具可以被适配进来。
- MCP(Model Context Protocol)集成——
- 没有沙箱化/带权限的”技能市场”概念,也没有除了从每个工具检测到的 import 自动生成
requirements.txt(Tool.to_dict(),tools.py:357-359)之外的技能版本/依赖管理体系。
自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)
- 未实现。
src/smolagents/中没有任何代码实现自我改进、学习型/自适应记忆,或 agent 自身 prompt/policy 的 eval 驱动纠错。曾在src/、docs/、examples/中搜索self.improve、fine-tun*、RLHF、reward、training data、benchmark等关键词——命中的只是(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.toml的telemetryextras group 里,与arize-phoenix、opentelemetry-sdk、opentelemetry-exporter-otlp并列)。经docs/.../tutorials/inspect_runs.md(全文保存)确认:SmolagentsInstrumentor().instrument()自动把所有 agent 运行插桩成 OTel span,可发送到 Arize Phoenix、MLflow(mlflow.smolagents.autolog())或 Langfuse。 - 核心库自带的是面向人类的 Rich 控制台日志器
AgentLogger(monitoring.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_durations、total_input_token_count、total_output_token_count),以ActionStep回调形式挂载(agents.py:434,self.step_callbacks.register(ActionStep, self.monitor.update_metrics))。- 不依赖外部工具的程序化内省:
agent.memory.get_full_steps()/get_succinct_steps()(每个 step 的字典转储,含原始model_input_messages、model_output_message、tool_calls、token_usage、timing)和agent.replay(detailed=bool)(memory.py:248-271,打印完整运行过程到控制台)——如果不接 OTel,这就是”trace 格式”:一份可经make_json_serializable序列化的、派生自 dataclass 的普通 Python 字典列表。 step_callbacks(CallbackRegistry,memory.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_checks(agents.py:287-291, 335, 589-593, 613-618)是一个校验门,但针对输出正确性而非安全——一组用户提供的谓词函数在最终答案被接受前运行,任一失败会抛AgentError迫使 agent 重试,不是安全控制。
沙箱与执行隔离
两层设计,官方文档(secure_code_execution.md,全文保存)与代码中均明确:
LocalPythonExecutor(local_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_call、evaluate_import、evaluate_for、evaluate_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.popen、os.system、posix.system(:143-153),由每个 AST-eval 函数返回值上的check_safer_result()/safer_eval装饰器强制执行(:156-208)。 - 默认阻断 dunder 属性/方法访问(
ALLOWED_DUNDER_METHODS = ["__init__","__str__","__repr__"],nodunder_getattr()在:68-71,evaluate_attribute(:383-391)和evaluate_call(:917)里再次检查)。 - 资源上限:
MAX_OPERATIONS = 10_000_000、MAX_WHILE_ITERATIONS = 1_000_000、MAX_EXECUTION_TIME_SECONDS = 30秒的墙钟timeout()装饰器(:57-60, 285-...),包裹整个_execute_code()调用(:1663-1667)。 - 官方文档明确记载的残余风险:一个被攻陷的授权包(如 Pillow 生成巨型文件占满磁盘)仍可造成危害;教程原话”no local python sandbox can ever be completely secure.”。
- Import 白名单:
- 远程执行器(
remote_executors.py)提供真正的 OS 级隔离,均实现共享的RemotePythonExecutorABC(: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 nobody、mem_limit、cpu_quota、pids_limit、security_opt=["no-new-privileges"]、cap_drop=["ALL"]——但这些加固参数是文档层面给用户自建DockerSandboxhelper 类的建议,不是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()。- 所有远程执行器都经
SafeSerializer(serialization.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,仅列过类名未深读),按后端有具体子类:VLLMModel、MLXModel、TransformersModel(本地推理),ApiModel→LiteLLMModel/LiteLLMRouterModel/InferenceClientModel/OpenAIModel→AzureOpenAIModel/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 的架构赌注极其单一——只有一个
MultiStepAgentReAct 循环 + 两种动作表达(代码 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.pysrc/smolagents/memory.pysrc/smolagents/monitoring.pysrc/smolagents/local_python_executor.pysrc/smolagents/remote_executors.pysrc/smolagents/tools.pysrc/smolagents/mcp_client.pysrc/smolagents/cli.pysrc/smolagents/default_tools.py(仅 grep 类名列表)src/smolagents/models.py(仅 grep 类名列表)src/smolagents/prompts/code_agent.yamlsrc/smolagents/prompts/toolcalling_agent.yamlsrc/smolagents/prompts/structured_code_agent.yamldocs/source/en/conceptual_guides/react.mddocs/source/en/tutorials/secure_code_execution.mddocs/source/en/tutorials/inspect_runs.mddocs/source/en/tutorials/memory.mddocs/source/en/examples/multiagents.mdAGENTS.md(repo 根,仅 4 行贡献者风格指南)pyproject.toml(仅 greptelemetryextras)examples/smolagents_benchmark/、examples/open_deep_research/(仅目录列表,外部评测脚本)
一手源存档(sources/)
/Users/zhao/projects/self-wiki/ai-research/sources/harness/smolagents/ 下保存的文件:
NOTES.md(第一阶段调研员的完整逐维度笔记,182 行)key-files/agents.pykey-files/cli.pykey-files/local_python_executor.pykey-files/mcp_client.pykey-files/memory.pykey-files/monitoring.pykey-files/remote_executors.pykey-files/tools.pykey-files/docs/inspect_runs.mdkey-files/docs/memory.mdkey-files/docs/multiagents.mdkey-files/docs/react.mdkey-files/docs/secure_code_execution.mdkey-files/prompts/code_agent.yamlkey-files/prompts/structured_code_agent.yamlkey-files/prompts/toolcalling_agent.yaml