跳到主要内容

演员、线程与放大:多智能体、依赖调度、记忆与持久化

30 秒导读: 第 2 章讲清了"一个演员被驱动一次"是怎么回事。本章讲的是把它放大:同一套回合循环,如何扩成多智能体协作、依赖任务的自动排程、跨进程的会话持久化、以及可召回的长期记忆。这四组部件彼此正交,但组合起来就是 Marvin 支撑真实工作流的骨架。

1. 这是什么(先建立全局观)

前面几章的主角是一次回合:编排器挑一个演员、给它工具、让它跑一轮、用一个类型化的 EndTurn 收尾。够用来做一个单任务。

但真实工作流不止一个任务、不止一个智能体、也不止一次进程:

  • 你想让几个不同角色的智能体分工(一个写、一个审)。
  • 你的目标要拆成有先后依赖的子任务(先抓数据,再分析,最后写报告)。
  • 你希望关掉进程再打开时,对话还在
  • 你希望智能体能记住跨会话的事实,并在需要时自动想起来。

本章讲的就是支撑这四件事的四组部件。它们的关系可以先记成一句话:

Actor 是"谁在做",Task 是"做什么、按什么顺序",Thread 是"说过的话存在哪",Memory 是"记住的事实怎么召回"。

┌───────────────────────────────────────┐
目标 / 一组任务 ──▶│ 编排器(第 2 章的回合循环,这里被放大) │
└───────────────────────────────────────┘
│ │ │
┌────────────┘ │ └────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Actor │ │ Task 调度 │ │ Thread │
│ 谁在做这一轮 │ │ 拓扑收集+门控 │ │ 持久会话/用量 │
│ Agent/Team │ │ 独立→并发 │ │ SQLAlchemy │
└─────────────┘ │ 依赖→串行 │ └──────────────┘
│ └──────────────┘ ▲
│ auto_use 自动召回 │ 写回消息
▼ │
┌─────────────┐ │
│ Memory │────────── 召回结果作为 info 消息 ──────────▶│
│ 向量记忆 │
└─────────────┘

四组部件各自独立,你可以只用 Task、不碰 Memory;也可以只用 Agent、不用 Team。本章逐个拆开讲部件本身,以及它们如何拼成规模化工作流——不重复第 2 章的回合内部机制。

2. Actor:统一"谁在做"的抽象基类

一切"能占用一个回合的东西",在 Marvin 里都是一个 Actor(演员)。Agent(单个智能体)是 Actor,Team(一群智能体)也是 Actor。这层抽象让编排器不必区分面对的是一个还是一群——它只管调用 Actor 的接口。

2.1 Actor 是什么:一份最小契约

Actor 是抽象基类(ABC),规定了每个演员必须能回答的几件事,见 src/marvin/agents/actor.py:31-103:

方法职责
get_agentlet(...)抽象方法,子类必须实现:产出一个 pydantic-ai agentlet(见第 3 章)
get_tools() / get_end_turn_tools()这一轮能用的普通工具 / 结束回合工具
get_memories()这个演员挂了哪些记忆模块
start_turn() / end_turn()回合开始 / 结束的钩子(默认只在 verbose 时写条 info 消息)
friendly_name()供日志和提示用的人类可读名字,如 Agent "Marvin" (a1b2c3d4)

get_agentlet 是唯一的抽象方法(src/marvin/agents/actor.py:96-103),这正是"多态"的支点:编排器只调 actor.get_agentlet(...),至于返回的 agentlet 是单个智能体的还是委派给了队友的,由子类决定。

2.2 关键机制:用 contextvars 把"当前演员"作用域化

它要解决的小问题: 回合循环里,很多深层代码(工具函数、记忆召回)需要知道"现在是谁在跑",但又不想把 actor 一层层当参数传下去。

思路: 用 Python 的 contextvars——一个协程安全的"隐式当前值"。进入某个演员的作用域时把它设为"当前演员",离开时还原。这样任何深层代码调 Actor.get_current() 就能拿到当前演员,无需显式传参。

Actor 因此实现了上下文管理器协议(__enter__ / __exit__),见 src/marvin/agents/actor.py:70-94:

# 示意,非源码 —— 演示 contextvar 作用域怎么用
with agent: # __enter__: _current_actor.set(agent),压入 token
... # 这段里 Actor.get_current() == agent
# __exit__: _current_actor.reset(token),弹出 token,还原到之前的值

