首页
/ caveman create-caveman-agent 初始化器详解:一条命令生成带预算治理与 Eval 门禁的 TypeScript Agent 项目

caveman create-caveman-agent 初始化器详解:一条命令生成带预算治理与 Eval 门禁的 TypeScript Agent 项目

2026-09-06 18:31:57作者:盛欣凯Ernestine

caveman 仓库中的 packages/create-caveman-agent@caveman-ai/agent 框架的原子初始化器(npm 包名 @caveman-ai/create-agent),它用一条 npm create 命令生成一个依赖极少、结构严格、内置预算上限与 Eval 门禁的 TypeScript Agent 项目。读完本文,你能完整掌握该初始化器的命令行用法、Provider 凭据选择策略、生成项目的每个文件的作用与预算配置语义,并能理解其"原子写入 + 环境变量白名单"的安全设计,以及生成项目从 devbuildcheck 的完整落地流程。

初始化器定位与快速上手

README(packages/create-caveman-agent/README.md)对初始化器的定义非常克制:@caveman-ai/agent 的原子初始化器(Atomic initializer)。它只负责四件事——创建一个必需的源码文件、一个 starter eval、一份严格的构建配置、一个 provider 选择,并安装依赖。

标准使用流程:

npm create @caveman-ai/agent@latest my-agent
cd my-agent
npm run doctor
npm run dev

npm run doctor 是一个零 provider 调用的就绪检查:如果它报告缺少签名运行时工件(signed runtime artifacts),按它给出的唯一动作 caveman setup --install 处理即可。npm run dev 启动交互式会话,且无需任何 Caveman 账户或托管服务——只要本机有 Node.js 22.19+ 和一个 provider 凭据即可运行。

初始化器的运行时约束在 packages/create-caveman-agent/package.json 中明确声明:

  • bin 入口为 create-caveman-agent,指向 ./dist/index.js,即 npm create 实际调用的是编译后的 Node 脚本;
  • engines 要求 node >= 22.19.0
  • 发布前通过 prepack: npm run build(tsc 编译)保证打包产物可执行,测试(packages/create-caveman-agent/tests/initializer.test.mjs)会校验 bin 目标文件首行是 #!/usr/bin/env node shebang。

从源码结构看,初始化器自身是零运行时依赖的:全部逻辑(参数解析、文件模板、凭据检测、子进程安装)都写在约 320 行的 packages/create-caveman-agent/src/index.ts 中,仅使用 node:child_processnode:fs/promisesnode:pathnode:readline/promises 等 Node 内置模块。

命令行参数与两种使用模式

非交互(脚本化)使用

README 给出的非交互形式是显式指定 provider:

npm create @caveman-ai/agent@latest my-agent -- --provider anthropic

支持的 provider 为 anthropicopenaigoogle。当依赖安装由其他工具接管时,跳过初始化器自带的安装:

npm create @caveman-ai/agent@latest my-agent -- --provider openai --no-install

参数解析逻辑在 parseArgs()src/index.ts,L55-L88)中实现,语义严格:

参数 行为
<project> 唯一的必填位置参数;目录名会被 safeName() 规范化为小写字母/数字/-/_,最长 96 字符,必须含至少一个字母或数字
--provider <anthropic|openai|google> 显式指定 provider;值缺失或以下划线 -- 开头时报错 --provider requires a value;值大小写不敏感,非法值报 unsupported provider
--no-install 关闭初始化器内置的 npm install,输出提示中会补一行 npm install 供调用方自行处理
未知 -- 选项 立即抛错 unknown option <flag>,且不产生任何部分目录
--help / -h 打印 usage 行后退出,不读取凭据、不写文件系统

位置参数必须恰好一个:多给或少给都会打印 usage 并以非零码退出。这种"未知选项即失败"的设计在测试 initializer rejects unknown options without writing target 中被验证:传入 --wat 时退出码为 1、stderr 匹配 unknown option --wat,且目标目录不存在。

