数据截至 (上游 commit 375d8081c48c)
Swarms — Agent 原子
本章讲清楚:一个
Agent到底怎么跑起来。这是整个框架的地基,上层所有 Swarm 都在反复调用它的run()。
1. Agent 是什么
Agent 是 Swarms 的唯一基本积木——一个封装了“LLM + 工具 + 记忆 + 循环控制”的自治实体。它定义在 swarms/structs/agent.py(Agent.__init__,structs/agent.py:331),是全仓库最重的一个类(6000+ 行)。
它的四件套:
| 组件 | 作用 | 由哪个参数/字段承载 |
|---|---|---|
| 模型 | 实际产生文字的 LLM | model_name(经 LiteLLM 调用) |
| 工具 | 让模型能“动手”的 Python 函数 | tools=[...] |
| 记忆 | 存对话历史、可跨进程持久化 | short_memory(一个 Conversation) |
| 循环 | 让它自己多想几轮 | max_loops(整数或 "auto") |
关键默认值(读自 Agent.__init__,structs/agent.py:331-419):
| 参数 | 默认 | 含义 |
|---|---|---|
agent_name | "swarm-worker-01" | 名字;也是 MEMORY.md 落盘目录的 key |
max_loops | 1 | 跑几轮就返回;"auto" = 自主到自认为完成 |
temperature | 0.5 | 采样温度 |
max_tokens | 4096 | 单次调用最大生成 token |
output_type | "str-all-except-first" | 返回时如何格式化对话 |
context_compression | True | 接近上下文上限时自动压缩记忆 |
persistent_memory | True | 跨会话读写 MEMORY.md |
reasoning_prompt_on | True | 多轮时自动注入“当前第几轮”的推理提示 |
2. 一次 run:主循环怎么转
这节讲什么: 追一次 agent.run("...") 从入口到返回,看清那个“思考循环”。
入口 run()(structs/agent.py:2974)先做分流:max_loops == "auto" 走自主结构(见第 4 节),否则进 _run()(structs/agent.py:1236)跑固定轮数的主循环。
主循环的直觉: 一轮 = 让模型看着“到目前为止的全部对话”再说一段话;如果这段话里带了工具调用,就把工具跑掉、把结果也写进对话;然后进入下一轮。轮数由 max_loops 控制。
agent.run(task)
│ (max_loops == "auto"? → 自主结构;否则 ↓)
▼
把 task 写进 short_memory (对话记忆)
│
┌──────────► 一轮 loop (循环 max_loops 次) ─────────┐
│ ① 把整段对话历史拼成 prompt │
│ ② call_llm → 模型回一段话(可能含工具调用) │
│ ③ parse_llm_output 解析,写回 short_memory │
│ ④ 若回答里有 handoff / 工具调用 → 执行,结果写回 │
│ ⑤ 检查停止条件 / 交互模式 / 自动保存 │
└────────────────────────────────────────────────────┘
│
按 output_type 格式化对话 → 返回
真实实现的骨架(Agent._run,structs/agent.py:1595-1815):
- 循环条件:
while self.max_loops == "auto" or loop_count < self.max_loops(structs/agent.py:1617)。 - 每轮先把历史拼成字符串:
task_prompt = self.short_memory.return_history_as_string()(structs/agent.py:1669)。 - 调模型:
response = self.call_llm(task=task_prompt, current_loop=loop_count, ...)(structs/agent.py:1702)。 - 解析并写回记忆:
response = self.parse_llm_output(response)后self.short_memory.add(role=self.agent_name, content=response)(structs/agent.py:1718-1723)。 - 若回答是工具调用列表 →
self.tool_execution_retry(response, loop_count)(structs/agent.py:1789)。
一个容易忽略的细节:重试是内层循环。 每一轮的模型调用外面还套了一层 while attempt < self.retry_attempts and not success(structs/agent.py:1675),模型/网络出错会自动重试(默认 retry_attempts=3),重试用尽再考虑 fallback 模型。
3. 记忆:Conversation 与 MEMORY.md
这节讲什么: Agent 的“记事本”长什么样、怎么跨重启还记得事。
Agent 的 short_memory 是一个 Conversation 对象,在 short_memory_init(structs/agent.py:1022)里创建。它同时承担两个角色:
- 本轮上下文 —— 每轮把
system_prompt + 历史拼给模型。 - 持久记忆 —— 当
persistent_memory=True(默认),它把交互追加写到磁盘上的MEMORY.md。
MEMORY.md 的路径按 agent_name 落盘(structs/agent.py:1043-1045):
{WORKSPACE_DIR}/agents/{agent_name}/MEMORY.md
源码注释点破了一个设计取舍:key 只用 agent_name、不带 self.id——因为 id 每次进程启动都是新 uuid,若带上就会每次生成一个空的新 MEMORY.md(structs/agent.py:1034-1036)。这意味着:同名 Agent 会共享同一份持久记忆;给多个 Agent 起同一个 agent_name 会让它们互相污染记忆(第 4 章“边界”会再提)。
压缩:上下文快满时自动瘦身。 构造时若 context_compression=True,会挂一个 ContextCompressor(threshold=0.9)(structs/agent.py:542-546);主循环每轮开头调 self._context_compressor.maybe_compress(self)(structs/agent.py:1386-1387),token 用量越过 90% 时压缩/改写记忆,避免撞上下文墙。
4. 工具调用:Python 函数怎么变成“手脚”
这节讲什么: 你传一个普通 Python 函数,模型怎么就能“调用”它。
思路。 大模型的“函数调用”能力需要一份 JSON schema(函数名、参数、类型)。Swarms 帮你从函数签名 + docstring 自动生成这份 schema,再在模型说“我要调 X(参数=…)”时,把它路由回真正的 Python 函数执行。
三步链路:
你的函数 def get_price(ticker: str) -> str: """..."""
│ ① tool_handling: 签名+docstring → OpenAI 函数 schema
▼
tools_list_dictionary ──② 随 call_llm 一起发给模型──► 模型
▲ │ 回:调用 get_price(ticker="AAPL")
│ ③ execute_tools: 按名字找回真函数并执行 │
└─────────────── 结果写回对话(role="Tool Executor") ◄┘
① 生成 schema(Agent.tool_handling,structs/agent.py:934):调 convert_multiple_functions_to_openai_function_schema(self.tools)(structs/agent.py:995-997)把每个函数转成 {"type":"function","function":{name, description, parameters}},存进 tools_list_dictionary。
② 发给模型:llm_handling(structs/agent.py:1066)把 tools_list_dictionary 塞进 LiteLLM 配置;有 ≥2 个工具或配 了 MCP 时开启并行工具调用 parallel_tool_calls=True(structs/agent.py:1093-1100)。
③ 执行(Agent.execute_tools,structs/agent.py:3813):把模型返回的工具调用交给 self.tool_struct.execute_function_calls_from_api_response(response)(structs/agent.py:3943),执行结果以 role="Tool Executor" 写回对话(structs/agent.py:3957-3960)——这样下一轮模型就“看得到”工具的输出。真正的执行器 BaseTool.execute_function_calls_from_api_response(swarms/tools/base_tool.py:2194)兼容 OpenAI/Anthropic 两种响应格式,默认用线程池并行执行多个调用(max_workers=4)。
教学示例:一个真能用的工具 Agent(示意,非源码):
from swarms import Agent
def get_stock_price(ticker: str) -> str:
"""查询某股票代码的当前价格。
Args:
ticker: 股票代码,例如 'AAPL'。
"""
# 这里放你的真实实现
return f"{ticker}: $123.45"
agent = Agent(
agent_name="StockAnalyst",
model_name="gpt-4o-mini",
tools=[get_stock_price], # 只需传函数,schema 自动生成
max_loops=3, # 允许它“调工具→看结果→再答”多轮
)
print(agent.run("苹果和微软现在多少钱?"))
重点看:你从头到尾没写任何 JSON schema——
tool_handling自动从get_stock_price的类型注解和 docstring 生成了它。
5. 自主模式:max_loops="auto" 的 plan→execute→summary
这节讲什么: 最有意思的一档。设 max_loops="auto",Agent 不再跑固定轮数,而是切换成一个“先规划、再逐子任务执行、最后总结”的自主结构。
run() 在 max_loops == "auto" 时不走普通 _run,而是走 _run_autonomous_loop(structs/agent.py:1864,由 run() 在 structs/agent.py:3085 分流)。它是三阶段:
Phase 1 规划 create_plan 工具 → 把任务拆成带依赖/优先级的子任务列表
│
▼
Phase 2 执行 逐个子任务:think(思考) → 调工具(动作) → observe(观察)
│ 防呆:连续 think 不超过 max_consecutive_thinks(=2)
▼ 每个子任务有迭代上限,总执行也有上限
Phase 3 总结 complete_task → 生成综合最终答复
它自动获得一批“内置工具”。 进入自主模式时,get_autonomous_planning_tools() 提供的规划工具会被注入,并注册对应处理器(structs/agent.py:2280-2319):
| 工具 | 用途 |
|---|---|
create_plan | 生成子任务计划 |
think | 显式思考一步(若设了 thinking_tokens 则移除,避免与原生扩展思考重复,structs/agent.py:2230-2235) |
subtask_done / complete_task | 标记子任务/整体完成 |
create_file / update_file / read_file / list_directory / delete_file | 文件读写 |
run_bash / grep | 跑 shell、搜文件 |
create_sub_agent / assign_task / check_sub_agent_status | 动态派生子 Agent 并派活 |
直觉: 固定 max_loops=N 像“想 N 步就交卷”;"auto" 像“给你纸笔和工具,自己列提纲、一条条做、做完再交”。适合开放式研究这类步数事先不知道的任务;代价是可能循环较久,生产环境更推荐显式的整数 max_loops。
6. 关键细节 / 坑
context_length会被构造函数强制改成 16000。 你在Agent(context_length=...)传的值,以及初始化中途的赋值,都会在structs/agent.py:514被一句self.context_length = 16000覆盖(上一行还留着基于模型算 max tokens 的注释代码)。所以别指望这个参数按你传的生效。tools_list_dictionary默认是可变的[]。 默认参数写成tools_list_dictionary: Optional[...] = [](structs/agent.py:388),是 Python 里“可变默认参数”这一经典写法;框架内部会先做 None 检查再 extend,实际使用中通常在实例上重新赋值,但读代码时要留意这一点。retry_attempts是每轮模型调用级别的重试,不是整个 run 的重试;fallback 模型在所有重试失败后才登场(run()的except分支,structs/agent.py:3107-3140)。
下一章看这些 Agent 是怎么被“串起来”的 → 02-orchestration-engine.md。