LightScientist:三层架构的 AI 科研系统

用简洁的三层架构支撑长期科研自动化,每层只做一件事。

背景与动机

AI Agent 做科研的核心矛盾:科研任务周期长、步骤多、需要持久会话和人机协作,但单体 Agent 很难同时做好"项目管控"和"具体执行"。

现有方案的常见问题:

  • Prompt 无限增长:历史日志全塞进 context,很快就撑爆上下文窗口。
  • 生命周期边界模糊:取消、挂起、恢复没有清晰的状态机,实现出来是一团乱麻。
  • 项目管控与执行耦合:同一个 Agent 既要管阶段流转,又要读文件跑命令,难以单独测试或替换。

LightScientist 的设计目标很明确:用确定性的第一层管项目状态机,用LLM 辅助的第二层做调度决策,用持久会话的第三层做具体执行。三层之间通过清晰的数据结构通信,不回放历史日志。

核心设计:为什么要三层?

单层 Agent 本质上是一个不断增长的 for-loop:每一轮把历史塞进去,让模型决定下一步。项目周期一长,这种方式必然失效。

三层分离的核心收益:

性质职责
第一层确定性代码项目状态机、阶段流转、交付验证
第二层LLM 辅助监督当前阶段的所有 worker、做调度决策
第三层持久会话具体的 DeepAgent 执行:读文件、跑命令、写产物

每层之间的通信数据结构是刻意设计的,不传原始日志,只传结构化更新:

  • 第一层 → 第二层:任务描述(目标、技能文件路径、要求输出路径)
  • 第三层 → 第二层:状态更新(状态、进度计数、最终结果)
  • 第二层内部:调度事件(只把有意义的变化送给 supervisor agent)

这样第二层的 supervisor agent 每次只收到一个增量事件,而不是全部历史。需要更多上下文时,supervisor 通过工具主动查询。

第一层:研究控制器(ResearchController)

第一层是纯确定性代码,不调用任何模型 API。它只做一件事:维护项目阶段状态机。

阶段表(Stage Table)

科研流程被建模为阶段(stage)的有向图,定义在阶段定义文件中:

idea.survey   → idea.generate → idea.evaluate → idea.gate
                                        ↓
                              idea.probe_batch → idea.probe_collect → idea.gate
                                                                       ↓
                                                        experiment.setup → experiment.loop → experiment.analyze → experiment.gate
                                                                                         ↓
                                                                            paper.plan → paper.figure → paper.write → paper.review → done

每个阶段包含:

  • 技能描述文件路径(第二层按需读取,不内联进 prompt)
  • 要求输出的文件路径(第一层验证交付物时检查)
  • 默认下一阶段
  • 允许的下一阶段列表
  • 是否需要人工审批

第二层如何请求转阶段——层间通信的关键机制

这是整个系统层间协作的核心:

  1. 第三层的 worker 完成当前工作后,调用"完成当前阶段"工具,建议下一个阶段。
  2. 第二层的 supervisor 收到 worker 完成事件后,决策是否结束当前阶段。
  3. 第二层调用"完成当前阶段"工具,向第一层交付阶段结果,并建议下一阶段。
  4. 第一层验证建议的下一阶段是否在允许列表中。验证通过才执行状态转移。

这个机制保证了:LLM 只能"建议"下一阶段,不能"擅自"推进项目。阶段转移的终极控制权在第一层确定性代码手里。

auto / manual 模式的状态转移

项目运行时可以选两种模式,决定 gate 阶段的行为:

auto 模式:gate 阶段自动通过,不需要人工审批。整个流程可以全自动运行。

auto 模式下的完整状态转移流程:

idea.probe_batch → idea.probe_collect → idea.gate(自动通过)
→ experiment.setup → experiment.loop(可多次循环)→ experiment.analyze → experiment.gate(自动通过)
→ paper.plan → paper.figure → paper.write → paper.review(自动通过)→ done

manual 模式:gate 阶段返回等待用户,CLI 发送回复后恢复。适合需要人工把关的关键决策点。

项目级记忆:PROCESS.md

第一层维护 PROCESS.md 作为精简的长期项目记忆。每个阶段成功交付后,第一层向 PROCESS.md 追加一条摘要(约 5-10 行)。后续阶段直接读 PROCESS.md,不需要回放全部日志。

这是三层分离的核心收益之一:prompt 大小不随项目周期增长

暴露给下层的工具

第一层只暴露两个工具给第二层:

  • 完成阶段工具:正常交付当前阶段结果。
  • 请求用户决策工具:需要项目级人工判断时调用。

第二层可以建议 next_stage,但第一层验证后才生效。这避免了 LLM"擅自"推进项目阶段。

第二层:运行时监督器——LLM 辅助的控制层

