Day 03 / 共 60 天 · 阶段1 入门与心智

Agent 是什么:拆开这个"AI 员工"

昨天你写了 Agent(role=..., goal=..., backstory=..., llm=...) 就跑起来了。但 Agent 类其实有几十个字段。今天不逐个背,而是分组认识:它是怎么两层继承来的、哪三个字段是"人设三件套"、哪一批是"能力开关"、哪些是 CrewAI 特有的高级能力(推理/规划/多模态),最后看藏在字段里那个真正干活的 agent_executor。源码在 agent/core.py(1931 行)与父类 agents/agent_builder/base_agent.py

📍 你在 60 天里的位置(阶段1:入门与心智 · 共 6 天)
D01 项目全景 D02 装环境跑通 D03 Agent D04 Task D05 Crew D06 kickoff 旅程 阶段2 Agent 深入
💡 先用一个类比兜住今天 一个 Agent 就是一份"员工档案 + 能力配置卡"role/goal/backstory 是这个员工"是谁、要干嘛、什么背景"(写进 prompt 的人设);llm 是他的大脑;tools 是他手里的工具箱;max_iter / allow_delegation / verbose 这些是行为开关(最多试几次、能不能把活转派给别人、要不要碎碎念)。而 agent_executor 是把这份档案真正"派上岗干活"的执行引擎——今天先认字段,阶段2 再看引擎内部。
L01

痛点:光有一个 LLM,凑不成一个"角色"

🤔 痛点直接调 LLM API,你每次都要手工拼一大段 system prompt("你是资深分析师……")、手工管工具调用、手工控制"最多循环几次别死循环"、手工处理超时和重试。这些东西散落在你的业务代码里,换个角色就得重写一遍。
💡 本质:Agent = 把"一个角色需要的一切"打包成一个对象CrewAI 把"人设 + 大脑 + 工具 + 行为策略 + 执行引擎"全部收进一个 Pydantic 模型 Agent。类的 docstring 开门见山(agent/core.py:170):
# agent/core.py:170
class Agent(BaseAgent):
    """Represents an agent in a system.

    Each agent has a role, a goal, a backstory, and an optional language model (llm).
    The agent can also have memory, can operate in verbose mode, and can delegate
    tasks to other agents.
    """
大白话Agent 就是"一个配置齐全的 AI 员工对象"。你填字段 = 填一张入职表;CrewAI 拿着这张表,帮你把琐碎的 prompt 拼装、工具调用、循环控制全干了。
L02

两层继承:Agent ← BaseAgent

字段分布在两层。通用字段(任何 agent 都有的:角色、工具、循环上限……)在抽象基类 BaseAgentCrewAI 官方实现特有的字段(推理、规划、多模态……)在子类 Agent

# agents/agent_builder/base_agent.py:200
class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
    # ...通用字段都在这里...

# agent/core.py:170
class Agent(BaseAgent):        # ← 继承通用字段,再加自己的高级字段
    ...
BaseModel两层都是 Pydantic 模型——意味着你传参会被自动校验(类型不对直接报错),且能序列化成 JSON。
ABC(抽象基类)BaseAgent 是抽象的,规定了子类必须实现 execute_task / create_agent_executor 等方法(:644、:662 有抽象声明)。你不能直接 BaseAgent(...),只能用具体子类 Agent
metaclass=AgentMeta用了元类——CrewAI 借它在类创建时做一些注册/增强(进阶细节,先知道有这么个机制)。
💡 设计取舍①:为什么要分 BaseAgent / Agent 两层,而不是一个类写到底? CrewAI 支持"接入外部 agent 框架"(源码里有 agent_adapters/)。抽出 BaseAgent 定义"一个 agent 至少要能干什么"(execute_task、create_agent_executor 等抽象方法),就等于定了一份契约:无论你是官方 Agent、还是包了别家框架的适配器,只要实现这份契约,Crew 就能一视同仁地调度你。代价是初学者要跨两个文件找字段;回报是可扩展性——Crew 的调度代码只依赖 BaseAgent 契约,不关心具体是谁。这就是"面向接口而非实现"。
L03

