跳到主要内容

smolagents — 本课题摘录

读了哪几篇: 01-agent-loop(通用多步 ReAct 骨架)、02-code-agent(把行动写成代码)、04-tools(工具抽象)。 其余三篇(受限 Python 执行器、模型与记忆、远程执行与安全)本轮没读——属沙箱与权限边界课题。

这一家的价值在于:它对"怎么认出模型要调工具"给了一个完全不同的答案——不认,让模型直接写代码。

它对本课题回答了什么

决定二:怎么认出模型要调工具 —— 让它写代码,不写调用

传统工具调用一步只能干一件事,要"搜三个关键词、各取首条、拼起来"就得三四轮往返。让模型直接写一段代码,一步之内就能连调多个工具、把结果存进变量、写循环和判断。 (依据:前沿库 · smolagents · CodeAgent(把行动写成代码) —— CodeAgent 让模型输出一整段 Python 代码,工具是预先注入执行环境的 Python 函数,一步内可连调多个工具并使用变量与控制流)

它给的理由是:代码本来就是为"组合调用 + 控制流"设计的,用 JSON 表达这些反而别扭。

怎么把代码从自由文本里抠出来 —— 用成对标签框定代码区,配一套四级兜底:

级别尝试什么
1用配置的标签正则抓
2抓不到就退回 markdown 代码围栏
3还抓不到就试着把整段文本当代码解析
4都失败,抛一个带"示范正确格式"的报错

(依据:前沿库 · smolagents · CodeAgent(把行动写成代码) —— parse_code_blobs 是四级兜底——配置标签正则 → markdown 围栏 → 整段文本当代码 ast.parse → 抛带示范格式的报错)

第 4 级的报错很讲究:文本里同时出现 "final" 和 "answer" 时,它会专门提示"你像是想给最终答案,应该这样写"。

这是全库反复出现的手法:报错本身就是给模型的教学。 跟 cline 的"错误即消息"是同一件事的两种说法。

停止序列的巧思: 把代码块的闭合标签加进停止序列,模型一写完代码就停,不浪费 token 继续编"观测"。反过来,如果模型输出没以闭合标签结尾,手动补上并写回历史——既保证能抠出代码,又把这个结尾"示范"给后续调用,诱导模型养成早停习惯。 (依据: shelf=frontier/smolagents#02-code-agent @e3a5b8994b30 事实=把 加进停止序列让模型写完代码即停;输出未以闭合标签结尾时手动补上并写回历史,诱导模型早停)

决定四:什么时候停 —— 用异常穿透,而且必须绕过模型自己的 try/except

收工靠模型在代码里调一个特殊函数。 难点是:这个函数在模型写的代码内部被调用,系统怎么知道该结束整个运行?

答案是把它包一层,调用即抛异常,执行循环在顶层捕获,把异常带的值当最终答案。

最妙的一处细节:这个异常继承的是最顶层的基类,不是普通异常类。 (依据:前沿库 · smolagents · CodeAgent(把行动写成代码) —— FinalAnswerException 继承 BaseException 而非 Exception,因为模型写的代码里常有宽泛的 except Exception 会把「我要交答案」这个信号吃掉)

为什么: 模型写的代码里常有 try: ... except Exception: ...。如果这个异常是普通异常类,模型代码里一个宽泛的 except 就会把"我要交答案"这个信号吃掉。继承最顶层基类让它绕过所有常规捕获,稳稳穿透到执行器顶层。

这条是"代码即动作"这条路独有的坑,而且极隐蔽。要抄这条路就必须抄这个细节。

还有一个小修补:模型有时写成 final_answer = 42final_answer(...),把函数名覆盖成了变量——它用正则把这类误赋值修回来。

决定二补充:同一份工具元数据,两副面孔

一个工具 = 四个声明式属性(名 / 描述 / 入参 / 出参)+ 一个干实事的方法。 同一份元数据渲染出两种说明书:

面孔给谁长什么样
代码签名写代码的那种 agent一个带文档字符串的 Python 函数声明
JSON schema走原生工具调用的那种 agent厂商标准的 function schema

(依据:前沿库 · smolagents · 工具抽象(一个函数的两副面孔) —— 同一份工具元数据用 to_code_prompt 渲染成带 docstring 的 Python 函数签名给 CodeAgent、用 to_tool_calling_prompt 渲染成 JSON schema 给 ToolCallingAgent)

这条对我们直接有用: 工具的定义应该跟"怎么讲给模型听"分开。一份定义,多种投影。

决定四:骨架与"一步怎么行动"解耦

骨架管:排第几步、要不要先规划、把记忆变成模型输入、捕获错误、判断到没到步数上限、什么时候算拿到最终答案。"一步怎么行动"是一个抽象方法,由子类填。 (依据:前沿库 · smolagents · 通用多步 ReAct 循环 —— MultiStepAgent 骨架负责重复/捕错/判终止,「一步怎么行动」是抽象方法 _step_stream 由子类实现,CodeAgent 与 ToolCallingAgent 各填一种)

错误不中断循环: 捕获到 agent 错误就记到这一步的记录里,下一轮重试。

整个循环用生成器驱动,每步产出的东西都往外抛。同一套代码既能"一次跑到底只要最后答案",也能"边跑边把中间过程吐给调用方"。

它的做法(可以抄的部分)

两条路线的取舍表(它自己给的,值得原样记住):

维度代码即动作传统工具调用
一步能做几件事多个工具 + 变量 + 控制流通常一个或几个并行调用
怎么执行受限 Python 解释器直接调函数
并行由代码自身表达线程池并行多个调用
终止代码里调特殊函数 → 抛异常工具名等于那个特殊名字
依赖不要求模型支持原生工具调用依赖模型的工具调用能力

(依据:前沿库 · smolagents · CodeAgent(把行动写成代码) —— CodeAgent 与 ToolCallingAgent 的对比表明确列出「代码即动作不要求模型支持 function calling」这一条)

最后一行是关键:代码即动作这条路不挑模型。

它没回答什么

  • 代码跑在哪、怎么不炸——受限解释器那一章本轮没读,属沙箱课题。
  • 历史怎么压——这三篇没讲。
  • 工具太多怎么办——工具全量注入执行环境,没有裁剪机制。

坑与代价

  • "代码即动作"把安全问题整个提前了。 传统工具调用的攻击面是"参数不对",这条路的攻击面是"模型能写任意代码"。它必须配一个受限解释器,那是一整章的工程量。

    判断(无锚): 对我们的最小原型,这条路先不走——我们只有一两个工具,代码带来的表达力用不上,却要背一个解释器。 如果错,会错在: 如果第一批工具就是"读文件/搜索/统计"这类需要组合的,往返轮数会明显变多,那时代码即动作的省步数优势就体现出来了。

  • 异常继承基类这个坑不写清楚就会踩。 上面那条细节是全库最容易被漏掉、漏掉就静默失效的地方。