跳到主要内容

01 · Agent 执行循环(框架的心脏)

本章讲什么: 一个 Agent 拿到任务后,内部怎么「思考一步、调一个工具、看结果、再思考」地循环,直到给出最终答案。这是整个 CrewAI 工程含量最高的一支——Crew 和 Flow 最终都落到这个循环上。

1.1 它要解决的小问题

LLM 一次只会「生成一段文本」。但一个真实任务往往要多步:先想一下、查个资料(调工具)、看到结果再决定下一步。怎么把「一次生成」变成「自主的多步行动」?

答案是一个循环:每轮把对话历史发给 LLM,LLM 要么说「我要调工具 X,参数是 Y」,要么说「我有最终答案了」。框架据此执行工具、把结果追加进历史、再发一轮——直到 LLM 收敛。这就是 ReAct(Reasoning + Acting,边推理边行动)的思路。

1.2 两条路径:文本 ReAct vs 原生 function calling

CrewAI 支持两种让 LLM「表达要调工具」的方式,开循环前先二选一:

路径LLM 怎么表达调工具怎么触发
原生 function callingLLM 直接返回结构化的 tool_calls(OpenAI/Anthropic 等原生能力)LLM 声明 supports_function_calling() 为真,且有工具
文本 ReActLLM 输出 Thought: … / Action: … / Action Input: … 纯文本,框架正则解析不支持原生、或原生调用被拒绝时降级

选择逻辑在 _invoke_loop(agents/crew_agent_executor.py:309):

# 真实源码摘要,crew_agent_executor.py:318-328
use_native_tools = (
hasattr(self.llm, "supports_function_calling")
and self.llm.supports_function_calling()
and self.original_tools # 且确实挂了工具
)
if use_native_tools:
return self._invoke_loop_native_tools()
return self._invoke_loop_react()

巧妙的降级: 如果选了原生路径但模型/网关其实不支持(运行时报错),它当场切回文本 ReAct,不让整次执行失败——见 _invoke_loop_native_tools 的异常分支 agents/crew_agent_executor.py:576-579:

# crew_agent_executor.py:576-579(摘)
except Exception as e:
if is_native_tool_calling_unsupported_error(e):
self._append_text_tool_calling_fallback_message() # 往历史里塞「请用文本格式调工具」的说明
return self._invoke_loop_react() # 改走 ReAct

1.3 ReAct 文本循环:逐步拆解

怎么读: 这是一个 while not AgentFinish 的循环,每轮做四件事——查上限、问 LLM、解析、(若要调工具则)执行工具并喂回。

┌─────────────────────────────────────────────┐
│ while 还没拿到最终答案: │
│ ① 到达 max_iter? → 强制收尾,break │
│ ② 限流(RPM)→ 调 LLM 拿到一段文本 │
│ ③ 解析这段文本: │
│ 含 "Final Answer:" → AgentFinish(完) │
│ 含 "Action:/Action Input:" → AgentAction│
│ ④ 若是 AgentAction:执行工具,把结果 │
│ 作为 Observation 追加进 messages,继续 │
└─────────────────────────────────────────────┘

循环主体在 _invoke_loop_react(agents/crew_agent_executor.py:330-468)。三步关键:

① 问 LLM。 get_llm_response(...) 拿回原始文本(crew_agent_executor.py:360)。

② 解析。 process_llm_response(answer_str, self.use_stop_words) 把文本变成 AgentActionAgentFinish(crew_agent_executor.py:398)。真正的解析在 agents/parser.pyparse(text):

# parser.py:91-114(摘)— 文本协议的解析规则
includes_answer = FINAL_ANSWER_ACTION in text # "Final Answer:"
action_match = ACTION_INPUT_REGEX.search(text)
if includes_answer:
return AgentFinish(thought=..., output=final_answer, text=text)
if action_match:
return AgentAction(thought=..., tool=clean_action,
tool_input=safe_tool_input, text=text)

注意两个真实细节:工具输入会先过 _safe_repair_json(parser.py:161)——LLM 经常吐出半残的 JSON,这里用 json_repair.repair_json 容错修复;若文本既没 Action 也没 Final Answer,抛 OutputParserError,循环里捕获后把格式错误说明喂回让 LLM 重试(crew_agent_executor.py:434-442)。

③ 执行工具。 若得到 AgentAction,调 execute_tool_and_check_finality(...)(crew_agent_executor.py:415)执行工具,再 _handle_agent_action 把结果追加进对话历史(crew_agent_executor.py:427)。

1.4 原生 function calling 循环