真实实现里,__enter__set 返回的 token 压进 self._tokens(actor.py:70-74),__exit__ 弹出并 reset(actor.py:76-89)。重点看:第 2 章的编排器正是用 with actor: 包住 agentlet 的迭代(src/marvin/engine/orchestrator.py:197),让整段回合都处在该演员的作用域内。

关键细节(并发下的坑): contextvars 的 token 有个约束——它只能在创建它的那个上下文里 reset。当独立任务用 asyncio.gather 并发跑时(见 §4.3),token 可能在另一个上下文里被创建,reset 就会抛 ValueError__exit__ 专门吞掉了这一种错误(actor.py:81-89):它检查错误信息里是否含 "was created in a different Context",是则安全忽略,否则重新抛出。这是把"单演员作用域"安全放大到"并发多任务"的一个必要补丁。

start_turn / end_turn 是回合边界钩子。基类默认几乎什么都不做,只在 verbose=True 时往线程里写一条"XX 开始/结束了它的回合"的 info 消息(actor.py:105-119)。它们的价值在于被子类重写——Team 就靠重写这两个钩子来轮换成员(见 §3)。

3. Agent 与 Team:从"一个"放大到"一群"

3.1 Agent:最常用的具体演员

AgentActor 的主力子类(src/marvin/agents/agent.py:49-50)。它把一个智能体需要的配置全摆在字段上:

字段作用
model用哪个模型;None 则回落到 marvin.defaults.model(agent.py:99-100)
tools普通工具函数列表
memories挂载的记忆模块(见 §6)
mcp_serversMCP 服务器,运行时转成 pydantic-ai 的 MCPServer(agent.py:109-121)
model_settings传给模型的设置;与全局温度合并(agent.py:123-127)
name不给就随机取一个(见下)

随机命名的小设计: Agentname 字段默认工厂是 random.choice(AGENT_NAMES)(agent.py:53-57)。也就是说你 Agent() 不起名,它会随机叫个 "Marvin"、"HAL" 之类的名字。这不是玩梗——多智能体场景里,日志和提示里靠 friendly_name() 区分谁是谁,一个稳定、可读的名字比一串 UUID 好认得多。

Agent.get_tools() 有个值得注意的组合(agent.py:102-104):它把自己的 tools 加上每个记忆模块暴露的工具。也就是说,给智能体挂一个 Memory,就等于自动给它加了 add / delete / search 三个记忆工具(见 §6.2)。

至于 get_agentlet 如何把这些工具、EndTurn 定义、MCP 服务器组装成一个 pydantic-ai agentlet——那是第 3 章的内容,这里不重复。

3.2 Team:把"一群演员"也包成一个 Actor

Team(src/marvin/agents/team.py:20-24)是"多智能体"的容器。它自己也是 Actor,所以对编排器透明——编排器看到的还是"一个演员"。

Team 的核心状态是 active_member(当前轮到谁),初始化时设为第一个成员(team.py:56)。它的多态实现全是委派给 active_member:

  • get_agentlet → 委派给 active_member.get_agentlet(...),并把 Team 自己的工具合并进去(team.py:87-97)。
  • start_turn / end_turn / get_memories / friendly_name → 全部转发给 active_member(team.py:71-100)。

Team 还重写了上下文管理器,递归进入 active_member 的作用域(team.py:58-69):进 Team 作用域时,active_member 也一并进作用域。这样 §2.2 的 "当前演员" 语义在嵌套演员下依然成立。

重要:Team 已废弃。 Team.__post_init__ 会主动发一条 MarvinDeprecationWarning(team.py:40-52),原文说 "Team functionality is incomplete and does not provide meaningful agent collaboration",建议改用"独立的 Agent + 显式协调"。所以下面讲 Team 家族,重点是理解它的机制设计(如何用 Actor 抽象放大到多智能体),而非推荐用法。

3.3 委派:DelegateToActor 如何改写 active_member

怎么让一个智能体把活交给队友? Team 的答案是把"委派"做成一个 EndTurn 工具。

Team.get_end_turn_tools() 会为当前 active_member 的每个"下家"(delegate)动态生成一个 DelegateToActor 工具(team.py:102-112)。delegates 是一张 dict[Actor, list[Actor]],记"谁能委派给谁"。

