分析对象:PKU-YuanGroup/OpenAI4S(Open AI for Scientist)
基线 commit:85e9fa0(Feat/retrosynthesis workflow triage #70,85e9fa062c354b2ea9a40a668f03669a5452e56d)
许可:MIT
产品一句话:用火山方舟 / 豆包 ¥9.9 套餐复刻 Claude Science 体验的开源混合式科研智能体——原生 JSON 工具做编排与权限控制平面,持久 Python/R 内核做科学执行平面;核心零第三方依赖(pure stdlib)。
读者对象:已经读过本系列 Claude Code / Open Design / Codex / OpenManus 至少一份分析的产品经理与资深工程师;希望把「科研 Agent」从口号落到可核对源码的人。
配套教程:从零构建OpenAI4S-开发全流程教程.md · HTML
本地基线路径:参考项目/OpenAI4S @ 85e9fa0
读前约定
| 约定 | 说明 |
|---|---|
| 引用格式 | 重大主张尽量落到 文件:行号 或至少 文件 + 符号名,便于回源码核对 |
| 术语 | 控制平面 = provider-native JSON Tool;科学平面 = Python/R Cell;Host = 内核内 host 单例背后的编排信封 |
| 完成信号 | 非科学回合 → Engine 自有 finalize_response;科学 Python Cell → 唯一内部完成信号 host.submit_output(...) |
| HTML | 配套站:OpenAI4S-解析.html(由 build-html.py 生成) |
目录
Part I · 它是什么
1. 一句话定位:JSON 控制平面 + Python/R 科学平面
2. 仓库地图与技术栈
3. 与 Claude Science / CodeAct / ReAct 的关系
Part II · 双平面混合引擎
4. AgentEngine 外循环
5. route_action 路由铁律
6. finalize_response vs host.submit_output
7. Action Ledger 与 Store SQLite 概览
Part III · Host · Kernel · Tools
8. host singleton 与 host_dispatch 编排信封
9. LazyKernel、沙箱与环境 allowlist
10. tools/ 类目录与只读并行波次
11. llm/:ark|openai|anthropic|gemini over urllib
Part IV · Web 工作台
12. serve → gateway.SessionRunner
13. WebSocket /api/v1/ws 事件面
14. 一次用户提示词的完整路径(核心时序)
15. Notebook 只读默认、Artifacts、Branch/Revert
Part V · Skills · Compute · Security
16. Skills:34 份代码食谱,非 JSON schema
17. BYOC 与 openai4s_compute_provider
18. 权限、审批持久化、biosecurity、egress
Part VI · 内核深挖与开发者品味(本章是全文重心之一)
19. 内核协议:dup2、帧上限与「错误比没有更糟」
20. 十条开发选择:反推搭建思维与品味
Part VII · 横向对比与总结
21. 对比表:Claude Code | Open Design | Codex | OpenManus | OpenAI4S
22. 五个独特设计决策与诚实边界
23. 源码导览索引
Part I · 它是什么
第 1 章 一句话定位:JSON 控制平面 + Python/R 科学平面
1.1 它不是又一个 ReAct 编程助手
OpenAI4S(Open AI for Scientist)把自己钉在一个很窄、也很硬的产品位上:
原生 JSON Tool Call 负责编排与权限;持久 Python/R Code-as-Action 内核负责科学执行。
官方中文 README 的口号是「💸 9.9 元豆包 API 复刻 Claude Science」(README_zh.md:7-10)。工程上它不是「Claude Code 的科研皮肤」,也不是「再写一个带 shell 工具的 ReAct」。它刻意拆成两个永不在同一步竞争的动作通道(docs/architecture.md:3-20):
| 平面 | 动作单元 | 适合干什么 | 不适合干什么 |
|---|---|---|---|
| JSON 控制平面 | 一个有序原生工具批次,或单独的 FinalizeAction |
权限、元数据、外部服务、工作流控制、会话分叉/回退 | 十万行 DataFrame 分析、仿真循环、图绘制流水线 |
| Python/R 科学平面 | 恰好一个完整 fenced Cell | 计算、探索、分析、仿真、长时任务;Python 可中途同步 host.* RPC |
把「读文件 / 搜网页 / 改权限」拆成十几次 tool_use 往返 |
README 里那张对照表写得很直白(README_zh.md:52-68):同样「找匹配文件 → 排序 → 读 CSV → 画图」的活,ReAct 可能要 ~14 次往返;OpenAI4S 压成一个代码 cell,大对象留在内核内存,上下文里只剩一句 "<DataFrame 100000×20>"。
1.2 产品经理视角:它在卖什么体验
| 卖点 | 工程落点 |
|---|---|
| ¥9.9 豆包 / 火山方舟 | llm/capabilities.py:206-221 内置 ark provider,wire="openai",默认 doubao-seed-2.0-pro |
| Claude Science 级科研工作台 | 静态 WebUI(无打包)+ Notebook 投影 + 版本化 Artifacts + 审批卡 |
| 开源、可自托管 | MIT;daemon 默认绑 127.0.0.1:8760 |
| 核心不拖科学栈 | pyproject.toml:9 dependencies = [];numpy/pandas 走 optional science extra |
🧠 一句话:别家在优化「怎么把 bash / 编辑器工具调得更聪明」;OpenAI4S 在优化「怎么让模型写完一段真的科学代码,并在内核里把状态留下」。
flowchart LR
U["科学家 / 用户提示"] --> CP["① JSON 控制平面
permissions · metadata · web · MCP · remote"]
U --> SP["② Python/R 科学平面
persistent kernel · host RPC · artifacts"]
CP -->|"route 铁律:native > finalize > one cell"| ENG["AgentEngine 外循环"]
SP --> ENG
ENG --> DONE["完成信号
finalize_response 或 host.submit_output"]
第 2 章 仓库地图与技术栈
2.1 数字化的项目形状(基线 85e9fa0)
| 指标 | 量级(约) |
|---|---|
| 包内 Python | openai4s/ ≈ 255 个 .py,约 114k LOC |
| 测试 | tests/ ≈ 286 个 test_*.py(默认离线门禁) |
| WebUI | server/webui/app.js ≈ 9.7k 行;无 bundler |
| Gateway | server/gateway.py ≈ 12.9k 行(HTTP + 手写 WS 组合门面) |
| 内置 Skills | 34 个 skills/*/SKILL.md |
| 控制工具 | TOOL_TYPES 70 个 Tool 子类(tools/registry.py:118-189) |
| 核心依赖 | 0(pyproject.toml:9) |
OpenAI4S/ # commit 85e9fa0 · MIT
├── openai4s/ # 产品主体(stdlib)
│ ├── agent/ # 外循环:engine · actions · finalize · ledger · loop
│ ├── kernel/ # 科学平面:manager · worker · lazy · r_kernel · sandbox 对接
│ ├── host/ + host_dispatch.py # Host 能力服务 + 共享编排信封
│ ├── sdk/host.py # 注入到 Python 内核的 host 单例门面
│ ├── tools/ # 70 个 native Tool 子类 + registry
│ ├── llm/ # urllib 传输 + openai/responses/anthropic/gemini 线
│ ├── server/ # gateway · agent_run · branching · webui/
│ ├── security/ # sandbox · permissions · biosecurity · injection
│ ├── skills_loader/ # SKILL.md 发现与渐进披露
│ ├── store.py + storage/ # 单连接 SQLite + 仓储
│ ├── compute/ # BYOC 主机侧
│ ├── egress.py # 出站域名 allowlist
│ └── …
├── openai4s_compute_provider/ # 远端 GPU 上跑的 stdlib 沙箱 SDK
├── openai4s_worker_runtime/ # worker 运行时附属包
├── skills/ # 34 份代码食谱(非 JSON tool schema)
├── envs/ # conda 环境配方
├── workflows/ # 科学工作流 benchmark 清单
├── harness/ # 脚本化场景 / golden(在生产 import 图之外)
├── docs/ # architecture · security · compute · webapp…
├── pyproject.toml # dependencies=[] ;science/chemistry extras
├── setup.sh / start.sh
└── tests/ # 离线默认;external/network/live_llm 需显式 -m
2.2 技术栈选择:为什么是「纯标准库」
| 层 | 选型 | 源码锚点 |
|---|---|---|
| 包管理 / 运行 | uv + setuptools |
pyproject.toml;./setup.sh、./start.sh |
| Agent 核心 | 纯 Python stdlib | agent/engine.py 无第三方 import |
| LLM 客户端 | urllib.request |
llm/transport.py:27-28 |
| HTTP 服务 | http.server |
server/gateway.py 模块头注释 |
| WebSocket | 手写帧编解码 | gateway.py 顶部:GET /api/v1/ws |
| 前端 | 静态 app.js / index.html / style.css |
无 build step(CLAUDE.md:100) |
| 持久化 | SQLite 单连接 | store.py |
| 可选科学栈 | uv sync --extra science |
pyproject.toml:36-41(numpy/pandas/matplotlib/sklearn) |
| 可选化学 | --extra chemistry(RDKit) |
pyproject.toml:45-47 |
硬约束写在 CLAUDE.md:90(核查修正 2026-08-04:原稿写 :11,但所引 "Never add a hard third-party import to the core" 这句真实位置是 :90;:11 是相关但不同的一句 "Core is zero-dependency by design")与 pyproject.toml:9:Never add a hard third-party import to the core. 科学库可以出现在 agent cells 里(内核继承 venv),但引擎本身必须 try/except ImportError 守护每一次 in-tree 使用。
2.3 两个入口,同一台引擎
flowchart TB
subgraph CLI["CLI 路径"]
RUN["openai4s run '…'"] --> AGENT["agent/loop.py · Agent.run"]
AGENT --> LK["LazyKernel"]
AGENT --> LAE["LocalActionExecutor"]
end
subgraph WEB["Web 路径"]
SERVE["openai4s serve / start.sh"] --> GW["server/gateway.py"]
GW --> SR["SessionRunner._loop"]
SR --> WAE["WebActionExecutor"]
SR --> WES["WebEventSink"]
end
AGENT --> ENG["AgentEngine.run while"]
SR --> ENG
ENG --> RA["route_action"]
CLI 用 Agent.run(agent/loop.py:352-447)组装 LazyKernel + LocalActionExecutor + ChatModel;Web 用 SessionRunner._loop(gateway.py:6337-6447)组装同一 AgentEngine,只是把执行器与事件投影换成 Web 版本。这是全文最重要的架构对称性:一个引擎,两层薄适配器(actions.py:3-6 自称 CoreCoder-style)。
第 3 章 与 Claude Science / CodeAct / ReAct 的关系
3.1 三组对照
| 范式 | 典型形态 | OpenAI4S 怎么站队 |
|---|---|---|
| ReAct | Thought → Action(tool) → Observation,工具原子、无中途回调 | 控制平面借用其「结构化工具」;科学平面拒绝把分析拆成原子 tool |
| CodeAct | 模型主要产出可执行代码 | 科学平面就是 Code-as-Action;但外循环仍保留 JSON 控制通道 |
| Claude Science(闭源) | 科研工作台 + 强模型 + 闭源运行时 | 产品体验对标;实现上用开源双平面 + ¥9.9 Ark 替代 |
3.2 关键主张:为什么「没有注册 shell 工具」
tools/registry.py:115-117 与 :192 写死:
- 故意 NO shell tool;
_FORBIDDEN_CONTROL_NAMES = frozenset({"bash", "submit_output"});register_tool遇到它们直接ValueError(:211)。
Shell 只能发生在 Python 内核里:host.bash(...)(sdk/host.py:854-865)。这不是风格偏好,而是平面分离的承重墙——一旦 shell 变成 JSON tool,模型就会退回 ReAct 式「一步一命令」,十万行分析再次被拆碎。
3.3 与「纯 tool_use」的关键差分:中途 Host RPC
docs/architecture.md:56:
This inner RPC loop does not exist in a
tool_usearchitecture — there, actions are atomic and never call back into the host mid-execution.
Python Cell 执行中,host.llm / host.delegate / host.compute 走独立于 stdout 的 host_call → host_ack → host_response 通道;Cell 阻塞、Host 服务、Cell 恢复。这是 Code-as-Action 相对 ReAct 的结构性优势,不是提示词技巧。
sequenceDiagram
participant M as Model
participant E as AgentEngine
participant K as Python Kernel
participant H as HostDispatcher
M->>E: assistant reply (fenced python)
E->>K: execute one CodeCell
K->>H: host_call(llm / compute / …)
H-->>K: host_response
K->>H: host.submit_output(...)
H-->>E: completion signal
E-->>M: stop_reason=submitted
Part II · 双平面混合引擎
第 4 章 AgentEngine 外循环
4.1 状态机本体只有 ~143 行
AgentEngine(agent/engine.py:34-143)是 provider-neutral 的外循环。它不 import 具体 kernel、dispatcher、store、server——那些都是 ports,由 CLI / Web 适配器注入(docs/architecture.md:58-59)。
核心 run while(engine.py:60-108)可以读成:
state ← RunState(messages, max_turns)
emit RunStarted
while turn < max_turns:
if cancelled → finish("cancelled")
if completion.completion() → finish("submitted") # 例如已有 submit_output
prepare context → model.complete(stream deltas)
append assistant message
action ← route_action(content, tool_calls)
outcome ← executor.execute(action, reply, state)
append history_messages; turn++
if outcome.stop_reason → finish(that)
if outcome.completion or completion port → finish("submitted")
finish("max_turns")
默认 max_turns=32(engine.py:47);Web 会话常从 cfg.max_turns 取(gateway 侧默认常见为 12,explore 可抬高,gateway.py:6349-6351)。
4.2 端口化的好处
| Port | 作用 | 默认实现 |
|---|---|---|
ModelPort |
调模型 | CLI/Web 的 ChatModel |
ActionExecutor |
执行 route 出的动作 | LocalActionExecutor / WebActionExecutor |
ContextPolicy |
压缩上下文 | CompactionPolicy / PassthroughContext |
EventSink |
投影事件 | Transcript / WebEventSink / Null |
CancellationPort |
Stop | NeverCancelled / EventCancellation |
CompletionPort |
侦测 submit_output |
CompletionSignal(dispatcher.last_output) |
🧠 产品含义:测试可以在不启动 daemon、不 spawn 内核的情况下,单独验证「取消 / 最大回合 / finalize / 路由优先级」。这是工程成熟度信号,不是过度抽象。
stateDiagram-v2
[*] --> Running: RunStarted
Running --> ModelCall: TurnStarted
ModelCall --> Routing: ReplyReceived
Routing --> Executing: ActionRouted
Executing --> Running: OutcomeProduced (continue)
Executing --> Submitted: stop_reason / completion
Running --> Cancelled: cancellation
Running --> MaxTurns: turn == max_turns
Submitted --> [*]
Cancelled --> [*]
MaxTurns --> [*]
第 5 章 route_action 路由铁律
5.1 单通道决策
route_action(agent/actions.py:165-185)是双循环共用的唯一动作决策点:
# 伪代码忠实于 actions.py:176-185
calls = normalize(tool_calls)
if len(calls) == 1 and calls[0].name == "finalize_response":
return FinalizeAction(calls[0])
if calls:
return NativeToolBatch(calls) # 任意原生调用压过代码
return extract_action(content) # 第一个完整 python/r fence
铁律可以背成三句:
- 有结构化原生调用 → 绝不跑代码(控制平面不能和科学平面抢同一回合);
- 唯一且名为
finalize_response→ FinalizeAction(混在其他 tool 里不算完成); - 否则 → 文档顺序上第一个完整的 python/r Cell(一步一格;未闭合 fence 不可执行)。
FINALIZE_RESPONSE_NAME 字面量放在 routing 边界(actions.py:40-43),刻意不 import registry——避免 actions 模块沾上工具注册副作用。
5.2 动作类型代数
| 类型 | 定义位置 | 含义 |
|---|---|---|
CodeCell |
actions.py:46-51 |
language ∈ {python,r} + code |
NativeToolCall |
:55-71 |
无损保留 raw_arguments / parse_error / provider_meta |
NativeToolBatch |
:74-78 |
有序元组 |
FinalizeAction |
:81-90 |
Engine 自有终态声明 |
Action |
:93 |
以上三者的并集 |
extract_action(:146-162)只认:
- Python:
info ∈ {"", "python", "py"}(裸 ``` 默认 python); - R:必须显式
```r。
5.3 隐藏「纯完成 Cell」
is_completion_only_cell(actions.py:96-143)用 AST 判断:若 Python Cell 整段只是 host.submit_output(...) 且参数里没有嵌套计算,则 Web Notebook 隐藏该格——完成是真实 RPC,但不是科学分析,不应污染只读 Notebook(docs/architecture.md / CLAUDE.md 用户可见完成投影约定)。
flowchart TD
R["Model reply"] --> Q1{"有 native tool_calls?"}
Q1 -->|否| EX["extract_action → 至多一个 Cell"]
Q1 -->|是| Q2{"恰好 1 个且名=finalize_response?"}
Q2 -->|是| F["FinalizeAction"]
Q2 -->|否| B["NativeToolBatch(整批)"]
EX --> C["CodeCell | None"]
B --> NOTE["代码 fence 即使存在也被忽略"]
第 6 章 finalize_response vs host.submit_output
这是整份分析里最容易被「工具列表截图」误导的一章。
6.1 finalize_response:Engine 自有,不是 Tool 注册项
agent/finalize.py:1-12 开宗明义:
finalize_responseis deliberately not a control-planeTooland is never registered inopenai4s.tools.registry.
它做什么:
| 机制 | 位置 | 行为 |
|---|---|---|
| 封闭 schema | finalize.py:35-83 |
要求 summary + completion_bullets;可选 findings/metrics/artifacts… |
| 提供给模型的 spec | finalize_response_tool_spec() :92-111 |
metadata-only ToolSpec |
| 拼进工具目录 | with_finalize_response() :114-123 |
追加 spec;若目录里已有同名 → 抛错(防止插件冒充终态) |
| 路由 | route_action |
仅当它是唯一 native call 时变成 FinalizeAction |
科学 Python Cell 不能用它替代内部完成:description 明文写了 does not replace host.submit_output(:106-107)。
6.2 host.submit_output:Cell 内唯一完成信号
sdk/host.py:803-827:
host.submit_output(output, completion_bullets, output_schema=None)
→ HostDispatcher CompletionService
→ dispatcher.last_output
→ AgentEngine CompletionPort 侦测到 → stop_reason="submitted"
约束:completion_bullets 必须 1–4 条「已完成动作」短语;可选 output_schema 校验失败则 soft-fail {"error":...} 让模型重试。
6.3 对照表(务必记住)
finalize_response |
host.submit_output |
|
|---|---|---|
| 谁拥有 | Engine(finalize.py) |
Kernel 内 Host RPC |
是否在 TOOL_TYPES |
否 | 否(且禁止注册) |
| 何时用 | 对话 / 纯工具回合收尾 | 科学 Python Cell 收尾 |
| 能否与其它 tool 同回合 | 不能(必须 sole) | N/A(在代码里调用) |
| R Cell | 不从 R 内发出 | R 无 Cell 内完成信号 |
| 普通 prose / 最大回合 | 不是完成 | 不是完成 |
flowchart LR
subgraph NonSci["非科学回合"]
T["Native tools…"] --> F["sole finalize_response"]
F --> CR["CompletionRecord"]
end
subgraph Sci["科学 Python Cell"]
CELL["```python …"] --> SO["host.submit_output"]
SO --> CR2["CompletionRecord 同源结构"]
end
CR --> UI["Gateway 投影:output + bullets + artifact delta"]
CR2 --> UI
第 7 章 Action Ledger 与 Store SQLite 概览
7.1 Ledger:先记账,再执行
agent/ledger.py 的模块文档(:1-12)说明设计目标:
- storage 仓储不懂 agent 语义;
RuntimeActionLedger把 typedAgentEngine事件翻译成不可变 groups/events;- Native 声明与结果原子归约;崩溃时用规范错误/取消结果补齐半开 batch,绝不把半开 tool batch 回灌给 LLM。
CLI 在 Agent.run 里构造 ledger(loop.py:405-409, 463+);Web 在 SessionRunner._loop 把 action_ledger 传入 WebEventSink(gateway.py:6343-6377)。
引擎是 ledger-first(docs/architecture.md:59-64):打开 append-only action group → 执行 → 以 tool result / Cell milestone 关闭 → 终态追加而非从 UI transcript 反推。
7.2 Store:一进程一连接的真相源
store.py 持有唯一 SQLite 连接与 schema/migration。基线可见的核心表包括(节选,store.py:114+ 与 storage/):
| 表 | 职责 |
|---|---|
frames |
会话 / turn 帧(树状深度、模型、token) |
messages |
聊天消息投影 |
execution_log |
执行日志 |
artifacts / artifact_versions |
版本化产物 |
lineage_edges |
数据血缘 |
host_call_log |
Host RPC 审计 |
permission_rules / permission_requests |
持久审批 |
plans |
计划模式 |
annotations / annotation_admissions |
图像批注与客户端 admission |
compute_jobs / compute_job_events |
远程算力作业 |
session_branches / session_checkpoints |
分支与检查点(storage/snapshots.py) |
kernel_generations |
内核世代(storage/kernels.py) |
recovery_journal |
恢复日志 |
settings / connectors / memories / shares |
配置、连接器、记忆、分享 |
Agent 侧通过 host.query 只读暴露 SQL(CLAUDE.md / architecture),写路径一律走服务与仓储。
erDiagram
frames ||--o{ messages : contains
frames ||--o{ artifacts : produces
artifacts ||--o{ artifact_versions : versions
artifact_versions ||--o{ lineage_edges : lineage
frames ||--o{ permission_requests : asks
frames ||--o{ session_checkpoints : checkpoints
session_branches ||--o{ session_checkpoints : head
frames ||--o{ kernel_generations : generations
frames ||--o{ compute_jobs : remote
Part III · Host · Kernel · Tools
第 8 章 host singleton 与 host_dispatch 编排信封
8.1 内核里的 host
sdk/host.py(约 1177 行)是注入 Python worker 的兼容门面。架构文档列出的能力面(docs/architecture.md:77-88)包括:
- 网络:
web_search/web_fetch/web_download - 文件系统:workspace-jailed 的 read/write/edit/grep/glob/list
- 模型与子代理:
llm/delegate/collect - 科学 API:
science.* - 远程算力:
compute.*/fold - 产物:
save_artifact/artifacts/view_image - 技能 / 环境 / MCP / 只读 SQL
- 完成:
submit_output - Shell:
bash(仅内核内)
8.2 HostDispatcher:共享编排信封,不是上帝类实现桶
HostDispatcher(host_dispatch.py:646+)注释写清定位:
Backs control tools and worker host.* RPC. One instance per session.
__call__(method, args)(:996)是统一入口。能力实现拆到 openai4s/host/* 服务:files、llm、completion、data、delegation、remote_science、progress、skills、mcp、endpoints、credentials…(模块 import 区 :26-51)。
软失败契约:handler 可返回单键 {"error": msg};worker 转成 RuntimeError。未捕获异常由 manager 同样压成 error 字典上线。
控制平面的 native Tool execute() 与内核 host.* 共用同一 dispatcher——权限、egress、注入筛查、activity step、审计日志共享(docs/architecture.md:142-147)。
flowchart TB
subgraph Surfaces["调用表面"]
NT["Native Tool.execute"]
HC["kernel host.* RPC"]
end
NT --> HD["HostDispatcher.__call__"]
HC --> HD
HD --> PERM["permissions / approval"]
HD --> AUD["audit / replay / injection"]
HD --> SVC["host/* services"]
SVC --> STORE[(SQLite Store)]
SVC --> LLM["llm.chat"]
SVC --> NET["webtools + egress"]
第 9 章 LazyKernel、沙箱与环境 allowlist
9.1 LazyKernel:工具回合不白白 spawn
LazyKernel(kernel/lazy.py:15-22):
Create a worker only when code first needs an interpreter. Control-tool and structured-finalization turns can carry this object without spawning a process.
Agent.run 组装方式(loop.py:385-389):factory + skill bootstrap + foreground publish。属性 spawned / generation / execute / shutdown 都围绕「线程安全的一次性所有权」。
R 内核是兄弟通道:r_kernel.py + r_worker.R,同一 manager 协议;通过 fd3/fd4 走帧,避免 print 污染协议(CLAUDE.md)。
9.2 OS 沙箱:Seatbelt / bubblewrap
security/sandbox.py 头注释(:7-8):
- macOS →
sandbox-exec(Seatbelt) - Linux →
bwrap(bubblewrap)
模式:OPENAI4S_KERNEL_SANDBOX=auto|enforce|off。auto 自检失败则可见降级;enforce 失败即关闭。写入限制在 workspace/private temp;默认拒绝对外原始网络(architecture / CLAUDE)。
9.3 子进程环境 allowlist
spawn 时 worker 环境从严格 allowlist 重建,而不是 os.environ.copy()——防止 daemon 的 provider/API/cloud secrets 与 loader 注入变量泄漏进 Python/R 及其子进程(docs/architecture.md:111-113,kernel/environment.py)。
9.4 host.bash 的一击令牌
CLAUDE.md 强调:host.bash 仍在内核本地执行,但子进程启动需要绑定 command hash、cwd、active worker generation、challenge 的一次性 Host token;Host 授权/审计,永不在 daemon 进程里执行 shell。
flowchart LR
CELL["Python Cell"] --> BASH["host.bash(cmd)"]
BASH --> TOK["one-shot Host token"]
TOK --> AUTH["BashAuthorizationService"]
AUTH -->|ok| SUB["subprocess inside worker"]
AUTH -->|deny| ERR["soft error"]
SUB --> SANDBOX["Seatbelt / bwrap wrapper"]
9.5 内核协议的硬纪律(读 worker 注释才能懂的品味)
kernel/worker.py 文件头不是装饰性 docstring,而是一份协议设计说明书。把它当成「开发者在怕什么」来读:
| 机制 | 怕什么 | 做法 |
|---|---|---|
| dup2 换轨 | C 扩展 / 杂讯 print 污染协议线 |
真协议 stdin/stdout 挪到高位不可继承 fd,并发布在 sys._openai4s_protocol_*;dup2(2,1) 让 fd1 别名到 stderr(worker.py:86-119) |
| 两把锁 | 帧交错 / 读到别人的 response | _PROTOCOL_WRITE_LOCK(写帧)+ _HOST_CALL_LOCK(整段 host_call 事务)(worker.py:12-15) |
| 15MB host_call 上限 | 一次 RPC 拖垮 daemon | _HOST_CALL_WIRE_CAP = 15_000_000 |
| stdout 分块 64KB | print("x"*2e8) 一次变 200MB JSON 行 |
_MAX_CHUNK_CHARS = 64_000(worker.py:47-51) |
| 帧字节上限按 UTF-8 最坏情形推导 | CJK / emoji 把「字符上限」撑破「字节上限」 | _JSON_WORST_BYTES_PER_CHAR = 12(代理对);注释明确写:六字节推演被测试打脸(worker.py:52-77) |
| SIGINT 纪律 | 用户 raise KeyboardInterrupt 与真信号混淆 |
one-shot handler + _in_user_code + _sigint_delivered |
| crash recovery | 读阻塞中 fd 身份漂了 | 每次读前后核对 (st_dev, st_ino);预算一次 os.dup(reserve) 重建 |
flowchart LR
subgraph Worker["worker.py 进程"]
CODE["用户 Cell / C 扩展"] -->|fd1 已 alias| STDERR["stderr 捕获"]
HOST["host.* SDK"] -->|高位 fd| PROTO["protocol JSON lines"]
HOST --> LOCK["_HOST_CALL_LOCK"]
LOCK --> CALL["host_call 帧"]
end
subgraph Manager["manager.py"]
CALL --> READ["单 reader 循环"]
READ --> DISP["HostDispatcher"]
DISP --> RESP["host_response"]
RESP --> LOCK
end
品味观察:他们不怕把协议写成「难读」——他们怕的是静默错乱。宁可注释写满「为什么这个数字是 12 不是 6」,也不愿意让一条 CJK 输出把 error_lineno/usage 整帧丢掉。
9.6 Artifact 环境指纹:绑定 kernel generation,不是 daemon
CLAUDE.md 写得很重的一句话(大意):
Artifact 的环境 provenance 来自产生文件的那个内核 generation,不是 daemon 进程的零参数冻结。曾经把 R Cell 产物盖上 Python 包列表——错误的 provenance 比没有更糟,因为它会被相信。
这是整份代码库反复出现的认识论:
| 坏味道 | OpenAI4S 的纠正 |
|---|---|
| 静默成功 / 假绿 | harness:声明应失败的场景若成功则判失败 |
| 假 provenance | 记「为何缺失」而不是借 daemon 的包列表 |
| stub 形状进契约 | stubbed_backend marker 暂停 schema recorder |
| 审批重启后重放参数 | 只记决议,要求 Continue/replan |
9.7 R 通道的「同协议、不同完成语义」
R 复用同一 Kernel manager 与帧合同,但:
- 协议走 fd3/fd4,杂讯进 stderr(shell 重定向等价于 Python 的 dup2);
- 没有 mid-cell
hostRPC,也没有 Cell 内submit_output; - 完成叙事仍偏 Python finalize / 后续 Python cell。
品味:不是「两个并列完整运行时」,而是「一个科学平面主通道 + 一个统计/绘图兄弟通道」。产品诚实标注差异,而不是假装对称。
第 10 章 tools/ 类目录与只读并行波次
10.1 TOOL_TYPES:70 个具名 Tool 子类
tools/registry.py:118-189 是唯一组合根。按域粗分:
| 域 | 代表工具 |
|---|---|
| 文件 | ListDirectory / ReadTextFile / WriteFile / Glob / ContentSearch / Edit |
| 环境 | EnvList / EnvUse / EnvCreate |
| Web | WebSearch / WebDownload / WebFetch |
| 科学库 | ScienceListDatabases / ScienceSearch(背后归一化 UniProt/PDB/…) |
| Skills | Search / Load / Status / History / Rollback |
| Artifacts | List / Metadata / Versions / Save / Restore |
| 会话控制 | Query / Frames / Lineage / Todos / Plan / Review / Checkpoint / Fork / RevertPreview / Permissions |
| 委托 | Delegate / ListChildren / Collect / Stop / SendMessage |
| 后台执行 | Submit / List / Peek / Interrupt |
| MCP | ListServers/Tools/Resources/Prompts + Call |
| 网络放行 | RequestNetworkAccess |
| 远程算力 | RemoteGPU / Register / Submit / Status / Result / Cancel / Close |
| 动态工具 | Define / List / Promote / Versions / Activate / Rollback |
没有:bash、submit_output、finalize_response。
10.2 只读并行波次
agent/control.py:
tool_parallel_policy(:152+)识别只读调度;_execute_read_only_waves(:167+)按资源键冲突分波;- 第一个 mutating / unknown 调用是屏障,之后串行;
- 结果按 provider 原始顺序写回历史(
docs/architecture.md:149-153)。
这让「连读多个文件 / 多库检索」降延迟,又不破坏 canonical tool group 顺序。
10.3 科学库不膨胀 tool 数量
只有 science_list_dbs + science_search 两个控制工具;连接器服务在背后归一化多个公共数据库,并附带 provenance envelope(时间戳 + 请求 + SHA-256)(architecture 后半)。Cell 内对应 host.science.*。
第 11 章 llm/:ark \| openai \| anthropic \| gemini over urllib
11.1 四条线,一个传输
SUPPORTED_WIRES = frozenset({"openai", "responses", "anthropic", "gemini"})(llm/capabilities.py:24)。
传输层 llm/transport.py 纯 stdlib:urllib.request + 有界重试(仅可安全重放的状态码;尊重 Retry-After;可取消;总预算上限)。
Provider 适配文件:
| 文件 | 线 |
|---|---|
llm/providers/openai.py |
OpenAI Chat 兼容(ark 走这条 wire) |
llm/providers/responses.py |
OpenAI Responses |
llm/providers/anthropic.py |
Anthropic Messages |
llm/providers/gemini.py |
Gemini generateContent |
11.2 ark / 豆包是一等公民
capabilities.py:206-221:
ark:
wire=openai
base=https://ark.cn-beijing.volces.com/api/plan/v3
default_model=doubao-seed-2.0-pro
context_window=262_144
tool_calling=True, parallel_tool_calls=True, vision=True, streaming=True
llm/catalog.py 预置多条 Ark 模型(doubao-seed 系列、glm、kimi、deepseek、minimax…)。这是「¥9.9 复刻」的工程底座,不是营销文案空转。
11.3 配置分层
CLAUDE.md:每个 api_key / base_url / model 解析为
per-provider 变量 → OPENAI4S_LLM_* → provider default。
Daemon 可无 key 启动,随后在 UI Customize → Models 或 .env 配置。
register_provider(llm/registry.py:47-60)只允许挂到已有 wire——不能动态加载任意传输代码。
flowchart TB
CFG["Config / UI model profile"] --> RES["llm/resolve"]
RES --> CAP["ProviderCapabilities"]
CAP --> WIRE{"wire"}
WIRE -->|openai| OA["providers/openai.py"]
WIRE -->|responses| RP["providers/responses.py"]
WIRE -->|anthropic| AN["providers/anthropic.py"]
WIRE -->|gemini| GE["providers/gemini.py"]
OA --> TR["transport.post_json · urllib"]
RP --> TR
AN --> TR
GE --> TR
Part IV · Web 工作台
第 12 章 serve → gateway.SessionRunner
12.1 进程与绑定
openai4s serve(start.sh 薄封装)拉起单例 daemon(pidfile);默认 OPENAI4S_HOST=127.0.0.1、OPENAI4S_PORT=8760。文档明确:信任网络上不要 0.0.0.0 裸奔,用 SSH tunnel。
gateway.py 是 stdlib HTTP/WebSocket 组合适配器(CLAUDE.md:对外科手术式修改,禁止整文件重写)。
12.2 SessionRunner._loop:Web 回合的组装现场
SessionRunner._loop(gateway.py:6337-6447)做的事:
- 取
max_turns(explore 可抬高); - 构造
WebEventSink(emit, rid, assistant_visible, add_usage, …); - 非 plan 模式:从 dispatcher 取
tool_catalog,用with_finalize_response(...)拼终态 spec; AgentEngine(ChatModel(...), WebActionExecutor(...), CompactionPolicy, …);WebActionExecutor注入:execute_cell→_execute_and_log、native wrapper →_invoke_control_with_artifacts、plan/explore 钩子。
这与 CLI Agent.run 对称,只是事件沉到 WebSocket,Cell 执行走会话 FIFO coordinator。
12.3 POST /frames/{id}/message
路由匹配(gateway.py:9654+):
- 解析
input_data.request(兼容顶层request); - 处理
annotation_ids+ 客户端生成的annotation_reservation_id; wait:false时返回 202 +execution_id,turn 在后台跑(fire-and-forget MessageJob)。
前端契约见下一章时序。
第 13 章 WebSocket `/api/v1/ws` 事件面
13.1 通道职责
gateway.py 文件头(:11 附近):
WebSocket
GET /api/v1/ws(view_session/ping;text_reset/text_chunk/ …)
WSHub(:650+)维护:
- 订阅连接集合;
- 每帧 live-turn buffer(断线重连可 replay);
- 单调
seq+ 进程epoch(防 daemon 重启后假「你已追上」)。
13.2 高频事件类型(产品可见)
| 事件 | 含义 |
|---|---|
text_reset / text_chunk |
流式助手正文 |
artifact_created / artifact_ref_problems |
产物面板(新产物 / 引用问题) |
permission_resolved |
人工审批结果 |
kernel_status / plan_progress / execution_queue / branch_activation_state |
内核状态 / 计划进度 / 执行队列 / 分支激活 |
⚠️ 核查修正(2026-08-04):原表把
cell/artifacts/permissions列为 WS 事件type,这是错误的。cell是 live-buffer 的scope值(gateway.py:1057),artifacts/permissions是投影字典的字段名,三者都不是 WS 事件type。真实的事件 type 是上面三行所列(gateway.py源码枚举)。 |frame_update| 帧级状态 | |step/step_update| Host 活动时间线 |
turn-scoped 类型集合见 _TURN_SCOPED_TYPES(约 :954):text_reset、text_chunk、frame_update。
第 14 章 一次用户提示词的完整路径(最重要)
这是工作台的「主血管」。下列步骤均可在基线源码核对。
14.1 前端 send()(openai4s/server/webui/app.js:5574+)
关键顺序(注释写明了为什么必须这样排):
- 处理 plan / explore /
/skillname指令改写; - 若无会话 →
POST /frames建帧并sub(id); - 乐观插入 user bubble;
- 若有图像批注:用 CSPRNG 生成
admissionId = "resv-" + hex,先rememberAdmission,再发请求(:5640-5656); sub(S.currentId)必须在 POST 之前(:5663-5669)——否则首回合text_chunk在订阅集合外被丢掉;POST /frames/{id}/message,body:
{
"input_data": { "request": "<payload>" },
"plan": false,
"explore": false,
"annotation_ids": [...],
"annotation_reservation_id": "resv-...",
"wait": false
}
(app.js:5676)
- 用 202 的
execution_id关联气泡;按annotations字段 reconcile admission。
14.2 服务端接球 → SessionRunner → AgentEngine
sequenceDiagram
autonumber
participant UI as app.js send()
participant API as gateway HTTP
participant Hub as WSHub
participant SR as SessionRunner
participant ENG as AgentEngine
participant EX as WebActionExecutor
participant K as LazyKernel/Worker
participant HD as HostDispatcher
UI->>UI: sub(frameId) 先订阅
UI->>API: POST /frames/{id}/message wait:false
API-->>UI: 202 execution_id
API->>SR: 后台 MessageJob / run turn
SR->>ENG: _loop 组装 Engine+Executor+Sink
loop 每回合
ENG->>ENG: model.complete → route_action
alt NativeToolBatch
ENG->>EX: 控制工具(可只读并行波)
EX->>HD: 同源 HostDispatcher
HD-->>Hub: step / permissions / artifacts…
else FinalizeAction
ENG->>ENG: CompletionRecord → stop
else CodeCell
ENG->>K: 执行一格
K->>HD: host_call mid-cell
HD-->>K: host_response
K->>HD: host.submit_output?
end
ENG-->>Hub: text_chunk / cell / frame_update
Hub-->>UI: WS 推送(含 seq)
end
ENG-->>SR: EngineResult
SR-->>Hub: 终态投影(bullets + artifact delta)
14.3 为什么 wait:false + 先 sub() 是产品级细节
阻塞 POST 会让浏览器像「卡死的表单」;202 + WS 才像科研工作台。但异步带来经典竞态:线程已开始 broadcast(text_chunk),而客户端还没进 conn.subs。send() 里那两行注释(:5663-5669)不是废话,是线上事故的墓碑。
同理,admissionId 必须客户端先生成再发出——因为机制要覆盖的恰恰是「永远收不到 202」的情况;服务端铸造的 id 在那种情形下对浏览器毫无用处(:5640-5646,服务端回声在 gateway.py:9680-9685)。
第 15 章 Notebook 只读默认、Artifacts、Branch/Revert
15.1 Notebook 默认只读
config.py:364-368:
read-only Notebook by default; set
OPENAI4S_NOTEBOOK_REPL=1to re-enable the in-Notebook developer REPL.
notebook_repl 默认 False。kernel_routes.py 多处 if not runner.cfg.notebook_repl 直接拒绝交互式写入路径。产品语义:Notebook 是 Agent 执行的只读投影,不是第二个 Jupyter 主循环(Jupyter KernelSpec bridge 也是可选旁路,不带 tool batch / finalize,见 CLAUDE.md)。
15.2 Artifacts 版本
- 表:
artifacts+artifact_versions; - Cell 写文件 /
host.save_artifact/ 声明writes_files=True的 native tool(Web 边界包装)都会生成版本; - 环境 provenance 绑到 kernel generation,避免 R 产物盖上 Python 包列表(CLAUDE.md 那句 wrong rather than absent);
- 完成投影会带上「本回合实际产生的 artifact-version delta」。
15.3 Branch / Revert
server/session_branching.py:
- checkpoint 不可变;
fork物化隔离 workspace(:116+);preview_revert/revert_and_continue:先记当前检查点,再安全恢复,追加 revert,不改写历史(:9-10, :240+);- 从 cell fork 仅当该 cell 带 cursor checkpoint,否则 409(CLAUDE.md)——没有检查点无法重建状态。
gitGraph
commit id: "checkpoint A"
commit id: "checkpoint B"
branch experiment
checkout experiment
commit id: "fork workspace"
commit id: "new cells"
checkout main
commit id: "continue"
commit id: "revert→B (append-only)"
Part V · Skills · Compute · Security
第 16 章 Skills:34 份代码食谱,非 JSON schema
16.1 模型
skills_loader/loader.py:1-8:
- Discovery — 扫
skills/<name>/SKILL.md(+ 可选kernel.py); - Progressive disclosure — 系统提示只列 name + 一行 summary;全文经
host.search_skills/load_skill拉取; - Sidecar gate —
kernel.py先 compile-check。
基线 find skills -name SKILL.md = 34。目录覆盖:蛋白折叠 / 对接 / 单细胞 / 文献 / 图表 / 远程 compute provider 食谱 / retrosynthesis_planning(本基线 PR 主题)等。
16.2 与 Tool 的本质区别
| Native Tool | Skill | |
|---|---|---|
| 形态 | Python Tool 子类 + JSON schema |
Markdown 食谱 + 可选 sidecar |
| 注册 | TOOL_TYPES |
文件系统发现 |
| 何时进上下文 | schema 进 tool catalogue | 默认仅索引;按需加载全文 |
| 失败模式 | schema / permission | 模型没 load → 技能从不跑(UI 用 /skill 指令硬注入,app.js:5590-5601) |
bundled 只读;用户技能在 <data_dir>/user-skills;重名时 bundled 胜出。
第 17 章 BYOC 与 `openai4s_compute_provider`
17.1 分工
| 包 | 跑在哪 | 职责 |
|---|---|---|
openai4s/compute/ |
本机 daemon | 注册表、作业编排、与 Host 对接 |
openai4s_compute_provider/ |
远端机器 | stdlib-only 沙箱 SDK;oneshot/repl |
skills/remote-compute-* |
技能树 | 具体 provider shim(如 nvidia;注意 remote-compute-ssh 只有 SKILL.md + README,无 provider.py,是纯食谱而非 shim,不宜作 shim 例证) |
__main__.py 强调两阶段 secret scrub:在 import provider.py 之前先做通用 scrub,resident prologue 再按 provider 声明的 secret_env_prefixes 二次擦除;凭证从 stdin/fd-3 读入,不进环境变量。
17.2 控制平面入口
Registry 中的 Remote* Tool(Submit/Status/Result/Cancel/Close…)与 host.compute.* / host.fold 共用审批与审计。host.fold(单序列折叠类)走严格 no-fabrication 策略(CLAUDE.md)。
寻址形态产品文档常见 ssh: / byoc: 风格端点——本机只编排,重计算在你自己的 GPU 上。
flowchart LR
AGENT["Agent / Cell"] --> HOST["host.compute / Remote* tools"]
HOST --> MGR["openai4s/compute manager"]
MGR -->|SSH/BYOC| REM["remote GPU"]
REM --> PROV["openai4s_compute_provider __main__"]
PROV --> SHIM["skills/.../provider.py"]
第 18 章 权限、审批持久化、biosecurity、egress
18.1 多层防御(独立层,不互相替代)
| 层 | 模块 | 要点 |
|---|---|---|
| OS sandbox | security/sandbox.py |
Seatbelt/bwrap;auto/enforce/off |
| 环境 allowlist | kernel/environment.py |
防 secret 泄漏进 worker |
| 权限 / 审批 | openai4s/permissions.py(771 行,opencode 式 allow/deny/ask gate;注意 security/permissions.py 是另一个文件,仅管数据目录文件权限位,与审批无关)+ storage/permissions.py Store 表 |
持久规则;无人值守默认 deny |
⚠️ 核查修正(2026-08-04):原稿写
security/permissions.py,属张冠李戴。审批 broker 是顶层openai4s/permissions.py;同名的security/permissions.py(116 行)管的是 SQLite 明文凭证默认 0644 的收紧,并非审批逻辑。 | 代码门 |security/classifier.py+ loop_pre_exec_gate| 拒跑不安全 Cell | | 生物安全 |security/biosecurity.py| trajectory screener;BLOCK 停 Cell | | 注入筛查 |security/injection.py| 不可信输出 | | 出站 |egress.py|OPENAI4S_EGRESS=allowlist时后缀域名白名单 | | bash 令牌 |host/bash.py| generation 绑定 one-shot |
18.2 审批重启语义(容易做错的产品细节)
docs/architecture.md:121-128:
- 持久化的是决策,不是 Python 调用栈;
- 进程内可恢复 exact blocked gate;
- daemon 重启后:从 SQLite 露出请求;记录无参
permission_resolutionledger marker;声明旧操作未执行;要求显式重新继续; - restart-only
once授权 15 分钟过期,精确匹配 conversation/tool/target,原子消费。
测试默认姿态(CLAUDE.md):OPENAI4S_UNATTENDED_APPROVAL=deny。
18.3 egress 的诚实边界
egress.py:9-19 自称是 host-tool 边界上的 best-effort fence(web_* 与静态 bash 检查);默认 mode off。打开 allowlist 后,科学数据库 / 包索引 / 数据仓库可达,其它域需 request_network_access 经审批放宽。它不是 OS sandbox 的替代品。
⚠️ 核查修正(2026-08-04):
egress.py:9-10的 docstring 原文写「openai4s does not ship an OS-level sandbox (Seatbelt/bubblewrap)」,与security/sandbox.py(863 行、真实实现 Seatbelt/bwrap)直接冲突——该 docstring 已过期。原稿把改写后的话仍归因给egress.py:9-19,读者按图索骥会读到相反表述。此处应显式标注 egress docstring 陈旧,而非替作者打补丁。
Part VI · 内核深挖与开发者品味
本章是全文重心之一。OpenAI4S 的作者群(北大—元空联合实验室开源)在公开叙事上对标 Claude Science,但源码里真正的签名不是「又一个 Agent while」,而是:对「假真相」的过敏、对依赖边界的偏执、以及对门面文件的外科手术纪律。下面先把内核协议读透,再反推十条搭建选择。
第 19 章 内核协议:dup2、帧上限与「错误比没有更糟」
19.1 技术链路(从 Cell 到 Host 再回来)
sequenceDiagram
participant M as 模型
participant Eng as AgentEngine
participant Ex as Web/Local Executor
participant Km as Kernel.manager
participant W as worker.py
participant H as HostDispatcher
participant Store as SQLite Store
M->>Eng: assistant + tool_calls 或 ```python
Eng->>Ex: route_action → CodeCell
Ex->>Km: execute(code)
Km->>W: {"type":"execute",...}
Note over W: stdout→捕获;协议在高位 fd
W->>Km: host_call(method,args) [持锁]
Km->>H: dispatcher(method,args)
H->>Store: audit / permission / ledger
H-->>Km: data | {"error":...}
Km->>W: host_response
W-->>Km: result(stdout, usage, error_lineno)
Km-->>Ex: observation
Ex-->>Eng: history + maybe CompletionSignal
关键不变量(CLAUDE.md + manager.py / worker.py):
- 单 reader:只有
Kernel读协议出站;supervisor/watchdog 禁偷读帧。 - 单飞行 host_call:整段 RPC 一把锁;并发会交错。
- generation 单调:respawn 必 bump;旧 bash token / lease 失效。
- 软失败契约:Host 可返回
{"error": msg},worker 转RuntimeError——Cell 看见异常而不是 daemon 崩。 - 不要在 worker 里 autoclose matplotlib:gateway 要先
savefig捕 Artifact(CLAUDE.md 明确禁令)。
19.2 为什么这套内核「品味很重」
大多数 Agent 把「跑代码」做成:
tool Bash → subprocess → 文本结果 → 塞回上下文
OpenAI4S 把「跑代码」做成:
持久 REPL + 协议分家 + 中途同步 Host RPC + 环境指纹 + 账本
前者优化的是「模型会不会用工具」;后者优化的是「科学计算会不会在一周后仍可辩护」。品味差在问题定义,不在循环语法。
19.3 搭建思维链(反推:他们先信什么)
按依赖顺序还原一条开发者内心独白:
- 若科学状态不能持久,Code-as-Action 就是笑话 → 先做 persistent kernel,而不是先堆 70 个 Tool。
- 若 print 能砸协议,一切可观测性都假 → 先做 dup2 换轨,再谈 notebook。
核查修正(2026-08-04):原稿写「dup2 / fd3·fd4」混淆了两套机制。Python
worker.py用os.dup()得到内核分配的匿名高位 fd +dup2(2,1)让 fd1 别名到 stderr,全文件无 fd3/fd4 常量;fd3/fd4是 R 通道专有(r_kernel.py:12-13「protocol OUT rides fd 3 / protocol IN rides fd 4」)。应改为「Python 走匿名高位 fd,R 走固定 fd3/fd4」。 - 若 mid-cell 不能回调 Host,科研循环次数会爆炸 → 内环 RPC 与外环 tool_use 必须并存。
- 若控制与科学抢同一回合,审计与计算会互相污染 →
route_action铁律。 - 若 UI 气泡是真相源,重启必说谎 → Ledger-first;气泡是投影(
completions.py)。 - 若 provenance 可以「猜」 → 宁愿记缺失原因。
- 若核心引入重依赖 → 边界糊掉、审计变难、¥9.9 叙事也站不住 → stdlib hard constraint。
- 若门面文件被整页重写 → 兼容/路由/传输契约会静默蒸发 → 外科修改约定写进 CLAUDE.md / PR template。
flowchart TB
P1["① 科学要持久状态"] --> P2["② 协议必须抗污染"]
P2 --> P3["③ 需要 mid-cell Host RPC"]
P3 --> P4["④ 控制/科学分平面"]
P4 --> P5["⑤ 账本先于 UI"]
P5 --> P6["⑥ 假真相不可接受"]
P6 --> P7["⑦ 核心零依赖"]
P7 --> P8["⑧ 巨石门面只许外科改"]
第 20 章 十条开发选择:反推搭建思维与品味
体例对齐本系列 CodexMonitor「品味十章」:每条选择给主张 → 源码证据 → 放弃了什么 → 品味标签。
选择一:对标体验,不抄实现 ——「Claude Science 开源复现」是产品叙事,不是 fork
主张:用公开架构思想(Code-as-Action、持久内核、host RPC、安全层)独立复现,MIT 开源;模型可走方舟 ¥9.9。
证据:README Acknowledgement;docs/architecture.md 双平面图;llm/capabilities.py 的 ark 一等公民。
放弃:逐行兼容闭源、绑定单一前沿模型。
品味:垂直领域的开放替代,用价格与可审计性换封闭飞轮。
选择二:stdlib 是硬约束,不是风格偏好
主张:engine / urllib LLM / http.server+手写 WS 零第三方;科学库只能 try/except ImportError。
证据:pyproject.toml dependencies = [];CLAUDE.md「Never add a hard third-party import」。
放弃:FastAPI、websockets 库、httpx、ORM。
品味:依赖纪律 = 信任边界纪律。少一个包,就少一条供应链与审计盲区。
选择三:双平面不竞争 —— ReAct「万物皆 tool」被显式拒绝
主张:每回合至多一个动作通道;native tools 优先;sole finalize_response 另案。
证据:actions.py:route_action;禁止注册 bash/submit_output(registry.py:_FORBIDDEN_CONTROL_NAMES)。
放弃:把 shell、submit、科学计算都塞进 JSON tool 表的「统一感」。
品味:统一感让位于可审计性。科学计算需要语言;编排需要 schema。
选择四:完成信号是产品契约,不是模型习惯
主张:对话/工具完成 ≠ 科学完成;UI 完成是投影。
证据:agent/finalize.py;host.submit_output;server/completions.py;纯 submit Cell 不进 Notebook 展示。
放弃:「模型说完再见就算完」。
品味:把「完了」从自然语言降级为结构化事实——科研产品必须能回答「你到底提交了什么指标」。
选择五:内核协议按「最坏字符」设计,不按 ASCII 幻想
主张:上限从测量推导;CJK/emoji 代理对要把字节预算拉到 12×。
证据:worker.py 关于 _MAX_FRAME_BYTES 的长注释与测试驱动修正。
放弃:拍脑袋常量、默契「输出不会太大」。
品味:注释里写失败史——这是实验室工程文化,不是教程工程文化。
选择六:假绿比红更可耻
主张:应失败的场景若成功,benchmark/harness 判失败;stub 不得污染 response schema 契约。
证据:CLAUDE.md harness/workflows 段;stubbed_backend marker;capture_response_schemas.py --check。
放弃:「无异常 = 通过」的偷懒评分。
品味:把拒绝能力当成产品一半——科学 Agent 的价值经常是「敢说不」。
选择七:巨石门面允许存在,但禁止整页重写
主张:gateway.py / host_dispatch.py / store.py / app.js / worker.py / manager.py / sdk/host.py(核查修正 2026-08-04:CLAUDE.md:98 列 7 个门面文件,原稿漏了 sdk/host.py)是兼容与路由合同的堆叠;新算法进 service/repo/tool。
证据:CLAUDE.md「Edit the compatibility/composition facades surgically」;PR template 确认项。
放弃:清爽的「理想分层一次到位」。
品味:承认历史债务,用纪律管理,而不是用大爆炸重构制造静默回归。
选择八:Skills 是食谱,不是插件 SDK 幻想
主张:SKILL.md + 可选 kernel.py;渐进披露;bundled 赢名字冲突;用户技能不能冒充信任。
证据:skills_loader/loader.py;34 个 bundled skills;Customize CRUD 进 user-skills。
放弃:把每个科学流程做成 JSON tool(爆炸)或动态 import 任意代码当一等公民。
品味:扩展面放在「代码食谱」,信任面仍收在 Host 与沙箱。
选择九:安全是叠层,默认姿态偏偏执
主张:loopback、env allowlist、Seatbelt/bwrap、审批(无头 deny)、biosecurity、injection screen、egress(默认 off)彼此独立。
证据:docs/security.md;OPENAI4S_UNATTENDED_APPROVAL=deny 测试默认;审批重启不重放参数。
放弃:单层「沙箱万能」叙事;无头自动放行的 DX 快感。
品味:每层只承诺自己能承诺的;egress 自称 best-effort 反而是诚实。
选择十:Web 是零构建工作台,不是前端事业部
主张:webui/ 静态直出 working tree;编辑即热;没有 bundler。
证据:server/webui/app.js 近万行仍手写;CLAUDE.md「no build step」。
放弃:React/Vite 工程美学与组件生态。
品味:产品重心在运行时与证据,不在前端栈——用一门语言(Python)+ 静态 JS 压运维面,服务「科学家本机一键起」。
一句话总结品味
用纯标准库守住可审计边界,用双平面拆开编排与计算,用内核协议对抗静默错乱,用账本与 provenance 对抗假真相——工程上偏执保守,产品上对科研完成定义进取,架构上宁可巨石门面加外科纪律,也不用大重构换虚假干净。
Part VII · 横向对比与总结
第 21 章 对比表
| 维度 | Claude Code | Open Design | OpenAI Codex | OpenManus / Suna | OpenAI4S | Claude Science(闭源) |
|---|---|---|---|---|---|---|
| 产品位 | 终端编程 Agent | 设计 Agent 宿主(不自研循环) | 云+本地编程 Agent | 通用任务 / 浏览器 Agent | 科研 Code-as-Action | 闭源科研工作台 |
| 主循环 | 自研 tool 循环 | 委托外部 CLI | 自研 | 自研 ReAct 变体 | 双平面 AgentEngine | 闭源 |
| 工具哲学 | Shell/编辑器一等公民 | 文件系统技能喂给 CLI | 沙箱+工具 | 工具多多益善 | 无 shell Tool;bash 仅内核 | 未知 |
| 科学内核 | 无持久科研内核 | 无 | 无 | 无 | 持久 Python/R + mid-cell host RPC | 有(闭源) |
| 完成信号 | 会话/任务约定 | CLI 退出/产物文件 | 任务完成事件 | 常靠最终消息 | finalize ⊕ submit_output | 未知 |
| 前端 | TUI | Next.js 重前端 | 多端 | Web | 零构建静态工作台 | 专有 UI |
| 核心依赖 | Node 生态 | 大 monorepo | Rust/TS 等 | Python 重依赖 | stdlib only | 云服务 |
| 扩展 | MCP/hooks/skills | 26 CLI + 内容库 | MCP/插件 | 工具/浏览器 | SKILL.md 食谱 + BYOC | 封闭 |
| 对标价格叙事 | Pro 订阅 | 自托管+自备 CLI | Plus/API | 自备 key | ¥9.9 Ark/Doubao | 高价闭源 |
quadrantChart
title Agent 产品地图(示意)
x-axis 通用任务 --> 垂直领域
y-axis 委托他人循环 --> 自研循环
quadrant-1 垂直自研
quadrant-2 通用自研
quadrant-3 通用宿主
quadrant-4 垂直宿主
Claude-Code: [0.25, 0.8]
Codex: [0.3, 0.75]
OpenManus: [0.35, 0.7]
Open-Design: [0.55, 0.2]
OpenAI4S: [0.85, 0.78]
Claude-Science: [0.9, 0.85]
第 22 章 五个独特设计决策与诚实边界
22.1 五个独特决策(浓缩;展开见第 20 章)
- 双平面不竞争 —
route_action铁律把控制与科学拆开;这是对 ReAct「万物皆 tool」的显式拒绝。 - 完成信号双轨且互不冒充 —
finalize_response永不进 registry;submit_output/bash禁止注册为控制工具。 - 中途 Host RPC — tool_use 架构做不到的「Cell 内同步回调」。
- Ledger-first + 投影分离 — UI transcript 不是真相;Action Ledger / Store 才是。
- 纯标准库核心 — 用依赖纪律换可审计边界与「科学栈可选」。
22.2 诚实边界
| 边界 | 说明 |
|---|---|
| 不是 Claude Science 的逐行克隆 | 体验对标;模型质量、实验室集成、闭源数据飞轮不可等同 |
| Notebook 默认只读 | 需要 REPL 请显式开 OPENAI4S_NOTEBOOK_REPL |
| egress 默认 off | 打开 allowlist 才是强约束;且仍是应用层 fence |
| R 无 Cell 内完成 | R 是分析通道,完成叙事仍偏 Python/finalize |
| Gateway / app.js 巨石 | 组合门面巨大;约定是外科修改,重构成本高 |
| 离线测试 ≠ 全部门禁 | harness、response schema capture、browser smoke、Linux CI 分支都是独立闸 |
22.3 何时选它
- 你要的是科研分析 Agent,不是又一个编程助手;
- 你接受「模型写代码,内核持状态」;
- 你希望核心可审计、少依赖、可挂廉价国内模型;
- 你需要 BYOC 把重计算放到自己的 GPU。
何时不选:纯软件工程仓库遍历、需要重型浏览器操作员、或要「零代码只点工具」的运营 Agent——那些更像 Claude Code / Manus 赛道。
第 23 章 源码导览索引
23.1 按阅读顺序(建议 1 个下午)
| 顺序 | 路径 | 看什么 |
|---|---|---|
| 1 | docs/architecture.md |
官方双循环叙事 |
| 2 | agent/actions.py |
route_action 铁律 |
| 3 | agent/engine.py |
AgentEngine.run while |
| 4 | agent/finalize.py |
为何 finalize 不是 Tool |
| 5 | agent/loop.py |
CLI Agent.run 组装 |
| 6 | server/gateway.py → SessionRunner._loop |
Web 组装 |
| 7 | server/webui/app.js → send() |
202 + 先 sub + admission |
| 8 | host_dispatch.py + sdk/host.py |
编排信封与内核门面 |
| 9 | kernel/lazy.py + manager.py + worker.py |
科学平面协议(先读 worker 文件头注释) |
| 9b | 本文第 19–20 章 | 内核品味与十条选择 |
| 10 | tools/registry.py |
70 tools 与禁止项 |
| 11 | llm/capabilities.py + transport.py |
ark 与 urllib |
| 12 | skills_loader/loader.py + skills/*/SKILL.md |
食谱模型 |
| 13 | security/sandbox.py + openai4s/egress.py(核查修正:在顶层,非 security/) + openai4s/permissions.py(审批,非 security/permissions.py) |
安全层 |
| 14 | store.py / storage/snapshots.py |
持久化与分支 |
| 15 | openai4s_compute_provider/ |
BYOC 远端 SDK |
23.2 按符号速查
| 符号 | 文件 |
|---|---|
AgentEngine.run |
openai4s/agent/engine.py |
route_action / FinalizeAction / CodeCell |
openai4s/agent/actions.py |
with_finalize_response |
openai4s/agent/finalize.py |
RuntimeActionLedger |
openai4s/agent/ledger.py |
Agent.run / LazyKernel 组装 |
openai4s/agent/loop.py |
_execute_read_only_waves |
openai4s/agent/control.py |
HostDispatcher |
openai4s/host_dispatch.py |
host.submit_output / host.bash |
openai4s/sdk/host.py |
TOOL_TYPES |
openai4s/tools/registry.py |
SessionRunner._loop |
openai4s/server/gateway.py |
send / admissionId |
openai4s/server/webui/app.js |
WebActionExecutor / WebEventSink |
openai4s/server/agent_run.py |
SessionBranchingService |
openai4s/server/session_branching.py |
SUPPORTED_WIRES / ark |
openai4s/llm/capabilities.py |
SkillLoader |
openai4s/skills_loader/loader.py |
23.3 本仓库内相关文档
- 系列对比与目录:
总目录.md、项目分析/下各项目源码分析 - 配套教程:
从零构建OpenAI4S-开发全流程教程.md/从零构建OpenAI4S-开发全流程教程.html - 上游:https://github.com/PKU-YuanGroup/OpenAI4S
结语
OpenAI4S 的源码魅力不在「又实现了一个 Agent while 循环」,而在它把循环劈成两半还不肯让它们抢回合,并且在内核协议层对「静默错乱 / 假 provenance」表现出近乎过敏的工程品味:JSON 控制平面负责可审计的编排与权限,Python/R 科学平面负责把真实计算留在持久内核里,并用 mid-cell host RPC 补上 tool_use 缺失的那一截。finalize_response 与 host.submit_output 的双轨完成、TOOL_TYPES 对 shell 的显式缺席、以及 Web 上 sub()-before-POST / 客户端 admissionId 这类「小到像 bugfix、大到像产品契约」的细节,共同构成了它相对 Claude Code / Open Design / Manus 系的差异化。
基线停留在 85e9fa0。对照本文时请以该 commit 为准;主线继续演进时,优先核对 agent/actions.py、agent/engine.py、tools/registry.py 与 server/webui/app.js::send 四条承重墙是否仍成立。
文档生成说明:面向本系列「产品经理 + 资深工程师」笔法;主张尽量回源。配套 HTML:OpenAI4S-解析.html。