数据截至 (上游 commit fc74d079a18c)
Julep 是什么 · 全景与阅读地图
30 秒导读: Julep(当前为 v3)是一个持久 化、可组合的 AI agent 框架:你用普通 Python 函数(
@flow)定义一个多步骤 agent 流程,Julep 在定义期就把这段函数"运行"一遍, 把其中出现的工具调用、模型调用、分支、并行收集成一张冻结的 IR(中间表示);部署时把每个 工具钉到内容哈希、逐关跑静态校验,然后交给 Temporal(或 DBOS)作为持久工作流执行—— 崩了能续跑、重试按工具契约分级、每一步都生成可回放的投影事件。
本章是这套文档的门厅:先让完全不了解 Julep 的人搞清"这是什么、给谁用、解决什么"(Layer 0), 再给一张编译-执行-观测全景图看懂大盘(Layer 1),最后给各章阅读顺序和"巧妙之处速览"。 本章不进代码细节——细节都在后面各章。
1. 这是什么(零基础也能懂)
一句话定义
Julep 是一个把"AI agent 的工作流"当编译目标的框架:你写普通的 Python,它编译出一份 冻结的、可内容寻址的中间表示,再在持久化执行引擎上跑这份 IR——流程能崩溃恢复、能安全重试、 每一步可解释(README.md:3 把它概括为 "durable, composable AI agents")。
先说清一个大 事:v1 → v3 是彻底重写
如果你听说过旧版 Julep(多服务的 agents API 平台:YAML 任务、agents-api + memory-store +
gateway + llm-proxy 那套),请注意:那些在本仓库主干里已经全部不存在了。README.md:236
明确写着 "Julep 3 is a ground-up rewrite; there is no migration path — v1 and v3 are different
products"(彻底重写、无迁移路径、是两个不同产品),旧平台只保留在 v1 分支。因此本文档
全部按 v3 源码重写;旧版机制在对应章节标注「已移除」。
解决什么问题 / 给谁用
设想你做一个"客服分诊"agent:查工单 → 让模型拟回复 → 必要时升级人工。自己手写会遇到一堆脏活:
- 流程跑到第 5 步进程挂了,重启后要从第 5 步续跑,而不是从头(模型调用很贵)。
- 哪些工具能重试、哪些重试会重复扣款?盲重试是事故之源。
- 模型说"我想调 send_email"——这个工具现在还允许调吗?工具的 schema 变了怎么办?
- 事后要能解释清楚每一步为什么发生、花了多少。
Julep 的答案是把这些问题前移到编译期:流程结构、工具面、重试契约、能力边界在部署时就 冻结并校验完,运行期只剩"照着 IR 走"。它面向要用生产级可靠性跑 agent 流程的 Python 团队。
它能做什么(功能一览)
| 能力 | 白话 |
|---|---|
| 持久执行 | 流程跑在 Temporal/DBOS 上,崩溃重启后从断点续跑 |
| 编译期冻结 | deploy() 把工具/模型面钉到内容哈希,运行期不许漂移 |
| 分级重试 | 按每个工具的 effect/幂等契约决定能不能重试、怎么重试 |
| 能力清单 | 工具/模型/服务器/预算的 deny-by-default 允许清单 |
| MCP 集成 | MCP 工具面冻结成快照,起跑前 preflight 校验不漂移 |
| 人闸/通道 | 内建 human gate(等人审批)、recv/emit(会话通道) |
| 可观测 | 每步产生 pomset 投影事件;跨 run 的轨迹可导出为训练数据 |
| 多后端 | 同一份 IR:进程内解释器(测试)、Temporal、DBOS/Postgres |
用起来什么样(最小示例)
摘自 README.md:55-72 的 quickstart(有删节):
# 示意,非源码:julep @flow 的最小用法
@tool(effect="read", idempotent=True) # 声明契约:只读且幂等
def lookup_ticket(ticket: str) -> dict[str, str]: ...
support_reply = Reasoner(name="support_reply", model="anthropic:claude-haiku-...", ...)
@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 # | 合并两条记录
deployment = deploy(triage, tools=[lookup_ticket], reasoners=[support_reply])
result = deployment.dry_run("Customer was charged twice.",
reasoners={"support_reply": fake_support_reply})
关键在 @flow 的语义:函数体只在定义时执行一次,里面的工具/纯函数/think() 拿到的都是
Handle(数据占位符),调用它们是往图里追加步骤而不是真的运行(README.md:76)。
dry_run 则在本地用假 reasoner 跑通全图。
一句话直觉
把 Julep 想成"给 agent 流程的编译器 + 存档点游戏机":Python 源码先"编译"成冻结 IR (像把脚本编译成字节码并记下哈希),跑的时候每走一步自动存档(Temporal 历史 + 投影事件), 断电重开从最近的档接着来。
2. 顶层全景(它大概怎么转)
v3 是单包多面的架构:一个 julep/ Python 包,从编写到执行到运维一条龙,但内部严格分层。
2.1 编译-执行-观测全景图
怎么读这张图: 从上到下是时间顺序。上半部是编译期(定义 → 冻结 → 校验 → 打包), 只跑一次;下半部是运行期(三种后端执行同一份 IR),旁边是派生的观测面。
你写的 Python 编译期(部署时一次)
───────────── ─────────────────────────────────
@flow 函数 ──定义期执行──▶ dag.Graph(单赋值步骤图)
@tool/@pure/Reasoner │ julep/dag.py: compile
▼
冻结 IR: Node 树(julep/ir.py)
│ julep/freeze.py: freeze(工具面→内容哈希)
│ julep/validate.py + capabilities + race 准入
▼
Deployment(冻结 IR + ToolManifest)
─────────────────────────────────────────────────────────────
运行期(同一份 IR,三种后端)
▼
┌──────────────────┬──────────────────────┐
▼ ▼ ▼
进程内解释器 Temporal harness DBOS 后端
(InMemoryEnv, (FlowWorkflow/ (dbos-transact,
测试/dry_run) AgentWorkflow, Postgres)
activities)
└──── 共享纯解释器 interpret() + effects 层 ────┘
│ 每步发投影事件
▼
projection(pomset,单 run 观测)+ trajectory(跨 run 轨迹)
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| @flow 前端 | 定义期执行用户函数,把调用收集成步骤图 | julep/define.py:631(flow) |
| DAG 编译器 | 单赋值步骤图 → wire-format IR 节点树 | julep/dag.py:306(compile) |
| IR | 全系统唯一共识的流程表示(Node 树) | julep/ir.py:425(Node) |
| 冻结器 | 工具面 → 内容哈希,绑进 manifest | julep/freeze.py:504(freeze) |
| 校验器 | 良构性/能力/审批闸/竞速准入四道门 | julep/deploy.py:904(deploy) |
| 解释器 | 纯的 IR 求值器,效果全部注入 | julep/execution/interpreter.py:209(interpret) |
| Temporal harness | 把效果接到 activity/子工作流 | julep/execution/harness.py:1189(FlowWorkflow) |
| DBOS 后端 | 同一份 IR 跑在 dbos-transact 上 | julep/execution/dbos_backend.py |
| 投影 | Planned/Did/Failed 事件的偏序集观测 | julep/projection.py:44(ProjectionEvent) |
| 轨迹 | 跨 run 因果历史/训练数据面 | julep/trajectory.py:812(TrajectoryRecorder) |
| 控制平面 | FastAPI:runs/releases/secrets 路由 | julep/server/app.py:57(create_app) |
| CLI | dbt 式的 agent 模块开发工具 | julep/cli/main.py |
一句话: 编译期(上层五个部件)产出不可变的部署物;运行期(下层)只是消费它—— 任何"运行期才发现流程有问题"都被刻意前移成了编译期诊断。
2.3 六个核心概念(数据模型骨架)
v3 的"名词"比 v1 少而深,它们的关系是理解一切的地基(详见 01-concepts-data-model.md):
| 概念 | 是什么 | 类比 |
|---|---|---|
| Flow(@flow) | 一个编译成 IR 的流程定义 | "流水线的剧本" |
| Tool(@tool / MCP) | 一个可调用的工具,带 effect/幂等契约 | "agent 的手脚" |
| Reasoner | 一次模型调用的声明(模型/系统提示/回复 schema) | "一个配置好的脑子" |
| Agent | 顶层形态:有界控制器循环(逐轮决策 + 预算守卫) | "一个自主的员工" |
| Session | 把有限流程包进无限 LOOP 的会话边界 | "一次持续的对话" |
| Run / Projection / Trajectory | 一次执行及其派生的观测数据 | "一场演出 + 监控录像" |
还有一条贯穿全库的暗线:Shape 形态格(julep/kinds.py:30)——每个流程按"贵不贵、
要不要拥有续体"归入 Pipeline → Dataflow → Branching → Feedback → Staged → Agent 六级,
组合取 join(最贵者)。它能答"这个流程需要多强的运行时保证"。
3. 主线:一个 @flow 怎么变成一次持久执行
如果只记住一件事,记这条主线——"Python 函数 → 冻结 IR → 持久工作流,逐步执行、逐步投影"。 这是整个框架价值的核心,详见 02-task-execution-engine.md。
高层走一遍(不进代码):
- 定义:你写
@flow函数。装饰器在定义期用Handle占位符执行它一次:每次 工具/纯函数/think()调用都向dag.Graph追加一个单赋值步骤(julep/define.py:946_append_step),h1 | h2合并记录、h["key"]取字段。 - 编译:
compile(julep/dag.py:306)把步骤图拓扑排序,编成 wire-format 的Node树 ——只有 11 种结构算子(julep/kinds.py:14的Op)。 - 冻结:
deploy()(julep/deploy.py:904)依次跑五道门:freeze(工具面钉哈希)→ validate(良构)→ 能力执行(只许用清单授予的)→ 审批闸(dangerous 工具必须被人闸支配)→ 竞速准入(race 分支必须只读或断言幂等)。任何 blocking 诊断直接终止部署。 - 执行:运行期由纯解释器
interpret()(julep/execution/interpreter.py:209)走 IR 树; 所有副作用(调工具、调模型、跑子流程、等人)都通过注入的Env发生。Temporal 后端把Env的每个 handler 接到 activity/子工作流,于是每一步都在 Temporal 历史里落账、可重放。 - 观测:每个节点激活先发
Planned事件、成功发Did(带内容寻址的值引用)、失败发Failed(julep/projection.py:37)。投影是派生的——重放时确定性重导出,不落库也成立。
@flow 函数 ──定义期──▶ Graph ──compile──▶ Node 树(IR)
│ deploy(): freeze + 4 道校验门
▼
Deployment(不可变)
│ start
▼
interpret() 逐节点求值(纯)
│ 副作用全部走 Env(activity)
▼
Temporal 历史(durability)+ 投影事件(可解释)
4. 各章阅读顺序
由浅入深,建议按序读;也可按 keyTopics 直接跳到关切的章:
| 顺序 | 章节 | 讲什么 | 什么时候读 |
|---|---|---|---|
| 0 | index.md(本章) | 全景 + 阅读地图 | 先读,建立大盘认知 |
| 1 | 01-concepts-data-model.md | IR 节点树、Shape 格、契约/清单、Reasoner/Agent/Session、投影/轨迹数据模型 | 想搞清"名词"与数据形状 |
| 2 | 02-task-execution-engine.md | 主线:纯解释器、Temporal/DBOS 双后端、重试代数、continue-as-new | 想懂"流程到底怎么被跑起来" |
| 3 | 03-workflow-steps-expressions.md | @flow 步骤与 11 种 IR 算子、组合子家族、纯函数注册(旧 $ 表达式已移除) | 写 @flow 流程时 |
| 4 | 04-tools-and-integrations.md | @tool 契约、MCP 快照/preflight、skills、能力清单 | 想给 agent 加工具时 |
| 5 | 05-memory-and-hybrid-search.md | 会话/转录/上下文作用域;旧版 docs+pgvector 混合检索已移除 | 做多轮会话时 |
| 6 | 06-platform-architecture.md | FastAPI 控制平面、CLI、发布(Helm/KEDA/S3)、密钥库 | 想部署运维时 |
5. 巧妙之处速览(读各章时重点看)
这几处是 Julep v3 值得带走的"精华",本章只点名,细节在对应章:
-
定义期执行(define-by-construction) ——
@flow函数只跑一次,跑的时候不是执行业务, 而是把每个调用"录"成图步骤(julep/define.py:1模块 docstring)。普通 Python 名字就是 数据流连线,不需要学新 DSL 语法。→ 见第 03 章。 -
一份 IR、三个后端 —— 纯解释器
interpret()不 import 任何引擎;Temporal/DBOS/内存 各自只是 提供一个Env(julep/execution/interpreter.py:155)。控制流逻辑可以在没有 Temporal 的单测里验证。→ 见第 02 章。 -
工具契约驱动的重试代数 —— MCP 注解默认不可信,未断言的工具一律按 write/none 保守处理(
julep/contracts.py:40CONSERVATIVE_DEFAULT),于是它不能进 race、不能盲目重试; 只有人写的 capability manifest 能"断言"契约。→ 见第 02/04 章。 -
Shape 形态格 + Joined 防火墙 —— 子流程对外只暴露一个 shape 合同 (
julep/ir.py:284SubContract),内部多贵不泄漏给父流程的投影。→ 见第 01 章。 -
投影是派生的,不是存的 —— 每步的 Planned/Did/Failed 事件由确定性代码在重放时同样 能重导出(
julep/projection.py:1模块 docstring);持久化只是一种缓存。→ 见第 02/06 章。 -
Agent = 有界循环,而不是无界生成 —— 控制器每轮只能从封闭的小决策集里选 (
julep/agent_loop.py:97Decision),预算守卫 + continue-as-new 保证历史有界 (julep/agent_loop.py:592)。→ 见第 02 章。
6. 边界与诚实说明
- v1 已成历史。 旧版 agents API 平台(多服务 docker-compose、YAML task、entries/docs
混合检索、simpleeval
$表达式)在主干全部移除,只活在v1分支(README.md:236)。 本套文档描述 v3。 - v3 是 release candidate。 安 装要
pip install --pre julep(README.md:12),API 仍可能变。 - Temporal 是可选依赖。 基础安装只有编写+编译;要 durable 执行需装
julep[temporal]或julep[dbos](README.md:215 的 extras 表)。 - DBOS 后端能力子集。 race/hedge/quorum 依赖取消能力,DBOS 不能取消在跑的 step,
因此这类流程在 DBOS 上被拒绝执行(
julep/execution/dbos_backend.py:1模块 docstring)。 - 本章为门厅,不含深入代码走读;每个"巧妙之处"的锚点已给出,深挖请进对应章。
7. 代码地图(导航索引)
从这里直接跳进源码。符号名比行号抗漂移,优先用符号 grep。
| 主题 | 文件路径(相对克隆根) | 符号 / 锚点 |
|---|---|---|
| 包入口与公共面 | julep/__init__.py | _BASE_EXPORTS / _TEMPORAL_EXPORTS |
| @flow 装饰器 | julep/define.py:631 | flow / Handle(:383) / FlowDef(:475) |
| 步骤图 → IR 编译 | julep/dag.py:306 | compile / Graph(:90) / StepNode(:61) |
| IR 节点与算子 | julep/ir.py:425 | Node / Op(julep/kinds.py:14) |
| Shape 形态格 | julep/kinds.py:30 | Shape / shape_join(:81) |
| 工具契约 | julep/contracts.py:22 | ToolContract / CONSERVATIVE_DEFAULT(:40) |
| 冻结 | julep/freeze.py:504 | freeze / FreezeResult(:126) |
| 部署五道门 | julep/deploy.py:904 | deploy / Deployment(:457) |
| 能力清单 | julep/capabilities.py:85 | CapabilityManifest / Budget(:49) |
| 纯解释器 | julep/execution/interpreter.py:209 | interpret / Env(:155) |
| Temporal 工作流 | julep/execution/harness.py:1189 | FlowWorkflow / AgentWorkflow(:2415) |
| 工具调用 activity | julep/execution/effects.py:909 | callTool / invokeReasoner(:1138) |
| Reasoner(dotctx) | julep/dotctx.py:172 | Reasoner / reasoner_from_settings(:475) |
| Agent 门面 | julep/agent.py:446 | Agent / tool(:354) |
| 控制器循环 | julep/agent_loop.py:97 | Decision / AgentConfig(:288) / should_continue_as_new(:592) |
| 会话 | julep/session.py:75 | Session / Channel(:43) / session(:187) |
| 投影 | julep/projection.py:44 | ProjectionEvent / ProjectionEmitter(:263) |
| 轨迹 | julep/trajectory.py:812 | TrajectoryRecorder / PostgresTrajectoryStore(:630) |
| 控制平面 | julep/server/app.py:57 | create_app / runs 路由(julep/server/routes/runs.py:173) |
| CLI 入口 | julep/cli/main.py:696 | run / deploy(:847) / worker(:1197) |