生成的工具本质是一个 EndTurn 子类,它的 run 方法直接改写 team.active_member,见 src/marvin/engine/end_turn.py:145-152:

# 真实逻辑(src/marvin/engine/end_turn.py:145-152)简化
async def run(self, thread, actor):
if team is not None:
team.active_member = delegate_actor # 轮换:下一轮换成被委派者
if self.message: # 可选:留一句话给下家
await thread.add_messages_async([AgentMessage(...)])

于是"委派"就成了回合循环的自然一环:当前成员在回合末尾选择 DelegateToActor 作为它的 EndTurn,编排器执行这个 EndTurn(orchestrator.py:233-235),active_member 被改写,下一轮编排器再拿 agentlet 时,Team 就委派给新的 active_member。轮换靠改一个字段完成,干净利落。

3.4 三种轮换策略:Swarm / RoundRobin / Random

Team 有三个子类,区别只在怎么决定下一个 active_member:

子类轮换规则实现
Swarm允许所有成员互相委派(全连接 delegates)team.py:115-125:__post_init__ 把 delegates 填成"每个成员→其余所有成员"
RoundRobinTeam每轮按顺序轮到下一个team.py:128-135:start_turn(index+1) % len
RandomTeam每轮随机挑一个team.py:138-144:start_turnrandom.choice(members)

注意 Swarm 靠的是委派工具(智能体自己选择交给谁),而 RoundRobin / Random 靠的是重写 start_turn(在回合一开始就切换 active_member,与智能体的选择无关)。这正好用上了 §2.1 说的 start_turn 钩子——同一个抽象接口,两种放大方式。

4. 任务依赖与调度:从"一个任务"放大到"一张任务图"

单个 Task(见第 1 章)声明"要什么类型的结果"。放大到工作流,就要能表达任务之间的关系,并让编排器按关系排程

4.1 任务图的三种边

一个 Task 通过三个字段织成一张图(src/marvin/tasks/task.py):

  • subtasks: set[Task](task.py:171)——子任务;父任务要等子任务全完成。
  • depends_on: set[Task](task.py:180)——依赖;本任务要等被依赖的先完成。
  • parent(属性,task.py:378-389)——反向指向父任务;给某任务设 parent 会自动把它加进父的 subtasks

4.2 拓扑收集:get_all_tasks 把整张图摊平成有序列表

它要解决的小问题: 你只把顶层任务交给编排器,但真正要跑的是它牵出的整张依赖图。得先把图完整收集出来,并排成"依赖在前、依赖者在后"的顺序。

编排器的 get_all_tasks(src/marvin/engine/orchestrator.py:92-134)用一次深度优先遍历做这件事。核心是内部的 collect_tasks:

# 真实逻辑(orchestrator.py:104-123)简化 —— 递归收集,保证依赖先入列
def collect_tasks(task):
if task in all_tasks: # 去重:图里可能多条路径指向同一任务
return
all_tasks.add(task)
for subtask in task.subtasks: # 先收子任务
collect_tasks(subtask)
for dep in task.depends_on: # 再收依赖
collect_tasks(dep)
ordered_tasks.append(task) # 自己排在依赖之后
if task.parent: # 最后把父任务也拉进来
collect_tasks(task.parent)

重点看排列顺序:因为"先递归子任务和依赖、再把自己 append",ordered_tasks 天然满足"被依赖的排在前面"。收集完还能按状态过滤:_filter="ready" 只留 is_ready() 为真的(orchestrator.py:130-131)。

4.3 依赖门控:is_ready 决定一个任务能不能跑

一个任务"就绪"的判据很简洁(src/marvin/tasks/task.py:637-644):

# src/marvin/tasks/task.py:637-644
def is_ready(self) -> bool:
return self.is_incomplete() and all(
t.is_complete() for t in (self.depends_on | self.subtasks)
)

一句话:它自己没完成,且它依赖的、以及它的子任务全都完成了。 这就是依赖门控——没就绪的任务不会被排进这一轮的候选。depends_on | subtasks 用集合并集把两类"前置"一并检查。

4.4 独立并发 vs 依赖串行:放大的两条路径

有了就绪判据,run_tasks 就能对整批任务做一个关键的分流(src/marvin/fns/run.py:34-54):

run_tasks_async(tasks)

┌────────────┴─────────────┐
多个且彼此独立? 否则(有依赖 / 单个)
│ │
▼ ▼
asyncio.gather 并发跑 交给 Orchestrator
每个 task.run_async() 按回合循环逐个推进
│ │
└────────────┬───────────────┘

