本节摘要:本节盘点 default_tools.py(698 行)自带的工具箱:FinalAnswerTool 是 CodeAgent 的终止信号,UserInputTool 提供 input() 式人工干预;DuckDuckGoSearchTool 免费无 key 即可搜索;GoogleSearchTool/ApiWebSearchTool/WebSearchTool 覆盖多种搜索后端;VisitWebpageTool 用 requests+markdownify 把网页转成 markdown 喂给 LLM;WikipediaSearchTool 查百科;SpeechToTextTool(Whisper)转写音频。最后是 gradio_ui.py 的 GradioUI——把 agent 包装成交互式 Gradio 界面,launch() 一行起服务、流式输出每一步思考与工具调用。
内容来源:
src/smolagents/default_tools.py、src/smolagents/gradio_ui.py
⚠️ 注意:default_tools.py 顶部的 TOOL_MAPPING 只含 PythonInterpreterTool、DuckDuckGoSearchTool、VisitWebpageTool 三个——
add_base_tools=True注入的就是这个小集合,其余工具都要按需显式传入。
FinalAnswerTool 只有五行,却是 CodeAgent 循环的刹车:
class FinalAnswerTool(Tool): name = "final_answer" description = "Provides a final answer to the given problem." inputs = {"answer": {"type": "any", "description": "The final answer to the problem"}} output_type = "any" def forward(self, answer: Any) -> Any: return answer
它什么都不做,只把参数原样返回——但第 6 章会看到,解释器里 final_answer() 被特判:调用即抛 FinalAnswerException 中断代码执行,agent 主循环捕获后结束 run。所以对 LLM 来说,final_answer(x) 就是「任务完成,答案是 x」的终止信号。UserInputTool 则反向——agent 主动找人:
class UserInputTool(Tool): name = "user_input" description = "Asks for user's input on a specific question" inputs = {"question": {"type": "string", "description": "The question to ask the user"}} output_type = "string" def forward(self, question): user_input = input(f"{question} => Type your answer here:") return user_input
LLM 写 user_input(question="两个方案选哪个?"),代码执行到这一行会阻塞等待键盘输入——工具调用成了人机对话的通道(注意:在远程沙箱里没有 stdin,这个工具只在本地执行器可用)。
DuckDuckGoSearchTool 是入门首选——免费、无需 API key:
class DuckDuckGoSearchTool(Tool): name = "web_search" ... def __init__(self, max_results: int = 10, rate_limit: float | None = 1.0, **kwargs): ... self._min_interval = 1.0 / rate_limit if rate_limit else 0.0 self._last_request_time = 0.0 try: from ddgs import DDGS except ImportError as e: raise ImportError("You must install package `ddgs` to run this tool: ...") self.ddgs = DDGS(**kwargs) def forward(self, query: str) -> str: self._enforce_rate_limit() results = self.ddgs.text(query, max_results=self.max_results) if len(results) == 0: raise Exception("No results found! Try a less restrictive/shorter query.") postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results] return "## Search Results\n\n" + "\n\n".join(postprocessed_results)
依赖第三方 ddgs 包;内置限流器(默认 1 QPS,_enforce_rate_limit 用时间差 sleep)防止 agent 高频查询被封。结果拼成 markdown 链接列表直接喂 LLM。GoogleSearchTool 需要环境变量 API key(provider 为 serpapi 时要 SERPAPI_API_KEY,serper 时要 SERPER_API_KEY),支持 filter_year 按年份过滤。ApiWebSearchTool 是通用 REST 搜索端点包装,默认 Brave Search(BRAVE_API_KEY),同样内置限流。WebSearchTool 走「自解析」路线:duckduckgo 引擎直接抓 lite 版页面用 HTMLParser 手工解析,bing 走 RSS,exa 走 API——零第三方包依赖的兜底方案。四个工具 name 都叫 web_search,一次只装一个,LLM 看到的接口完全一致。
搜索给了 LLM 链接,阅读要靠 VisitWebpageTool:
def forward(self, url: str) -> str: ... try: # Send a GET request to the URL with a 20-second timeout response = requests.get(url, timeout=20) response.raise_for_status() # Convert the HTML content to Markdown markdown_content = markdownify(response.text).strip() # Remove multiple line breaks markdown_content = re.sub(r"\n{3,}", "\n\n", markdown_content) return self._truncate_content(markdown_content, self.max_output_length) except requests.exceptions.Timeout: return "The request timed out. Please try again later or check the URL." except RequestException as e: return f"Error fetching the webpage: {str(e)}"
管道三步:requests 抓页面(20 秒超时)→ markdownify 把 HTML 转成 markdown(导航栏、脚本等噪音大幅减少,LLM 阅读 token 更省)→ 压缩连续空行后截断到 max_output_length(默认 40000 字符,防止一个网页吃光上下文)。异常不抛出而是返回友好提示文本——让 LLM 自己决定换 URL 还是放弃,而不是让整步崩掉。
WikipediaSearchTool 包 wikipedia-api 库,四个可配参数值得一看:
def __init__(self, user_agent="Smolagents (myemail@example.com)", language="en", content_type="text", extract_format="WIKI"):
user_agent 是维基百科 API 政策的强制要求(必须留可联系的标识),content_type 可选 summary(短摘要)或 text(全文),extract_format 可选 WIKI 或 HTML。SpeechToTextTool 则是上一节 PipelineTool 的标准示范——三个类属性定住 Whisper 模型:
class SpeechToTextTool(PipelineTool): default_checkpoint = "openai/whisper-large-v3-turbo" description = "This is a tool that transcribes an audio into text. It returns the transcribed text." name = "transcriber" inputs = {"audio": {"type": "audio", "description": "The audio to transcribe. ..."}} output_type = "string"
gradio_ui.py 把 agent 包装成聊天界面,最短用法:
from smolagents import CodeAgent, GradioUI, InferenceClientModel model = InferenceClientModel(model_id="meta-llama/Meta-Llama-3.1-8B-Instruct") agent = CodeAgent(tools=[], model=model) GradioUI(agent, file_upload_folder="./uploads").launch()
核心是 stream_to_gradio 与 _stream_response 的流式渲染——它们消费 agent.run(stream=True) 吐出的事件流:
for event in agent.run(task, images=task_images, stream=True, reset=reset_agent_memory, ...): if isinstance(event, ActionStep | PlanningStep | FinalAnswerStep): for message in pull_messages_from_step(event, skip_model_outputs=...): yield message accumulated_events = [] elif isinstance(event, ChatMessageStreamDelta): accumulated_events.append(event) text = agglomerate_stream_deltas(accumulated_events).render_as_markdown() yield text
两类事件两种呈现:完整步骤(ActionStep 等)经 pull_messages_from_step 展开成结构化 ChatMessage;流式增量(ChatMessageStreamDelta)则边攒边渲染,实现打字机效果。_process_action_step 把一步动作拆成多条气泡(节选):
# For tool calls, create a parent message if getattr(step_log, "tool_calls", []): used_code = first_tool_call.name == "python_interpreter" ... yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=content, metadata={"title": f"🛠️ Used tool {first_tool_call.name}", "status": "done"}) # Display any images in observations if getattr(step_log, "observations_images", []): for image in step_log.observations_images: path_image = AgentImage(image).to_string() yield gr.ChatMessage(role=MessageRole.ASSISTANT, content={"path": path_image, "mime_type": f"image/{path_image.split('.')[-1]}"}, metadata={"title": "🖼️ Output Image", "status": "done"}) # Handle errors if getattr(step_log, "error", None): yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=str(step_log.error), metadata={"title": "💥 Error", "status": "done"})
用户在界面上看到的就是 agent 的记忆流水线实时投屏:「第 N 步 → 思考 → 🛠️ 用了什么工具 → 📝 执行日志 → 🖼️ 生成的图 → 💥 出错了吗 → 脚注(token 消耗/耗时)」。每步脚注由 get_step_footnote_content 拼出 Input/Output tokens 与 Duration——第 4 章学的 ActionStep.token_usage 与 timing 在 UI 层兑现。
create_app 用 gr.ChatInterface 搭界面,可选多模态文件上传(file_upload_folder 不为 None 时开启,上传的文件路径会附进任务文本);launch(share=True) 默认生成公网链接,reset_agent_memory 控制每次对话是否清空记忆。另外 stream_to_gradio 作为独立函数导出——已有 Gradio 应用想嵌一个 agent,拿它接事件流即可,不必用 GradioUI 整套。
💡 阶梯要点:本阶把工具箱填满并装上「演示」外设。内置工具遵循同一哲学:全都是上一节 Tool 四件套的普通实例,没有特权——final_answer 的「魔法」在解释器层而非工具层。GradioUI 则证明记忆结构化(第 4 章)的红利:渲染层只需遍历步骤类型,不用碰任何 agent 内部状态。
下一节:进入全书第二个高潮★——第 6 章
01 自研 AST 解释器,看 smolagents 为什么、以及如何不靠 exec 执行 LLM 写的代码。