这条路径里 LLM 不再输出文本格式,而是直接返回结构化工具调用。框架把工具转成 OpenAI schema(convert_tools_to_openai_schema,crew_agent_executor.py:497),然后:

  • 拿到的若是「工具调用列表」(_is_tool_call_list,crew_agent_executor.py:634)→ 执行后继续循环;
  • 拿到的若是普通字符串/Pydantic 对象 → 直接当作 AgentFinish 返回。

一个关键设计:每轮只执行第一个工具调用,然后让 LLM 反思。_handle_native_tool_calls 的注释与实现(crew_agent_executor.py:667-807):

Executes only the FIRST tool call ... This enables sequential tool execution with reflection after each tool, allowing the LLM to reason about results before deciding on next steps.

执行完工具后,它会塞一条「请基于刚才的结果继续推理」的提示(I18N_DEFAULT.slice("post_tool_reasoning"),crew_agent_executor.py:778-783),再进下一轮。

例外——并行批处理: 当 LLM 一次返回多个工具调用,且这批工具里没有 result_as_answer 也没有 max_usage_count 限制时,会用 ThreadPoolExecutor(最多 8 线程)并行执行,并用 contextvars.copy_context().run 保留上下文(crew_agent_executor.py:742-767)。一旦涉及上述特殊工具,就退回串行,保证语义正确。

1.5 三个「兜底」与终止条件

循环不是无限的。三处保护:

情况处理位置
迭代超过 max_iterhandle_max_iterations_exceeded 强制收尾,逼出一个 AgentFinishcrew_agent_executor.py:343503
上下文长度超限respect_context_window=True,handle_context_length 裁剪历史后 continue 重试crew_agent_executor.py:447-456
解析失败handle_output_parser_exception 把格式说明喂回,让 LLM 改正crew_agent_executor.py:434

终止的硬契约:循环只在拿到 AgentFinish 时退出;否则末尾抛 RuntimeError("... ended without reaching a final answer")(crew_agent_executor.py:462-466)。AgentFinish / AgentAction 的定义见 parser.py:25-42——前者带 output,后者带 tool + tool_input

1.6 原理演示(把核心想法演出来)

下面这段把 ReAct 循环的骨架抽出来,帮你建立直觉(示意,非源码):

# 示意,非源码:ReAct 循环的最小骨架
def run_react(llm, tools, messages, max_iter=25):
for _ in range(max_iter):
text = llm.call(messages) # 问 LLM 要一段文本
step = parse(text) # 解析成 Action 或 Finish
if isinstance(step, AgentFinish):
return step.output # 收敛,返回最终答案
result = tools[step.tool](step.tool_input) # 执行工具
messages.append(f"Observation: {result}") # 把结果喂回历史
return force_finish(messages) # 到上限,强制收尾

重点看:「解析→执行→喂回」三步是循环不变量,无论 ReAct 还是原生路径都是这个骨架,区别只在「LLM 怎么表达要调工具」和「结果以什么结构喂回」。

1.7 关键细节 / 坑

  • CrewAgentExecutor 已弃用。 构造时会发 DeprecationWarning:Crew 里的 Agent 现在默认用 crewai.experimental.AgentExecutor(crew_agent_executor.py:145-151)。本章用旧执行器讲解,因为它的循环最直白、最易读;新执行器逻辑同构但额外用 Flow 编排(见 1.8)。
  • 停用词(stop words)。 ReAct 模式靠 Observation 之类停用词截断 LLM 输出,_llm_stop_words_applied 上下文管理器临时给 LLM 注入停用词(crew_agent_executor.py:229);use_stop_words 取决于模型是否 supports_stop_words()(crew_agent_executor.py:157-168)。
  • 工具缓存与钩子。 工具执行前后跑 before/after_tool_call 钩子,结果可进 tools_handler.cache(crew_agent_executor.py:929-1063),钩子返回 False 可拦截工具调用。

1.8 巧妙之处:新执行器本身就是一个 Flow

新默认执行器 AgentExecutor 的类签名是 class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor)(experimental/agent_executor.py:164)——它继承自 Flow。也就是说,「Agent 的多步循环」被重新表达成了一张事件驱动的流程图(Flow 见 03-flows)。这是 CrewAI「两套范式互相嵌套」理念的最深体现:连最底层的 Agent 循环都跑在 Flow 引擎上。它用 _skip_auto_memory 避免 Flow 自动分配记忆,复用 Agent/Crew 的记忆(experimental/agent_executor.py:166-179)。

→ 下一章:多个这样的 Agent 怎么被 Crew 编排起来协作。