跳到主要内容

核心概念与数据模型

30 秒导读: Julep 是一个把「AI agent + 有状态工作流」当成后端资源来管理的平台。本章不讲它怎么跑,只讲它脑子里有哪些名词、每个名词有哪些字段、它们怎么相互挂钩、又存在数据库的哪张表里。读完你应该能一眼说清:Agent、Task、Session、Tool、Doc、User、Execution、Transition 分别是什么,以及为什么 Task 是"菜谱"、Execution 是"照着菜谱做的这一顿饭"

本章是后续所有章节的地基。执行流程(Temporal 怎么把一个 Task 跑起来)留给 02;步骤类型与 $ 表达式留给 03;工具的具体实现留给 04;文档检索留给 05。全景与阅读地图见 index


1. 先建立直觉:这些名词大概是什么

Julep 的世界里,一切资源都属于某个 developer(开发者账号,多租户的根)。你可以把整套模型想成三组东西:

  • "人设"这一组 —— 你先配置好、可以反复用的东西:Agent(有指令和模型的 AI 角色)、Tool(它能用的工具)、Doc(它能查的知识库)、User(和它对话的终端用户)。
  • "对话"这一组 —— Session(一次对话的容器)和 Entry(对话里的一条条消息)。这是聊天这条线。
  • "工作流"这一组 —— Task(多步骤工作流的定义)、Execution(把 Task 跑一次产生的实例)、Transition(这次运行里一步步的状态变化)。这是自动化这条线。

一句类比:

名词类比一句话
Agent一位员工的岗位说明书用什么模型、遵守什么指令、能用哪些工具
Task一份 SOP / 菜谱「先做 A,再判断 B,循环做 C」的步骤规范,带版本号
Execution照着菜谱做的这一顿饭某个 Task 的一次具体运行,有输入、有产出
Transition做饭时的每一步流水账「开火 → 下锅 → 出锅」这样的状态事件,运行状态从这里派生
Session一个聊天窗口装着一串来回消息、维持上下文
Entry聊天窗口里的一条气泡一条 user/assistant/tool 消息
Tool员工手上的一件工具函数、外部集成、系统调用……
Doc知识库里的一篇资料会被切块、向量化,供 agent 检索
User通讯录里的一个联系人和 agent 对话的终端用户

本章最重要的一个区分(§5 详讲):Task 是规范(spec),Execution 是一次运行。 就像"类"和"实例"、"菜谱"和"这顿饭"。很多困惑都来自把这两者混为一谈。


2. 顶层关系图(谁挂在谁下面)

Julep 用 PostgreSQL(TimescaleDB) 落库,schema 在 src/memory-store/migrations/。所有业务表都遵循同一个多租户模式:主键或外键的第一列几乎都是 developer_id(见任意 *.up.sql,如 000004_agents.up.sql:25PRIMARY KEY (developer_id, agent_id))。

怎么读这张图:箭头表示「拥有 / 引用」,从父指向子。顶点是 developer

┌─────────────┐
│ developer │ ← 多租户根,一切资源的所有者
└──────┬──────┘
┌──────────────┬───────┼────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼
┌───────┐ ┌──────┐ ┌──────┐ ┌─────────┐ ┌──────┐
│ agent │ │ user │ │ docs │ │ session │ │ tasks│
└───┬───┘ └──────┘ └──────┘ └────┬────┘ └──┬───┘
│ 拥有 │ 装着 │ 定义了
▼ ▼ ▼
┌───────┐ ┌─────────┐ ┌───────────┐
│ tools │ │ entries │ │ workflows │ ← task 的步骤(规范)
└───────┘ └─────────┘ └───────────┘

Task 被「跑一次」→ 产生 Execution → Execution 一步步走 → 记为 Transition:

tasks(task_id, version) ──┐

┌────────────┐ 引用某个 task 版本
│ executions │
└─────┬──────┘
│ 每前进一步写一条

┌─────────────┐ hypertable,状态机
│ transitions │
└─────────────┘

各实体的落库位置与所有权:

