课程首页 › Module 1 · ReAct 循环与第一个 Agent › L3

L3 用 smolagents 30 行代码构建第一个 Agent

课程目标:跑通第一个真正能用的 Agent——亲眼看到 LLM 怎么"决定调工具"、怎么"读工具结果"。


一、为什么要先动手?#

L1 和 L2 我们一直在讲理论——Agent 是什么、ReAct 循环怎么转。这节课我们要亲手跑一次。

为什么先讲 smolagents 而不是 LangGraph?因为 smolagents 是 HuggingFace 推出的极简框架,30 行代码就能跑通一个 ReAct Agent,适合第一课建立直觉。LangGraph 更适合复杂生产环境,留到 L5。

学完这节课你会掌握:

  1. @tool 装饰器怎么把普通 Python 函数变成 Agent 工具

  2. CodeAgent 怎么在内部跑 ReAct 循环

  3. 怎么选模型(免费 HF、本地 Ollama、付费 OpenAI/Anthropic)

  4. 怎么读懂 Agent 的运行日志,看它"思考—行动"的全过程


二、用一个比喻理解 smolagents#

还记得 L1 的"私人管家"比喻吗?

最后这条是关键差异:smolagents 的 CodeAgent 让 LLM 直接生成 Python 代码并执行。比如让 Agent 查天气,它生成的代码可能是:

weather = get_weather(city="北京")
print(weather)

而不是传统 Function Calling 的 JSON:

