第 2 章 · agent 主循环
本章讲 agent 怎么「动」:谁来跑、一步里做什么、何时继续、何时停。重点对比 v3 极简循环与老 MemGPT 的心跳模型。
2.1 谁来跑:AgentLoop 工厂按类型分发
一个 agent 该用哪种循环实现,由 AgentLoop.load() 这个工厂按 agent_type 决定(letta/agents/agent_loop.py:18):
# 真实源码节选,agent_loop.py:20 —— 按类型选实现
if agent_state.agent_type in [AgentType.letta_v1_agent, AgentType.sleeptime_agent]:
if agent_state.enable_sleeptime:
return SleeptimeMultiAgentV4(...) # 带后台「睡眠时间」记忆 agent
return LettaAgentV3(...) # 当前主力实现
elif agent_state.enable_sleeptime and ...:
return SleeptimeMultiAgentV3(...)
else:
return LettaAgentV2(...) # 旧实现
要点:
LettaAgentV3是当前主力(letta_v1_agent类型),继承自LettaAgentV2,把循环「简化 + 通用化」(agents/letta_agent_v3.py:100-110)。- sleeptime 系列是「多 agent」变体:在你正常对话之外,有一个后台记忆 agent在「睡眠时间」整理记忆(见 2.5)。
- Letta 同时保留了 v2/v3 和多种 agent 类型枚举(
schemas/enums.py:81:memgpt_agent、memgpt_v2_agent、letta_v1_agent、react_agent、workflow_agent、voice_*等)——这是个还在演进、向后兼容多代实现的活项目。
2.2 v3 的循环规则:一句话——「调工具就继续」
老 MemGPT 的循环靠「心跳(heartbeat)」:模型调函数时可以请求一个心跳,函数跑完后系统再唤醒它,从而把多次函数调用串起来。老 prompt 里就明说了这点(prompts/system_prompts/memgpt_v2_chat.py:22-23):「你的大脑会定时心跳运行……调用函数时还能请求心跳事件,函数完成后再次运行你的程序」。
v3 把这套砍掉了,换成极简规则。看 _decide_continuation 的 docstring(agents/letta_agent_v3.py:1977-1985):
1. 没调工具? 循环结束。
2. 调了工具? 循环继续(无论工具成功/失败/被规则拦截)。
类比:「想继续就再调 一个工具,想交回控制权就直接回话(不调工具)。」 系统提示里也是这么告诉模型的(prompts/system_prompts/letta_v1.py:22):
Continue executing and calling tools until the current task is complete or you need user input. To continue: call another tool. To yield control: end your response without calling a tool.
所以 send_message(给用户发话)本身不带「继续」语义——发完话又没调别的工具,循环自然停。这比心跳模型干净得多。
2.3 主循环骨架:step() 跑 max_steps 步
外层 step()(agents/letta_agent_v3.py:222)就是一个跑最多 max_steps(默认 DEFAULT_MAX_STEPS = 50,constants.py:75)步的 for 循环,每步调一次内层 _step():
# 真实源码精简(letta_agent_v3.py:328-395,略去额度检查/取消两个分支;break 在 :386,max_steps 收尾在 :394)
for i in range(max_steps):
response = self._step(
messages=list(self.in_context_messages + input_messages_to_persist),
input_messages_to_persist=input_messages_to_persist,
llm_adapter=llm_adapter, run_id=run_id, ...
)
async for chunk in response:
response_letta_messages.append(chunk)
if not self.should_continue: # _decide_continuation 设的标志
break
if i == max_steps - 1 and self.stop_reason is None:
self.stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value)
重点看: 循环是否继续,完全由 self.should_continue 控制,而它由每步末尾的 _decide_continuation 设置。跑满 50 步还没停,就以 max_steps 作为停止原因收尾。
2.4 一步内部(_step)做了什么
_step()(agents/letta_agent_v3.py:895)是真正干活的地方,一步内大致是:
组装上下文 调模型 执行工具 + 决定续步 持久化
┌───────────┐ ┌───────────┐ ┌──────────────────────┐ ┌──────────┐
│ 重建系统提示 │ ──▶ │ build req │ ──▶ │ _handle_ai_response │──▶ │ checkpoint│
│ (只在变化时)│ │ + invoke │ │ 执行工具/审批/续步判断 │ │ 写消息 │
└───────────┘ └───────────┘ └──────────────────────┘ └──────────┘
│ 若 ContextWindowExceeded │
└──────────────▶ compact 压缩后重试 ───────────┘
几个值得记住的点:
- 并行工具调用会按 provider 和「有没有 tool rule」动态开关:无 tool rule 且 config 允许时才放开并行(
letta_agent_v3.py:1112-1147,对 anthropic/openai/gemini 各有处理)。 - 工具返回截断。 单个工具返回过长会被截断,上限按「约 20% 上下文窗口 × 4 字符/token,最少 5000 字符」算(
_compute_tool_return_truncation_chars,letta_agent_v3.py:143-153)——防一个工具结果吃掉整个上下文。 - 上下文超限即压缩重试。 调模型若抛
ContextWindowExceededError,会触发compact_messages压缩后重试(letta_agent_v3.py:1218-1247),详见第 4 章。
2.5 续步判断的完整逻辑(_decide_continuation)
虽然规则一句话能说清,但 _decide_continuation(letta_agent_v3.py:1967)还要处理 tool rules(工具调用约束)。要点:
| 情况 | 结果 |
|---|---|
| 没调工具,但有「退出前必须调」的工具还没调 | 继续,并给模型一句 ToolRuleViolated 提示(:1994-1997) |
| 没调工具,无未尽工具 | 结束(end_turn);若是模型撞了 max_tokens 则记 max_tokens_exceeded(:2000-2002) |
| 调了终止工具(terminal tool) | 停,停止原因 tool_rule(:2010-2012) |
| 调了普通工具 | 继续(:2014-2020 处理 child / continue 规则) |
| 已是最后一步 | 强制停,记 max_steps(:2023-2025) |
Tool rules 是 Letta 的一个有用机制:可以声明「某工具是终止工具」「调了 A 必须接着调 B(child)」「某工具必须在退出前至少调一次」等。审批类工具(requires_approval)则会在 _handle_ai_response 里被拦下,返回 requires_approval 停止原因、等用户批准(letta_agent_v3.py:1681-1709)。
2.6 sleeptime:后台整理记忆的「第二个 agent」
当 enable_sleeptime=True,主力换成 SleeptimeMultiAgentV4(agent_loop.py:35)。直觉:你和「对话 agent」聊天,另一个「记忆 agent」在后台(对话之外的「睡眠时间」)整理 core memory / 归档,让主对话 agent 不必在 对话中频繁停下来自我整理。SleeptimeMultiAgentV3 断言它绑定的 group 必须是 ManagerType.sleeptime 类型(groups/sleeptime_multi_agent_v3.py:33)。本库未深读其内部 step 逻辑,这里只点出概念。
→ 继续 03-system-prompt-assembly.md
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 按类型选循环实现 | letta/agents/agent_loop.py | AgentLoop.load |
| v3 主力实现 | letta/agents/letta_agent_v3.py | LettaAgentV3 |
| 外层多步循环 | letta/agents/letta_agent_v3.py | step(for i in range(max_steps)) |
| 单步执行 | letta/agents/letta_agent_v3.py | _step |
| 工具执行 + 审批 | letta/agents/letta_agent_v3.py | _handle_ai_response |
| 续步规则 | letta/agents/letta_agent_v3.py | _decide_continuation |
| agent 类型枚举 | letta/schemas/enums.py | AgentType |
| 基座系统提示(v1) | letta/prompts/system_prompts/letta_v1.py | PROMPT |
| 老心跳模型(对比) | letta/prompts/system_prompts/memgpt_v2_chat.py | (prompt 文本) |