实体主表(migration)主键归属
Agentagents(000004)(developer_id, agent_id)developer
Userusers(000003)(developer_id, user_id)developer
Tooltools(000008)(developer_id, agent_id, tool_id)agent(或 task)
Docdocs(000006)(developer_id, doc_id, index)doc_owners 归 user/agent
Sessionsessions(000009)(developer_id, session_id)session_lookup 关联 participants
Entryentries(000015)(session_id, entry_id, created_at)session
Tasktasks(000010)(developer_id, task_id, version)agent
Executionexecutions(000011)(execution_id)task 的某个 version
Transitiontransitions(000012)(created_at, execution_id, transition_id)execution

一个贯穿全库的模式: 复合主键 (developer_id, <entity>_id) = 多租户隔离;JSONB 的 metadata 字段 = 给每个资源留的自由扩展位;created_at/updated_at + 触发器 = 自动时间戳。下面逐个概念展开时不再重复这三点。


3. 逐个概念:白话定义 + 字段 + 关系

每个概念有三种表示,注意别混淆(§6 会画成一张对照表):

  • API 模型:开发者通过 REST API 看到的形状,由 TypeSpec 生成到 src/agents-api/agents_api/autogen/
  • 内部模型 / spec:服务内部流转用的形状(如 TaskSpecDef)。
  • 持久化 schema:真正落库的表结构,在 src/memory-store/migrations/

3.1 Agent —— 有指令和模型的 AI 角色

它是什么: 一个可复用的 AI 配置:用哪个模型、遵守哪些指令、默认参数是什么。它是 Session 和 Task 的"主人"。

核心字段(建表见 000004_agents.up.sql:4-30;API 模型见 autogen/Agents.py:12 class Agent):

字段类型含义
agent_idUUID主键的一半(另一半是 developer_id)
name / canonical_nameTEXT / citext展示名 / 唯一的机读标识(须是合法标识符,000004_agents.up.sql:27)
aboutTEXT对这个 agent 的描述
modelTEXT用哪个模型(如 gpt-4o,agents.py 里必填 NOT NULL)
instructionsTEXT[]指令列表(库里存数组,000004_agents.up.sql:19)
default_settingsJSONB该 agent 建的所有 session 的默认参数

API 的 Agent 模型还带一个 default_system_template(autogen/Agents.py:60)——一大段 Jinja 模板,决定组 prompt 时怎么把 agent/user/session/docs 拼进去;它的默认值由 migration 000025_default_system_template 灌入。

关系: agent 1—N tools、1—N tasks;通过 session_lookup 参与 sessions;可作为 docs 的 owner。

3.2 User —— 和 agent 对话的终端用户

它是什么: 最简单的实体。代表和 agent 交互的人,主要用于给对话提供"对方是谁"的上下文,以及作为 docs 的 owner。

核心字段(000003_users.up.sql:4-13;autogen/Users.py:108):user_idnameaboutmetadata。就这些——User 本身几乎不带行为,行为都在 agent 那边。

关系:session_lookup 进入 session;可作为 docs 的 owner。

3.3 Session —— 一次对话的容器

它是什么: 装着一串来回消息、维持上下文的对话。它把「哪些 participant(agent/user)在对话」「上下文怎么裁剪」「要不要检索文档」这些策略聚在一起。

核心字段(000009_sessions.up.sql:4-34;autogen/Sessions.py:212 class Session):

字段类型含义
situationTEXT这次对话的背景设定
system_templateTEXT覆盖 agent 默认的系统模板
render_templatesBOOL是否把消息内容当 Jinja 模板渲染
token_budgetINT上下文的 token 预算阈值
context_overflowTEXT超预算时怎么办:'truncate''adaptive'(000009_sessions.up.sql:23-26)
recall_optionsJSONB检索文档的配置(向量/文本/混合,详见 05)

API 模型还多两个派生/行为字段:summary(对话摘要,自动生成)与 auto_run_tools(autogen/Sessions.py:224:240),这些库表里没有对应列。

participant 是多对多。 session 和它的参与者不直接放在 sessions 表里,而是用一张关联表 session_lookup(000009_sessions.up.sql:73-85):

session_lookup(developer_id, session_id, participant_type, participant_id)

└─ ENUM('user','agent') (000009:68)

插入时有触发器 validate_participant(000009_sessions.up.sql:91)去 users/agents 表核对 participant 真实存在。正因为是多对多,Julep 才能表达"单 agent 单 user""单 agent 多 user""多 agent"等会话形态(见 autogen/Sessions.py:272 起的 SingleAgentMultiUserSession 等子类)。

