smolagents 构建高质量 Agent 实战指南:从简化工作流、信息流优化到系统提示定制与规划机制
2026/9/19 12:05:38 网站建设 项目流程

smolagents 构建高质量 Agent 实战指南:从简化工作流、信息流优化到系统提示定制与规划机制

【免费下载链接】smolagents🤗 smolagents: a barebones library for agents that think in code.项目地址: https://gitcode.com/gh_mirrors/smo/smolagents

在 smolagents 中,"能用的 agent" 与"不能用的 agent" 之间往往只差几个工程决策:工作流是否足够简单、流向 LLM 的信息是否充分、出了问题时如何系统性调试。本文基于 smolagents 官方中文教程 构建好用的 agent 展开,结合仓库源码印证每条最佳实践背后的实现机制,帮助你在CodeAgent/ToolCallingAgent上把 LLM 的自主权用在对的地方、把错误率压到最低。如果你是 agent 构建新手,建议先阅读 agent 介绍 与 smolagents 导览。

最佳实践一:最好的 agent 系统是最简单的——尽可能简化工作流

在工作流中赋予 LLM 更多的自主权,就会引入更多错误风险。经过良好编程的 agent 系统通常具有完善的错误日志与重试机制,LLM 引擎有机会自我纠错;但为了最大限度降低 LLM 出错概率,核心原则是简化工作流

主要指导原则:尽可能减少 LLM 调用的次数。

官方教程以"冲浪旅行公司客服机器人"为例:与其让 agent 每次被问到一个新冲浪地点时,分别调用"旅行距离 API"和"天气 API"两个工具再自行拼合结果,不如直接创建一个统一工具return_spot_information,在函数内部同时调用两个 API 并返回组合输出。这样做可以同时降低成本、延迟和错误风险。

由此可以推出两条可直接落地的启发:

  • 尽可能把两个工具合并为一个,就像上面的两个 API 的例子;
  • 尽可能基于确定性函数(而非 agent 决策)来实现逻辑——能用 Python 代码写死的分支、循环、格式转换,就不要让 LLM 在每一步"想"一遍。

这与 smolagents 的设计哲学一致:CodeAgent每步生成的是一段 Python 代码,代码本身是确定性的,LLM 只需要决定"调用哪些工具、传什么参数"。工具层设计得越收敛,LLM 需要做的决策就越少,出错面也越小。

最佳实践二:改善流向 LLM 引擎的信息流

记住一个比喻:你的 LLM 引擎就像一个机器人被关在一个房间里,与外界唯一的交流方式是通过门缝传递的纸条。如果你没有明确地把信息放进提示里,它就什么都不知道。因此需要从两个层面改善信息流。

1. 让任务表述非常清晰

由于 agent 由 LLM 驱动,任务表述的微小变化可能产生完全不同的结果。从源码看,任务字符串会原样进入记忆系统并成为后续每一步推理的锚点:在 MultiStepAgent.run 中,task会被写入self.task,随后通过TaskStep(task=self.task, task_images=images)追加到memory.steps,日志里也会记录完整任务。任务写得含糊,后面每一步都会带着含糊走。

2. 改善工具使用中流向 agent 的信息流

具体指南是:每个工具都应该把对 LLM 引擎可能有用的所有信息记录下来(只需在工具的forward方法中使用print语句),尤其是工具执行错误的详细信息。错误详情会进入记忆的 Observation 字段,帮助 LLM"逆向工程"工具来修复错误——但为什么要让它做这么多繁重的工作呢?与其让 LLM 从晦涩的报错中猜,不如在工具内部就给出明确、可读的引导。

下面对比官方给出的一个"根据位置和日期时间检索天气数据"工具的糟糕版本与改进版本。

糟糕的版本:

import datetime from smolagents import tool def get_weather_report_at_coordinates(coordinates, date_time): # 虚拟函数,返回 [温度(°C),降雨风险(0-1),浪高(m)] return [28.0, 0.35, 0.85] def get_coordinates_from_location(location): # 返回虚拟坐标 return [3.3, -42.0] @tool def get_weather_api(location: str, date_time: str) -> str: """ Returns the weather report. Args: location: the name of the place that you want the weather for. date_time: the date and time for which you want the report. """ lon, lat = convert_location_to_coordinates(location) date_time = datetime.strptime(date_time) return str(get_weather_report_at_coordinates((lon, lat), date_time))

它不好的原因有四:

  • 没有说明date_time应该使用的格式;
  • 没有说明位置应该如何指定;
  • 没有记录机制来处理明确的报错情况,如位置格式不正确或 date_time 格式不正确;
  • 输出格式难以理解(一个裸的列表字符串)。

更好的版本:

@tool def get_weather_api(location: str, date_time: str) -> str: """ Returns the weather report. Args: location: the name of the place that you want the weather for. Should be a place name, followed by possibly a city name, then a country, like "Anchor Point, Taghazout, Morocco". date_time: the date and time for which you want the report, formatted as '%m/%d/%y %H:%M:%S'. """ lon, lat = convert_location_to_coordinates(location) try: date_time = datetime.strptime(date_time) except Exception as e: raise ValueError("Conversion of `date_time` to datetime format failed, make sure to provide a string in format '%m/%d/%y %H:%M:%S'. Full trace:" + str(e)) temperature_celsius, risk_of_rain, wave_height = get_weather_report_at_coordinates((lon, lat), date_time) return f"Weather report for {location}, {date_time}: Temperature will be {temperature_celsius}°C, risk of rain is {risk_of_rain*100:.0f}%, wave height is {wave_height}m."

改进点:docstring 里写清了location的书写规范(地点 + 可能的城市 + 国家)和date_time的精确格式;解析失败时抛出带有格式提示与完整 trace 的ValueError;返回值是人类可读的自然语言句子而非裸数据结构。

从源码层面可以印证这套实践为什么有效。smolagents 的 @tool 装饰器 在装饰时就会解析函数签名类型提示与 docstring(要求每个参数有类型提示、函数有返回值类型提示、docstring 中包含Args:部分逐参数描述),并据此动态生成SimpleToolnamedescriptioninputsoutput_type等属性。而 Tool.to_code_prompt 会把descriptionArgs:参数说明拼装成一段 Python 函数签名的"文档字符串",随系统提示注入给模型;Tool.to_tool_calling_prompt 则用于ToolCallingAgent的工具清单。也就是说,你写在 docstring 里的每一个字——包括参数格式约定——都会原封不动地成为 LLM 看到的工具说明书。这就是"工具 docstring 是主要指导通道"的实现依据。

一般来说,为了减轻 LLM 的负担,写工具时要问自己一个好问题:"如果我是一个第一次使用这个工具的傻瓜,使用这个工具编程并纠正自己的错误有多容易?"

给 agent 更多参数:additional_args

除了任务描述字符串,agent.run()还支持通过additional_args参数传递任何类型的对象:

from smolagents import CodeAgent, InferenceClientModel model_id = "meta-llama/Llama-3.3-70B-Instruct" agent = CodeAgent(tools=[], model=InferenceClientModel(model_id=model_id), add_base_tools=True) agent.run( "Why does Mike not know many people in New York?", additional_args={"mp3_sound_file_url":'https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/recording.mp3'} )

例如,你可以用它传递希望 agent 利用的图像、音频 URL 或字符串。从 MultiStepAgent.run 的实现看,additional_args并非"附加信息",而是被真正注入了执行环境:

  1. self.state.update(additional_args)——键值对直接进入 agent 状态,后续每步生成的 Python 代码都能以变量名直接使用它们;
  2. 同时会向任务文本追加一段说明:"You have been provided with these additional arguments, that you can access directly using the keys as variables",让 LLM 明确知道有哪些额外变量可用;
  3. 若 agent 配置了远程 Python 执行器,self.python_executor.send_variables(variables=self.state)还会把这些变量同步到沙箱环境。

