3994 字
约 13 分钟
3
第六讲. 让 agent 每次工作前先初始化

第六讲. 让 agent 每次工作前先初始化

在使用 AI 编码 agent 时,一个常见的低效模式是:让 agent 直接开始做功能,它上来就写代码,但很快会发现测试框架没配好、环境有问题、项目结构不清晰,大量时间花在了"搞清楚这个项目怎么运作"上面,真正用于写功能的时间反而很少。

更好的做法是,在让 agent 开始干活之前,先用一个独立的阶段把基础环境搭好、验证命令跑通、项目结构搞清楚。初始化工作应该和功能实现分开,它们是两种性质完全不同的任务。

这节课要讨论的,就是为什么初始化必须是独立的阶段,不能跟实现混在一起。

两种不同的工作

初始化和实现的优化目标完全不同。实现阶段的目标是最大化已验证功能的数量和质量,初始化阶段的目标则是最大化后续所有实现的可靠性和效率。

当你把初始化和实现混在一起的时候,agent 面临一个多目标优化问题:它要同时搭基础设施和写功能代码。在没有显式优先级设定的情况下,agent 自然倾向于写代码(因为那是直接可见的产出),而牺牲基础设施(因为它的价值只能在后续会话中体现)。结果就是基础设施没搭牢,功能代码的可靠性也打了折扣。

初始化生命周期

flowchart TB
    subgraph Wrong["混在一起的一次会话(错误)"]
        W1["一上来就开始做功能"] --> W2["做到一半才发现环境和测试缺口"]
        W2 --> W3["累积未经验证的代码"]
        W3 --> W4["下个会话还得重新摸项目状态"]
    end

    subgraph Right["独立初始化阶段(正确)"]
        R1["会话 1:环境可运行"] --> R2["示例测试通过"]
        R2 --> R3["写出启动契约 + 任务清单"]
        R3 --> R4["提交干净检查点"]
        R4 --> R5["后续会话直接开始做已准备好的任务"]
    end

初始化与实现同时进行的问题

最直接的问题是基础设施搭不牢。Agent 花了 80% 的精力写功能代码,剩下 20% 随便搭了点基础设施。测试框架配了但没验证过,lint 规则设了但太宽松,进度文件没创建。这些缺陷在第一个会话里不明显(因为 agent 还记得它做了什么),但到第二个会话就暴露了:新 agent 不知道项目怎么跑、怎么测、做到哪了。

更隐蔽的代价是"未验证的累积"。在测试框架配好之前写的功能代码,等回头补测试的时候可能发现设计上就有问题,早知道的话应该用不同的方式实现。前面写的代码越多,后面需要推翻重来的就越多。

上下文预算也在被浪费。初始化工作(配环境、配测试、理解项目结构)消耗了大量预算,留给实际功能实现的反而不够了。结果第一个会话只完成了一半的功能,第二个会话还得从头理解项目。预算花在了初始化上,但初始化也没做好,两头都没占着。

最容易被忽略的是隐式假设埋下的雷。Agent 在初始化过程中做的决策(用什么测试框架、目录怎么组织、依赖怎么管理)如果不显式记录下来,后续会话就可能做出矛盾的选择。第一个会话选了 Vitest 做测试框架,第二个会话的 agent 不知道,又引入了 Jest,两套测试框架共存,维护成本翻倍。

Anthropic 在他们的长运行应用开发研究中明确建议把初始化和实现分离。他们的实验数据:使用独立初始化阶段的项目,多会话场景中的功能完成率比混合方式高 31%。而且初始化阶段投入的时间在后续 3-4 个会话中就能完全收回。

OpenAI 的 Codex harness engineering 指南也强调"仓库作为操作记录"的原则:第一次运行就要建立清晰的操作结构,否则每次新会话都得重新推断项目约定。

核心概念

  • 初始化阶段:agent 生命周期中的第一个阶段,只建立后续实现所需的执行前提,不做功能开发。它的产出是基础设施,而不是业务代码。
  • 启动就绪清单:一个项目能被全新 agent 会话无歧义操作的条件:能启动、能测试、能看进度、能接手下一步。四个条件缺一不可。
  • 从零开始 vs 从模板开始:从零开始意味着 agent 需要自行推断项目结构,效果差;从模板开始意味着基础设施已经就位,效果好得多。能用模板就用模板。
  • 随时可接手:项目在任何时刻都处于"可以被全新 agent 接手"的状态。不需要口头解释,只看仓库内容就能接着干。
  • 从开始到第一次测试通过:衡量初始化效率的核心指标。时间越短,初始化越高效。
  • 后续会话的成功率:后续会话不需要依赖隐式知识就能成功执行任务的比例,这是初始化质量的最佳衡量标准。

初始化的正确做法

