Agent 数据模型:一份"角色人设 + 运行策略"的全字段清单
阶段1 你已经能跑通一个 Crew,也知道 Agent 有 role/goal/backstory。今天进入阶段2,把 Agent 这个类逐字段拆开:它到底存了哪些东西?哪些是"人设"、哪些是"运行刹车"、哪些是"接大模型"、哪些是"高级开关"?源码在 agent/core.py,它的父类 BaseAgent 在 agents/agent_builder/base_agent.py。看懂这份清单,后面 6 天的执行循环、工具、解析才有地基。
Agent 想成公司里的一名员工档案。档案里有三类信息:① 身份栏(他是谁、干什么、什么背景 = role/goal/backstory);② 工作纪律栏(最多加班几轮、每分钟能发几次请求、能不能把活转给同事 = max_iter/max_rpm/allow_delegation);③ 装备与技能栏(配哪个大脑、会不会用工具缓存、要不要先做计划 = llm/cache/planning)。Agent 类本身只是"档案"——它不干活;真正干活的是 Day 08 要讲的"执行器"。今天就是逐栏读这份档案。痛点:一个 Agent 为什么要几十个字段?
Agent(role=..., goal=..., backstory=...) 三行就能跑,于是以为 Agent 就三个字段。可翻开源码,Agent 加上父类 BaseAgent 有几十个字段:max_iter、cache、respect_context_window、guardrail、executor_class……它们分别管什么?哪些平时用不到?不搞清楚这张"字段地图",后面读执行循环时会一头雾水——因为循环里每一步几乎都在读某个字段。Agent 是一个 Pydantic 数据模型(配置对象),不是一段执行逻辑。它把"这个 Agent 该怎么表现、怎么受约束、接什么大脑、有什么高级能力"全部声明成带默认值、带校验的字段。运行时,执行器逐个读这些字段来决定行为。理解 Agent = 理解"哪些旋钮可以拧、默认拧到几档"。先看类的定义头部:Agent 继承自 BaseAgent(agent/core.py:170),而 BaseAgent 是一个 Pydantic BaseModel(base_agent.py:200):
# 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.
"""
model_config = ConfigDict()
# base_agent.py:200
class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
...
BaseAgent;标准 Agent 特有的高级字段(如 planning、guardrail、模板)放在子类 Agent。读源码要两个文件一起看。今天按"用途"分组讲,而不是按文件顺序念。① 身份三件套 + id(Agent 的"人设")
最核心的四个字段在父类 BaseAgent(base_agent.py:262):
# base_agent.py:262
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
role: str = Field(description="Role of the agent")
goal: str = Field(description="Objective of the agent")
backstory: str = Field(description="Backstory of the agent")
config: dict[str, Any] | None = Field(
description="Configuration for the agent", default=None, exclude=True
)
id (frozen=True)每个 Agent 自动生成一个 UUID,且 frozen 不可改。它是 Agent 的唯一身份证——事件系统、委派、缓存都靠它区分"哪个 Agent"。role / goal / backstory三个必填字符串(没有默认值)。它们会被拼进给 LLM 的系统提示词里,构成 Agent 的"人格"。role=职位、goal=目标、backstory=背景故事。config (exclude=True)一个可选的字典配置入口(常来自 YAML)。exclude=True 表示序列化成 JSON 时不导出它——因为它只是"原料",会被展开成上面那些字段。Agent(role="资深天气分析师", goal="用最新数据回答天气问题", backstory="你在气象台工作了 10 年,习惯先查实测数据再下结论")。这三段话会被组装进系统提示,让同一个 GPT-4 表现得像"气象台老兵"而不是通用助手。② 运行控制字段(Agent 的"工作纪律")
这一组决定 Agent 干活时受哪些约束,全在 BaseAgent(base_agent.py:269):
# base_agent.py:269
cache: bool = Field(
default=True, description="Whether the agent should use a cache for tool usage.")
verbose: bool = Field(
default=False, description="Verbose mode for the Agent Execution")
max_rpm: int | None = Field(
default=None,
description="Maximum number of requests per minute ... to be respected.")
allow_delegation: bool = Field(
default=False,
description="Enable agent to delegate and ask questions among each other.")
tools: list[BaseTool] | None = Field(
default_factory=list, description="Tools at agents' disposal")
max_iter: int = Field(
default=25, description="Maximum iterations for an agent to execute a task")
cache=True默认开启工具结果缓存:同一个工具、同样的输入,第二次直接取上次结果,省钱省时间(D11 详解)。verbose=False默认安静。设 True 会打印 Agent 每一步的思考/工具调用,调试时非常有用。max_rpm=None每分钟最多几次 LLM 请求。None = 不限。设了值,执行器会 enforce_rpm_limit 卡住等待,防止触发 API 限流。allow_delegation=False★默认不能把活转给同事。开了之后,框架会自动给它加两个"委派工具"(把任务交给别的 Agent / 向别的 Agent 提问)。tools=[]Agent 手里的工具列表,默认空。default_factory=list 是为了避免"可变默认值"陷阱(见本讲边界)。max_iter=25★ReAct 循环最多转 25 圈的硬刹车。到了还没给答案,就强制收尾(D08 会看到 has_reached_max_iterations)。防止 Agent 无限打转烧钱。default=[],这个空列表会被所有 Agent 实例共享——你给 A 加个工具,B 也莫名多了个工具。default_factory=list 表示"每次新建实例时现造一个新的空列表",各实例互不干扰。Pydantic 里所有可变默认值(list/dict)都必须这么写。这是框架代码的必备常识。max_iter=25 而不是无限:先给个保守的刹车。③ LLM 与模板字段(接哪个"大脑"、怎么说话)
这一组在子类 Agent(agent/core.py:210):
# agent/core.py:210
use_system_prompt: bool | None = Field(
default=True, description="Use system prompt for the agent.")
llm: Annotated[
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(description="Language model that will run the agent.", default=None)
function_calling_llm: Annotated[
str | BaseLLM | None, ...
] = Field(
...,
deprecated="function_calling_llm is deprecated and will be removed in a future release.")
# :228 三个模板字段
system_template: str | None = Field(default=None, description="System format for the agent.")
prompt_template: str | None = Field(default=None, description="Prompt format for the agent.")
response_template: str | None = Field(default=None, description="Response format for the agent.")
llm (Annotated + 校验器)★接哪个大模型。可以直接传 "gpt-4o" 字符串,也可传一个 BaseLLM 对象。BeforeValidator(_validate_llm_ref) 会在赋值前把字符串翻译成真正的 LLM 对象;PlainSerializer 负责序列化成 JSON 时怎么导出。use_system_prompt=True默认用"系统提示词"这个角色来放人设。少数老模型不支持 system role,可关掉改用普通消息。function_calling_llm (deprecated)曾用来指定"专门负责工具调用的模型"。现在标了 deprecated——保留字段但会警告,未来删除。system/prompt/response_template三个自定义模板槽。默认 None(用框架内置模板)。高级用户想完全掌控 prompt 长相时才填。Annotated[类型, 校验器, 序列化器] 让一个字段又能被赋值时自动转换、又能被导出时自定义格式。这里的效果就是:你写 llm="gpt-4o"(方便),框架内部帮你变成完整 LLM 对象(能用);存档时又能变回一个干净的 dict(可序列化)。"对用户友好的输入 + 对内部友好的对象 + 对存储友好的格式"三者靠一个 Annotated 打通。function_calling_llm、multimodal、reasoning 都标了 deprecated 却仍留在类里。直接删当然更干净,但会让老代码一升级就崩。源码选择"保留字段 + 加弃用标记 + 文档说明用什么替代",给用户一个平滑的迁移窗口。库的向后兼容性 > 代码整洁度——这是被广泛使用的框架必须付的税。代价是类里堆着一些"僵尸字段",读源码时要能一眼认出它们(看 deprecated=)。④ 高级能力字段(可选的"增强开关")
这一组是标准 Agent 的进阶能力(agent/core.py:242):
# agent/core.py:242
respect_context_window: bool = Field(
default=True,
description="Keep messages under the context window size by summarizing content.")
max_retry_limit: int = Field(
default=2, description="Maximum number of retries ... when an error occurs.")
inject_date: bool = Field(
default=False, description="Whether to automatically inject the current date into tasks.")
# :268 计划相关
planning: bool = Field(
default=False,
description="Whether the agent should reflect and create a plan before executing a task.")
# :306 输出护栏
guardrail: Annotated[GuardrailType | None, ...] = Field(
default=None,
description="Function or string description of a guardrail to validate agent output")
guardrail_max_retries: int = Field(
default=3, description="Maximum number of retries when guardrail fails")
respect_context_window=True★默认开:当对话历史快撑爆模型上下文窗口时,自动摘要压缩历史而不是直接报错(D08 的 handle_context_length)。max_retry_limit=2执行任务出错时最多重试 2 次。区别于 max_iter(那是"想几轮"),这是"崩了重来几次"。inject_date=False要不要自动把"今天日期"注入任务。做时效性任务(今天的新闻/天气)时打开很有用。planning=False默认关。打开后 Agent 会先想一个计划再动手(阶段4 D23 讲规划)。guardrail (护栏)给 Agent 输出加一道验收关:可以是一个函数或一句自然语言描述(如"必须是合法 JSON")。不通过就打回重做,最多重试 guardrail_max_retries(默认 3)次。planning;要求输出格式严格→配 guardrail;对话特别长→靠 respect_context_window 兜底。知道有这些旋钮,遇到对应问题时才知道去哪拧。reasoning、max_reasoning_attempts、multimodal、allow_code_execution、code_execution_mode 都带 deprecated(agent/core.py:237,250,276)。它们是历史遗留,新代码别用——分别被 planning_config、原生文件传参等替代。⑤ 执行器与私有属性(档案怎么变成"会干活的人")
Agent 是配置,那"干活的"是谁?看这两个字段(agent/core.py:333):
# agent/core.py:333
agent_executor: CrewAgentExecutor | AgentExecutor | None = Field(
default=None, description="An instance of the CrewAgentExecutor class.")
executor_class: Annotated[
type[CrewAgentExecutor] | type[AgentExecutor],
BeforeValidator(_validate_executor_class),
PlainSerializer(_serialize_executor_class, return_type=str, when_used="json"),
] = Field(
default=AgentExecutor,
description="Class to use for the agent executor. Defaults to AgentExecutor ...")
而工具缓存/结果的容器在父类(base_agent.py:317):
# base_agent.py:317
cache_handler: CacheHandler | None = Field(default=None, ...)
tools_handler: ToolsHandler = Field(default_factory=ToolsHandler, ...)
tools_results: list[dict[str, Any]] = Field(default_factory=list, ...)
# core.py:199 私有属性(下划线开头,不对外、不序列化)
_times_executed: int = PrivateAttr(default=0)
_last_messages: list[LLMMessage] = PrivateAttr(default_factory=list)
executor_class★"用哪个类来执行"。默认 AgentExecutor,可换成 CrewAgentExecutor(D08 主角)。这就是把"配置"和"执行逻辑"解耦的关键——Agent 只声明"我要用哪种执行器",不自己写循环。agent_executor真正的执行器实例。初始为 None,用到时才按 executor_class 造出来(懒加载)。tools_handler工具调用的记录/缓存中枢(D11 主角),每个 Agent 默认自带一个。_times_executed (PrivateAttr)下划线开头 = 私有状态,不对外暴露、不进 JSON。记录这个 Agent 执行了几次,用于内部计数/重试判断。Agent 的方法里。但源码刻意分离:Agent 只当"数据模型 + 声明用哪个执行器",真正的循环放在 CrewAgentExecutor。好处:① Agent 可以被安全地序列化/存档/传输(纯数据,没有运行时状态耦合);② 想换一套执行策略(如 LiteAgent、自定义执行器)只需换 executor_class,不动 Agent;③ 单元测试时能脱离执行器单独构造 Agent。"数据与行为分离"让配置可复用、执行可替换。代价是多一层间接,读代码要在两个类之间跳。边界抉择 + 今日小结
cache=True 意味着"同工具 + 同输入 → 直接返回上次结果"。对纯函数(如加法、格式转换)这很棒。但对有时效性的工具(查实时股价、查当前时间、查最新新闻)就是坑——你第二次查"今天股价",可能拿到 5 分钟前缓存的旧值,还以为是实时的。解决办法:给这类工具设 cache_function 决定"什么情况才缓存"(D11 详解),或在 Agent 上把 cache=False。缓存的第一原则永远是"确认这份数据可以被缓存"。👶 小白:我只写了 role/goal/backstory,其它几十个字段是不是就没值?会不会报错?
👨🏫 老师:不会报错,它们全都有默认值(max_iter=25、cache=True、allow_delegation=False…)。只有 role/goal/backstory 是必填(没默认值)。这就是"三行就能跑"的原因——其余字段是"合理默认 + 需要时再拧"。读源码时看 Field(default=...) 就知道不填时是什么行为。
👶 小白:注意到文件叫 agent/core.py,可大纲写的是 agent/agent.py?
👨🏫 老师:好眼力。这个版本的源码里 Agent 类实际在 agent/core.py:170(agent/ 是个包,__init__.py 会把它 re-export 成 crewai.Agent)。读源码时以 grep 到的真实文件为准,别信文档里的旧路径——这也是"源码走读"的基本功。
🧠 今天你应该能回答
- Agent 本质是什么?(Pydantic 配置对象,不含执行逻辑)
- 字段分哪五组?各组代表作是什么?
- 哪三个字段是必填的?(role/goal/backstory)
max_iter和max_retry_limit有何区别?(想几轮 vs 崩了重来几次)allow_delegation和cache的默认值?为什么这么定?executor_class存在的意义?(配置与执行解耦、可替换)- 怎么一眼看出一个字段是弃用的?(
deprecated=)
✋ 10 分钟动手
P=lib/crewai/src/crewai
# 1. 读子类 Agent 的字段(高级能力 + 执行器)
sed -n '197,343p' $P/agent/core.py
# 2. 读父类 BaseAgent 的通用字段(身份 + 运行控制)
sed -n '262,327p' $P/agents/agent_builder/base_agent.py
# 3. 亲手打印所有字段的默认值
python -c "
from crewai import Agent
a = Agent(role='测试员', goal='验证字段', backstory='我在读源码')
for name in ['max_iter','cache','allow_delegation','verbose','max_rpm','respect_context_window']:
print(name, '=', getattr(a, name))
"
executor_class 指向的执行器。明天就钻进 agents/crew_agent_executor.py 的 CrewAgentExecutor,逐行读它的 ReAct 执行循环:怎么问 LLM、怎么分流"原生函数调用 vs 文本解析"、怎么把工具结果喂回去、怎么用今天讲的 max_iter 刹车。