因此使用additional_args时,变量名要起得清晰(源码 docstring 里也专门提示 "Give them clear names!"),因为它们既是 LLM 的提示词素材,也是代码里的真实变量。

如何调试你的 agent

1. 使用更强大的 LLM

agent 工作流中的错误分两类:实际错误(工具真的失败了)与 LLM 引擎没有正确推理的结果。后者换更强的模型往往直接解决。

官方教程给出了一个典型例子——要求CodeAgent创建一张汽车图片的运行记录:

==================================================================================================== New task ==================================================================================================== Make me a cool car picture ──────────────────────────────────────────────────────────────────────────────────────────────────── New step ───────────────────────────────────────────────────────────────────────────────────────────────────── Agent is executing the code below: ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── image_generator(prompt="A cool, futuristic sports car with LED headlights, aerodynamic design, and vibrant color, high-res, photorealistic") ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Last output from code snippet: ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /var/folders/6m/9b1tts6d5w960j80wbw9tx3m0000gn/T/tmpx09qfsdd/652f0007-3ee9-44e2-94ac-90dae6bb89a4.png Step 1: - Time taken: 16.35 seconds - Input tokens: 1,383 - Output tokens: 77 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Agent is executing the code below: ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── final_answer("/var/folders/6m/9b1tts6d5w960j80wbw9tx3m0000gn/T/tmpx09qfsdd/652f0007-3ee9-44e2-94ac-90dae6bb89a4.png") ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Print outputs: Last output from code snippet: ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /var/folders/6m/9b1tts6d5w960j80wbw9tx3m0000gn/T/tmpx09qfsdd/652f0007-3ee9-44e2-94ac-90dae6bb89a4.png Final answer: /var/folders/6m/9b1tts6d5w960j80wbw9tx3m0000gn/T/tmpx09qfsdd/652f0007-3ee9-44e2-94ac-90dae6bb89a4.png

用户看到的是返回了一个路径,而不是图像。这看起来像系统错误,实际上 agent 系统并没有出错:只是 LLM 大脑犯了一个错误——没有把图像对象保存到变量中,而是只留下了image_generator工具print出来的保存路径,于是它无法再次访问图像对象,只能返回这个路径。像Qwen2.5-72B-Instruct这样更强的模型不会犯这种错误。调试 agent 的第一步就是"换更强的 LLM"。

2. 提供更多指导 / 更多信息

也可以继续使用不太强大的模型,只要你更有效地指导它们。方法是站在模型的角度思考:如果你是模型在解决这个任务,你会因为"系统提示 + 任务表述 + 工具描述"中提供的信息而挣扎吗?你需要一些额外的说明吗?

为了提供额外信息,官方不建议立即改动系统提示——默认系统提示有许多精细调整,除非你非常了解提示工程,否则很容易翻车。更好的做法是:

  • 如果缺的是任务层面的信息:把所有细节添加到任务(task)中。任务描述可以非常长;
  • 如果缺的是工具使用层面的信息:完善工具的description(即 docstring)属性。

这与前文"改善信息流"是同一套思路的延续:把指导信息放到最贴近其作用域的位置。

3. 更改系统提示(通常不建议)

如果上述方法都不够,才考虑改系统提示。先看看CodeAgent的默认系统提示长什么样。可以通过如下方式查看:

print(agent.prompt_templates["system_prompt"])

模板的主体结构(官方教程中的版本,零样本示例部分有所删节)如下:

You are an expert assistant who can solve any task using code blobs. You will be given a task to solve as best you can. To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code. To solve the task, you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Code:', and 'Observation:' sequences. At each step, in the 'Thought:' sequence, you should first explain your reasoning towards solving the task and the tools that you want to use. Then in the 'Code:' sequence, you should write the code in simple Python. The code sequence must end with '<end_code>' sequence. During each intermediate step, you can use 'print()' to save whatever important information you will then need. These print outputs will then appear in the 'Observation:' field, which will be available as input for the next step. In the end you have to return a final answer using the `final_answer` tool. Here are a few examples using notional tools: --- Task: "Generate an image of the oldest person in this document." Thought: I will proceed step by step and use the following tools: `document_qa` to find the oldest person in the document, then `image_generator` to generate an image according to the answer. Code: ```py answer = document_qa(document=document, question="Who is the oldest person mentioned?") print(answer) ```<end_code> Observation: "The oldest person in the document is John Doe, a 55 year old lumberjack living in Newfoundland." Thought: I will now generate an image showcasing the oldest person. Code: ```py image = image_generator("A portrait of John Doe, a 55-year-old man living in Canada.") final_answer(image) ```<end_code> ...(教程中还有多个零样本示例,此处省略)... Above example were using notional tools that might not exist for you. On top of performing computations in the Python code snippets that you create, you only have access to these tools: {%- for tool in tools.values() %} - {{ tool.to_tool_calling_prompt() }} {%- endfor %} {%- if managed_agents and managed_agents.values() | list %} You can also give tasks to team members. Calling a team member works similarly to calling a tool: provide the task description as the 'task' argument. Since this team member is a real human, be as detailed and verbose as necessary in your task description. You can also include any relevant variables or context using the 'additional_args' argument. Here is a list of the team members that you can call: {%- for agent in managed_agents.values() %} - {{ agent.name }}: {{ agent.description }} {%- endfor %} {%- endif %} Here are the rules you should always follow to solve your task: 1. Always provide a 'Thought:' sequence, and a 'Code:\n```py' sequence ending with '```<end_code>' sequence, else you will fail. 2. Use only variables that you have defined! 3. Always use the right arguments for the tools. DO NOT pass the arguments as a dict as in 'answer = wiki({'query': "What is the place where James Bond lives?"})', but use the arguments directly as in 'answer = wiki(query="What is the place where James Bond lives?")'. 4. Take care to not chain too many sequential tool calls in the same code block, especially when the output format is unpredictable. For instance, a call to search has an unpredictable return format, so do not have another tool call that depends on its output in the same block: rather output results with print() to use them in the next block. 5. Call a tool only when needed, and never re-do a tool call that you previously did with the exact same parameters. 6. Don't name any new variable with the same name as a tool: for instance don't name a variable 'final_answer'. 7. Never create any notional variables in our code, as having these in your logs will derail you from the true variables. 8. You can use imports in your code, but only from the following list of modules: {{authorized_imports}} 9. The state persists between code executions: so if in one step you've created variables or imported modules, these will all persist. 10. Don't give up! You're in charge of solving the task, not providing directions to solve it.

可以看到,模板中有一系列 Jinja 占位符(如{{ tool.description }}风格的片段):它们会在 agent 初始化时用于插入自动生成的工具描述、被管理 agent 的描述等。当前仓库中,CodeAgent的完整提示模板存放在 prompts/code_agent.yaml,其中包含system_prompt与规划提示(planning)等键。与教程展示的早期版本相比,当前版本的代码块定界符使用了{{code_block_opening_tag}}/{{code_block_closing_tag}}占位符,并新增了针对"带 JSON output schema 的工具可放心链式调用"的规则,以及{{custom_instructions}}占位符;但占位符机制本身一致。

因此,虽然你可以通过自定义提示来覆盖系统提示模板,但新的系统提示必须保留以下占位符

  • 用于插入工具描述:
{%- for tool in tools.values() %} - {{ tool.to_tool_calling_prompt() }} {%- endfor %}
  • 用于插入 managed agent 的描述(如果有):
{%- if managed_agents and managed_agents.values() | list %} You can also give tasks to team members. Calling a team member works similarly to calling a tool: provide the task description as the 'task' argument. Since this team member is a real human, be as detailed and verbose as necessary in your task description. You can also include any relevant variables or context using the 'additional_args' argument. Here is a list of the team members that you can call: {%- for agent in managed_agents.values() %} - {{ agent.name }}: {{ agent.description }} {%- endfor %} {%- endif %}
  • 仅限CodeAgent"{{authorized_imports}}",用于插入授权导入模块列表。