3.4 Entry —— 对话里的一条消息

它是什么: session 里的一条 ChatML 消息或事件(user 提问、assistant 回复、tool 结果……)。Entry 是追加式的历史

为什么单独一张 hypertable: entries 量大、按时间增长,所以按 created_at 做时间分区、按 session_id 做哈希分区(000015_entries.up.sql:51-63),这是 TimescaleDB 的典型用法。

核心字段(000015_entries.up.sql:31-48;API 基类 autogen/Entries.py:21 class BaseEntry):

字段类型含义
rolechat_role ENUMuser/assistant/tool/system/developer(000015:4-10)
sourceTEXT这条消息的来源(api_request/api_response/tool_response/summarizer…,见 Entries.py:50)
contentJSONB[]消息内容(可以是多模态数组;约束每个元素必须是 object,000015:47)
tool_calls / tool_call_idJSONB[] / TEXT模型发起的工具调用 / 这条 tool 结果对应的调用 id
model / tokenizer / token_countTEXT / TEXT / INT产生它的模型与 token 计数
timestampTIMESTAMPTZ这条事件所指的时间

Entry 不只是线性列表——它是一张图。 另有 entry_relations(session_id, head, relation, tail)(000016_entry_relations.up.sql),用 head→tail 的有向关系把 entries 串成 DAG,以支持分支/非线性的历史。插入 entry 时触发器还会顺手更新父 session 的 updated_at(000015_entries.up.sql:121-135)。

3.5 Tool —— agent 能用的一件工具

它是什么: agent(或 task)可调用的一项能力。可能是一个函数签名、一个外部集成、一次 API 调用或一个系统调用。本章只讲它的模型;怎么真正执行见 04

核心字段(000008_tools.up.sql:4-27;autogen/Tools.py:2967 class Tool):

字段类型含义
tool_idUUID主键的一部分
agent_idUUID归属的 agent
task_idUUID(可空)若非空,则这是某个 task 专属的工具(000008:8)
typeTEXT工具类别
nameTEXT工具名(须是合法 python 标识符,Tools.py:2971)
specJSONB工具的具体规格(参数 schema、集成配置等)

type 的取值是个封闭集合(autogen/Tools.py:2975-2983):functionintegrationsystemapi_callcomputer_20241022text_editor_20241022bash_20241022

一个巧妙约束: tools.task_id 可空,意味着同一张表既存"agent 全局工具"(task_id 为 NULL)也存"task 专属工具"。这也是为什么 taskstools 之间存在循环引用,migration 用延迟外键约束来打破这个环(000010_tasks.up.sql:3-8 的注释)。唯一性是 (agent_id, name, task_id)(000008:25)。

3.6 Doc —— 会被切块、向量化的知识

它是什么: 供 agent 检索的一篇知识库文档。检索机制(混合搜索)在 05;这里只看它长什么样、怎么存

关键点:一篇文档 = 多行。 主键是 (developer_id, doc_id, index)(000006_docs.up.sql:27)——index块序号。一篇长文被切成若干 chunk,每个 chunk 是一行,共享同一个 doc_id

核心字段(000006_docs.up.sql:14-33;autogen/Docs.py:51 class Doc):

字段类型含义
doc_id + indexUUID + INT文档 id + 块序号
title / contentTEXT标题 / 这一块的正文
modalityTEXTtext/image/mixed(000006:29)
embedding_model / embedding_dimensionsTEXT / INT用哪个模型嵌入、维度多少
languageTEXT语言(必须是合法的 PG 文本搜索配置,000006:31)
search_tsvtsvector全文检索列,由触发器从 title+content 生成(000006:156-186)

归属经中间表。 doc_owners(developer_id, doc_id, owner_type, owner_id)(000006:62-71)把一篇 doc 挂到某个 useragent 下,插入时触发器 validate_doc_owner 核对 owner 存在(000006:79)。

3.7 Task —— 工作流的"规范(spec)"

它是什么: 一份多步骤工作流的定义,归属某个 agent。注意:Task 只是蓝图,它本身不"运行";运行的是 Execution(§3.8)。