第二层的搭建逻辑:控制循环本身是确定性的,LLM 只做调度决策。它监督当前阶段的所有 worker,决定何时启动新 worker、何时恢复挂起的 worker、何时结束当前阶段。

Worker 记录与事件队列

每个 worker 有一条完整的运行记录,包含:运行时唯一标识、所属任务、工作目标、状态、会话标识符、挂起模式、进度计数、工作区目录。

第三层发送的状态更新首先进入第二层的原始更新队列。第二层处理时分两步:

  1. 更新函数:更新 worker 记录和缓存结果。
  2. 事件过滤函数:判断是否有意义,决定是否创建调度事件入队。

过滤规则:普通进度更新只更新记录,不入调度队列。这避免了 supervisor 被每一步日志淹没。

调度 Agent

第二层的调度 agent 本身也是一个持久会话(复用第三层的运行机制),但它的工具是运行时管理工具集:获取任务信息、列出所有 worker、获取指定 worker 记录、启动新 worker(非阻塞发射)、恢复指定 worker、取消指定 worker、预定未来直接恢复 worker。

调度 agent 的输入是增量的:每次只接收一个调度事件,而不是全部历史。如果需要更多上下文,通过工具主动查询文件、worker 状态、阶段输出。

输出约定(一行文本):完成、失败、或继续。

设计约束

调度 agent 的 prompt 里刻意加入了行为约束:优先复用已有 worker,少取消,少创建并行 worker。

如何请求第一层转阶段

这是层间协作的核心流程:

  1. 第三层的 worker 完成当前工作后,调用"完成当前阶段"工具,建议下一个阶段。
  2. 第二层的 supervisor 收到 worker 完成事件后,决策是否结束当前阶段。
  3. Supervisor 调用"完成阶段"工具,向第一层交付阶段结果,并建议下一阶段。
  4. 第一层验证建议的下一阶段是否在允许列表中。验证通过才执行状态转移。

这个机制保证了:LLM 只能"建议"下一阶段,不能"擅自"推进项目。阶段转移的终极控制权在第一层确定性代码手里。

第三层:执行运行时——持久 DeepAgent 会话

第三层的搭建逻辑:每个 worker 是有状态的会话,不是一次性函数调用。使用 LangGraph 的持久化机制实现跨轮次会话保持。

持久化会话设计

关键设计:第三层不再是无状态的"调一次模型拿结果",而是有状态的会话。每次模型调用后,LangGraph 自动 checkpoint 会话状态。只要进程活着,同一个线程 ID 可以无限次恢复,会话状态不丢失。

Worker 生命周期状态

状态含义恢复方式
运行中正在执行不需要恢复
等待输入需要外部输入才能继续LangGraph 中断机制 → 恢复时传入答案
后台挂起主动挂起,稍后恢复同一线程 ID 发普通消息
已完成任务完成不可恢复
已失败执行失败不可恢复
已取消被上层取消不可恢复

Worker 工具

每个 worker 处于一个受限的工作区,可以看到标准工具集:文件读写、搜索、任务管理、命令执行。

此外还有生命周期工具(仅 worker 自身可调用):请求输入、主动挂起、取消时整理交付。

关键设计决策

waiting vs background:两种挂起,两种恢复

这是整个系统最重要的设计决策之一。Worker 主动挂起时有两种语义完全不同的场景:

等待输入后台挂起
触发方式请求外部输入主动挂起,稍后检查
含义现在就需要答案才能继续工作已委托出去,稍后检查
恢复方式LangGraph 中断机制 → 恢复时传入答案同一线程 ID 发普通消息

预定恢复工具 允许 supervisor 现在做决定、写好未来要发的消息,到时间后第二层直接 resume worker,不再先问 supervisor。

事件驱动的监督

第三层的每一次状态变化都通过状态更新向上传递,但第二层不会把所有更新都送给 supervisor agent。

第三层 状态更新
  → 第二层 更新 worker 记录
  → 第二层 过滤并创建调度事件
  → Supervisor 队列  supervisor 空闲时每次只处理一个事件

普通 running → running 的进度更新只更新记录,不入 supervisor 队列。Supervisor 不会被每一步日志淹没。

协作式取消

取消从第二层流入第三层,是协作式的:

  1. 第二层调用取消接口
  2. 第三层尝试让 worker 自己调用取消完成工具整理交付
  3. Worker 保留有用产物(交付记录、调试日志、工作区文件)
  4. 如果整理超时,运行时返回兜底结果
  5. 如果有正在运行的子进程,终止进程组
  6. 会话从运行时状态中删除,后续无法恢复

Python 线程不能被强制杀掉(不安全),所以取消是协作式的:先请求 worker 整理交付,超时后才做清理。

会话持久化与 LangGraph Checkpoint