修改方式是直接改写prompt_templates,例如:

agent.prompt_templates["system_prompt"] = agent.prompt_templates["system_prompt"] + "\nHere you go!"

源码印证了两点细节。其一,MultiStepAgent 的 system_prompt 属性 是只读的——直接赋值会抛出AttributeError,并明确提示"Use 'self.prompt_templates["system_prompt"]' instead",这与教程的修改方式完全对应。其二,ToolCallingAgent同样支持这套模板机制,该改法对它同样适用。

4. 额外规划(planning)

smolagents 提供了用于补充规划步骤的机制:agent 可以在正常操作步骤之间定期运行一个规划步骤。在该步骤中没有工具调用,LLM 只是被要求更新"已知事实"列表,并据此反推下一步该做什么。启用方式是给CodeAgent传入planning_interval参数:

from smolagents import load_tool, CodeAgent, InferenceClientModel, WebSearchTool from dotenv import load_dotenv load_dotenv() # 从 Hub 导入工具 image_generation_tool = load_tool("m-ric/text-to-image", trust_remote_code=True) search_tool = WebSearchTool() agent = CodeAgent( tools=[search_tool], model=InferenceClientModel(model_id="Qwen/Qwen2.5-72B-Instruct"), planning_interval=3 # 这是你激活规划的地方! ) # 运行它! result = agent.run( "How long would a cheetah at full speed take to run the length of Pont Alexandre III?", )

从源码可以看清规划步骤的精确触发时机。在 MultiStepAgent.init中,planning_interval默认为None(即关闭);在 主循环 中,满足step_number == 1(step_number - 1) % planning_interval == 0时,就会先执行一个规划步骤再执行动作步骤。也就是说planning_interval=3时,agent 会在第 1 步(初始规划)及之后每 3 个动作步骤各插入一次再规划。

规划提示本身也值得留意,定义在 code_agent.yaml 的 planning 段:

  • initial_plan:要求模型先做"事实盘点"(1.1 任务给定的事实、1.2 需要查证的事实及出处、1.3 需要推导的事实),再写出高层步骤计划,且明确"不要细化到逐个工具调用",最后以<end_plan>标签结束;
  • update_plan_pre_messages/update_plan_post_messages:用于中途再规划,让模型基于已有的执行历史更新"已知/未知事实"(1.1 任务给定、1.2 已学到、1.3 仍待查证、1.4 仍待推导),并提示"注意你还剩 {remaining_steps} 步",如果之前的尝试已经小有成果,更新后的计划可以建立在既有结果之上;如果卡住了,可以从头制定全新计划。

这一机制适合任务步数多、容易中途跑偏的场景:它用一次"无工具调用"的纯推理,把 LLM 从局部细节里拉回全局目标。

小结:一张可执行的检查清单

结合全文,构建一个"好用"的 smolagents agent 可以按以下顺序自查:

  1. 简化工作流:能合并的工具就合并,能写成确定性代码的逻辑就不让 LLM 决策,减少 LLM 调用次数;
  2. 任务与工具信息流:任务表述清晰完整;工具 docstring 写清参数格式与输出含义;forward中用print记录有用信息,出错时抛出带明确修复指引的异常;
  3. 善用additional_args:把图像、音频 URL 等上下文作为变量注入状态,变量名要清晰;
  4. 调试顺序:先换更强的 LLM → 再补任务/工具描述 → 最后才改系统提示(改时必须保留工具、managed agents、{{authorized_imports}}占位符);
  5. 复杂任务加规划planning_interval让 agent 周期性重新盘点事实与计划,降低长任务跑偏概率。

更多背景可参考英文原版教程 Building good agents、概念指南 agent 介绍 与 ReAct 范式说明。

【免费下载链接】smolagents🤗 smolagents: a barebones library for agents that think in code.项目地址: https://gitcode.com/gh_mirrors/smo/smolagents

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询