smolagents框架是一个极简的 ReAct agent 框架,比较适合我们这种零基础的同学入门 agent 开发,学习并理解 anegt 的设计逻辑。
入口函数——main()
观察入口函数可以让我们快速了解其流程:
def main() -> None:
args = parse_arguments()
# Check if we should run in interactive mode
# Interactive mode is triggered when no prompt is provided
if args.prompt is None:
prompt, tools, model_type, model_id, provider, api_base, api_key, imports, action_type = interactive_mode()
else:
prompt = args.prompt
tools = args.tools
model_type = args.model_type
model_id = args.model_id
provider = args.provider
api_base = args.api_base
api_key = args.api_key
imports = args.imports
action_type = args.action_type
run_smolagent(
prompt,
tools,
model_type,
model_id,
provider=provider,
api_base=api_base,
api_key=api_key,
imports=imports,
action_type=action_type,
)
可以看到,main()方法就只是拿到启动参数,然后调用run_smolagent()方法。
核心方法——run_smolagent()
这 40 行是整个文件的价值所在。
1. 加载环境变量
load_dotenv()
先调用load_dotenv()方法,从.env文件中加载环境变量,把项目根目录 .env 文件的内容读进 os.environ:
def load_dotenv(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
verbose: bool = False,
override: bool = False,
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
) -> bool:
"""Parse a .env file and then load all the variables found as environment variables.
Parameters:
dotenv_path: Absolute or relative path to .env file.
stream: Text stream (such as `io.StringIO`) with .env content, used if
`dotenv_path` is `None`.
verbose: Whether to output a warning the .env file is missing.
override: Whether to override the system environment variables with the variables
from the `.env` file.
interpolate: Whether to interpolate variables using POSIX variable expansion.
encoding: Encoding to be used to read the file.
Returns:
Bool: True if at least one environment variable is set else False
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
.env file with its default parameters. If you need to change the default parameters
of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
to this function as `dotenv_path`.
If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value,
.env loading is disabled.
"""
if _load_dotenv_disabled():
logger.debug(
"python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable"
)
return False
if dotenv_path is None and stream is None:
dotenv_path = find_dotenv()
dotenv = DotEnv(
dotenv_path=dotenv_path,
stream=stream,
verbose=verbose,
interpolate=interpolate,
override=override,
encoding=encoding,
)
return dotenv.set_as_environment_variables()
之后使用.env文件中的配置项,用os.getenv()方法即可,例如:
import os
from dotenv import load_dotenv
load_dotenv() # ① 只需调用一次,放在最开头
key = os.getenv("HF_API_KEY") # ② 之后随处取用
接下来的load_model()方法中会用到.env文件中的配置项。
2. 构造模型
这里的实现简单粗暴,兼容了4中模型配置方式:
model = load_model(model_type, model_id, api_base=api_base, api_key=api_key, provider=provider)
def load_model(
model_type: str,
model_id: str,
api_base: str | None = None,
api_key: str | None = None,
provider: str | None = None,
) -> Model:
if model_type == "OpenAIModel":
return OpenAIModel(
api_key=api_key or os.getenv("FIREWORKS_API_KEY"),
api_base=api_base or "https://api.fireworks.ai/inference/v1",
model_id=model_id,
)
elif model_type == "LiteLLMModel":
return LiteLLMModel(
model_id=model_id,
api_key=api_key,
api_base=api_base,
)
elif model_type == "TransformersModel":
return TransformersModel(model_id=model_id, device_map="auto")
elif model_type == "InferenceClientModel":
return InferenceClientModel(
model_id=model_id,
token=api_key or os.getenv("HF_API_KEY"),
provider=provider,
)
else:
raise ValueError(f"Unsupported model type: {model_type}")
通过if-elif提供了四种选择:
| model_type | 说明 |
|---|---|
InferenceClientModel | HF Inference API,CLI 默认,读 HF_API_KEY |
OpenAIModel | OpenAI 兼容接口。注意默认 api_base 指向 Fireworks,不是 OpenAI 官方 |
LiteLLMModel | LiteLLM 代理,一个接口打通上百家厂商 |
TransformersModel | 本地推理,需要 torch + 显卡 |
3. 构造工具
这一步开始准备工具集,给模型使用,工具有本地工具和远程工具,通过不同的方式获取:
available_tools = []
for tool_name in tools:
if "/" in tool_name:
space_name = tool_name.split("/")[-1].lower().replace("-", "_").replace(".", "_")
description = f"Tool loaded from Hugging Face Space: {tool_name}"
available_tools.append(Tool.from_space(space_id=tool_name, name=space_name, description=description))
else:
if tool_name in TOOL_MAPPING:
available_tools.append(TOOL_MAPPING[tool_name]())
else:
raise ValueError(f"Tool {tool_name} is not recognized either as a default tool or a Space.")
如果是远程工具的话,用Tool.from_space()方法获取,如果是本地工具的话,就直接从 TOOL_MAPPING 里拿到工具的类,然后自己实例化。
我们可以看看 TOOL_MAPPING 的定义:
TOOL_MAPPING = {
tool_class.name: tool_class
for tool_class in [
PythonInterpreterTool,
DuckDuckGoSearchTool,
VisitWebpageTool,
]
}
可以看到,默认是三个工具,那么 TOOL_MAPPING 的结构应该是这样:
{
'python_interpreter': <class 'smolagents.default_tools.PythonInterpreterTool'>,
'web_search': <class 'smolagents.default_tools.DuckDuckGoSearchTool'>,
'visit_webpage': <class 'smolagents.default_tools.VisitWebpageTool'>,
}
所以实际上就是,我们配置使用哪些工具,它就给工具集添加哪些工具,而不是一股脑把本地工具全添加进去。
4. 构造Agent
这是最后一部,构造 Agent ,这里提供了两种 Agent 类型, CodeAgent 和 ToolCallingAgent ,官方推荐使用 CodeAgent , CodeAgent 能在沙盒里写代码, ToolCallingAgent 只能调用工具并且让工具输出。
if action_type == "code":
agent = CodeAgent(
tools=available_tools,
model=model,
additional_authorized_imports=imports,
stream_outputs=True,
)
elif action_type == "tool_calling":
agent = ToolCallingAgent(tools=available_tools, model=model, stream_outputs=True)
else:
raise ValueError(f"Unsupported action type: {action_type}")
我们会另开一篇博客,深度研究 CodeAgent 和 ToolCallingAgent 。
5. 启动Agent
最后,调用 agent.run() 方法,让 Agent 跑起来,流程就全部结束了。
agent.run(prompt)
结语
我们看完了 cli.py ,它是一个真正可用的产品功能,能够根据用户的输入组装工具、启动 Agent ,下一步我们会一起深度研究,smolagents 框架是如何实现一个 Agent 的。