{"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}

这种"代码即行动"的范式更强大(LLM 能循环、调用多个工具、组合结果),但也更危险(LLM 可能写出 os.system("rm -rf /"))。所以 smolagents 提供沙箱执行环境来隔离风险。


三、环境准备(5 分钟)#

Step 1:安装 smolagents#

pip install 'smolagents[toolkit,litellm]'

Step 2:选择你的模型(三种方案)#

根据你的预算和账号情况三选一:

方案 A:免费 + HuggingFace(推荐新手)#

export HF_TOKEN="hf_xxxxxxxxxxxxxx"  # 从 huggingface.co/settings/tokens 获取

注册 HuggingFace 账号 → Settings → Access Tokens → Create new token(选 read 权限即可,免费)。

方案 B:OpenAI / Anthropic(推荐已有 API Key 的)#

export OPENAI_API_KEY="sk-xxxxxxxx"
# 或
export ANTHROPIC_API_KEY="sk-ant-xxxxxxxx"

方案 C:本地 Ollama(推荐想完全离线跑)#

# 1. 安装 Ollama(https://ollama.com)
ollama pull qwen2.5-coder:7b

四、第一个 Agent:算术小助手(30 行代码)#

我们先做一个最简单的 Agent——会做数学题的算术助手。它有两个工具:加法 和 乘法。LLM 会自己决定什么时候该用哪个工具。

完整代码#

from smolagents import tool, CodeAgent, InferenceClientModel

# ============ 1. 定义工具 ============

@tool
def add(a: int, b: int) -> int:
    """返回两个整数的和。
    Args:
        a: 第一个整数
        b: 第二个整数
    """
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """返回两个整数的乘积。
    Args:
        a: 第一个整数
        b: 第二个整数
    """
    return a * b

# ============ 2. 准备模型 ============
# 方案 A:免费 HF(默认用 Qwen2.5-Coder-32B-Instruct)
model = InferenceClientModel(token="YOUR_HF_TOKEN")

# 方案 B:用 OpenAI(注释掉 A 取消注释 B)
# from smolagents import LiteLLMModel
# model = LiteLLMModel(model_id="gpt-4o-mini")

# 方案 C:用 Anthropic
# model = LiteLLMModel(model_id="anthropic/claude-3-5-sonnet-latest")

# ============ 3. 创建 Agent 并运行 ============
agent = CodeAgent(tools=[add, multiply], model=model)

# 问一个需要两步推理的问题
result = agent.run("(5 + 3) 乘以 7 等于多少?")
print(f"最终答案: {result}")
# 预期输出:最终答案: 56

这 30 行代码发生了什么?#

我们一行一行看:

① @tool 装饰器(4-15 行)#

@tool
def add(a: int, b: int) -> int:
    """返回两个整数的和。
    Args:
        a: 第一个整数
        b: 第二个整数
    """
    return a + b

关键点:

💡 经验法则:docstring 写得越清楚,LLM 越会用对工具。这是 L4 课要深入讲的"工具设计"。

② 模型选择(17-25 行)#

四种模型类对应四种场景:

类用途是否需要额外配置
InferenceClientModelHuggingFace Inference API需要 HF_TOKEN
LiteLLMModel100+ 商业模型(OpenAI/Anthropic/Ollama 等)需要对应服务的 API Key
TransformersModel完全本地运行开源模型需要 GPU
OpenAIModelOpenAI 兼容的 API 端点需要 API Key + base_url

③ CodeAgent(27 行)#

agent = CodeAgent(tools=[add, multiply], model=model)

④ agent.run(...)(30 行)#

这是触发 Agent 运行的入口。它会:

  1. 把你的问题拼进 prompt

  2. 让 LLM 生成 Python 代码(ReAct 的 Thought + Action 合并成代码)

  3. 在沙箱里执行代码

  4. 把执行结果(Observation)喂回 LLM

  5. 循环直到 LLM 写出"最终答案"代码


五、亲眼看到 ReAct 循环:开启 stream_outputs#

上面的代码我们只看到最终答案 56。真实跑起来时,Agent 在背后做了什么? 加上 stream_outputs=True 就能看到完整过程:

agent = CodeAgent(tools=[add, multiply], model=model, stream_outputs=True)
result = agent.run("(5 + 3) 乘以 7 等于多少?")

典型输出(简化版):

=== 思考 1 ===
我需要先计算 5 + 3 = 8,然后把结果乘以 7。

=== 代码 1 ===
```python
step1 = add(a=5, b=3)
result = multiply(a=step1, b=7)
print(result)

=== 执行结果 === 56

=== 思考 2 === 代码已经算出结果 56。

=== 最终答案 === 56


**这就是 ReAct 循环的真实样貌**!对比 L2 的伪代码:
- L2 是 "Thought: ... Action: add[5,3] Observation: 8" 这种纯文本格式
- smolagents 是 "Thought: ... Action: <Python 代码> Observation: <执行输出>"

**哪种更好?** 代码形式更强大:LLM 能在一段代码里调用多个工具、组合结果、做分支判断。文本形式的 Thought/Action 只能一步步走。

---

## 六、一个更有意思的例子:旅行规划 Agent

算术太简单了。我们做一个能查"巴黎一日游"的 Agent(来自 [smolagents 官方博客](https://huggingface.co/blog/smolagents)):

```python
import os
from smolagents import tool, CodeAgent, HfApiModel

@tool
def get_travel_duration(start: str, destination: str, mode: str = "driving") -> str:
    """获取两个地点之间的旅行时间。

    Args:
        start: 出发地
        destination: 目的地
        mode: 交通方式,可选 'driving' / 'walking' / 'bicycling' / 'transit'
    """
    # 这里假装调用 Google Maps API(实际使用需要 googlemaps 库 + API Key)
    import googlemaps
    gmaps = googlemaps.Client(os.getenv("GMAPS_API_KEY"))
    directions = gmaps.directions(start, destination, mode=mode)
    if not directions:
        return f"未找到从 {start} 到 {destination} 的 {mode} 路线"
    return directions[0]["legs"][0]["duration"]["text"]

agent = CodeAgent(
    tools=[get_travel_duration],
    model=HfApiModel(),
    additional_authorized_imports=["datetime"]  # 允许 Agent 在代码里 import datetime
)

result = agent.run(
    "帮我规划巴黎一日游,安排 3 个景点,并告诉我每个景点之间的交通时间"
)
print(result)

Agent 内部生成的代码可能是:

# 1. 查埃菲尔铁塔到卢浮宫的时间
leg1 = get_travel_duration(start="埃菲尔铁塔", destination="卢浮宫", mode="transit")
print(f"铁塔→卢浮宫: {leg1}")

# 2. 查卢浮宫到巴黎圣母院的时间
leg2 = get_travel_duration(start="卢浮宫", destination="巴黎圣母院", mode="transit")
print(f"卢浮宫→圣母院: {leg2}")

# 3. 汇总
print("建议行程:上午铁塔 → 中午卢浮宫 → 下午圣母院")

对比传统 Function Calling:JSON 形式下 LLM 要分三次调用,每次只返回一个数字。代码形式下 LLM 一次生成所有逻辑,循环、判断、汇总一步到位。


七、调试技巧:怎么看 Agent 到底在想什么#

如果 Agent 答错了或者卡住,怎么排查?smolagents 提供了几个工具:

1. 加详细日志#

agent = CodeAgent(
    tools=[add, multiply],
    model=model,
    stream_outputs=True,      # 实时打印每一步
    max_steps=10,             # 最多 10 步,防止死循环
    verbosity_level=2         # 0=安静, 1=正常, 2=详细
)

2. 加一个 final_answer_checks 检查器#

def check_answer(answer, task):
    """Agent 自检:答案是否合理"""
    if "error" in answer.lower():
        return False
    return True

agent = CodeAgent(
    tools=[add, multiply],
    model=model,
    final_answer_checks=[check_answer]
)

3. 看中间执行步骤#

result = agent.run("你的问题")
# 拿到所有中间步骤
print(agent.memory.steps)  # 每个 step 是 Thought + Code + Observation

八、安全警示:沙箱为什么重要?#

smolagents 默认使用 E2BSandbox 或 Docker 隔离代码执行。这是 CodeAgent 范式的最大风险点。

举个例子。如果 LLM 在生成的代码里写了:

import os
os.system("rm -rf /")

没有沙箱的话,你的电脑就完了。

生产环境的解法:

  1. 用 Docker 沙箱跑 Agent 代码

  2. 用 E2B 云沙箱(smolagents 内置支持)

  3. 关键场景用 ToolCallingAgent(让 LLM 生成 JSON 而不是代码,更安全但功能弱)

经验法则:能在沙箱里跑就别在本地跑。生产环境的 Agent 代码执行必须隔离。


九、和 L2 的连接#

L2 概念L3 对应实现
ThoughtLLM 生成 Python 代码前的推理文字
Action生成的 Python 代码(可能调用多个工具)
Observation代码在沙箱执行的输出(print / 返回值)
FinishLLM 把最终答案通过 final_answer() 函数返回

你跑完上面的代码后,应该能在脑子里清晰画出:LLM 思考 → 生成代码 → 执行 → 把结果带回 prompt → 再思考 → 再生成代码 → ... → 输出 final_answer。这就是 ReAct 循环的代码形态。


十、动手时间:亲手跑 + 改#

请按顺序完成以下三步:

步骤 1:跑通最小例子(5 分钟)#

把上面"算术小助手"的代码复制到本地 agent_demo.py,配置好 API Key,跑通。预期看到 (5+3)*7=56 的输出。

步骤 2:故意为难它(5 分钟)#

修改问题,让 LLM 必须分多步:

result = agent.run("计算 (12 + 8) * (15 - 7) / (3 + 4) 的值,保留两位小数")

观察它是不是真的生成了多行代码调用了多次工具。

步骤 3:故意给坏工具名(5 分钟)#

把工具名改成中文:

@tool
def 加法(a: int, b: int) -> int:
    """计算两个整数之和。
    Args:
        a: 第一个整数
        b: 第二个整数
    """
    return a + b

agent = CodeAgent(tools=[加法], model=model)
result = agent.run("5 加 3 等于多少?")

观察它能不能调用中文函数名。这是个真实问题——很多框架的反射机制对中文函数名支持不好,亲手测一下你能更深刻地理解 smolagents 的工作原理。

📋 步骤 3 的答案(点击展开)

结果:可能成功,可能失败。取决于:

如果失败:把工具函数名改成英文(add),docstring 用中文。这是 L4 课要讲的"工具设计"最佳实践。


十一、小结#

知识点一句话回顾
安装pip install 'smolagents[toolkit,litellm]'
工具定义@tool + docstring + 类型注解
Agent 类型CodeAgent(代码执行)vs ToolCallingAgent(JSON 调用)
模型选择InferenceClientModel / LiteLLMModel / TransformersModel
调试stream_outputs=True + verbosity_level=2
安全永远在沙箱里跑 CodeAgent

十二、Read next#

下一节课 L4 Tool Use 深入:让 Agent 拥有"双手",我们深入 Tool Use 的设计模式:怎么写 docstring 让 LLM 更会用工具、Function Calling 在底层是怎么发生的、怎么让 Agent 同时调用多个工具。

主要参考:


十三、Sources#


💬 有疑问? 试试问我:"CodeAgent 和 ToolCallingAgent 怎么选?"、"@tool 装饰器到底做了什么魔法?"、"为什么沙箱这么重要?"——我会用代码或比喻为你解答。

自测 · 检验是否真的掌握了

1. @tool 装饰器在 smolagents 里到底做了什么?

@tool 读取函数的 docstring 和类型注解,把它们转换成模型能看懂的工具描述,并把它挂到 Agent 可用的工具列表上。docstring 就是给模型看的说明书,类型注解同样被用来生成参数结构。装饰器不会改写函数体,也不会删除注解或把函数变成命令行进程。

2. CodeAgent 与常见的 Tool Calling Agent 最本质的差别是什么?

CodeAgent 的行动是一段可执行的 Python 代码,模型可以在一段代码里连续调用多个工具、做分支判断和结果汇总;Tool Calling 则是模型返回结构化的函数名与参数,由框架逐次执行。两者循环能力可以相同,CodeAgent 反而更需要沙箱,模型选型也与是否用代码无关。

3. 为什么 smolagents 强调生产环境必须给 CodeAgent 配沙箱?

CodeAgent 会直接执行模型生成的任意 Python 代码,模型完全可能写出破坏系统的代码,因此执行必须与真实环境隔离,这是该范式最大的风险点。沙箱解决的是安全隔离问题,它不负责整理输出、不降低 token 成本,更不会替模型纠正代码里的错误。