本节摘要:本节精读
agents.py:1215-1502的ToolCallingAgent——agency 等级表 ★★☆ 的规范实现。它让 LLM 用 JSON 动作 blob(形如{"name": "search", "arguments": {...}})表达每一步行动:单步内"组装记忆消息 →model.generate请求原生 tool calling → 解析 tool_calls → 执行工具(多个工具调用并行)→ 观察结果回填 ActionStep"。这构成完整的 ReAct 循环(Thought 推理 → Action 行动 → Observation 观察)。本节同时精读它的提示词模板prompts/toolcalling_agent.yaml(Jinja2 渲染)与utils.py的parse_json_blob,并对比 LangChain 的 ReAct 实现——同样是 ReAct,smolagents 没有 Chain/AgentExecutor 抽象,就是一个裸 while 循环。
内容来源:原项目源码
src/smolagents/agents.py(ToolCallingAgent)、src/smolagents/utils.py(parse_json_blob)、src/smolagents/prompts/toolcalling_agent.yaml。
⚠️ 注意:本版 ToolCallingAgent 优先使用 LLM 引擎的原生 tool calling(
generate(..., tools_to_call_from=...));只有模型不返回原生 tool_calls 时才回退到文本解析(model.parse_tool_calls→parse_json_blob)。老资料说"smolagents 全靠正则抠 JSON"已过时,但解析路径仍是必备的兜底。
阅读完本节,你应当能够:
_step_stream 的六个阶段。tools_to_call_from 参数如何让模型后端拿到工具的 JSON schema。{"name": ..., "arguments": {...}}。parse_json_blob 的"找首 { 与尾 }"策略及其错误提示。agents.py:1215-1274(节选):
1215 class ToolCallingAgent(MultiStepAgent): 1216 """ 1217 This agent uses JSON-like tool calls, using method `model.get_tool_call` 1218 to leverage the LLM engine's tool calling capabilities. 1219 """ 1231 def __init__(self, tools, model, prompt_templates=None, planning_interval=None, 1233 stream_outputs=False, max_tool_threads=None, **kwargs): 1241 prompt_templates = prompt_templates or yaml.safe_load( 1242 importlib.resources.files("smolagents.prompts") 1243 .joinpath("toolcalling_agent.yaml").read_text() 1244 ) 1244 super().__init__(tools=tools, model=model, prompt_templates=prompt_templates, ...) 1258 self.max_tool_threads = max_tool_threads 1265 def initialize_system_prompt(self) -> str: 1266 system_prompt = populate_template( 1267 self.prompt_templates["system_prompt"], 1268 variables={ 1269 "tools": self.tools, 1270 "managed_agents": self.managed_agents, 1271 "custom_instructions": self.instructions, 1272 }, 1273 ) 1274 return system_prompt
两个要点:①默认提示词从随包分发的 toolcalling_agent.yaml 读入,yaml.safe_load 后是 PromptTemplates TypedDict(第 3 章第三节详拆四段结构);②initialize_system_prompt 用 populate_template(agents.py:102,内部 jinja2.Template + StrictUndefined)渲染,变量 tools 是 {name: Tool} 字典——每个 Tool 对象经 to_tool_calling_prompt() 展开为"函数名 + 描述 + 参数 + 返回类型"的文本。
agents.py:1276-1359(节选并注释):
1284 memory_messages = self.write_memory_to_messages() # ① 记忆 → 消息 1286 input_messages = memory_messages.copy() 1309 chat_message: ChatMessage = self.model.generate( 1310 input_messages, 1311 stop_sequences=["Observation:", "Calling tools:"], 1312 tools_to_call_from=self.tools_and_managed_agents, # ② 带工具表生成 1313 ) 1321 memory_step.model_output_message = chat_message # 记录模型输出 1323 memory_step.token_usage = chat_message.token_usage 1327 if chat_message.tool_calls is None or len(chat_message.tool_calls) == 0: 1329 chat_message = self.model.parse_tool_calls(chat_message) # ③ 兜底解析 1333 for tool_call in chat_message.tool_calls: 1334 tool_call.function.arguments = parse_json_if_needed(...) # 参数字符串转 dict 1336 for output in self.process_tool_calls(chat_message, memory_step): # ④ 执行工具 1337 yield output 1338 if isinstance(output, ToolOutput) and output.is_final_answer: 1340 ... # ⑤ final_answer 则收尾 1350 final_answer = output.output 1351 got_final_answer = True 1356 yield ActionOutput(output=final_answer, is_final_answer=got_final_answer)
六个阶段:①记忆组装——上节的 write_memory_to_messages;②带工具生成——把工具表传给 model.generate,后端负责转成 OpenAI 风格的 tools=[{"type": "function", "function": {...schema}}](schema 由 Tool 的类型提示自动生成,第 5 章);③兜底解析——模型没给原生 tool_calls 时,parse_tool_calls 从文本抠 JSON;④执行——见下节;⑤终止判定——工具名是 final_answer 即返回答案(还处理"final_answer 与其它工具混用""多个 final_answer"两类违规,抛 AgentExecutionError);⑥yield ActionOutput——_run_stream 据此翻 returned_final_answer 标志。
注意 1311 行的 stop_sequences=["Observation:", "Calling tools:"]:防止 LLM 自己"脑补"观察结果——观察必须来自真实执行,这是 ReAct 数据流不做假的保险丝。
agents.py:1361-1442 的执行主干(节选):
1383 def process_single_tool_call(tool_call: ToolCall) -> ToolOutput: 1384 tool_name = tool_call.name 1385 tool_arguments = tool_call.arguments or {} 1390 tool_call_result = self.execute_tool_call(tool_name, tool_arguments) 1391 if type(tool_call_result) in [AgentImage, AgentAudio]: # 多模态特殊存 1398 self.state[observation_name] = tool_call_result 1399 observation = f"Stored '{observation_name}' in memory." 1400 else: 1401 observation = str(tool_call_result).strip() # 观察就是结果字符串 1406 is_final_answer = tool_name == "final_answer" 1408 return ToolOutput(id=..., output=..., is_final_answer=..., observation=...) 1418 if len(parallel_calls) == 1: # 单调用:直接执行 1426 else: # 多调用:线程池并行 1427 with ThreadPoolExecutor(self.max_tool_threads) as executor: 1429 for tool_call in parallel_calls.values(): 1430 ctx = copy_context() 1431 futures.append(executor.submit(ctx.run, process_single_tool_call, tool_call)) 1436 memory_step.tool_calls = [parallel_calls[k] for k in sorted(parallel_calls.keys())] 1437 memory_step.observations = memory_step.observations or "" 1439 for tool_output in ...: 1439 memory_step.observations += tool_output.observation + "\n" # 观察回填
三个细节:①并行执行——LLM 一次返回多个 tool_calls 时用 ThreadPoolExecutor 并行跑,copy_context() 保证上下文变量(如请求元数据)传入子线程;②观察即字符串——工具返回值 str() 化后拼进 memory_step.observations,下一轮 LLM 就"看见"了;③多模态绕道——返回 AgentImage/AgentAudio 时不序列化进文本,而是存 self.state["image.png"],观察里只写一句"已存入记忆",后续工具按文件名引用。
execute_tool_call(1453-1502)再做三道防线:工具名存在性检查(否则 AgentToolExecutionError)→ _substitute_state_variables 把参数值替换成 state 里的真实对象(字符串 "image.png" 换成真图)→ validate_tool_arguments 校验参数后调用 tool(**arguments)。子 agent(第 8 章)也走同一入口——"子 agent 即工具"。
utils.py:166-186:
166 def parse_json_blob(json_blob: str) -> tuple[dict[str, str], str]: 168 try: 169 first_accolade_index = json_blob.find("{") 170 last_accolade_index = [a.start() for a in list(re.finditer("}", json_blob))][-1] 171 json_str = json_blob[first_accolade_index : last_accolade_index + 1] 172 json_data = json.loads(json_str, strict=False) 173 return json_data, json_blob[:first_accolade_index] 174 except IndexError: 175 raise ValueError("The model output does not contain any JSON blob.") 176 except json.JSONDecodeError as e: 178 if json_blob[place - 1 : place + 2] == "},\n": 179 raise ValueError("JSON is invalid: you probably tried to provide multiple tool calls " "in one action. PROVIDE ONLY ONE TOOL CALL.")
策略朴素而稳:定位第一个 { 与最后一个 } 之间的一段当 JSON 解析,strict=False 容忍控制字符。两个错误提示本身就是提示词工程——"你可能想一次给多个工具调用,一次只给一个!"直接把纠错指令写进异常,下轮 LLM 读到就能改。
模板开篇(第 1-26 行)就是 ReAct 协议的完整声明:
You are an expert assistant who can solve any task using tool calls. The tool call you write is an action: after the tool is executed, you will get the result of the tool call as an "observation". This Action/Observation can repeat N times, ... Action: { "name": "image_transformer", "arguments": {"image": "image_1.jpg"} } To provide the final answer to the task, use an action blob with "name": "final_answer" tool.
随后是 4 个 few-shot 示例(文档问答→生成图像→算术→城市人口对比),再用 Jinja2 循环列出真实工具:
{%- for tool in tools.values() %} - {{ tool.to_tool_calling_prompt() }} {%- endfor %}
to_tool_calling_prompt() 输出形如 - web_search: Performs a web search.\n - Takes inputs: {'query': ...}\n - Returns an output of type: string——name/description/inputs/output_type 四件套。把整个循环串起来,一次 run() 的轨迹就是:
Thought(隐含在生成中) → Action: {"name":"web_search","arguments":{"query":"..."}} → Observation: "...15 million..." → Action: {"name":"web_search", ...} → Observation: "...26 million..." → Action: {"name":"final_answer","arguments":"Shanghai"}
结构化输出方面,ToolCallingAgent 靠模型后端的 JSON schema tool calling(OpenAI/OpenRouter 等原生支持);CodeAgent 侧的对应物是 structured_code_agent.yaml(第 3 章第三节)。
LangChain 经典 ReAct 是 AgentExecutor 驱动 create_react_agent:输出格式靠 Thought/Action/Action Input/Observation 的文本协议 + 输出解析器(OutputParser),链在 Chain/Runnable 抽象里跑,错误处理靠 handle_parsing_errors 回调。smolagents 的差异:①无链抽象——就是 _run_stream 一个裸 while,单步语义集中在 _step_stream,可整函数替换;②优先原生 tool calling,文本协议只是兜底;③错误写入记忆参与下轮推理,而非仅打日志;④并行工具调用开箱即用。代价是生态组件(检索器/记忆插件)要自己接——第 5 章的 from_langchain 桥接正好补这一环。
💡 阶梯要点:ReAct = "Thought → Action → Observation"交替 + 记忆滚动。★★☆ 的天花板也很明显:一步只能发一组工具调用,前一步的结果要么整个塞进下一步参数、要么变成纯文本观察——组合与复用都很难。这正是 ★★★ 代码 agent 要解的问题,下一章见。
{"name": 工具名, "arguments": {...}};final_answer 是唯一终止工具。parse_json_blob:首 { 到尾 } 截取解析,错误信息内嵌纠错指令。stop_sequences 禁止 LLM 自导自演 Observation,保证观察来自真实执行。下一章进入全书高潮★:把"动作的语言"从 JSON 换成 Python 代码——CodeAgent 与代码执行循环。