Provider 选择:静默、提问与失败三种路径

README 中的一句产品描述——"恰好检测到 1 个 provider 凭据时静默选择;0 个或多个时只问一次"——在 chooseProvider()src/index.ts,L131-L152)中逐字落地:

  1. 若命令行已传 --provider,直接解析使用,不再检测环境;
  2. 否则检测环境变量凭据:
    • ANTHROPIC_API_KEYanthropic
    • OPENAI_API_KEYopenai
    • GEMINI_API_KEYGOOGLE_API_KEYgoogle
  3. 恰好 1 个 → 静默选中该 provider(对应测试 exactly one credential selects provider without question);
  4. 0 个或多个 → 若 stdin/stdout 是 TTY,交互式提问一次 Provider (anthropic/openai/google): ;若非 TTY(CI/管道等),直接失败:无凭据时提示 no provider credential detected; pass --provider,多凭据时提示 multiple provider credentials detected; pass --provider

多凭据的非交互失败场景有专门的测试保证:同时设置 ANTHROPIC_API_KEYOPENAI_API_KEY 时,退出码为 1、stderr 匹配 multiple provider credentials,并且目标目录不会有任何残留assert.rejects(stat(target), { code: "ENOENT" })。

选择结果写入生成项目的 .caveman/provider.json,内容为 { provider, model }。每个 provider 对应一个基线模型,定义在源码头部的 MODELS 表中:

provider 基线模型 生成的凭据提示
anthropic anthropic/claude-haiku-4-5 ANTHROPIC_API_KEY
openai openai/gpt-5.4-mini OPENAI_API_KEY
google google/gemini-2.5-flash GEMINI_API_KEY (or GOOGLE_API_KEY)

测试用 openai 场景断言了 .caveman/provider.jsonmodel 等于 openai/gpt-5.4-mini。生成的项目 README 也会写明"在 shell 中设置对应环境变量,绝不提交 provider 凭据"。

凭据的三重保密约束

README 承诺"Secrets are never printed or written"(秘密永不打印、永不落盘)。实现层面有三重约束:

  1. 输出脱敏:测试 initializer writes one required agent source and deterministic provider choice 设置 OPENAI_API_KEY: "secret-never-print",然后断言 stdout+stderr 中不出现该值;
  2. 不落盘.caveman/provider.json 只写 provider 与 model 字段,不含任何密钥;
  3. 子进程隔离:初始化器安装依赖时不继承父进程完整环境,见下一节的环境白名单。

原子写入:临时目录 + rename

生成过程不是直接往目标目录里写文件,而是走"临时目录 → rename"的原子路径(src/index.ts,L29-L43):