人设三件套:role / goal / backstory

这三个是必填字段,在 BaseAgent 里(agents/agent_builder/base_agent.py:263):

# agents/agent_builder/base_agent.py:262
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)   # 每个 agent 一个不变的身份证
role: str = Field(description="Role of the agent")            # 角色:"资深市场分析师"
goal: str = Field(description="Objective of the agent")       # 目标:他要达成什么
backstory: str = Field(description="Backstory of the agent")  # 背景故事:塑造语气/专长
role角色名。会写进 system prompt,告诉模型"你现在扮演谁"。也用于日志和委派时的身份标识。
goal这个角色的总目标。注意它和"任务"不同:goal 是长期人设目标,Task 才是这次具体要交付的活。
backstory背景故事。看似"废话",实际很影响输出风格和专业度——给模型足够的"人设锚点",它更能稳定地扮演该角色。
id 是 frozen 的frozen=True 表示身份证一旦生成就不可改。委派、记忆、事件追踪都靠它认人。
💡 为什么偏偏是这三件套?大模型扮演角色,本质靠 prompt 里的"身份设定"。CrewAI 把这件事结构化成固定三问——你是谁(role)、要干嘛(goal)、什么来头(backstory)——比让用户自由写一大段 system prompt 更好:既降低了写好人设的门槛,又能被框架统一拼装进标准模板(阶段2 会看到 prompt 是怎么由这三件套拼出来的)。
📝 真实值 role="Senior Market Analyst"goal="Conduct deep market analysis with expert insight"backstory="You're a veteran analyst known for identifying subtle market patterns"。这三句最终会被塞进送给模型的 system prompt,让它"入戏"。
L04

能力/行为开关字段群(多在 BaseAgent)

三件套之外,一大批字段是"给这个员工配能力、定行为边界"(agents/agent_builder/base_agent.py:269 起):

# agents/agent_builder/base_agent.py:269
cache: bool = Field(default=...)                 # 是否缓存工具调用结果
verbose: bool = Field(default=...)               # 是否打印详细执行日志
max_rpm: int | None = Field(default=...)         # 每分钟最多请求数(限速)
allow_delegation: bool = Field(default=...)      # ★能否把活委派给别的 agent
tools: list[BaseTool] | None = Field(default=...)# 手里的工具箱
max_iter: int = Field(default=...)               # ★最多推理循环几次(防死循环)
# ...
tools_handler: ToolsHandler = Field(default=...) # 工具调用的管理器
security_config: SecurityConfig = Field(...)     # 安全配置
knowledge: Knowledge | None = Field(default=...) # 挂载的知识库
字段作用不填会怎样
toolsAgent 能调用的工具列表没工具,只能靠模型自己的知识答
max_iter思考→用工具→再思考 的最大循环次数用默认值兜底,防止无限循环烧钱
allow_delegation允许把子任务转派给团队其他成员默认 False,自己干到底
max_rpm限速,保护你的 API 额度None = 不限速
cache相同工具调用复用结果省钱省时的小开关
verbose是否刷执行日志调试期打开,生产关掉
大白话这一组就是"给员工划规矩":最多试几次、能不能求助同事、每分钟别打太多电话、要不要写工作日志。绝大多数你都可以不填,用默认值。
L05

Agent 独有的高级字段(在 agent/core.py)

下面这些是官方 Agent 子类特有的"加强能力"(agent/core.py:202 起):