第三层 DeepAgent 会话的持久化依赖 LangGraph 的 MemorySaver

  • 每次模型调用后,LangGraph 自动 checkpoint 会话状态
  • 同一个 thread_id 可以跨多次 start / resume 调用保持会话
  • interrupt() 是 LangGraph 的原生机制,用于实现 waiting

当前实现是纯内存版:进程结束后会话丢失。磁盘持久化是已知未完成项。

文档记忆而非向量记忆

LightScientist 刻意不使用向量数据库做长期记忆。原因:

  • 科研项目的上下文是结构化的(阶段输出、实验记录、论文草稿)
  • 文档比向量检索更可控、更易调试
  • PROCESS.md 作为项目级记忆,agent-run.md 作为 worker 级交付记录

每层 prompt 只注入当前需要的信息,而不是无限增长的对话历史。这是三层分离的核心收益之一。

研究工作流:阶段状态机

项目级流程:idea → experiment → paper → done

每个阶段有允许的下一阶段列表。跨 phase 的转移必须经过 gate 阶段:

idea.gate      → experiment.setup   (需要审批或 auto 模式)
experiment.gate → paper.plan       (需要审批或 auto 模式)
paper.review    → done                (论文评审通过后结束)

auto 模式的状态转移

auto 模式下,gate 阶段自动通过,整个流程可以全自动运行:

idea.survey → idea.generate → idea.evaluate → idea.gate(自动通过)
                                                  ↓
                                    idea.probe_batch → idea.probe_collect → idea.gate(自动通过)
                                                                               ↓
                                                                    experiment.setup → experiment.loop(可多次循环)→ experiment.analyze → experiment.gate(自动通过)
                                                                                                                                     ↓
                                                                                                                        paper.plan → paper.figure → paper.write → paper.review(自动通过)→ done

Phase 内的循环是自由的:例如 experiment.loop 可以多次执行,每次 supervisor 根据实验结果决定继续 loop 还是进入 experiment.analyze

manual 模式的状态转移

manual 模式下,gate 阶段返回等待用户,CLI 发送回复后恢复:

idea.gate → 返回 waiting_user → CLI 发送 --reply y/n → 恢复执行

第二层可以建议下一阶段,但第一层验证后才生效。这避免了 LLM"擅自"推进项目阶段。

可观测性:事件流

LightScientist 有一个侧通道事件流,用于观察 Agent 行为(不驱动控制流)。

核心组件

  • 事件数据结构:包含层、类型、消息、任务ID、worker ID、阶段、数据、时间戳
  • 事件总线:事件分发器
  • JSONL事件写入器:写入 .lightscientist/events.jsonl
  • 终端事件输出器:--watch 模式终端实时输出

事件覆盖

事件类型
第一层stage start / finish / transition、user decision
第二层worker created / status、supervisor event / decision、scheduled resume、stall detected
第三层session start/end、model call/output、tool call/result、waiting/background/cancelled

使用示例

# 实时观察 Agent 行为
PYTHONPATH=src python -m esnext run "你的工作目录是什么" --agent --watch
PYTHONPATH=src python -m esnext research "复现某篇论文" --mode auto --stage experiment.setup --watch

事件流只观测行为,不暴露隐藏推理。这符合可观测性的基本原则:能看到 Agent 做了什么,但不需要看到每一次内部思考。

当前限制与未来工作

以下限制是刻意的,当前系统优先保证清晰的层边界,而不是更多功能:

  • 第三层会话纯内存:进程结束即丢失,不支持跨进程恢复。未来可接入 LangGraph SqliteSaver 或 PostgresSaver。
  • 顶层 CLI 不暴露通用会话恢复管理:恢复能力只在内层验证,不作为顶层产品接口。未来可暴露 listresume <agent_id> 等命令。
  • 第一层是确定性代码,不是 LangGraph 控制器:当前第一层是纯 Python 状态机。未来可将其也 LangGraph 化,支持更复杂的 stage 流转逻辑。
  • 项目记忆是文档式,不是向量式:当前用 PROCESS.md 做项目级记忆。对于非常大的项目,未来可引入向量检索作为补充。

总结

LightScientist 的三层架构可以概括为:

CLI / 用户
  → 第一层(ResearchController,确定性)
    → 第二层(RuntimeSupervisor,LLM 辅助)
      → 第三层(ExecutionRuntime,DeepAgent 会话)

核心设计准则

  1. 关注点分离:每层只做一件事,边界清晰,可独立测试和替换。
  2. 增量通信:不回放历史,只传递有意义的事件和摘要,prompt 大小不随项目周期增长。
  3. 生命周期管理waitingbackground 刻意区分,取消是协作式的。
  4. 可观测性:事件流独立,侧通道设计,不影响控制流。
  5. 文档优先的记忆:结构化文档(PROCESS.md、阶段输出文件)而非向量检索。

项目地址:E:/LightScientist