返回 tasks
  • 独立任务 → 并发。len(tasks) > 1 且彼此独立,用 asyncio.gather 同时跑(run.py:42-45)。这就是"放大吞吐"的地方——没有依赖关系,就没必要排队。
  • 有依赖 / 单个 → 串行。 否则交给 Orchestrator,让它一轮一轮按 is_ready 门控推进(run.py:46-53)。

编排器内部还有一层更细的分流。run_once 拿到某个演员的候选就绪任务后,如果它们彼此独立,一次只指派一个(避免多个 EndTurn 抢答造成冲突);如果互相依赖,才一起指派(orchestrator.py:150-164)。这是把"避免 EndTurn 冲突"这一约束下放到单轮粒度。

4.5 自动拆子任务:marvin.plan 与 PlanSubtasks

放大工作流还有一条路是让模型自己拆任务

离线拆分:marvin.plan 给它一个目标,它跑一个 Task[list[PlanTask]] 让模型产出一份计划,再把计划转成真正的 Task 对象(src/marvin/fns/plan.py:147-199)。转换函数 create_tasks(plan.py:67-144)做了两件关键事:

  1. 拓扑排序(plan.py:73-114):先处理父任务和依赖,再处理子任务和依赖者,顺便检测环——若发现循环依赖抛 ValueError(plan.py:80-83)。
  2. 按序建 Task(plan.py:118-142):因为已拓扑排序,建每个 Task 时它的 parentdepends_on 引用的 Task 都已建好,直接从 tasks 字典取。

回合内拆分:PlanSubtasks 这是个 EndTurn 工具(src/marvin/engine/end_turn.py:158-208),让智能体在自己的回合里为当前父任务动态生成子任务。它的 run 会开一个新线程、调 marvin.plan_async 拆子任务并挂到 parent_task 下(end_turn.py:193-205)。任务默认就带着这个工具——Task.get_end_turn_tools 会为可拆分的任务加上 create_plan_subtasks(src/marvin/tasks/task.py:528)。

于是"目标 → 计划 → 子任务 → 依赖调度"闭环了:模型拆出的子任务,立刻进入 §4.2–4.4 那套拓扑收集 + 门控 + 并发/串行的调度机制。这就是把单任务放大成工作流的完整链路。

5. Thread:把会话持久化到 SQLAlchemy

放大到"跨进程、可回溯"的工作流,靠 Thread。它是运行时会话对象 + SQLAlchemy 持久层的组合(src/marvin/thread.py:107-109)。

5.1 Thread 的双重身份

Thread 在内存里是个轻量 dataclass(有 id、可选 parent_id),但它的读写方法全都落到数据库:

  • 写: add_messages_async(thread.py:167-189)先确保线程记录存在(_ensure_thread_exists),再把每条消息 DBMessage.from_message 序列化后批量 session.add_all + commit。上面还封了一层语义化的 add_user_message / add_agent_message / add_system_message / add_info_message(thread.py:191-230)。
  • 读: get_messages_async(thread.py:258-306)按 thread_id 查、按时间排序。

一个读取细节:过滤系统消息。 默认 include_system_messages=False,方法会遍历结果、把带 system-prompt 部分的 ModelRequest 剔掉(thread.py:299-304)。也就是说,系统提示存进了库(可回溯),但默认拉取历史时不返回它——避免把每轮重建的系统提示混进对话历史。

5.2 底层表:DBThread / DBMessage / DBLLMCall

持久层是三张 SQLAlchemy 表(src/marvin/database.py),符号级速览:

表 / 类存什么关键点
DBThread(database.py:143)线程本身parent_thread_id 自引用,支持线程分叉
DBMessage(database.py:198)每条消息message 存成 JSON;from_message / to_message 负责序列化往返(database.py:223-244)
DBLLMCall(database.py:270)每次 LLM 调用usage;通过 DBLLMCallMessage 关联表把"这次调用用了哪些消息"连起来
DBLLMCallMessage(database.py:181)调用↔消息映射in_initial_prompt 区分是入参(prompt)还是产出(completion),order 记顺序

DBMessage 上还建了 (thread_id, created_at desc) 复合索引(database.py:210-216),让"拉某线程最近 N 条"这类查询走索引。

5.3 用量追踪:每次调用记 usage,可按线程汇总