Task 是有版本的。 主键 (developer_id, task_id, version)(000010_tasks.up.sql:32)——同一个 task_id 的每次改动都是新的一行、新的 version。这样已经在跑的 Execution 永远指向它启动时那个固定版本,不受后续编辑影响。

核心字段(000010_tasks.up.sql:10-39;API 模型 autogen/Tasks.py:1144 class Task):

字段类型含义
task_id + versionUUID + INT复合身份;version 必 > 0(000010:38)
name / canonical_nameTEXT / citext展示名 / 机读名
input_schemaJSONB对 execution 输入的 JSON Schema 校验(null 表示任意输入,Tasks.py:1191)
inherit_toolsBOOL是否继承父 agent 的工具(Tasks.py:1199)
main(仅 API 模型)工作流入口:一串 step(Tasks.py:1167)

步骤存在哪?不在 tasks 表里,而在单独的 workflows(000010_tasks.up.sql:106-122),做了规范化:

workflows(developer_id, task_id, version, name, step_idx, step_type, step_definition)
│ │ │ │
子工作流名 第几步 步骤类型 步骤定义(JSONB)
(入口叫 "main",其它命名子流程各占一批行)

也就是说:API 里你写一个带 main(和可选命名子流程)的 Task;落库时,每一步拆成 workflows 表里的一行,step_idx 记顺序,step_type 记类型(step 类型详见 03)。这套"API 形状 ↔ 内部 spec ↔ workflows 行"的转换就是下面 §5 的 task_to_spec

3.8 Execution —— Task 的一次运行

它是什么: 把某个 Task 版本"跑一次"产生的实例。它记录这次运行的输入,并指向它启动时的那个 task 版本。

核心字段(000011_executions.up.sql:4-21;autogen/Executions.py:39 class Execution):

字段类型含义
execution_idUUID主键(注意:不含 developer_id,000011:17)
task_id + task_versionUUID + INT外键指向 tasks某个具体版本(000011:19)
inputJSONB这次运行的输入
metadataJSONB自由扩展位

最反直觉、也最关键的一点:executions 表里没有 status/output/error/updated_at 列。 建表脚本把它们注释掉了,并写明"这些将由 transitions 用连续聚合(continuous aggregates)生成"(000011_executions.up.sql:10-14)。

也就是说:Execution 的"当前状态"不是一个存下来的字段,而是从它的 transition 事件流里派生出来的。 而 API 层的 Execution.status 是一个封闭枚举(autogen/Executions.py:47-58):

queued → starting → running → awaiting_input → succeeded / failed / cancelled

这是面向用户的高层状态,由底层 transition 事件推导得到(推导逻辑见 02)。

3.9 Transition —— 运行中的一步状态变化

它是什么: Execution 每前进一步,就往 transitions 追加一条不可变事件。它是"运行状态"的唯一真相来源

为什么是 hypertable: transitions 高频、只增,按 created_at 时间分区 + 按 execution_id 哈希分区(000012_transitions.up.sql:56-68),主键 (created_at, execution_id, transition_id)

核心字段(000012_transitions.up.sql:40-53;API 模型 autogen/Executions.py:187 class Transition):

字段类型含义
typetransition_type ENUM这一步的事件类型(见下)
current_step / next_steptransition_cursor当前/下一步的游标 (workflow_name, step_index)(000012:32-35)
outputJSONB这一步的产出
step_labelTEXT可选的步骤标签(便于按名字引用)
task_tokenTEXT等待外部输入时用的续跑令牌(wait/resume 场景)

transition_type 是个九值枚举(000012_transitions.up.sql:14-24):initfinishinit_branchfinish_branchwaitresumeerrorstepcancelled

注意两套词汇别混: transition.type(上面九个)是底层事件;execution.status(§3.8 那七个)是从事件派生的高层状态。前者细、后者粗。

状态机就写在数据库里。 一个 BEFORE INSERT 触发器 check_valid_transition(000012_transitions.up.sql:110)在插入每条 transition 前,查这条 execution 上一条 transition 的 type,再用一张"合法后继"表校验这次跳转是否允许(000012:129-150)。例如:

  • 第一条 transition 只能是 init/init_branch/error/cancelled(000012:124);
  • init 之后可接 wait/error/step/cancelled/init_branch/finish(000012:130-131);
  • finish/error/cancelled终态,后面不允许再有任何 transition(000012:142-147)。