把初始化当作一个独立的阶段来执行。 第一个会话只做初始化,不写任何业务功能代码。初始化的产出是:

1. 可运行的环境。 项目能启动、依赖都装好、没有环境问题。

2. 可验证的测试框架。 至少有一个示例测试能通过,证明测试框架本身是配好的。

3. 启动就绪清单文档。 一个明确的文档告诉后续会话:

# 初始化契约

## 启动命令
- 安装依赖:`make setup`
- 启动开发服务器:`make dev`
- 运行测试:`make test`
- 完整验证:`make check`

## 当前状态
- 所有依赖已安装并锁定
- 测试框架已配置(Vitest + React Testing Library)
- 示例测试通过(1/1)
- Lint 规则已配置(ESLint + Prettier)

## 项目结构
- src/ — 源代码
- src/components/ — React 组件
- src/api/ — API 客户端
- tests/ — 测试文件

4. 任务分解。 把整个项目拆成有序的任务列表,每个任务有明确的验收标准:

# 任务分解

## Task 1: 用户认证基础
- 实现 JWT 认证中间件
- 添加登录/注册端点
- 验收标准:pytest tests/test_auth.py 全部通过

## Task 2: 用户资料页面
- 实现用户资料 CRUD
- 添加资料编辑表单
- 验收标准:pytest tests/test_profile.py 全部通过

## Task 3: 搜索功能
- ...

5. Git 提交作为检查点。 初始化完成后提交一个干净的 checkpoint。后续所有工作都从这个 checkpoint 开始。

热启动策略:不要从空目录开始。用一个项目模板(create-react-app、fastapi-template 等)预置好标准的目录结构、依赖配置和测试框架。把通用的初始化步骤预置到模板里,只留下项目特有的初始化工作。

初始化的完成条件:启动就绪清单的四个条件全部满足——能启动、能测试、能看进度、能接手下一步。用这个检查清单验收初始化:

## 初始化验收清单
- [ ] `make setup` 从零开始能成功
- [ ] `make test` 至少有一个测试通过
- [ ] 新的 agent 会话能只看仓库回答"怎么跑"和"怎么测"
- [ ] 任务分解文件存在且有至少 3 个任务
- [ ] 所有内容已提交到 git

实际案例

一个 React 前端项目的两种初始化方式对比:

混合方式:agent 在第一个会话中同时做了项目脚手架创建和首个功能实现。会话结束时,仓库有可运行的代码,但没有显式的启动和测试命令文档、没有进度跟踪文件、没有任务分解。第二个会话花了约 20 分钟推断项目结构、测试框架和构建流程。

独立初始化:第一个会话只做初始化,用项目模板创建目录结构、配置测试框架(Vitest + React Testing Library)、写一个示例测试并验证通过、创建启动就绪清单文档和任务分解文件、提交初始检查点。第二个会话的重建时间不到 3 分钟,直接从任务列表开始工作。

整个项目周期对比:混合方式的总重建时间(跨所有会话)比独立初始化多约 60%。独立初始化多花的那 20 分钟在后续会话中被成倍收回。前期多投入一点时间把初始化做扎实,后续的效率反而更高。

核心要点

  • 初始化和实现的优化目标不同,混在一起只会互相拖后腿。
  • 初始化的产出是基础设施:可运行的环境、可验证的测试、启动就绪清单、任务分解。
  • 用"启动就绪清单"的四个条件验收初始化:能启动、能测试、能看进度、能接手下一步。
  • 热启动优于冷启动,用项目模板预置标准化的基础设施。
  • 初始化投入的时间会在后续 3-4 个会话中完全收回,这是前期投资,不是额外成本。

延伸阅读

练习

  1. 启动就绪清单设计:为你正在开发的项目写一个完整的启动就绪清单。然后开一个全新的 agent 会话,只给它看仓库内容(不给任何口头上下文),让它尝试启动项目、跑测试、了解当前进度。记录它遇到的问题,每个问题都对应启动就绪清单中缺失的一个条款。
  2. 对比实验:选一个中等复杂度的新项目。方式 A 让 agent 初始化和首次实现同时做,方式 B 先花一个会话做独立初始化,第二个会话再开始实现。在 4 个会话后对比首次验证时间、重建成本和功能完成率。
  3. 初始化验收清单:为你的项目设计一个初始化验收清单,让一个全新的 agent 会话逐项执行,记录哪些项通过了、哪些没通过。没通过的项就是 harness 需要补强的地方。

代码示例

init-check.ts

/**
 * init-check.ts
 *
 * Programmatically checks initialization prerequisites:
 *   - Node.js version
 *   - Dependencies installed (node_modules exists)
 *   - Data directory exists
 *   - Config files present
 *
 * Simulates running with and without an explicit init phase,
 * showing how missing prerequisites cause silent failures later.
 *
 * Run: npx tsx docs/lectures/lecture-06-why-initialization-needs-its-own-phase/code/init-check.ts
 */