每跑完一轮,编排器都会 DBLLMCall.create(...) 记一次调用,连同 run.usage()(orchestrator.py:217-222)。DBLLMCall.create 会把 prompt 消息和 completion 消息分别写进关联表,并用 order 保序(database.py:285-332)。

于是"这次工作流花了多少 token"可以按线程汇总:Thread.get_usage_async 拉出线程所有 LLMCall、把每个的 usage 累加(thread.py:370-392)。你还能通过 LLMCall.get_messages_async 回溯"这一次调用具体用了哪些消息"(thread.py:63-94),它按 in_initial_prompt 把消息分成 prompt / completion 两组。

5.4 用 contextvar + get_thread 统一"当前线程"

和 Actor 一样(§2.2),Thread 也用 contextvar 作用域化(thread.py:98-101410-427)。配套的 get_thread(thread.py:449-464)是个便利解析器,把三种输入归一成一个 Thread:

# src/marvin/thread.py:449-464 简化 —— 三种入参归一
def get_thread(thread):
if isinstance(thread, Thread):
return thread # 已是 Thread,直接用
elif thread is not None:
return Thread(id=thread) # 是个字符串 id,包成 Thread
else:
return get_current_thread() or Thread() # 都没给:用当前上下文,或新建

这让"传 Thread、传 id、或什么都不传"三种调用方式殊途同归——放大工作流时不必到处显式传线程,with thread: 一包,深层代码 get_current_thread() 就能拿到。

6. Memory:向量记忆与自动召回

Thread 是"这次会话说过的话";Memory 是"跨会话记住的事实"。它让工作流放大到有长期记忆

6.1 provider 抽象:一套接口,多种向量后端

Memory(src/marvin/memory/memory.py:40-44)是一个"分区的向量记忆集合",真正的存储委托给一个 MemoryProviderMemoryProvider 是抽象基类(memory.py:22-37),只规定三个方法:add / delete / search

Memory 自己的 add / delete / search 都是薄转发,带上 self.key 作为分区键调 provider(memory.py:111-118)。key 就是这块记忆的命名空间,构造时会校验只含字母数字下划线(memory.py:72-78)。

后端由字符串选择,get_memory_provider(memory.py:143-177)按前缀分派:

provider 字符串后端
chroma-ephemeral / chroma-db / chroma-cloudChroma(内存 / 本地 / 云)
lancedbLanceDB
postgresPostgres(pgvector)
qdrantQdrant

一套 add/delete/search 接口,四种向量库任意换——这是典型的策略模式,把"记忆的语义"和"存在哪"解耦。

6.2 get_tools:把记忆暴露成三个工具

记忆要能被智能体主动使用。Memory.get_tools()(memory.py:120-137)把 add / delete / search 三个方法包成带命名空间的工具:

  • add_memory__{key} —— 存一条新记忆
  • delete_memory__{key} —— 按 ID 删
  • search_memories__{key} —— 按查询串检索(描述里还会拼上该记忆的 instructions)

工具名带 key 后缀,是为了让多块记忆共存时工具名不撞车。回忆 §3.1:Agent.get_tools() 会自动把每块挂载记忆的这三个工具并进去——所以给智能体挂 Memory,它就自动获得了读写这块记忆的能力。

6.3 auto_use:回合前自动召回

除了让模型主动 search,记忆还能自动召回。Memory.auto_use(memory.py:57-62)若为真,编排器会在每轮跑之前替智能体查一次记忆。

这段逻辑在 _check_memories(src/marvin/engine/orchestrator.py:316-337):

# 真实逻辑(orchestrator.py:316-337)简化 —— auto_use 记忆的自动召回
async def _check_memories(self, actor, assigned_tasks):
memories = {m for t in assigned_tasks for m in t.memories if m.auto_use}
if isinstance(actor, Agent):
memories |= {m for m in actor.get_memories() if m.auto_use}
if memories:
recent = await self.thread.get_messages_async(limit=3) # 拿最近 3 条当查询
query = message_adapter.dump_json(recent).decode()
for memory in memories:
result = {memory.friendly_name(): await memory.search(query=query, n=3)}
await self.thread.add_info_message_async( # 召回结果写回线程
result, prefix="Automatically recalled memories")

