数据截至 (上游 commit fc74d079a18c)
@flow 步骤、组合子与纯函数
30 秒导读: 在 v3 里,一个"流程"就是一个被
@flow装饰的普通 Python 函数。装饰器在 定义期执行它一次:参数和每次调用的返回值都是Handle(占位符),于是工具调用、模型 调用、条件、循环都被"录"成图步骤。运行期没有解释你的 Python——只有编出来的 IR。 这章讲清你能写什么步骤、组合子家族有哪些,以及旧版$表达式被什么取代了。
本章聚焦编写面。IR 与数据结构见 01;编出来的图怎么被执行见 02;工具本身见 04。
旧版对照(已移除): v1 的 YAML 任务(15 种
kind_步骤)、$前缀表达式、 simpleeval 沙箱(base_evaluate/SimpleEval)、{{}}模板兼容层全部不存在于主干。 本章 §7 专门讲"表达式"被什么取代、为什么。
1. 这是什么(零基础也能懂)
一句话定义: @flow 是一种"定义即构造(define-by-construction)"的流程编写法——
你写的是普通 Python,但函数体在定义时跑一遍,跑的目的不是产生结果,而是产生图。
它解决什么问题。 声明式 YAML(v1)表达能力有限、没有类型检查;命令式 Python 运行期
任意执行,没法冻结、没法校验、没法安全重放。@flow 取中间路线:编写体验是 Python
(IDE 补全、类型、重构工具全可用),产物是数据(可哈希、可校验的 IR)。
一个最小流程长这样(摘自 README.md:55-64,有删节):
# 示意,非源码:@flow 里的每行都是一个图步骤
@flow
def triage(ticket: str) -> dict[str, str]:
hit = lookup_ticket(ticket, retries=2, timeout_s=5) # 工具步(带重试/超时)
prompt = ticket_prompt(hit) # 纯函数步
answer = think(support_reply, prompt, timeout_s=10) # 模型步
return hit | answer # 记录合并(std.merge)
读这段的直觉,记住三点:
- 每个赋值是一个步骤,变量名(单赋值)就是这条边在图里的名字。
- kwargs 是执行策略:
retries=、timeout_s=、name=直接写在调用上,编进Ann。 - 函数返回值必须是 Handle(或 Handle 组合),返回别的会得到带源码位置的报错。
一句话类比: 像 React Hooks——"写的是顺序代码,框架在'渲染'时录下结构";也像 BUILD 文件的 Python 化:Bazel 只执行配置代码来生成依赖图,不执行构建本身。
2. 顶层全景(一行代码怎么变成 IR)
怎么读这张图: 从上到下是编译管道。左列是你写的东西,右列是每步产物。
你写的 产物
──────────────────────────────────────────────────────────
@flow def triage(ticket): ─▶ FlowDef(包装函数 + 源码信息)
hit = tool(h) ─▶ Graph.add_step(TOOL) ┐
prompt = pure(h) ─▶ Graph.add_step(PURE) │ dag.Graph(StepNode 单赋值)
cond/s each/switch ─▶ 嵌套子 Graph ┘
return h1 | h2 ─▶ std.merge 步骤
│ dag.compile(julep/dag.py:306)
▼
Node 树(wire-format IR,11 种 Op)
│ deploy(): freeze + 校验(第 02/04 章)
▼
Deployment(不可变,内容寻址)
三个关键部件:
| 部件 | 干什么 | 在哪 |
|---|---|---|
Handle | 定义期的数据占位符;支持 h1 | h2(merge)、h["key"]/h.key(pluck) | julep/define.py:383 |
_append_step | 所有步骤追加的公共入口:解析 name/retries 等 kwargs、记录绑定 | julep/define.py:946 |
Graph/StepNode | 单赋值中间图,拓扑排序后编 IR | julep/dag.py:90/:61 |
定义期执行是怎么发生的? @flow 包装你的函数;调用时它把参数换成 Handle 压进
构建上下文栈,再执行函数体。函数体里出现的任何注册可调用对象(Tool/Pure/Reasoner
包装器)在"作者期"模式下(apply_if_authoring,julep/define.py:892)不是真调用,而是
_append_step。不在 @flow 里调用它们则照常真执行(所以 @tool 函数可以单测)。