import * as fs from "node:fs";
import * as path from "node:path";
import * as childProcess from "node:child_process";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface CheckItem {
  name: string;
  category: string;
  check: () => { pass: boolean; detail: string };
  impactIfMissing: string;
}

interface CheckResult {
  name: string;
  category: string;
  passed: boolean;
  detail: string;
  impactIfMissing: string;
}

// ---------------------------------------------------------------------------
// Checks
// ---------------------------------------------------------------------------

function createChecks(targetDir: string): CheckItem[] {
  return [
    {
      name: "Node.js version >= 18",
      category: "Runtime",
      check: () => {
        try {
          const version = process.version; // e.g. "v20.11.0"
          const major = parseInt(version.replace("v", "").split(".")[0], 10);
          return {
            pass: major >= 18,
            detail: `Detected: ${version} (need >= v18)`,
          };
        } catch {
          return { pass: false, detail: "Could not detect Node.js version" };
        }
      },
      impactIfMissing: "TypeScript features and built-in APIs unavailable",
    },
    {
      name: "package.json exists",
      category: "Config",
      check: () => {
        const p = path.join(targetDir, "package.json");
        const exists = fs.existsSync(p);
        return {
          pass: exists,
          detail: exists ? `Found: ${p}` : "Not found in target directory",
        };
      },
      impactIfMissing: "Cannot install dependencies or run scripts",
    },
    {
      name: "Dependencies installed (node_modules)",
      category: "Dependencies",
      check: () => {
        const p = path.join(targetDir, "node_modules");
        const exists = fs.existsSync(p);
        return {
          pass: exists,
          detail: exists ? `Found: ${p}` : "node_modules/ not found -- run npm install",
        };
      },
      impactIfMissing: "All imports fail at runtime",
    },
    {
      name: "TypeScript available",
      category: "Toolchain",
      check: () => {
        try {
          const result = childProcess.execSync("npx tsc --version", {
            encoding: "utf-8",
            timeout: 10000,
            stdio: ["pipe", "pipe", "pipe"],
          }).trim();
          return { pass: true, detail: `Detected: ${result}` };
        } catch {
          return { pass: false, detail: "TypeScript not available via npx" };
        }
      },
      impactIfMissing: "Cannot compile TypeScript files",
    },
    {
      name: "tsconfig.json exists",
      category: "Config",
      check: () => {
        const p = path.join(targetDir, "tsconfig.json");
        const exists = fs.existsSync(p);
        return {
          pass: exists,
          detail: exists ? `Found: ${p}` : "Not found",
        };
      },
      impactIfMissing: "TypeScript compiler uses defaults, may not match project needs",
    },
    {
      name: "Source directory exists",
      category: "Structure",
      check: () => {
        const candidates = ["src", "lib", "app"];
        const found = candidates.filter((d) => {
          const p = path.join(targetDir, d);
          return fs.existsSync(p) && fs.statSync(p).isDirectory();
        });
        return {
          pass: found.length > 0,
          detail: found.length > 0 ? `Found: ${found.join(", ")}` : "No src/ lib/ or app/ directory found",
        };
      },
      impactIfMissing: "Agent cannot locate source files to modify",
    },
    {
      name: "Test directory exists",
      category: "Structure",
      check: () => {
        const candidates = ["test", "tests", "__tests__", "spec"];
        const found = candidates.filter((d) => {
          const p = path.join(targetDir, d);
          return fs.existsSync(p) && fs.statSync(p).isDirectory();
        });
        return {
          pass: found.length > 0,
          detail: found.length > 0 ? `Found: ${found.join(", ")}` : "No test directory found",
        };
      },
      impactIfMissing: "Agent cannot find or run existing tests",
    },
    {
      name: "Git repository initialized",
      category: "Version Control",
      check: () => {
        const gitDir = path.join(targetDir, ".git");
        const exists = fs.existsSync(gitDir);
        return {
          pass: exists,
          detail: exists ? "Git repository detected" : "No .git directory found",
        };
      },
      impactIfMissing: "No rollback capability, no change history",
    },
  ];
}

// ---------------------------------------------------------------------------
// Simulation: with and without init phase
// ---------------------------------------------------------------------------

interface SimResult {
  scenario: string;
  checksRun: boolean;
  failuresBeforeWork: number;
  workAttempted: boolean;
  workSucceeded: boolean;
  timeWastedMs: number;
}