重点看这条链:收集所有 auto_use 记忆(任务上的 + 智能体上的)→ 用最近 3 条消息当查询串 → 每块记忆各 search 出 3 条 → 把结果作为一条 info 消息写回线程。于是召回的记忆自然进入下一轮的上下文,模型看得到。这是"长期记忆"接回"当前回合"的接缝。

7. 四组部件如何拼成规模化工作流(小结)

把本章串起来,一个放大后的工作流大致这样转:

1. 你给一批 Task(可能带 subtasks/depends_on,或让 marvin.plan 自动拆)
2. Thread 作为持久会话被 with 进上下文;深层代码靠 get_current_thread 取
3. run_tasks 分流:独立 → asyncio.gather 并发;有依赖 → 交编排器串行
4. 编排器每轮:
- get_all_tasks 拓扑收集整张图,按 is_ready 门控挑就绪任务
- 选一个 Actor(Agent,或 Team 委派到 active_member)进作用域
- _check_memories 把 auto_use 记忆召回、写回线程
- 跑一轮回合循环(第 2/3 章),EndTurn 收尾
· 可能是 DelegateToActor:改写 active_member,下轮换人
· 可能是 PlanSubtasks:动态拆更多子任务,回到步骤 4
- DBLLMCall 记这次的 usage;新消息写回 Thread
5. 所有任务 is_complete,工作流结束;用量可按 Thread 汇总

四组部件正交但咬合:Actor 决定谁跑、Task 调度决定按什么顺序跑、Thread 决定说过的话存哪、Memory 决定记住的事怎么回来。回合循环本身(第 2、3 章)不变——变的是它被这四组部件放大成了工作流。

8. 边界与局限(诚实)

  • Team 家族已废弃。 Team.__post_init__ 主动告警,说多智能体协作"功能不完整、不提供有意义的协作",建议改用独立 Agent + 显式协调(team.py:46-52)。本章讲 Team 是为讲清"用 Actor 抽象放大到多智能体"的机制,不代表推荐用它做生产。
  • 多个 EndTurn 时输出类型退化。 当一轮里有多个 EndTurn 工具,get_agentlet 会把输出类型设为 NoneType 并 warning,注释里明说"可能限制多轮场景 / TODO 待重构"(agent.py:182-190)。编排器也因此在独立任务时一次只指派一个(orchestrator.py:152-164)。
  • 并发下的 contextvar reset 异常被吞。 asyncio.gather 并发跑独立任务时,Actor 的 token 可能跨上下文,__exit__ 靠字符串匹配吞掉这一种 ValueError(actor.py:81-89)。这是能用的补丁,但依赖错误信息文本,较脆。
  • 记忆自动召回的查询很粗。 auto_use 召回只拿"最近 3 条消息"拼成查询串、每块记忆取 3 条(orchestrator.py:325-332),没有更细的相关性调参;够用,但不是精细的检索策略。

9. 横向对比 / 延伸

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

主题文件关键符号
Actor 抽象基类、contextvar 作用域src/marvin/agents/actor.pyActor__enter__/__exit__get_currentstart_turn/end_turnget_agentlet
Agent 配置与随机命名src/marvin/agents/agent.pyAgentAGENT_NAMESget_toolsget_modelget_agentlet
Team / Swarm / RoundRobin / Randomsrc/marvin/agents/team.pyTeamSwarmRoundRobinTeamRandomTeamactive_memberdelegatesget_end_turn_tools
委派与拆子任务的 EndTurnsrc/marvin/engine/end_turn.pyDelegateToActorcreate_delegate_to_actorPlanSubtaskscreate_plan_subtasks
拓扑收集与调度分流src/marvin/engine/orchestrator.pyget_all_taskscollect_tasksrun_once_check_memories
任务图字段与就绪判据src/marvin/tasks/task.pysubtasksdepends_onparentis_ready
独立并发 vs 依赖串行src/marvin/fns/run.pyrun_tasks_async_tasks_are_independent
计划拓扑排序与建任务src/marvin/fns/plan.pyplan_asynccreate_tasksPlanTaskvisit(环检测)
Thread 持久会话src/marvin/thread.pyThreadadd_messages_asyncget_messages_asyncget_usage_asyncLLMCallget_threadget_current_thread
持久层表src/marvin/database.pyDBThreadDBMessageDBLLMCallDBLLMCallMessage
向量记忆与 providersrc/marvin/memory/memory.pyMemoryMemoryProviderauto_useget_toolsget_memory_provider