用一张精简的状态流转图看主干(省略了 branch/cancel 分支):

(start)


init ──► step ──► step ──► finish (正常完成)
│ │
│ ▼
│ wait ──► resume ──► step … (等外部输入再续跑)

error / cancelled (异常终止,终态)

这套"状态从事件流派生 + 状态机由 DB 触发器强约束"的设计,是理解 Julep 执行引擎的钥匙。为什么要这么设计、Temporal 如何驱动它,全部留给 02


4. Developer —— 多租户的根

前面反复出现的 developer_id 来自 developers 表(migration 000002_developers)。它是整个数据模型的根节点:agents、users、tasks、docs、sessions、executions 无一例外都以 developer_id 作为主键或外键的第一列(例如 000004_agents.up.sql:35fk_agents_developer)。

它带来两个后果:

  1. 数据隔离 —— 每次查询都天然带 developer_id,不同开发者的数据互不可见。
  2. 命名唯一性是"每 developer 唯一" —— 例如 agent 的 canonical_name 唯一约束是 (developer_id, canonical_name)(000004_agents.up.sql:26),不同 developer 可以用同名 agent。

(developer_id 之上还有 projects 做资源分组,但它对本章的核心模型不构成结构性影响,此处从略。)


5. 核心区分:Task(规范)vs Execution(一次运行)

这是本章的重头,也是最容易搞混的地方。把它彻底讲清:

类比: Task 就像编程里的类 / 函数定义,Execution 就像一次调用。你可以拿同一个 Task 跑一百次 Execution,每次输入不同、产出不同,但它们都遵循同一份步骤规范。

三种形状的转换。 同一个"工作流"在系统里有三副面孔:

开发者写的 内部规范 落库
┌──────────┐ task ┌─────────────┐ ┌───────────────┐
│ Task │ ──_to_ ▶│ TaskSpecDef │ ─▶ │ tasks 表 + │
│ (API模型) │ spec │ (内部模型) │ │ workflows 表 │
└──────────┘ └─────────────┘ └───────────────┘
main + 命名子流程 workflows: [Workflow(name,steps)…] 每步一行

转换发生在 task_to_spec()(src/agents-api/agents_api/common/protocol/models.py:57)。读它的 docstring 和实现能看到几件关键的事:

  • 入口 main 变成一个 Workflow。 workflows = [Workflow(name="main", steps=task_data.pop("main"))](models.py:95)。
  • 其它顶层键变成命名子工作流。 剩下不属于 TaskSpec 字段的键,各自成为一个 Workflow(models.py:97-100)。
  • 字段改名以避开 Python 关键字。 docstring 写明(models.py:64-74):"if" 步骤变成 IfElseWorkflowStep,带 kind_="if_else";if/in/from 分别改名成 if_/in_/from_,并在 JSON 里以别名保留原名。这解释了为什么原始 task 字典和转换后的 Pydantic 模型形状不同——做表达式校验时要同时处理这两种形态。
  • 产物类型分两种: patch 请求用 PartialTaskSpecDef,其余用 TaskSpecDef(models.py:102)。这些模型定义在 autogen/openapi_model.py:468-485:TaskSpec 继承 Task抹掉 main 字段、加上 workflowstools(openapi_model.py:468-475)。

Execution 如何接上 Task。 一次运行不是引用"某个 task",而是引用某个 task 的某个 version:外键 (developer_id, task_id, task_version)(000011_executions.up.sql:19)。运行时,服务把这些拼进一个内部对象 ExecutionInput(models.py:32)——它同时含 executiontask(类型正是 TaskSpecDef)、agentagent_toolsarguments,构成"跑这次工作流所需的一切"。至于这个对象怎么被 Temporal 消费,见 02

一句话收尾: Task 定义"要做什么"并被版本化冻结;Execution 记录"这一次用什么输入去做";Transition 记录"这一次做到了哪一步"。 三者构成从规范到运行到状态的完整链条。


6. 同一概念的三副面孔(对照表)

把 §3 的"三种表示"汇成一张导航表,方便你在读源码时对上号:

