跳到主要内容

数据截至 (上游 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)
冻结器工具面 → 内容哈希,绑进 manifestjulep/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)
CLIdbt 式的 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

高层走一遍(不进代码):

  1. 定义:你写 @flow 函数。装饰器在定义期Handle 占位符执行它一次:每次 工具/纯函数/think() 调用都向 dag.Graph 追加一个单赋值步骤(julep/define.py:946 _append_step),h1 | h2 合并记录、h["key"] 取字段。
  2. 编译:compile(julep/dag.py:306)把步骤图拓扑排序,编成 wire-format 的 Node 树 ——只有 11 种结构算子(julep/kinds.py:14Op)。
  3. 冻结:deploy()(julep/deploy.py:904)依次跑五道门:freeze(工具面钉哈希)→ validate(良构)→ 能力执行(只许用清单授予的)→ 审批闸(dangerous 工具必须被人闸支配)→ 竞速准入(race 分支必须只读或断言幂等)。任何 blocking 诊断直接终止部署。
  4. 执行:运行期由纯解释器 interpret()(julep/execution/interpreter.py:209)走 IR 树; 所有副作用(调工具、调模型、跑子流程、等人)都通过注入的 Env 发生。Temporal 后端把 Env 的每个 handler 接到 activity/子工作流,于是每一步都在 Temporal 历史里落账、可重放。
  5. 观测:每个节点激活先发 Planned 事件、成功发 Did(带内容寻址的值引用)、失败发 Failed(julep/projection.py:37)。投影是派生的——重放时确定性重导出,不落库也成立。
@flow 函数 ──定义期──▶ Graph ──compile──▶ Node 树(IR)
│ deploy(): freeze + 4 道校验门

Deployment(不可变)
│ start

interpret() 逐节点求值(纯)
│ 副作用全部走 Env(activity)

Temporal 历史(durability)+ 投影事件(可解释)

4. 各章阅读顺序

由浅入深,建议按序读;也可按 keyTopics 直接跳到关切的章:

顺序章节讲什么什么时候读
0index.md(本章)全景 + 阅读地图先读,建立大盘认知
101-concepts-data-model.mdIR 节点树、Shape 格、契约/清单、Reasoner/Agent/Session、投影/轨迹数据模型想搞清"名词"与数据形状
202-task-execution-engine.md主线:纯解释器、Temporal/DBOS 双后端、重试代数、continue-as-new想懂"流程到底怎么被跑起来"
303-workflow-steps-expressions.md@flow 步骤与 11 种 IR 算子、组合子家族、纯函数注册(旧 $ 表达式已移除)写 @flow 流程时
404-tools-and-integrations.md@tool 契约、MCP 快照/preflight、skills、能力清单想给 agent 加工具时
505-memory-and-hybrid-search.md会话/转录/上下文作用域;旧版 docs+pgvector 混合检索已移除做多轮会话时
606-platform-architecture.mdFastAPI 控制平面、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:40 CONSERVATIVE_DEFAULT),于是它不能进 race、不能盲目重试; 只有人写的 capability manifest 能"断言"契约。→ 见第 02/04 章。

  • Shape 形态格 + Joined 防火墙 —— 子流程对外只暴露一个 shape 合同 (julep/ir.py:284 SubContract),内部多贵不泄漏给父流程的投影。→ 见第 01 章。

  • 投影是派生的,不是存的 —— 每步的 Planned/Did/Failed 事件由确定性代码在重放时同样 能重导出(julep/projection.py:1 模块 docstring);持久化只是一种缓存。→ 见第 02/06 章。

  • Agent = 有界循环,而不是无界生成 —— 控制器每轮只能从封闭的小决策集里选 (julep/agent_loop.py:97 Decision),预算守卫 + 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:631flow / Handle(:383) / FlowDef(:475)
步骤图 → IR 编译julep/dag.py:306compile / Graph(:90) / StepNode(:61)
IR 节点与算子julep/ir.py:425Node / Op(julep/kinds.py:14)
Shape 形态格julep/kinds.py:30Shape / shape_join(:81)
工具契约julep/contracts.py:22ToolContract / CONSERVATIVE_DEFAULT(:40)
冻结julep/freeze.py:504freeze / FreezeResult(:126)
部署五道门julep/deploy.py:904deploy / Deployment(:457)
能力清单julep/capabilities.py:85CapabilityManifest / Budget(:49)
纯解释器julep/execution/interpreter.py:209interpret / Env(:155)
Temporal 工作流julep/execution/harness.py:1189FlowWorkflow / AgentWorkflow(:2415)
工具调用 activityjulep/execution/effects.py:909callTool / invokeReasoner(:1138)
Reasoner(dotctx)julep/dotctx.py:172Reasoner / reasoner_from_settings(:475)
Agent 门面julep/agent.py:446Agent / tool(:354)
控制器循环julep/agent_loop.py:97Decision / AgentConfig(:288) / should_continue_as_new(:592)
会话julep/session.py:75Session / Channel(:43) / session(:187)
投影julep/projection.py:44ProjectionEvent / ProjectionEmitter(:263)
轨迹julep/trajectory.py:812TrajectoryRecorder / PostgresTrajectoryStore(:630)
控制平面julep/server/app.py:57create_app / runs 路由(julep/server/routes/runs.py:173)
CLI 入口julep/cli/main.py:696run / deploy(:847) / worker(:1197)