- 人工智能
- AI 应用
- AI 评测
- RAG
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
本篇基于 Kiln 仓库specs/projects/cloud_sandbox_dx项目的 Phase 5 规划文档,讲解如何把一台被快照的 Claude Code 云沙箱 VM 变成"每次会话都是热启动"的环境:通过WARM_CACHE预热缓存并把node_modules整棵温树放到仓库外供后续会话硬链接复用、用 provisioning marker 标记"这台机器被setup_env.sh装配过"、再给setup_startup.sh加一道容器门禁,使本地开发机与云 VM 各走各的路径。读完本文,你能完整理解这三块的实现位置(.config/utils/setup_env.sh、.config/utils/setup_startup.sh)、关键常量与失败兜底策略,并能对照 Phase 5 规划 中的行为测试矩阵复现验证。
一、为什么需要"温 node_modules":uv 与 npm 缓存机制的不对称
Phase 2 和 2b 阶段已经解决了 Python 侧的冷启动成本,但 Node 侧几乎没动。根因在于两个包管理器取缓存的方式不同:
- uv 从缓存中硬链接(hardlink):预热的
~/.cache/uv让新会话的uv sync从Prepared 182 packages in 14.67s降到Prepared 3 packages in 428ms——约 300 MB 的下载、togethergit 依赖的 fetch、google-crc32c的 sdist 构建全部消失,只剩三个本地 workspace 包仍在构建,而这部分不可消除,因为它们来自 checkout 自身的源码。 - npm 从缓存中复制(copy):即使
~/.npm已温热,把 748 个包物化进一个空的node_modules仍要 ~21 s(24 s → 21 s,几乎没改善)。
Phase 5 的解法是跳动物化本身:把预热运行构建好的 pristinenode_modules整棵树放在仓库外(/opt/kiln-vm-setup/node_modules),使其存活进 VM 快照,之后每个会话用cp -al硬链接复制进自己的 checkout。在/opt与/home/user同设备的条件下,601 MB 的树只需 0.54 s,且共享 inode。
Phase 6 修正后的诚实数字:0.54 s 这类逐命令数据是在热页缓存(hot page cache)下测得,不是新快照会话真实看到的。在真实快照启动的会话上,首次setup_startup.sh有温树耗时22.9 s,无温树32.3 s——温树的真实价值是9.4 s,而非组件数据暗示的约 20 s。温树仍然保留(functional_spec.md F9 记录了修正后数字与取舍),但应以冷启动数字为准。
二、WARM_CACHE标志:恢复"粘贴脚本就是仓库文件"的核心契约
在 Phase 5 之前,做预热的WARM_CACHE标志只存在于用户粘贴进云环境对话框的那份脚本副本里,这破坏了项目的核心契约(F1):粘贴的脚本应当是仓库文件本身、只有CONFIGURATION块被编辑过。Phase 5 把标志落地到仓库源码中,恢复该契约。
2.1 CONFIGURATION 块与命令行标志
setup_env.sh 的CONFIGURATION块新增(默认false,本地运行与其他复用该环境的仓库不受影响):
WARM_CACHE=false # true: warm the machine's caches from a throwaway clone (cloud VMs)参数解析器新增--warm-cache标志,usage中同步补了说明行。云端环境的配置文件(.config/utils/claude_code_vm_setup.sh)以命令行方式传入:--upgrade-tools --best-effort --warm-cache --create-startup-script --add-playwright。
2.2 VM 装配目录常量
紧挨UV_MIN定义,setup_env.sh 定义了这组常量:
VM_SETUP_DIR="${KILN_VM_SETUP_DIR:-/opt/kiln-vm-setup}" VM_SETUP_MARKER="$VM_SETUP_DIR/.setup_for_kiln_repo_v1" WARM_NODE_MODULES="$VM_SETUP_DIR/node_modules" KILN_REPO_URL="${KILN_REPO_URL:-https://github.com/Kiln-AI/Kiln.git}"设计要点:
- 一个目录同时承载 marker 与温树,二者都在任何 checkout 之外,因此能存活进快照;
- 文件名里的
_v1是契约版本号:当 setup 提供的内容发生不兼容变化时递增它,让旧脚本装配的 VM 被正确报告为"未装配"而非"已就绪"; KILN_VM_SETUP_DIR与KILN_REPO_URL可被环境变量覆盖,使整套行为无需写/opt、无需走网络克隆即可测试。
2.3warm_from_throwaway_clone:只在一个精确场景下运行
实现见 setup_env.sh。它仅在WARM_CACHE=true且没有 checkout时运行——正是云环境构建的场景。流程:
- 浅克隆 Kiln 到
$VM_SETUP_DIR/throwaway-clone。克隆放在$VM_SETUP_DIR内(而不是/tmp)是为了让最后node_modules的mv是同一设备上的 rename,而不是 601 MB 的跨设备拷贝; - 克隆后先写
.python-version再同步——否则 uv 会按系统 Python 建.venv、缓存另一解释器的 wheels,等于给错误解释器做了预热; - 并行执行
uv sync --frozen --all-packages与npm ci(与正式依赖安装的wait-both 并行模式一致),然后mv克隆中的app/web_ui/node_modules到$WARM_NODE_MODULES,删除克隆; - 若同时启用了 Playwright,在移动和删除克隆之后,用温树自己的 playwright 安装 e2e 浏览器——因为
playwright install会在浏览器目录的.links注册调用者路径,随后删除克隆会使该注册失效,下一次 install 就会把刚装好的浏览器当作孤儿清除。
失败语义是"警告而非fail":预热是纯优化,若把失败计入fail,会让结尾行翻转为 "Resolve the errors above"——对一台 uv 和 Python 都正常的机器这是误导。setup_startup.sh反正会在每个会话重新校验所有必须为真的前提。
三、Provisioning Marker:让启动脚本能分辨"被装配过的 VM"
write_vm_setup_marker(setup_env.sh)在每一次运行的 checkout 与 no-checkout 两条路径上都会写$VM_SETUP_MARKER,记录:setup_version、时间戳、uv 版本、Python pin、温树路径、温树是否存在、温树由哪个 commit 构建、本次运行是否创建了它(另加上 Phase 6 的session_start_hook与playwright_cli字段)——足够事后诊断一台过期或错误的 VM。
两条值得注意的设计规则:
- 目录不可写时只打印一条低调提示并继续:本地机器上
/opt可能不可写,而 marker 只在 VM 上有意义; - 树字段描述的是"磁盘上现在的状态",而不是"本次运行做了什么"。不带
--warm-cache的重跑(正是AGENTS.md给出的修复命令)不会创建树,但已存在的树继续存活并持续给每个会话播种。若此时把 marker 重写成created=false, commit=none,等于对"每个会话继承的 601 MB 的来历"撒谎。因此只要树比创建它的运行活得更久,previous_marker_value就把旧 marker 的 commit 传递下去;只有当树存在却没有任何 marker 可解释它时,才写unknown。
写入方式本身也有讲究:先写到带$$后缀的临时文件再mv -f覆盖——直接重定向到 marker 自身会先截断再写,一旦失败会留下零字节文件,而setup_startup.sh只检测 marker是否存在,那样会把"唯一站在会话与来路不明的 601 MB 树之间的检查"变成恒真的假阳性。
四、容器门禁:setup_startup.sh只在容器里干活
setup_startup.sh的存在理由是"云会话从全新文件系统启动"。但在开发机上,环境已经装配好且被所有 checkout 共享——一个会从机器级温树播种node_modules的脚本在那里毫无存在必要。因此 Phase 5 在参数解析之后、项目根发现之前加了门禁(setup_startup.sh):
is_flag_set() { case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in "" | false | f | 0 | no | n | off) return 1 ;; *) return 0 ;; esac } if ! is_flag_set "${CLAUDE_CODE_REMOTE:-}" && ! is_flag_set "${IS_CONTAINERIZED:-}"; then echo "Not running in a VM/container, will not run custom setup." # ... 告知本地开发者应改用 setup_env.sh,或手动 uv sync / npm install exit 0 fi判定规则是"非空且非假值":CLAUDE_CODE_REMOTE(Claude Code 在云会话中置为true)或IS_CONTAINERIZED(手工逃生口)任一成立即视为容器内。假值列表覆盖人们真正会输入的拼法(no、0、false等)——如果有人设置了IS_CONTAINERIZED=no却被当成 yes,是这个门禁唯一"不可能被察觉"的失败模式。两个变量都不成立时,脚本打印提示(本地应运行setup_env.sh --human,或手动uv sync+npm install)并exit 0——正常结局,不是错误。
五、Marker 检查与温树播种:seed_warm_node_modules的完整机制
5.1 Marker 检查:非致命,但禁用硬链接
setup_startup.sh 使用与setup_env.sh相同的三个常量(两脚本不能共享代码,因为后者会被整体粘贴进云设置框),在项目根发现之后、硬依赖检查之前做 marker 检查——这样它能"框住"随后很可能出现的失败原因。marker 缺失时:
- 打印显著警告块:VM setup 脚本本应运行而未运行;
- 设置标志禁用硬链接播种——没有 marker 意味着温树(如果有)来路不明,不应链接进任何 checkout;
- 其余流程照常继续。缺失 marker 是警告而非致命错误:一台未装配但 uv、npm 正常的机器仍能以更慢的速度完成全部工作。
5.2 播种的三个前置条件
播种发生在依赖同步之前,且仅当三者同时成立(setup_startup.sh):
- checkout 中没有
node_modules; - marker 存在(
VM_WAS_PROVISIONED=true); $WARM_NODE_MODULES存在。
seed_warm_node_modules的动作序列是承重的,顺序不可交换:
cp -al温树到目标旁的 staging 目录(staging 必须落在目标文件系统上,否则mv会退化成 601 MB 拷贝并丢掉所有硬链接);- Unshare:对 staging 根部的普通文件逐个
cp -p+mv -f断开硬链接,然后才把 staging rename 进node_modules位置。理由是硬链接意味着"经由一个路径的写就是经由另一个路径的写":npm install恰好会原地改写node_modules/.package-lock.json(实测同 inode、新 mtime),而该文件是 npm 记录"已安装什么"的依据,漂移它就可能让后续npm install跳过真正缺失的包。实测本仓库当前工具链下,seed +npm install+ 完整npm run build之后,46,375 个 inode 中只有这一个被原地改写,vite 缓存与 npm 新增包都落在 checkout 自己的目录项新 inode 上——因此只需断开根部那几百 KB 的普通文件,下面约 4.6 万个包文件保持链接。若树先 rename 再 unshare,会存在"node_modules已存在但完全共享"的窗口:此窗口内运行被杀,下一次运行会看到node_modules存在而跳过播种,其npm install就直接穿透硬链接写坏基线。unshare 失败即播种失败,回退到普通npm install。
5.3 三个防御性细节
- Staging 按 PID 命名(
.node_modules.warm.$$)。共享命名被实测否决过:两个播种竞争同一名称,60 次中 57 次静默损坏node_modules(一次rm -rf从另一棵树的cp -al中途 unlink 条目,幸存项被 rename 进位;或rm -rf对仍在创建的目录失败,第二个cp -al复制进了它,嵌套出一整棵树)。按 PID 命名的代价是被杀运行会泄漏 staging,于是每次运行无条件执行一次按年龄清扫:回收app/web_ui下超过一小时的.node_modules.warm*。它必须无条件运行,因为播种块在node_modules已存在时整体跳过——正是泄漏目录会永远滞留的状态。源码中同时澄清:让清扫安全的是"不存在第二个进程",而不是年龄过滤——cp -al隐含--preserve=all,会把温树的 mtime 打到 staging 目录上,快照恢复的 VM 上活着的 staging 目录一完成就可读作"几天前"(architecture.md 对此有专节分析); - 跨设备兜底:温树在另一文件系统(测试用
/dev/shm)时cp -al逐文件失败——实测 46,436 行、15 MB。诊断写入临时文件而非 shell 变量,只打印前 3 行加一个总数(grep -c ''计数,因为wc -l对无换行尾的单行会数成零——"为打破沉默而写的函数自己沉默了"),然后清除 staging、落到今天本来就会执行的普通npm install; mv -T收尾:若node_modules在守卫之后才出现,-T使 rename 失败而不是把 staging 静默吞成子目录——"不播种"是唯一比这更好的结局。
六、文档配套修改
Phase 5 的收尾步骤是文档:AGENTS.md与 CONTRIBUTING.md 必须停止暗示setup_startup.sh会在所有地方同步依赖——容器外它现在是 no-op,需说明本地贡献者应该怎么做;functional_spec.md 的 F8 补上门禁、marker 与播种,其 "Environment-side changes" 配置块补WARM_CACHE=true;architecture.md §1 与 §1B 同步,并补变更清单行;implementation_plan.md 补 Phase 5 与更新后的云端CONFIGURATION块;phase_2.md 与 phase_2b.md 各加一行指向 Phase 5 的扩展说明,避免只读旧计划的读者被误导。
七、行为验证:无单元测试,全部实跑
Phase 5 只涉及 shell 与文档,没有单元测试——验证是行为式的,每个用例都是被执行而非被推理,且KILN_VM_SETUP_DIR与KILN_REPO_URL两个覆盖变量让每个用例都不碰/opt与网络。核心用例包括:
| 用例 | 期望结果 |
|---|---|
门禁,两个变量均未设置 /CLAUDE_CODE_REMOTE=false/0/空 /IS_CONTAINERIZED=no | 打印 "not running in a VM/container",exit 0,不动 checkout |
门禁,IS_CONTAINERIZED=true而CLAUDE_CODE_REMOTE未设置 | 完整执行启动流程 |
| Marker 缺失(即使有温树) | 显著警告、不尝试硬链接,运行仍 exit 0 且正常打印Ready. |
| Marker 在、温树不在 | 无警告、无播种、普通npm install |
Marker 在、温树在、node_modules缺失 | 播种成功,ls -i证明 checkout 与温树文件是同一 inode |
温树在另一文件系统(/dev/shm) | cp -al跨设备失败,打印上限内提示,无 staging 残留,npm install兜底 |
node_modules已存在 | 不播种,行为不变 |
| 原地写风险 | 记录温树全部文件的 inode/size/mtime,跑npm install与 web build 后重比,证明 npm 与构建工具是替换文件(安全)还是原地编辑(会污染快照基线) |
setup_env.sh --warm-cache无 checkout | 克隆、同步,marker 与满node_modules都留在$KILN_VM_SETUP_DIR,克隆被删除 |
setup_env.sh有 checkout | 写 marker、记warm_node_modules_created_this_run=false、不克隆 |
不带--warm-cache重跑覆盖已有温树 | marker 仍报告树存在且保留原始 commit |
| 跨设备回退输出有界 | 三行加计数,而非逐文件一行 |
| Unshare 失败 | 播种失败、staging 被清除、回退npm install,而不是谎报播种成功 |
| 两个播种竞争同一 checkout | 对 52,037 项温树跑 30 次竞争,每次都留下正确的node_modules且无 staging 残留;对照的共享 staging 命名 10 次竞争损坏 10 次 |
| 年龄清扫 | 两小时前的.node_modules.warm.*被一次"完全跳过播种"的运行回收;新鲜的不动 |
最终门槛:uv run ./checks.sh --agent-mode绿色,测试套件保持 6369 passed / 10020 skipped,git status --porcelain除预期改动外为空。
八、小结:把"机器"与"checkout"的准备工作彻底分开
Phase 5 的三块拼图合起来完成了一个清晰的职责划分:setup_env.sh(一次性、快照前)准备机器——uv 缓存、npm 缓存、温树、marker;setup_startup.sh(每会话)准备checkout——容器门禁、marker 校验、硬链接播种、分支级同步。所有性能数字都以 Phase 6 的冷启动修正版为准(温树值 9.4 s / 601 MB 镜像增量的显式取舍),所有失败路径都降级为"失去加速、失去不了正确性"。相关完整脉络可继续阅读 project_overview.md、research_findings.md、functional_spec.md(F8/F9/F10)与 architecture.md §1.3b,以及 Phase 6 的 SessionStart 钩子方案。
- 人工智能
- AI 应用
- AI 评测
- RAG
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
相关推荐
Kiln 云沙箱环境:setup_env.sh 单入口化改造的设计与实现(Cloud Sandbox DX Phase 2)
Kiln 云沙箱环境:setup_env.sh 单入口化改造的设计与实现(Cloud Sandbox DX Phase 2) 本文围绕 Kiln 仓库 clou
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOpsKiln 云端沙箱会话启动检查:setup_startup.sh 的设计与实现
Kiln 云端沙箱会话启动检查:setup_startup.sh 的设计与实现 在 Kiln 的云端沙箱(如 Claude Code cloud session
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOpsKiln 云沙箱开发体验实战:setup_env.sh / setup_startup.sh 如何零知识启动 Claude Code 云会话
Kiln 云沙箱开发体验实战:setup_env.sh / setup_startup.sh 如何零知识启动 Claude Code 云会话 本文基于 Kiln
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考