function simulateWithoutInit(): SimResult {
  // Agent skips init, goes straight to work.
  // Encounters failures one at a time as it hits missing prerequisites.
  const checks = createChecks(process.cwd());
  let failures = 0;
  let timeWasted = 0;

  for (const check of checks) {
    const result = check.check();
    if (!result.pass) {
      failures++;
      timeWasted += 200; // Agent spends time discovering each missing piece
    }
  }

  return {
    scenario: "NO INIT PHASE",
    checksRun: false,
    failuresBeforeWork: 0, // Didn't check upfront
    workAttempted: true,
    workSucceeded: failures === 0,
    timeWastedMs: timeWasted,
  };
}

function simulateWithInit(): SimResult {
  // Agent runs init phase first, discovers all issues upfront.
  const checks = createChecks(process.cwd());
  let failures = 0;

  for (const check of checks) {
    if (!check.check().pass) {
      failures++;
    }
  }

  return {
    scenario: "WITH INIT PHASE",
    checksRun: true,
    failuresBeforeWork: failures,
    workAttempted: failures === 0,
    workSucceeded: failures === 0,
    timeWastedMs: 0,
  };
}

// ---------------------------------------------------------------------------
// Reporting
// ---------------------------------------------------------------------------

function pad(s: string, len: number): string {
  return s.length >= len ? s : s + " ".repeat(len - s.length);
}

function run(): void {
  const targetDir = process.cwd();
  console.log("\n" + "=".repeat(80));
  console.log("  INITIALIZATION PREREQUISITE CHECK");
  console.log("=".repeat(80));
  console.log(`  Target: ${targetDir}\n`);

  const checks = createChecks(targetDir);
  const results: CheckResult[] = checks.map((c) => {
    const r = c.check();
    return {
      name: c.name,
      category: c.category,
      passed: r.pass,
      detail: r.detail,
      impactIfMissing: c.impactIfMissing,
    };
  });

  // Detailed report
  const header = `| ${pad("Check", 35)}| ${pad("Category", 12)}| ${pad("Status", 6)}| Detail`;
  const sep = `|${"-".repeat(37)}|${"-".repeat(14)}|${"-".repeat(8)}|${"-".repeat(40)}`;
  console.log(header);
  console.log(sep);

  for (const r of results) {
    const status = r.passed ? "PASS" : "FAIL";
    console.log(`| ${pad(r.name, 35)}| ${pad(r.category, 12)}| ${pad(status, 6)}| ${r.detail}`);
  }

  const passCount = results.filter((r) => r.passed).length;
  const failCount = results.filter((r) => !r.passed).length;

  console.log("\n" + "-".repeat(80));
  console.log(`  Results: ${passCount} passed, ${failCount} failed out of ${results.length} checks`);

  if (failCount > 0) {
    console.log("\n  FAILED CHECKS -- Impact if not resolved:");
    for (const r of results.filter((r) => !r.passed)) {
      console.log(`    - ${r.name}: ${r.impactIfMissing}`);
    }
  }

  // Comparison: with vs without init
  const noInit = simulateWithoutInit();
  const withInit = simulateWithInit();

  console.log("\n" + "=".repeat(80));
  console.log("  INIT PHASE COMPARISON");
  console.log("=".repeat(80) + "\n");

  const cHeader = `| ${pad("Metric", 35)}| ${pad("No Init Phase", 18)}| ${pad("With Init Phase", 18)}|`;
  const cSep = `|${"-".repeat(37)}|${"-".repeat(20)}|${"-".repeat(20)}|`;
  console.log(cHeader);
  console.log(cSep);
  console.log(`| ${pad("Prerequisites checked upfront", 35)}| ${pad("No", 18)}| ${pad("Yes", 18)}|`);
  console.log(`| ${pad("Work attempted despite issues", 35)}| ${pad(String(noInit.workAttempted), 18)}| ${pad(String(withInit.workAttempted), 18)}|`);
  console.log(`| ${pad("Time wasted discovering issues late", 35)}| ${pad(noInit.timeWastedMs + "ms", 18)}| ${pad(withInit.timeWastedMs + "ms", 18)}|`);
  console.log(`| ${pad("Work succeeded", 35)}| ${pad(String(noInit.workSucceeded), 18)}| ${pad(String(withInit.workSucceeded), 18)}|`);

  console.log("\n  An explicit init phase catches problems before they waste time.\n");
}

run();

init.sh

#!/usr/bin/env bash
set -euo pipefail

echo "[init] installing dependencies"
npm install

echo "[init] starting the docs site is optional"
echo "[init] use npm run docs:dev for course docs"

echo "[init] project-specific startup would go here"

初始化器输出检查清单

初始化器输出检查清单

  • 是否有标准的启动命令?
  • 是否有标准的验证命令?
  • 是否有第一个进度产物?
  • 是否有稳定的首次提交?
  • 是否有供后续运行使用的可见功能面?
第六讲. 让 agent 每次工作前先初始化
http://www.clxhxhhr.top/posts/271/
作者
clxstart
发布于
2026-07-26
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。