Day 03 / 共 20 天 · 第 1 周 核心概念
Graph:Agent 就是 Block 图
第二个核心概念。一个 AI 智能体 = 一张由 Block 连成的图。今天读 data/graph.py:Node(节点)+ Link(连线)+ Graph(容器)怎么表达一个智能体。
📍 你在整门课的位置 · 第 1 周 核心概念
D2 Block 概念→
D3 Graph 图→
D4 运行平台→
D5 执行旅程
💡 今天的类比世界观:Graph = 一张"地铁线路图"
一个 Agent = 三样东西,用地铁来记:Node = 站点(图里摆的一台具体设备)、Link = 连接站点的轨道(数据顺着轨道从上一站流到下一站)、Graph = 整张线路网。starting node = 始发站(列车从哪发车);图校验 = 通车前的安全检查(有没有断头轨、孤岛站、接错口)。今天都用"线路图"来想。
👶 小白:Node 和 Block 到底啥区别?听起来都是"一个方块"。
👨🏫 老师:Block 是"设备型号",Node 是"这张图里摆的那一台具体设备"。就像"格力空调 KFR-35"是型号(Block 类),你家客厅那台、卧室那台是两个实例(两个 Node)——同型号,但各有自己的位置、配置和连线。所以一个 Block 可以在同一张 Graph 里被摆成好几个 Node,各干各的。
L01
一个 Agent = 三样东西
🤔 痛点:有了一堆 Block,怎么"记住"它们该怎么连、按什么顺序跑?
Day 02 我们有了积木,但积木散落一地没用。谁的输出接谁的输入?先跑哪个后跑哪个?这套"连接关系"必须被存成一份能保存、能校验、能执行的数据,而不是只画在纸上。
💡 本质:Agent = 一张有向图(Graph)= 节点 + 连线
把每个 Block 摆成一个节点(Node),用连线(Link)把上游输出口接到下游输入口,整张图(Graph)就完整描述了"这个 Agent 由哪些能力、按什么数据流组成"。所以"搭 Agent"本质就是"建一张图",前端画布画的、后端存的、引擎跑的,都是这同一张图。
在数据层,一个 Agent 被表达成三样东西(data/graph.py):
- Node(节点):图里的一个格子,绑定一个 Block(Day 02)。
- Link(连线):一根"管子",把某节点的输出口连到另一节点的输入口。
- Graph(图):把 Node 和 Link 装起来的容器 = 一个智能体。
Node1读网页—Link→
Node2LLM总结—Link→
Node3发邮件
对应前端画布
前端画布上一个方块 = 一个 Node(绑定某 block_id);方块之间的连线 = 一条 Link;整个画布 = 一个 Graph。你在界面上拖 Block、连线,前端就在构造这个 Graph 数据结构,存到后端。运行时执行引擎(第3周)按图拓扑跑各 Node。可视化画布和后端数据模型是一一对应的——理解了 Node/Link/Graph,就理解了画布背后的一切。
L02
Link:连线
# graph.py:82
class Link(BaseDbModel):
source_id: str # 源节点 id
sink_id: str # 目标节点 id
source_name: str # 源节点的哪个输出口
sink_name: str # 目标节点的哪个输入口
is_static: bool = False
读法:一条 Link 精确描述"A 节点的
result 输出口 → B 节点的 topic 输入口"。source_name/sink_name 让同一节点能有多个输入/输出口,图能精确表达数据流向。一条 Link = 四个字段精确定位:把 source 节点的某个"输出口"接到 sink 节点的某个"输入口"。因此同一节点能有多个口,数据流向毫不含糊。
is_static(静态连线)是什么?
普通连线:值流过去,被下游消费一次就没了。静态连线:源是一个"常量型"输出(Day 02 的 static_output Block),值可以被反复复用——比如一个"配置值"块的输出连到多个下游、或在循环里每轮都能用。执行引擎(Day 13)对静态连线有特殊处理:会用"最近一次的值"回填等待中的节点。static 让"常量/配置"能在图里被多处、多次引用,不会用一次就消失。
L03
Node:节点
# graph.py:104
class Node(BaseDbModel):
block_id: str # 指向某个 Block 的实现(Day 02)
input_default: BlockInput # dict[输入口名, 用户手填的默认/常量值]
input_links: list[Link] # 进来的连线
output_links: list[Link] # 出去的连线
# node.block 属性(graph.py:122)用 get_block(block_id) 取回真正的 Block
读法:Node 本身不含逻辑——逻辑在它引用的 Block 里(
node.block)。Node 只存"我用哪个 Block(block_id)+ 用户在画布上手填了哪些输入(input_default)+ 我连了哪些线"。input_default 是什么?
有些输入口的值来自上游连线(动态),有些是用户在画布上直接手填的常量(比如"发邮件"块的"收件人"填死是你的邮箱)——后者就存在
input_default。所以一个节点的完整输入 = input_default(手填的)+ 入边传来的(上游给的)。如果一个必填口既没手填也没连线,校验会报错(L06)。若 Block 被删了,node.block 返回占位对象,运行时只 yield 一个 error,不会让整图崩——健壮性设计。L04
Graph 容器与自动 I/O schema
Graph(graph.py 分层:GraphBaseMeta → BaseGraph → Graph → GraphModel)持有 nodes + links + 元数据(version、name、instructions…)。最妙的是计算字段自动推导 Agent 的对外 I/O:
# graph.py:218 input_schema —— 扫描所有 BlockType.INPUT 节点
# graph.py:230 output_schema —— 扫描所有 BlockType.OUTPUT 节点
# 合成整个 Agent 对外的输入/输出 JSON schema
读法:Agent 有哪些输入参数,不用人工声明——框架扫描图里有哪些 INPUT 类型的 Block 节点自动推导出来。你在图里放了一个"输入:topic"的 INPUT 块,这个 Agent 就自动有了一个 topic 输入参数。
为什么"自动推导 I/O"很聪明?
前端表单、API 文档、调用参数都需要知道"这个 Agent 要什么输入、产出什么输出"。如果让你手动维护一份 schema,容易和图不一致。从图结构自动推导——图里放了哪些 INPUT/OUTPUT 块,Agent 就有哪些输入/输出,永远同步。还有
has_external_trigger、has_human_in_the_loop 等计算字段,都是"扫一遍节点判断图有没有某能力"。让数据结构自己描述自己——减少人工维护、杜绝不一致。L05
起始节点:从哪开始跑
# graph.py:401
@property
def starting_nodes(self):
outbound_nodes = {link.sink_id for link in self.links} # 所有"被连入"的节点
input_nodes = {node.id for node in self.nodes if node.block.block_type == BlockType.INPUT}
return [node for node in self.nodes
if node.id not in outbound_nodes or node.id in input_nodes]
读法:起始节点 = 没有任何入边的节点(没人给它喂数据,所以它是源头)+ 所有 INPUT 节点。执行引擎从这些点开始"灌数据",然后顺着连线往下流。
📝 举个例子:这张图的起点算出来是哪几个
图有 3 个节点、2 条线:
•
• N1 不在 outbound_nodes 里 → N1 是起始节点;
• 若把 N1 换成一个
结论:引擎从
读网页(N1) → LLM总结(N2) → 发邮件(N3)。•
outbound_nodes(被连入的)= {N2, N3}(N2 被 N1 连、N3 被 N2 连);• N1 不在 outbound_nodes 里 → N1 是起始节点;
• 若把 N1 换成一个
BlockType.INPUT 节点,即使它被连入也会因"是 INPUT"被强制算作起点。结论:引擎从
[N1] 开跑,跑完 N1 的输出灌给 N2,再灌给 N3。为什么这么定起点?
拓扑执行(Day 13)需要知道"从哪开始"。一个没有入边的节点,意味着它不依赖任何上游、可以立即执行(比如"定时触发"块、或用户手填了所有输入的块)。INPUT 节点则是"接收 Agent 外部输入"的入口。从源头灌入,数据像水一样顺着连线往下流,直到没有节点可跑——这就是图执行的直觉。Day 12/13 看执行引擎怎么实现。
L06
图校验
validate_graph(graph.py:686)分两类检查,对主图和每个子图都跑:
- 结构校验(
:995):每条 Link 两端节点必须存在、Block 必须存在、连接的输入/输出口名字必须在 Block schema 里。 - 节点校验(
:735):必填字段是否已填或已连线、字段间依赖、凭证字段等。
"保存时轻校验、运行时严校验"
有个参数
for_run:for_run=True(真要跑之前)做全量严格校验——把所有坑堵住;否则只校验 I/O 节点。为什么分两档?让你编辑中的半成品图也能存盘(不强求填完所有字段),但真要执行前把所有问题暴露。这又是"fail-fast + 兼顾编辑体验"的平衡——和 CrewAI 的声明式校验、eino 的编译期检查同一哲学:错误尽早暴露,但别妨碍开发中途保存。L07
版本、fork 与子图
- 版本:每个 (id, version) 是一行;保存时若版本已存在自动 +1(
graph.py:1662)。is_active标记当前活跃版本。改了图不覆盖旧版,而是存新版——能回溯、能回滚。 - fork(
fork_graph):复制整图,给所有 node/link 重新生成 UUID、清掉凭证引用、记forked_from_id。像 git fork——基于别人的 Agent 改出自己的。 - 子图(Agent-as-Block):一个 Agent 能作为一个 Block 嵌进另一个 Agent!
AgentExecutorBlock(blocks/agent.py,block_type=AGENT)——它的输入存"要跑哪个子图 + 喂什么",运行时发起一次完整的子图执行。
Agent-as-Block 是杀手锏
你可以把一个搭好的 Agent 当成一块积木,嵌到更大的 Agent 里。比如把"网页摘要"Agent 当一个块,用在"每日简报"Agent 里。这就是递归组合——复杂 Agent 由小 Agent 拼装,执行引擎对子图一视同仁(子图就是又一次完整的图执行)。和 eino 的 AddGraphNode(子图嵌套)、CrewAI 的 Flow 内嵌 Crew 同一思想——组合是复用的终极形态。
L08
今日小结 + 动手
🧠 今天你应该能回答
- Agent 由哪三样东西表达?和前端画布怎么对应?
- Link 的四个关键字段?is_static 是什么?
- Node 为什么不含逻辑?input_default 是什么?
- Agent 的 I/O schema 为什么能自动推导?起始节点怎么定?
- 版本/fork/子图各解决什么?Agent-as-Block 为什么强大?
✋ 动手
P=autogpt_platform/backend/backend
sed -n '82,133p' $P/data/graph.py # Link + Node
sed -n '218,240p' $P/data/graph.py # 自动 I/O schema
sed -n '401,411p' $P/data/graph.py # 起始节点
sed -n '24,50p' $P/blocks/agent.py # Agent-as-Block
明天预告 · Day 04:概念够了,跑起来!运行平台——docker compose 启动后端 + 前端 + Postgres + Redis + RabbitMQ + Supabase 全家桶,看这个生产级 SaaS 的进程编排。