如何写出合法的 CrewAI crewai.flow/v1 声明式 Flow:AGENTS.md 编写规范全解析
【免费下载链接】crewAIFramework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks.项目地址: https://gitcode.com/GitHub_Trending/cr/crewAI
本篇以 CrewAI CLI 声明式 Flow 项目模板中内置的 AGENTS.md 为核心,逐条解析编写合法crewai.flow/v1YAML/JSON 声明的完整规则:从状态(state)建模、方法(method)编排、三类动作(expression / agent / crew)选型,到 CEL 表达式插值、路由(router/emit)机制与完整字段级 API 参考。读完本文,你既能约束 AI 代理替你自动生成 Flow 声明,也能人工写出结构正确、可被crewai run直接执行的声明式 Flow 定义。
一、这份 AGENTS.md 是什么、从哪里来
AGENTS.md 位于 CLI 的声明式 Flow 项目模板目录 templates/declarative_flow/ 下,它不面向人类用户阅读,而是一份写给 AI 代理的“作者指令”:当用户要求 AI 创建或编辑 CrewAI Flow 时,代理必须依据这份文档输出一份单一的、合法的crewai.flow/v1YAML 或 JSON 文档。
从源码看,执行crewai flow create --declarative(入口见 create_flow.py 中的_create_declarative_flow)时,CLI 会把该模板目录整体复制到新项目根目录。其中root_template_files显式包含.gitignore、AGENTS.md、README.md、pyproject.toml,也就是说AGENTS.md 会随每个声明式 Flow 项目一起落地到项目根目录,成为该项目后续 AI 协作编辑的常备规范。模板同时生成一个最小可运行的 flow.yaml:
schema: crewai.flow/v1 name: {{flow_name}} description: A declarative CrewAI Flow. state: type: dict default: topic: AI agents methods: start: start: true do: call: expression expr: state.topic这个起步文件展示了 Flow 的最小骨架:schema标识、name、state、以及带start: true的单个方法。而 模板 README 给出的项目操作方式是:
crewai install # 安装依赖 crewai run # 运行声明式 Flow并可按需在src/<folder>/crews/(可复用 Crew)、src/<folder>/tools/(自定义 Python 工具)、src/<folder>/knowledge/(共享知识文件)中扩展。AGENTS.md 的价值正在于此:它把“如何写一个能通过校验、正确接线、正确传数的 flow.yaml”沉淀为可复制的规则,覆盖从骨架到路由分支的全部细节。
二、输出契约:只返回一份合法的声明文档
文档开篇即规定了两条硬约束:
- 只输出一份合法的
crewai.flow/v1Flow 声明,不要附带解释性文字(除非用户明确要求); - 先照示例掌握形状与格式,再用文末 API 参考核对精确字段——示例管“形状”,参考管“字段名、必填项、链接类型与允许的 action/state 形状”。
同时文档声明了自己的定位:“把它当作对你的指令,而不是展示给用户的文字”。这正是 AGENTS.md 类文件的典型用法:它是给 LLM 的系统级写作约束。
三、按固定顺序构建 Flow(Build It In This Order)
AGENTS.md 给出的 7 步构建顺序是整份规范的主干,也是人工编写 Flow 时可直接套用的检查清单:
- 先定义
state。使用type: json_schema,并把 JSON Schema 内联写入; - 必填输入字段放在
state.json_schema.required中。不要指望用state.default让字段变必填——默认值与必填性是两回事; - 恰好一个方法带
start: true(CLI 模板变体中是单入口规则); - 后续方法通过
listen接入上游; - 每个方法有且仅有一个
do动作对象,do绝不能是列表; - 用
${...}映射从state和已完成的outputs传递数据; - 产出前检查所有
listen、emit、outputs.some_method引用是否有效。
另有两条全局约定:
- 可选字段只在确有必要时设置,否则信任 CrewAI 默认值并省略;
- 方法名必须匹配正则
^[A-Za-z_][A-Za-z0-9_]*$,即合法的 Python 标识符形式。
四、每个方法只选一种动作,且选最简单的
文档要求“选择能完成任务的最简单动作”,并给出三类动作的选型边界:
| 动作 | 适用场景 | 关键写法约束 |
|---|---|---|
call: expression | 简单读取、过滤、计算值、确定性路由 | 在expr中写原始 CEL,不要用${...}包裹 |
call: agent | 单个 AI 工作者:分类、决策、总结、写作、起草 | role、goal、backstory、input都放在with下;agent 动作没有动作级inputs映射 |
call: crew | 多 Agent / 多任务协同 | Crew 定义放在with下;运行期值用动作级inputs映射传入 |
这三类动作与仓库中的示例文件 flow_definition_example.yaml 完全对应:其中research_brief方法使用call: crew,route_followup与write_followup方法使用call: agent,正是“Crew 做协同、Agent 做单点判断/写作”的组合示范。
五、显式接线:start、listen、router 与 emit
这一节规定了 Flow 事件模型的核心规则,是声明式 Flow 与 Python 装饰器写法差异最大、也最容易写错的部分:
state是初始共享数据形状。动作结果不会自动合并回state——这是与直觉最容易冲突的一点,跨方法传值必须显式走outputs;- 方法结果通过
outputs.method_name读取,且必须在该方法可以运行之后; listen的目标是方法名,或 router 发出的事件名;- 方法绝不能监听自己的方法名——包括
listen的值恰好是与其方法名相同的路由标签(例如方法create_video上写listen: create_video); - 方法名与发出事件名共享同一个命名空间,不要用同一个字符串既当方法名又当
listen目标; - 当一个方法要在多个命名分支间选择时,用
router: true+emit; - router 动作必须恰好返回一个发出事件名字符串,不能返回 JSON、列表或解释文字;
start: true标记唯一入口。
对于“用 Agent 当路由”的场景,文档给出了可直接复用的 goal 写法:
Return exactly one bare value: approved, rejected, or needs_review. Do not include explanation.
并且明确:路由如果能用计算得出,优先用call: expression而不是 Agent——确定性路由不该消耗 LLM 调用。
六、CEL 与动态值:${...} 插值的精确语义
CEL(Common Expression Language)是 Flow 中“读取数据、做小决定”的表达式语言;更大的工作或有副作用的操作交给 agent 和 crew。文档对表达式形式的规定可以归纳为一张规则表:
| 场景 | 正确写法 | 说明 |
|---|---|---|
| 原始 CEL | 写在expr里 | 不要用${...}包裹原始 CEL |
| 映射字符串中读 Flow 数据 | Ticket: ${state.ticket_id} | 字面量文本留在${...}外,直接插值 |
| 输入数据 | state | 如state.ticket.subject |
| 已完成方法结果 | outputs.step_name | 如outputs.classify_ticket |
值本身就是单个${...} | domains: "${state.domains}" | 结果保留原始类型(数字、布尔、对象、列表) |
| 字符串里还有其他文本 | 最终值为文本 | 非文本值会被序列化为 JSON,null变成空文本 |
两个标准示例:
# 混排文本与 Flow 数据 query: "News about ${state.topic}"# 保留列表 / 数字类型 domains: "${state.domains}" limit: "${state.limit}"Crew 侧还有一套独立的{name}占位符插值(不是 CEL):
- Crew 文本用
{name}占位符引用 crew inputs,例如Research {topic}; - crew inputs 只有在 agent 或 task 文本中引用了对应
{name}占位符时,才真正成为提示词的一部分——传了一个没有任何占位符引用的输入,等于没有“grounding”(事实锚定);若 Crew 确实需要某字段,就把占位符写进 agent 的goal、task 的description或expected_output。
关于取值的具体规则还包括:
- Agent 需要多个字段时,写一个带标签和分隔符的文本值,例如
Ticket ID: ${state.ticket_id}; Message: ${state.message}; - Crew 动作级
inputs才是真正的 Crew kickoff 输入,运行期数据用${...}从state/outputs取;仅靠inputs不构成 grounding,必须配合占位符; - Crew 输出是对象:取文本用
${outputs.research_brief.raw};结构化输出用${outputs.research_brief.json_dict.field}或${outputs.research_brief.pydantic.field}; - 不要把整个 Crew 输出塞给 Agent 输入(如
${outputs.research_brief}是错误的); - Agent 输出也可能是对象:
${outputs.classify_ticket.raw}或${outputs.classify_ticket.pydantic.category}; with.inputs(Crew 定义内)只用于静态默认值;agent 动作的with.input是该 agent 的单一输入值。
七、“Do Not”清单:十一条高频错误红线
AGENTS.md 用一整节列出禁止项,这些基本都对应真实校验失败或行为异常,逐条核对价值很高:
- 不要发明 Flow 声明形状之外的顶层键;
- 不要使用声明 schema 之外的字段;
- 不要在一个方法的
do下放多个动作; - 不要让
do变成列表; - 不要用 CEL 的
+在动作映射里拼接文本——保持文本字面量,动态值各自用${...}插入; - 不要在
some_method可以运行之前引用outputs.some_method; - 不要把方法的
listen设为自己的方法名(含相同的路由标签,如方法create_video上listen: create_video); - 不要用同一个字符串同时作为方法的
listen目标与方法名; - 不要在没有
router: true的情况下使用emit; - 不要指望 crew 动作级
inputs单独完成 grounding——没有匹配占位符的输入对提示词而言基本无效; - 不要在精确性重要时让 Agent“自行推断缺失事实”,应要求它把缺失的日期、金额、报价、日志或约束标记为 unknown;
- 不要在调用方不会消费流式结果时设置
config.stream: true——常规生成的 Flow 与 CLI 冒烟测试都应省略它。
八、完整示例:Crew 研究 + 路由式跟进
文档给出的主示例是一个“Crew 研究评审 + 路由式跟进”的 Flow,展示了 state 建模、crew 动作、router 方法与分支 agent 的完整组合(该示例与仓库中 flow_definition_example.yaml 一致):
schema: crewai.flow/v1 name: ResearchReviewFlow state: type: json_schema json_schema: type: object properties: topic: type: string audience: type: string required: - topic - audience default: topic: AI agent orchestration audience: platform engineering leaders methods: research_brief: start: true do: call: crew with: agents: researcher: role: Research analyst goal: Research {topic} for {audience} backstory: Expert at concise technical research. reviewer: role: Strategy reviewer goal: Decide whether the research needs an executive follow-up backstory: Experienced at reviewing technical briefs for leaders. tasks: - name: research_task description: Research {topic} for {audience}. expected_output: Key findings and tradeoffs. agent: researcher - name: review_task description: Review the research and decide if an executive follow-up is needed. expected_output: 'A brief review ending with `needs_followup: true` or `needs_followup: false`.' agent: reviewer inputs: topic: Default topic audience: Default audience inputs: topic: "${state.topic}" audience: "${state.audience}" route_followup: listen: research_brief router: true emit: - followup - done do: call: agent with: role: Follow-up router goal: 'Return exactly one bare value: followup or done. Do not include explanation.' backstory: Skilled at routing reviewed research briefs. input: "Reviewed research: ${outputs.research_brief.raw}" write_followup: listen: followup do: call: agent with: role: Executive communications specialist goal: Draft a concise executive follow-up from the reviewed research backstory: Writes crisp follow-ups for technical leaders. input: "${outputs.research_brief.raw}"这个示例几乎把前述全部规则串了一遍,值得逐行对照:
state用json_schema内联声明,required明确列出topic/audience,default仅提供缺省值(二者不互相替代);research_brief是start: true的唯一入口,call: crew;Crew 定义内inputs是静态默认,而动作级inputs用"${state.topic}"把运行期值真正注入 kickoff——同时Research {topic} for {audience}中的{name}占位符完成了 grounding;route_followup是router: true方法,emit声明了followup/done两个合法分支,其 agent 的 goal 严格限定“只返回一个裸值”;它读取的是${outputs.research_brief.raw}(对象输出的文本视图);write_followup监听的是事件名followup而非方法名——这正是“事件与方法共享命名空间、分支方法挂事件名”的标准接法;done分支没有后续方法,流程自然终止。
九、字段级 API 参考
这一节按文档附录顺序整理全部字段,编写声明时可直接对照。
9.1 Flow 顶层定义
| 字段 | 必填 | 说明 |
|---|---|---|
schema | 可选(默认crewai.flow/v1) | 必须为crewai.flow/v1;手写声明应显式包含 |
name | 必填 | 唯一 Flow 名称,用于日志、事件与追踪 |
description | 可选(默认null) | 人类可读的摘要 |
state | 必填 | 初始状态与执行期更新的状态契约 |
config | 可选 | 可序列化的 Flow 级执行配置 |
methods | 必填 | 方法名 → 方法定义的映射 |
9.2 JSON Schema State(state[type=json_schema])
| 字段 | 必填 | 说明 |
|---|---|---|
type | 可选(默认json_schema) | 固定为json_schema,表示内联 JSON Schema 作为状态契约 |
json_schema | 必填 | 用于校验和文档化状态的 JSON Schema;必填字段用其中的required数组声明 |
default | 可选(默认null) | 初始化 Flow 状态的默认值;默认值不等于 schema 必填 |
9.3 Method(methods.<name>)
| 字段 | 必填 | 说明 |
|---|---|---|
description | 可选(默认null) | 方法的人类可读摘要 |
do | 必填 | 方法执行时运行的单个动作对象 |
start | 可选(默认null) | 标记入口,使用true |
listen | 可选(默认null) | 在某个上游方法或 router 事件之后运行 |
router | 可选(默认false) | 方法输出是否作为下一个事件名;router 必须返回单个事件名字符串 |
emit | 可选(默认null) | 该方法可能发出的事件声明列表;事件名应唯一且不与方法名冲突 |
9.4 Action:按call判别的联合类型
允许的三种形状:call: crew、call: agent、call: expression。
Crew Action(methods.<name>.do[call=crew])
| 字段 | 必填 | 说明 |
|---|---|---|
call | 必填 | 判别字段,固定crew |
with | 必填 | 内联 Crew 定义 |
inputs | 可选(默认null) | 传给 Crew 的运行期输入;用${...}插入 Flow 值,并在 agent/task 文本中以{name}引用,如{"topic": "${state.topic}"} |
Crew Definition(...do[call=crew].with)
| 字段 | 必填 | 说明 |
|---|---|---|
agents | 必填 | 按名称索引的内联 agent 映射 |
tasks | 必填 | 有序任务列表 |
inputs | 可选 | 静态默认输入,作为{name}占位符参与插值;运行期值优先用动作级inputs |
Crew Agent Definition(...with.agents.<name>)
| 字段 | 必填 | 说明 |
|---|---|---|
role/goal/backstory | 必填 | 三者的插值都使用{name}占位符(不是 CEL) |
settings | 可选 | 透传给 loader 的附加设置,如{"llm": "openai/gpt-4o-mini"} |
llm | 可选(默认null) | 模型字符串或内联 LLM 配置对象(如{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}) |
planning_config | 可选(默认null) | 计划配置,max_attempts限制任务执行前计划修正次数 |
allow_delegation | 可选(默认null) | 允许 agent 之间委派与提问 |
max_iter | 可选(默认null) | 单个 agent 执行任务的最大迭代次数 |
max_rpm | 可选(默认null) | agent 执行时每分钟最大请求数 |
max_execution_time | 可选(默认null) | agent 执行任务的超时秒数 |
tools | 可选(默认null) | 工具引用或序列化定义;字符串引用可用 CrewAI 工具名、custom:<name>或module:Class全限定引用 |
apps | 可选(默认null) | 平台应用,如gmail或slack/send_message |
mcps | 可选(默认null) | MCP 服务器引用或配置;支持 HTTPS URL、集成 slug、#tool_name后缀,或内联对象配置 |
LLM Definition
| 字段 | 必填 | 说明 |
|---|---|---|
model | 必填 | 模型标识,如openai/gpt-4o-mini |
max_tokens | 可选(默认null) | 输出 token 上限;为 null 时由 provider 默认值生效 |
Crew Task Definition(...with.tasks[])
| 字段 | 必填 | 说明 |
|---|---|---|
description | 必填 | 任务指令,支持{name}插值 |
expected_output | 必填 | 期望输出描述,支持{name}插值 |
name | 可选(默认null) | 任务名 |
agent | 可选(默认null) | 承接该任务的 crew agent 名称 |
Agent Action(methods.<name>.do[call=agent])
| 字段 | 必填 | 说明 |
|---|---|---|
call | 必填 | 固定agent |
with | 必填 | 单个 Agent 定义;输入放在with.input,agent 动作不支持动作级inputs |
Agent Definition(...do[call=agent].with):字段集合与 Crew Agent Definition 相同(role/goal/backstory必填,settings、llm、planning_config、allow_delegation、max_iter、max_rpm、max_execution_time、tools、apps、mcps可选),另加一个必填字段:
| 字段 | 必填 | 说明 |
|---|---|---|
input | 必填 | Agent 提示词模板,用${...}插入 Flow 值,如Ticket: ${state.ticket_id} |
Expression Action(methods.<name>.do[call=expression])
| 字段 | 必填 | 说明 |
|---|---|---|
call | 必填 | 固定expression |
expr | 必填 | 针对 state、outputs 与局部上下文求值的 CEL 表达式 |
9.5 Config(config)
| 字段 | 默认 | 说明 |
|---|---|---|
tracing | null | 覆盖 Flow 追踪;省略时使用执行默认值 |
stream | false | 是否发出流式事件(仅在调用方会消费流时设置) |
memory | null | 传给 Flow 执行的可序列化记忆配置 |
input_provider | null | 用于提供初始 state 的 provider 键 |
suppress_flow_events | false | 为本定义禁用 Flow 事件发出 |
max_method_calls | 100 | 单次 kickoff 允许的最大方法执行次数 |
defer_trace_finalization | false | 延迟 trace 终结,允许调用方稍后完成追踪 |
checkpoint | null | 检查点配置;true表示使用默认检查点 |
9.6 跨字段规则(Cross-Field Rules)
- 每个方法恰好一个
do动作对象、一个call判别值; listen目标位于“方法名 + router 事件名”的同一命名空间中;- 方法不能监听自己的方法名;
- router 方法的结果必须命中一个已声明的
emit值; - Crew 动作级
inputs即 Crew kickoff 输入,运行期值在那里使用 CEL 包裹字符串; - Crew agent/task 插值使用来自求值后 crew inputs 的
{name}占位符; - Agent 的
with.input必须是文本——用${outputs.method_name.raw}或${outputs.method_name.json_dict.summary}这类文本字段。
十、源码纵深:CLI 模板是参数化 Skill 模板的“精简变体”
把 AGENTS.md 放回仓库语境中看,会发现它并不是孤立文本。在 lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 中存在一份 Jinja2 模板,内容与 AGENTS.md 同源,但通过特性开关参数化——例如include_non_linear_flows(允许多入口与非线性listen,listen支持and/or组合)、include_tool_action(call: tool打包确定性工作)、include_script_action(call: script内联可信 Python,明确标注“脚本不做沙箱”)、include_each_action(call: each逐项重复子管道)、include_hitl(human_feedback人工检查点)等。从源码结构看,CLI 项目模板中的 AGENTS.md 正是该参数化模板在“仅 expression / agent / crew 三种动作、单入口”配置下实例化出的保守版本——这也是为什么 CLI 模板写死“恰好一个start: true方法”,而参数化版本在非线性 Flow 下会放宽为“至少一个”。
声明的加载与执行侧在 run_declarative_flow.py 中:load_declarative_flow负责把 flow.yaml/JSON 定义加载为 Flow,crewai run会优先检测当前是否为声明式 Flow 项目环境(is_declarative_flow_project_env)并走声明式执行路径,crewai flow plot则可对定义做可视化。声明解析与校验的库侧实现见 flow_definition.py,相关行为有 test_flow_definition.py、test_flow_from_definition.py 等测试覆盖。
十一、实践要点小结
把 AGENTS.md 的规则浓缩成可执行的写作纪律:
- state 先行:
json_schema内联、required显式、default只补缺省; - 单动作原则:每个方法一个
do,按“能算则 expression、单点 AI 用 agent、协同用 crew”的梯度选型; - 显式接线:
outputs.method_name读结果、listen挂上游、router 方法router: true+emit声明分支、分支方法监听事件名; - 两套插值体系不混用:Flow 层用
${...}(CEL),Crew 层用{name}占位符,且后者必须被 agent/task 文本实际引用才生效; - 产出前自检:逐个核对
listen、emit、outputs.*引用与方法名/事件名命名空间冲突; - 省略可选配置:
stream、memory等字段除非确有需要,否则交给默认值。
遵循这套规范写出的crewai.flow/v1声明,既能通过 CLI 项目的声明式校验并被crewai run直接执行,也能作为 AI 协作编辑 Flow 的稳定契约——这正是 AGENTS.md 在 CrewAI 声明式 Flow 工作流中的定位。
【免费下载链接】crewAIFramework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks.项目地址: https://gitcode.com/GitHub_Trending/cr/crewAI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考