# agent/core.py:202
max_execution_time: int | None = Field(default=None, ...)  # 单个任务最长执行秒数(超时报错)
use_system_prompt: bool | None = Field(default=True, ...)  # 是否用 system 角色下 prompt
llm: ... = Field(default=None, ...)                        # ★大脑:可传字符串或 LLM 对象
respect_context_window: bool = Field(default=..., ...)     # 自动裁剪以适配上下文窗口
max_retry_limit: int = Field(default=..., ...)             # 出错重试次数上限
multimodal: bool = Field(default=..., ...)                 # 是否多模态(能看图等)
planning: bool = Field(default=..., ...)                   # 执行前先做规划
reasoning: bool = Field(default=..., ...)                  # ★启用"先推理再动手"
max_reasoning_attempts: int | None = Field(default=None,..)# 推理尝试上限
inject_date: bool = Field(default=..., ...)                # 自动把当前日期注入 prompt
llm★最重要。它的类型是 str | BaseLLM | None,还挂了一个 BeforeValidator(_validate_llm_ref)——所以你传字符串 "gpt-4o" 时,Pydantic 校验阶段就会把它解析成一个 LLM 对象。这正是 D02 里"只写模型名就行"的原理。
reasoning / planningCrewAI 的"先想后做"能力:开 reasoning,agent 会先产出推理再执行;开 planning,crew 级别会先规划任务顺序。阶段2/4 深挖。
respect_context_window当对话/上下文太长超过模型窗口时,自动裁剪,避免"token 超限"报错。生产环境很实用。
max_execution_time给单个任务设超时。配合 execute_task 里的超时执行分支(L06 会提到)。
multimodal / inject_date多模态(看图)、自动注入当前日期(让模型知道"今天几号")等便利开关。
数据结构:Agent 字段分层 BaseAgent(通用契约层) role/goal/backstory tools max_iter allow_delegation cache max_rpm verbose knowledge agent_executor Agent(官方实现·高级能力层) llm reasoning planning multimodal max_execution_time respect_context_window max_retry_limit inject_date
图注:上层是"任何 agent 都有"的通用字段,下层是官方 Agent 加的高级能力。红=必填三件套,紫=执行引擎,橙/蓝=开关。
L06

agent_executor:藏在字段里的"干活引擎"

字段里有一个特别的成员:agent_executoragent/core.py:333)。它不是配置,而是真正执行任务的引擎对象。Agent 自己不直接跟模型对话,而是委托给它。

# agent/core.py:333
agent_executor: CrewAgentExecutor | AgentExecutor | None = Field(
    default=None, description="An instance of the CrewAgentExecutor class.")

# agent/core.py:1028 —— 用之前先"装配"好这个引擎
def create_agent_executor(self, tools=None, task=None) -> None:
    raw_tools = tools or self.tools or []
    parsed_tools = parse_tools(raw_tools)
    prompt, stop_words, rpm_limit_fn = self._build_execution_prompt(raw_tools)  # ★用三件套等拼 prompt
    if self.agent_executor is not None:
        self._update_executor_parameters(...)      # 已有就更新参数
    else:
        if not isinstance(self.llm, BaseLLM):
            raise RuntimeError("LLM must be resolved before creating agent executor.")
        self.agent_executor = self.executor_class(
            llm=self.llm, task=task, agent=self, crew=self.crew, tools=parsed_tools, ...)