const temporary = resolve(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
try {
  // 依次创建 src/、evals/、.caveman/ 并在临时目录中写入全部模板文件
  if (parsed.install) await installDependencies(temporary);
  await rename(temporary, target);
} catch (error) {
  await rm(temporary, { recursive: true, force: true });
  throw error;
}

设计要点:

  • 临时目录名带 PID 与 UUID,避免同目录并发初始化互相踩踏;
  • 所有模板文件以 flag: "wx"(排他创建)、mode: 0o600 写入;
  • 依赖安装在临时目录内完成、成功后才 rename 到目标路径——这意味着失败的运行不会留下"装了一半依赖的半成品项目";
  • 目标路径事先通过 assertAbsent() 校验必须不存在(已存在则报 target already exists),配合 rename 保证幂等语义。

安装子进程本身的实现(installDependencies(),L90-L115)也有讲究:

  • 优先复用 npm_execpath(当初始化器本身经由 npm create 启动时)直接以 node <npm-cli.js> install ... 执行,保证与用户 npm 版本一致;Windows 下回落到 ComSpeccmd.exe /d /s /c,其他平台直接 npm
  • 安装参数固定为 install --no-audit --no-fund --ignore-scripts——跳过审计与资金信息、禁止执行依赖包的安装脚本,缩小供应链攻击面;
  • 退出码非 0 时抛错并清理临时目录。

环境变量白名单:秘密到不了安装器

dependencyInstallEnv()(L117-L129)构造子进程环境时,只白名单透传约 27 个键:PATHHOME、代理变量(HTTP_PROXY 等)、locale 与终端颜色变量,以及 npm 自身的 NPM_CONFIG_CACHE/REGISTRY/USERCONFIG(含小写形式)。其余一切——包括 OPENAI_API_KEYCAVE_API_KEYAWS_SECRET_ACCESS_KEY——都不会传给安装子进程。

测试 initializer installs dependencies by default and leaves two-command first-run flow 用假 npm 捕获了子进程环境并逐一断言:三个密钥均不在安装器环境中,而 PATH 在;同时断言安装参数恰好是 ["install", "--no-audit", "--no-fund", "--ignore-scripts"]

生成项目逐文件解读

初始化器一次性写出 9 个文件(projectFiles()src/index.ts,L154-L291)。下面按文件说明其内容与设计意图。

package.json:六个 npm 脚本

生成的 package.json 是 ESM("type": "module"),engines.node >= 22.19.0,依赖面极小:

{
  "scripts": {
    "doctor": "caveman-agent doctor",
    "dev": "caveman-agent dev src/agent.ts",
    "run": "node --experimental-strip-types src/run.ts",
    "build": "caveman-agent build caveman.config.ts",
    "check": "caveman-agent check caveman.config.ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": { "@caveman-ai/agent": "^0.1.0" },
  "devDependencies": { "typescript": "5.9.3" }
}

脚本分工(与 packages/agent/README.md 的 CLI 章节一致:devbuildcheckdoctor):

  • doctor:零模型调用的就绪检查;
  • dev:交互式开发会话,入口固定 src/agent.ts
  • run:用 node --experimental-strip-types 直接跑 src/run.ts 做一次性程序化调用(这也是"最小依赖"跑法的来源——Node 原生类型剥离);
  • build:基于 caveman.config.ts 执行有限搜索并尝试写锁定构建(Cave Build);
  • check:部署前校验,拒绝漂移(drift),在模型调用前就拦截配置/构建不一致。

测试断言了上述每个脚本字符串与 typescript: 5.9.3 的精确版本。

tsconfig.json:严格构建配置

{
  "compilerOptions": {
    "target": "ES2024",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts", "evals/**/*.ts", "caveman.config.ts"]
}

这就是 README 所说的"strict build config":strict: true + noEmitallowImportingTsExtensions 配合 Node 原生 strip-types 支持 import ... from "./agent.ts" 这类带扩展名导入。include 精确覆盖三个位置,不额外纳入其他目录。

src/agent.ts:唯一必需的源码文件

import { agent, auto, schema, tool } from "@caveman-ai/agent";

const readiness = tool({
  name: "readiness",
  description: "Read fixture readiness before answering.",
  input: schema.object({}),
  effect: "read",
  execute: () => ({ status: "ready" }),
});

export default agent({
  id: "<project-name>",
  instructions: "Call readiness once. Then reply with exactly: ready",
  model: auto(),
  tools: [readiness],
});

要点:

  • 工具必须显式声明副作用类别 effect: "read"——这是框架对工具的强制要求(效果声明:read/write/idempotent/external,见 packages/agent/README.md Tools 章节);
  • model: auto()CAVE_MODEL.caveman/provider.json → 唯一凭据的基线模型顺序解析,不做任务分类或模型路由——所以初始化器写入的 provider.json 就是 auto() 的直接依据;
  • id 为规范化后的项目名(测试以 support-agent 为例断言生成文件匹配 agent()。

src/run.ts:预算"经济旋钮"第一天就摆上台面

这是生成模板中注释最密集的文件,它把预算配置写成显式代码而不是留给用户日后发现:

const result = await run(definition, "Reply with exactly: ready", {
  rootDir,
  entryPath: "src/agent.ts",
  budget: {
    // 准入上限:每次调用先把"估算最坏情况"计入该上限做预留;
    // 若 provider 最终用量超过预留,收据记录 capBreached/overspent 并停止运行。
    maxUsd: 0.5,
    // 预留余量逼近上限时的策略。"compact"(默认)会重写上下文、
    // 让同一上限在压缩后继续支撑有用工作;
    // "stop" 则跳过压缩,直接钳制输出并停止。
    onExhausted: "compact",
  },
});

console.log(result.stopReason, result.text);
console.log(`estimated list-price subtotal: $${result.receipt.totalEstimatedUsd}`);

这些字段不是模板自造的概念,而是 @caveman-ai/agent 的真实预算 API。在 packages/agent/src/budget.ts 中可以确认:

  • maxUsdmaxTokens 互斥(二选一声明计量单位);
  • onExhausted: "compact" | "stop",规范化时默认值就是 "compact"const onExhausted = budget.onExhausted ?? "compact");
  • 预留机制逐次调用执行:"run reserves each call's estimated worst case against this before the request",超额由收据(receipt)以 capBreached/overspent 字段诚实地记录,而不是掩盖。

另外两个来自框架文档的重要语义在模板注释里同样被写明:所有金额都是"公开目录价估算小计(estimated public-catalog list-price subtotal),从不是发票";当准入触顶时,run 返回带 reason 的部分结果,而 provider 报告的超额仍然保留在收据中可见。测试会用 mock 版 @caveman-ai/agent 实际执行 src/run.ts,断言 run() 收到的 rootDir 解析到项目根、entryPathsrc/agent.ts、prompt 为 Reply with exactly: ready——验证了模板与框架调用契约的对接。

evals/smoke.eval.ts:初始状态为"未批准"

import { eval as defineEval } from "@caveman-ai/agent";

export const smoke = defineEval({
  id: "smoke",
  approved: false,
  input: "Reply with exactly: ready",
  quality: [
    { type: "exact_match", expected: "ready" },
    { type: "tool_called", tools: ["readiness"] },
  ],
});

approved: false 是有意为之(README 原文:"Generated eval starts unapproved"):强制用户先审阅预期行为、确认后再改为 approved: true。这对应框架的构建门禁——npm run build 对每个已批准 fixture 做有限搜索,只有在声明的 eval 全部通过时才写锁定的 Cave Build。

caveman.config.ts.caveman/provider.json

import { defineBuild } from "@caveman-ai/agent/build";

export default defineBuild({
  entry: "src/agent.ts",
  evals: "evals/*.eval.ts",
  maxSearchCostUsd: 2,
});

构建配置把入口、eval glob 与搜索成本上限(maxSearchCostUsd: 2)显式固定。.caveman/provider.json 则保存上文的 provider 与基线模型二元组。

.gitignore 与生成的 README.md

.gitignore 只排除三类内容:node_modules/.caveman/traces/.env*——与"凭据永不落盘、trace 是本地运行产物"的原则一致。生成的 README.md 按 provider 注入对应的凭据变量名,并给出两段操作说明:doctor 报缺签名工件时跑 caveman setup --install 后重跑 doctor;以及"确认 eval 行为后把 approved: false 改为 true,再 npm run buildnpm run check"。测试还特意断言生成 README 不包含 "never spend past" 这类绝对化措辞,而包含 capBreached/overspent——即文档必须描述预算机制的真实语义(超额会被记录而非"保证绝不多花")。

Eval 批准流程与"诚实数字"原则

把 README 末段与框架文档串起来,生成项目上线前的完整路径是:

  1. npm run doctor:零 provider 调用检查 Node 版本、沙箱包含性、engine/gateway 可达性等(缺 engine 只是 WARN,因为 observe-only 运行仍可用);
  2. npm run dev:迭代 Agent 行为,确认 starter eval 的预期输出;
  3. evals/smoke.eval.ts 中将 approved: false 改为 approved: true
  4. npm run build:执行 eval 门禁构建。按 packages/agent/README.md,当 usage 缺失、模型无法定价、缓存回退、恢复失败、沙箱/隐私失败、质量下降、搜索未完成或超成本上限时,不会写出优化锁;
  5. 锁定构建之后、部署之前执行 npm run check,在模型调用前拒绝漂移。

贯穿这套流程的是 caveman 的"诚实数字"原则,README 原文与生成模板注释高度一致:

  • 本地证据的 basis 永远是 inferred(推断值);
  • 已验证的节省(verified savings)在真实生产流量通过独立的 rollout 与 ledger 门禁之前保持 $0
  • doctor 的人类可读输出中会打印 verified savings: $0——本地机器"不铸造"任何节省声明。

这也解释了为什么初始化器把"eval 未批准""$0""inferred"这些看似琐碎的字符串写进模板:它们不是营销话术,而是框架证据体系的入口约束。

测试如何固化这套行为

packages/create-caveman-agent/tests/initializer.test.mjsnode:test 驱动编译后的 dist/index.js,六个用例覆盖了 README 的每条承诺:

用例 验证的 README 承诺
package lifecycle builds executable initializer before packing 包元数据完整、bin 文件带 shebang(可执行)
initializer exposes help without credentials or filesystem writes --help/-h 零副作用
initializer writes one required agent source and deterministic provider choice 只写一个必需源码文件;provider 选择确定可复现;密钥不出现;run.ts 语义完整
exactly one credential selects provider without question 单凭据静默选择
noninteractive ambiguous provider fails without partial directory 多凭据非交互失败且无残留
initializer installs dependencies by default and leaves two-command first-run flow 默认安装依赖、环境白名单生效、输出收敛为两条命令(cd/npm install 不再需要单独提示,直接 npm run dev
initializer rejects unknown options without writing target 严格参数解析

最后一条用例中"two-command first-run flow"值得注意:默认安装成功后,成功输出只剩 created <name> / provider <p> (model) / cd <dir> / npm run dev 四行;--no-install 时插入 npm install 行。初始化器的输出契约本身就是稳定的,可被脚本消费。

适用前提与限制

  • Node.js ≥ 22.19:生成项目的 engines、初始化器自身的 engines@caveman-ai/agent 三者同要求;run 脚本依赖 --experimental-strip-types
  • 网络与凭据:无需 Caveman 账户,但需要 provider 凭据与网络直连 provider;@caveman-ai/agent 0.1.0 的运行时依赖包含 Pi agent 内核、MCP SDK、TypeBox、zod 等(见 packages/agent/package.json);
  • observe-only 默认:未安装 Caveman Engine 时,run/dev 以 observe-only 模式直连 provider(无转换、无 gateway 遥测),provider 用量与本地上下文估算仍然可用;要启用压缩优化需另行安装 CLI 并 caveman start(见 packages/agent/README.md 的 Quick start 章节);
  • 本目录的历史定位packages/create-caveman-agent/CLAUDE.md 注明该目录的 source of truth 在上游 SDK 仓库,本仓库中是"pinned integration"的消费副本——阅读实现时以当前快照为准。

小结

create-caveman-agent 用 300 行零依赖的 TypeScript,把 caveman 框架对"可信 Agent 起点"的定义物化成了 9 个文件:一个显式声明副作用的 agent 定义、一个未批准的 smoke eval、一份严格 tsconfig、一个把 maxUsd/onExhausted 预算语义写成可见代码的 run.ts,以及 provider 选择与构建配置。它的三个工程特色——原子临时目录写入、安装子进程的环境白名单、以及"本地证据恒为 inferred、验证节省恒为 $0"的诚实数字约束——都由 initializer.test.mjs 的用例逐条固化。对需要快速搭建一个带预算上限、Eval 门禁与部署前 check 流程的 TypeScript Agent 项目的读者,这条 npm create 命令就是官方给出的最短路径。

登录后查看全文
热门项目推荐
相关项目推荐