概念API 模型(autogen)内部/spec 模型持久化表(migration)
AgentAgents.py:12 Agent——agents(000004)
UserUsers.py:108 User——users(000003)
SessionSessions.py:212 Sessioncommon/protocol/sessions.pysessions + session_lookup(000009)
EntryEntries.py:21 BaseEntry / :169 Entry——entries + entry_relations(000015/16)
ToolTools.py:2967 ToolTaskToolDef(openapi_model.py:458)tools(000008)
DocDocs.py:51 Doc——docs + doc_owners(000006)
TaskTasks.py:1144 TaskTaskSpecDef(openapi_model.py:478)tasks + workflows(000010)
ExecutionExecutions.py:39 ExecutionExecutionInput(models.py:32)executions(000011)
TransitionExecutions.py:187 Transitioncommon/protocol/tasks.pytransitions(000012)

API 模型的来源: autogen/ 下的文件是生成物,真正的 source-of-truth 是 src/typespec/(TypeSpec)与 src/schemas/,经 codegen 产出 src/openapi.yaml 和这些 Pydantic 模型。想改模型要改 TypeSpec,而不是手改 autogen/


7. 边界与容易踩的点

  • 运行态字段是派生的,不是存的。 别指望在 executions 表里 SELECT status——它被注释掉了(000011:10-14),状态来自 transitions 的连续聚合。
  • Task 一改就是新版本。 tasks 的主键含 version(000010:32);"更新一个 task"实际是插入新版本行,旧 Execution 仍锚在旧版本。
  • 步骤不在 tasks 表里。 步骤被规范化进 workflows 表(000010:106-122);只看 tasks 表看不到工作流长什么样。
  • Task ↔ Tool 是循环引用。 靠延迟外键约束打破(000010:3-8 注释);tools.task_id 可空,同表混存 agent 级与 task 级工具。
  • Doc 一篇多行。 (developer_id, doc_id, index) 里的 index 是 chunk 序号,聚合成整篇要按 doc_id 合并。
  • DB 游标 vs API 目标形状略有差异。 库里 transition_cursor(workflow_name, step_index)(000012:32-35);API 的 TransitionTarget(workflow, step, scope_id)(Executions.py:130-147)——概念对应但字段不完全一一对齐,读代码时留意。
  • token 计数触发器被禁用。 entries 里那个自动算 token 的触发器因性能问题被注释掉了(000015:108-118),别以为 token_count 会自动回填。

8. 代码地图(导航索引)

主题文件路径符号 / 表名
Agent 建表src/memory-store/migrations/000004_agents.up.sqlagents
Agent API 模型src/agents-api/agents_api/autogen/Agents.pyAgent
User 建表 / 模型.../migrations/000003_users.up.sql · autogen/Users.pyusers · User
Session 建表 + participants.../migrations/000009_sessions.up.sqlsessions · session_lookup · validate_participant
Session API 模型autogen/Sessions.pySession
Entry 建表(hypertable).../migrations/000015_entries.up.sqlentries · chat_role
Entry 关系图.../migrations/000016_entry_relations.up.sqlentry_relations
Entry API 模型autogen/Entries.pyBaseEntry · Entry
Tool 建表 / 模型.../migrations/000008_tools.up.sql · autogen/Tools.pytools · Tool · TaskToolDef
Doc 建表 + owner + tsvector.../migrations/000006_docs.up.sqldocs · doc_owners · docs_update_search_tsv
Doc API 模型autogen/Docs.pyDoc
Task 建表 + 步骤规范化.../migrations/000010_tasks.up.sqltasks · workflows
Task API 模型autogen/Tasks.pyTask
Task spec 内部模型agents_api/autogen/openapi_model.pyTaskSpec · TaskSpecDef · Workflow
Task→spec 转换agents_api/common/protocol/models.pytask_to_spec · spec_to_task · ExecutionInput
Execution 建表 / 模型.../migrations/000011_executions.up.sql · autogen/Executions.pyexecutions · Execution
Transition 建表 + 状态机.../migrations/000012_transitions.up.sqltransitions · transition_type · check_valid_transition
Transition API 模型autogen/Executions.pyTransition · TransitionEvent · TransitionTarget
模型 source-of-truthsrc/typespec/ · src/schemas/ · src/openapi.yaml(TypeSpec → codegen)

下一步: 想知道这些静态模型如何"活"起来——一个 Execution 怎么被 Temporal 驱动、transitions 怎么一条条产生、派生状态怎么算——请读 02 任务执行引擎