_build_execution_prompt★把 role/goal/backstory + 工具说明拼成最终 system prompt——这就是 L03 说的"三件套如何变成 prompt"的入口。
已有则更新,否则新建幂等设计:executor 是懒创建的,第二次任务只更新参数不重建,省开销。
LLM must be resolved边界:创建引擎前,self.llm 必须已经是真正的 BaseLLM 对象(字符串 "gpt-4o" 得先被解析)。没解析好就直接 raise RuntimeError,不让你带着"半成品大脑"上岗。
execute_task(:740)真正执行时,execute_task 会拼 prompt、按 max_execution_time 选"带超时/不带超时"执行,最后把结果整理返回。阶段2 会逐行拆这个循环。
控制流:从"字符串 llm"到"能干活的 executor" llm="gpt-4o"(字符串) BeforeValidator解析成 LLM 对象 create_agent_executor装配引擎 可干活 ★边界检查 llm 不是 BaseLLM → raise RuntimeError
图注:只有 llm 被解析成真正的 BaseLLM 对象,才能建 executor——否则装配阶段直接报错。
💡 设计取舍②:为什么把"配置"(Agent 字段)和"执行"(agent_executor)分开? 朴素做法是让 Agent 类自己既存配置又跑循环。CrewAI 把执行逻辑抽到独立的 CrewAgentExecutor,Agent 只持有它的引用。好处:① 配置对象轻、可序列化——Agent 能被存档/复制/传递,而重量级的运行时状态(消息历史、迭代计数)都在 executor 里;② 可换引擎——字段类型是 CrewAgentExecutor | AgentExecutor,说明执行引擎是可替换的(还有个实验性的 AgentExecutor)。代价是"一个 agent 干活"要跨 Agent 和 Executor 两个对象理解,但这换来了清晰的"配置/运行"分离。
L07

边界 + 今日小结

⚠️ 边界:字符串 llm 什么时候才变成真正的 LLM 对象? 你写 Agent(llm="gpt-4o"),此刻 self.llm 还是字符串吗?看 L05:llm 字段挂了 BeforeValidator(_validate_llm_ref),所以在 Pydantic 构造/校验阶段字符串就会被尝试解析。但 L06 又检查 if not isinstance(self.llm, BaseLLM): raise RuntimeError——这说明存在"尚未解析成对象"的时刻(比如某些延迟解析路径)。坑点:如果你自定义 LLM 或用了不认识的 model 名,解析可能失败,报错往往出现在"创建 executor"这一步而不是"构造 Agent"这一步。记住这条链路:字符串 → BeforeValidator 解析 → 必须是 BaseLLM 才能建 executor,排查模型相关报错就有方向了。

👶 小白:goal 和 Task 的 description 有啥区别?都在说"要干嘛"啊。

👨‍🏫 老师:goalagent 的长期人设目标("我这个分析师追求什么"),跟具体任务无关,一个 agent 只有一个 goal。Task.description这一次要交付的具体活,一个 agent 可以接很多不同的 task。类比:goal 是"我的职业理想",task 是"今天这张派工单"。明天 D04 专门讲 Task。

🧠 今天你应该能回答

  • Agent 类在哪个文件?父类是谁?(agent/core.py;父类 BaseAgent
  • 为什么分两层继承?(BaseAgent 定契约、支持适配外部框架;Agent 加官方高级能力)
  • 人设三件套是哪三个?为什么必填?(role/goal/backstory;结构化角色设定,拼进 prompt)
  • id 为什么 frozen?(身份证不可变,委派/记忆/事件靠它认人)
  • llm="gpt-4o" 为什么能只写字符串?(字段挂了 BeforeValidator,校验时解析成 LLM 对象)
  • agent_executor 是干嘛的?为什么和 Agent 分开?(真正的执行引擎;分离配置与运行,轻量可序列化+可换引擎)

✋ 10 分钟动手

# 1. 看两层类定义与三件套
sed -n '200,296p' lib/crewai/src/crewai/agents/agent_builder/base_agent.py
sed -n '170,335p' lib/crewai/src/crewai/agent/core.py

# 2. Python 里打印一个 agent 有哪些字段
uv run python -c "
from crewai import Agent
a = Agent(role='R', goal='G', backstory='B', llm='gpt-4o')
print(sorted(a.model_fields.keys()))   # 一览所有字段名
print(type(a.llm))                     # 看 llm 被解析成了什么
"
明日预告 · Day 04:员工有了,该发派工单了。明天打开 task.py,看 Task 模型的全字段——description / expected_output / agent / context / async_execution / output_pydantic……以及一个任务是怎么通过 execute_sync → _execute_core 交给 agent 去干、再包成 TaskOutput 的。
← Day 02 装环境跑通 Day 04 · Task 是什么 →