Vibe Coding进阶版,如何写出漂亮的代码

前言

作为从一开始使用vibe coding编程的一批人,非常震惊AI的发展,同时也逐渐的升级编码的方式,从一开始的写小功能点,再到后面的大功能,最后到实现一个具体的需求。随着需求的不断提升,规范工具也是非常重要的,以下是我近期的一些感悟,尤其是Hooks的概念。

一、先把 Vibe Coding 的概念摆在桌面上

1. 从 Vibe Coding 到工程化 Agent

在这里插入图片描述

1.1 Vibe Coding 到底是什么

Vibe Coding 通常指通过自然语言描述目标,让大模型生成、修改、运行和调试代码,人主要负责表达意图、观察结果并继续反馈。这个说法在 2025 年由 Andrej Karpathy 推广开来,最初带有一种很轻松甚至略显随意的意味:先让想法跑起来,再边看边改。

这种方式非常适合原型、小工具和探索性开发,但一旦进入长期维护的生产项目,问题也会迅速出现:

(1)模型可能记住目标,却漏掉约束

例如需求是实现登录功能,模型通常不会忘记登录本身,却可能在长任务后半段忘记日志脱敏、业务步骤注释、StopWatch 阶段命名、数据库兼容性等细节。

(2)上下文越长,不等于细节越可靠

上下文窗口解决的是“能放进去多少信息”,并不保证每条信息都获得相同注意力。大量代码、日志、测试输出和中间推理会让重要规则被噪声淹没。

(3)生产级 Vibe Coding 需要一个工程闭环

我更愿意把生产级 Vibe Coding 理解为:

人负责目标、边界和验收标准,Agent 负责执行,Rules 负责持续提醒,Tools 与 MCP 提供能力,Hooks 与 CI 负责校验。

它不是“忘记代码存在”,而是把人的关注点从逐行输入代码,提升到意图、架构、风险和验证。

1.2 Agent Harness

在这里插入图片描述

Agent Harness 可以理解为大模型外面的运行框架。它通常由模型、指令、工具、上下文管理、权限、执行循环和验证机制组成。Cursor 将其概括为 Instructions、Tools 与 Model 三个核心部分,实际产品还会加入沙箱、Hooks、子 Agent、压缩和审计。

在这里插入图片描述

同一个模型放进不同 Harness,编码效果可能差异很大。原因并不神秘:模型是否能准确找到代码、是否能运行测试、失败后是否继续修正、结束前是否被强制复核,都会直接改变最终结果。

2. Rules 与 AGENTS.md

2.1 Rules 是持续注入的项目约定

Rules 用来告诉 Agent:

  • 当前项目如何组织代码;
  • 哪些技术与写法允许使用;
  • 哪些行为禁止发生;
  • 修改完成后需要做什么验证;
  • 面对歧义、风险和只读任务时如何处理。

Cursor 的项目规则位于 .cursor/rules/*.mdc,可以设置为始终加载、按文件 Glob 自动加载、由 Agent 按描述选择,或者手动引用。Codex 则主要使用 AGENTS.md 承载自然语言项目约定。

这里有一个容易混淆的地方:Codex 文档中的 Rules 还可以特指命令权限规则,而代码风格、架构和验证要求应放在 AGENTS.md。因此跨平台迁移时,不能只看名称相同就认为语义完全一致。

2.2 Rules 适合放什么

(1)适合
  • 命名与分层原则;
  • 构建、测试和格式化命令;
  • 架构边界与安全禁令;
  • 修改范围、沟通方式和验收要求;
  • 指向项目标准实现的简短索引。
(2)不适合
  • 完整复制几十页编码手册;
  • 很少触发的特殊业务流程;
  • 可以由编译器、Linter 或测试确定判断的全部细节;
  • 容易变化的版本、令牌和临时环境状态;
  • 希望百分之百强制执行的安全边界。

Rules 本质上仍然是上下文。它能够提高遵循概率,却不是强制执行引擎。

2.3 Codex 的 32 KiB 限制

Codex 会从全局到项目目录逐层发现 AGENTS.md,越靠近当前工作目录的规则优先级越高。默认情况下,合并后的项目指令达到 32 KiB 就会停止继续加载。这个限制可以配置,但简单粗暴地扩大容量通常不是最佳答案。

更好的做法是:

  • 根目录只放全项目必须知道的约定;
  • 模块差异放在更靠近代码的嵌套 AGENTS.md
  • 详细教程移到普通文档或 Skill 的 references;
  • 机械规则交给 Hook、静态分析和 CI。

官方细节可参考 Codex AGENTS.md 文档

3. Skills

3.1 Skill 是按需加载的工作方法

Skill 不是普通规则的另一种写法。它更像一个面向特定任务的操作包,至少包含 SKILL.md,还可以包含:

  • scripts/:可执行脚本;
  • references/:需要时读取的参考资料;
  • assets/:模板、图片或固定资源。

Agent 启动时通常只看到 Skill 的名称和描述,确认任务相关后才加载完整内容。这种渐进式加载可以减少长期上下文污染。Agent Skills 规范建议将主 SKILL.md 控制在 500 行以内,详细资料按需拆分。

3.2 Skill 与 Rule 的核心区别

Rule 回答“这个项目始终应该怎么做”,Skill 回答“遇到这类任务时,完整流程应该怎么走”。

例如:

  • Java 命名、禁止循环 SQL:Rule;
  • 根据 Figma 设计实现页面并截图验收:Skill;
  • 飞书文档查询、导出、编辑的完整流程:Skill;
  • 每次 Java 修改都要执行相关测试:Rule 或 Hook。

4. Tools 与 MCP

4.1 Tool 是 Agent 真正执行动作的接口

大模型本身只能生成内容。读取文件、修改代码、运行命令、访问浏览器、查询数据库,都需要通过 Tool 完成。

一个好的 Tool 应该具备:

  • 明确的名称和使用场景;
  • 结构化输入与输出;
  • 尽可能小而稳定的职责;
  • 清晰的权限和失败信息;
  • 可被日志与 Hook 观察。

Tool 是否存在,不代表模型一定会正确调用它。工具描述模糊、工具数量过多、返回内容过长,都会降低选择质量。

4.2 MCP 是工具与上下文的标准连接层

MCP,即 Model Context Protocol,是 AI 应用连接外部系统的开放标准。它解决的不是“模型会不会写代码”,而是“不同 Agent 如何用统一方式接入代码搜索、数据库、GitHub、飞书、浏览器和内部平台”。

MCP Server 主要可以暴露三类能力:

  • Tools:模型主动调用的动作;
  • Resources:应用提供给模型的上下文资源;
  • Prompts:由用户触发的可复用模板。

完整定义可参考 MCP Server Concepts

4.3 MCP 不等于 Tool

Tool 是可执行能力本身,MCP 是发现、描述和调用这些能力的一种标准协议。内置 Shell 是 Tool,但不一定来自 MCP;CodeGraph 可以通过 MCP 暴露代码查询 Tool;飞书服务也可以通过 MCP 同时暴露文档资源和写入动作。

5. Hooks

在这里插入图片描述

5.1 Hook 是 Agent 生命周期中的自动检查点

Hook 会在会话开始、工具执行前后、上下文压缩前后或 Agent 准备停止时自动运行脚本。它不依赖模型是否记得调用,因此很适合解决“长任务后规则淡化”的问题。

常见事件包括:

  • SessionStart:会话开始;
  • PreToolUse:工具执行前;
  • PostToolUse:工具执行后;
  • PreCompact / PostCompact:上下文压缩前后;
  • Stop:Agent 准备结束;
  • SubagentStart / SubagentStop:子 Agent 开始或结束。

不同产品支持的事件名和返回格式并不完全相同。Cursor 使用 .cursor/hooks.json,Codex 使用 .codex/hooks.jsonconfig.toml。配置不能直接复制,但底层检查脚本可以共享。

5.2 Hook 最适合做什么

(1)执行前拦截

例如禁止裸 mvn、禁止使用旧版 Maven、禁止永久修改全局 JAVA_HOME、阻止明显危险命令。

(2)执行后记录

记录本轮修改过的文件、工具调用结果和审计信息,为结束检查准备准确范围。

(3)结束前复核

运行静态检查、测试或生成复核清单。如果失败,就让主 Agent 继续修复,而不是带着问题结束任务。

Codex 当前主要执行 command 类型 Hook;某些特殊工具路径也可能绕开标准 Hook 路径,因此 Hook 是重要护栏,但不能代替沙箱、权限与 CI。详细行为可参考 Codex Hooks 文档Cursor Agent Best Practices

6. Subagents

6.1 Subagent 是被委派边界任务的独立 Agent

主 Agent 可以把代码探索、测试分析、安全审查或资料检索交给不同子 Agent,然后汇总结果。它的价值不只是并行,更重要的是隔离噪声:测试日志和大范围搜索留在子线程,主线程继续保留需求、决策和最终输出。

Codex 官方将这类问题称为 Context Pollution 和 Context Rot,并建议优先把读取密集型任务并行化,对多个 Agent 同时写代码保持谨慎。Codex Subagents 文档还支持在 .codex/agents/*.toml 中定义项目级自定义 Agent。

6.2 Subagent 的代价

  • 每个子 Agent 都会消耗自己的 Token 和工具调用;
  • 错误结论可能被主 Agent继续传播;
  • 多个 Agent 同时修改代码容易冲突;
  • 任务拆分、结果格式和完成条件不清楚时,沟通成本可能超过并行收益;
  • 主 Agent 仍然必须验证结果,不能把“子 Agent 已完成”当成“任务已正确完成”。

7. Plugins、Plan、Tests 与 CI

7.1 Plugin 是能力分发包

Plugin 可以把 Rules、Skills、Subagents、MCP、Hooks 和资源打包安装。它适合跨项目、跨团队分发成熟能力,而不是用来替代每个概念本身。Cursor 已经把这些能力组合进插件体系,说明生态正在从零散配置走向可版本化的 Agent 工程组件。

7.2 Plan 是长任务的临时执行合同

Plan 不属于永久规则。它针对当前任务,把需求文档转成可核对的步骤、文件范围和验收标准。Rule 解决长期一致性,Plan 解决本次任务细节不丢失。

7.3 Tests 与 CI 是最后的硬边界

Rules 和模型复核都具有概率性;Hook 也可能因为事件覆盖、运行环境或配置问题没有触发。编译器、测试、静态分析和 CI 才是合并前稳定、可重复的硬门禁。

我习惯把它们分成三层:

  • Rules:事前告诉 Agent 应该怎么做;
  • Hooks:过程中发现遗漏并要求修正;
  • CI:最终决定代码能不能进入主分支。

二、不同场景下,应该选择什么机制

1. 小型、一次性任务

1.1 场景

例如修改一个错误提示、补一个单元测试、调整某个 DTO 字段名。

1.2 最佳组合

(1)Prompt + 精简 Rules + 相关测试

这类任务不需要创建 Skill,也不需要启动多个子 Agent。Prompt 中给出当前目标和验收标准,Rules 提供项目已有风格,修改后运行最小相关测试即可。

这么做的原因是任务边界已经很小。额外编排会增加上下文和沟通成本,却不会明显提升结果。

1.3 限制

如果所谓“小修改”会改变数据库结构、权限或公共 API,就不能只按代码行数判断风险。任务小不代表影响面小。

2. 长任务与详细设计文档

2.1 场景

例如根据一份完整设计文档实现注册、登录、租户、审计和 TraceId 链路。模型通常能记住大目标,却可能在十几个文件之后遗漏某个字段、失败分支或日志要求。

2.2 最佳组合

(1)Rules 保留稳定原则

保留命名、分层、日志脱敏、禁止循环 SQL、验证要求等跨需求仍然成立的内容。

(2)Plan 展开当前设计

把设计文档转成可核对清单:

1. 建立数据结构
   验证:字段、约束、迁移脚本与设计文档一致
2. 实现业务入口
   验证:成功、拒绝、异常路径完整
3. 接入日志与耗时
   验证:关键节点可追踪,无敏感信息
4. 补充测试
   验证:核心分支与回归场景通过
(3)Stop Hook 强制回看

在 Agent 准备结束时,让 Hook 检查本轮变更,并返回语义复核清单,要求主 Agent 对照设计文档再检查一次。

这么做比继续向 Rules 追加内容更有效,因为 Plan 与 Stop Hook 会在任务开始和任务结束两个关键时点重新提升细节权重。

2.3 推荐流程

失败

通过

失败

通过

用户目标与设计文档

Rules / AGENTS.md 提供长期约束

Plan 转换为本次验收清单

MCP / Tools 查询并修改代码

PostToolUse 记录变更范围

Stop Hook 执行静态检查

主 Agent 修正

主 Agent 执行语义复核

编译、测试与 CI

交付结果

3. 代码风格、注释和耗时统计

3.1 为什么不能只靠 Rules

“业务方法需要 Step 注释”与“所有方法都要 Step 注释”不是一回事。“性能敏感阶段需要 StopWatch”也不能简单等价为“所有 Service 方法都加 StopWatch”。

这类约束同时包含机械部分和语义部分:

  • 是否出现禁用 import,可以机械判断;
  • Step 注释是否描述真实业务动作,需要语义判断;
  • StopWatch 是否成对停止,可以部分机械判断;
  • 某个方法到底需不需要计时,需要结合业务判断。

3.2 最佳组合

(1)高置信度规则交给脚本

项目的 scripts/agent-hooks/policy.json 维护可扩展策略,例如:

  • 禁止 Spring StringUtils / CollectionUtils 做通用判空;
  • 禁止 UUID.randomUUID().toString()
  • 禁止内联项目全限定类名;
  • 限制 TraceIdHolder.get() 的使用边界;
  • 阻止错误 Maven 与永久 JAVA_HOME 修改。

新增同类校验时,通常只增加一条配置,而不是重写 Hook 生命周期代码。

(2)语义问题交给主 Agent

当前项目的 Stop Hook 会要求主 Agent复核:

  • 设计文档是否遗漏;
  • Step 注释是否必要且准确;
  • 日志是否充分、过量或泄密;
  • StopWatch 阶段是否合理;
  • 是否存在循环 SQL、事务或数据库兼容风险;
  • 测试是否覆盖成功与失败路径。
(3)不要让 Hook 自动修改业务代码

Hook 负责发现和反馈,主 Agent 负责理解上下文并修改。否则很容易出现:

  • Hook 修改触发新的 Hook,形成递归;
  • 脚本和 Agent 同时写文件,产生覆盖;
  • 自动修复改变业务语义,却没有明确责任方;
  • 出错后难以审计修改来源。

3.3 推荐项目的双平台结构

因为我使用codex和cursor配合进行编码,主力还是cursor,codex扮演的是决策者,一般在需求无法完整描述的时候才会让codex进行编码。

.cursor/
├── rules/*.mdc
├── hooks.json
└── hooks/run-agent-hook.ps1

.codex/
├── hooks.json
└── hooks/run-agent-hook.ps1

AGENTS.md

scripts/agent-hooks/
├── agent-hook.mjs
└── policy.json

Cursor 与 Codex 的规则位置、Hook 事件名和返回 JSON 不同,因此保留各自配置;真正的检查逻辑只维护一份。这个结构比复制两套脚本更不容易漂移。

3.4 真实示例,重点展示Hooks

这里我们注意,不要过多的去研究怎么写Hooks,只需要明白这是干什么的,具体让AI去执行。

hooks.json

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .cursor/hooks/run-agent-hook.ps1 -Platform cursor -Event pre-shell",
        "timeout": 10
      }
    ],
    "postToolUse": [
      {
        "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .cursor/hooks/run-agent-hook.ps1 -Platform cursor -Event track-change",
        "timeout": 10
      }
    ],
    "stop": [
      {
        "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .cursor/hooks/run-agent-hook.ps1 -Platform cursor -Event stop",
        "loop_limit": 3,
        "timeout": 30
      }
    ]
  }
}

agent-hook.mjs

import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";

const [platform = "codex", event = "stop"] = process.argv.slice(2);
const rawInput = await readStdin();
const input = parseHookInput(rawInput);
const repoRoot = findRepoRoot(input);
const policy = readJson(path.join(repoRoot, "scripts", "agent-hooks", "policy.json"));
const statePath = getStatePath(repoRoot, input);
const state = readJson(statePath, { files: [], stopAttempts: 0 });

if (event === "pre-shell") {
  handlePreShell();
} else if (event === "track-change") {
  handleTrackChange();
} else if (event === "stop") {
  handleStop();
} else {
  emit({});
}

function handlePreShell() {
  const command = findCommand(input);
  const violation = checkShellCommand(command);
  if (!violation) {
    emit({});
    return;
  }

  if (platform === "cursor") {
    emit({
      permission: "deny",
      user_message: violation,
      agent_message: `${violation} 请改用规则中允许的命令。`
    });
    return;
  }

  emit({
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: violation
    }
  });
}

function handleTrackChange() {
  const files = extractCandidateFiles(input)
    .map((file) => normalizeCandidate(file))
    .filter(Boolean);

  state.files = [...new Set([...state.files, ...files])].sort();
  writeJson(statePath, state);
  emit({});
}

function handleStop() {
  const changedFiles = [...new Set([...state.files, ...readGitChangedFiles()])]
    .filter((file) => file.endsWith(".java"))
    .filter((file) => fs.existsSync(path.join(repoRoot, file)));

  if (changedFiles.length === 0) {
    resetState();
    emit({});
    return;
  }

  const violations = changedFiles.flatMap(checkJavaFile);
  state.stopAttempts = Number(state.stopAttempts || 0) + 1;
  writeJson(statePath, state);

  if (violations.length > 0) {
    const reason = formatViolations(violations);
    if (state.stopAttempts >= 3) {
      resetState();
      if (platform === "cursor") {
        emit({ user_message: reason });
      } else {
        emit({ continue: false, stopReason: reason, systemMessage: reason });
      }
      return;
    }
    continueAgent(reason);
    return;
  }

  const alreadyContinued = Boolean(input.stop_hook_active)
    || Number(input.loop_count || 0) > 0
    || Boolean(state.semanticReviewRequested);

  if (!alreadyContinued) {
    state.semanticReviewRequested = true;
    writeJson(statePath, state);
    const checklist = policy.semanticReviewChecklist
      .map((item, index) => `${index + 1}. ${item}`)
      .join("\n");
    continueAgent(
      `静态 Hook 已通过。结束前请由当前主 Agent 对本轮改动执行一次语义复核并直接修正发现的问题:\n${checklist}\n复核后重新运行必要的编译/测试,再结束任务。`
    );
    return;
  }

  resetState();
  emit({});
}

function checkShellCommand(command) {
  if (!command) {
    return null;
  }

  for (const rule of policy.forbiddenShellRules || []) {
    if (new RegExp(rule.pattern, rule.flags || "").test(command)) {
      return `[${rule.id}] ${rule.message}`;
    }
  }

  const normalized = command.replaceAll("/", "\\");
  const invokesBareMaven = /(^|[\s;&|])(?:mvn|mvn\.cmd)(?=\s|$)/i.test(command);
  const hasRequiredMaven = normalized.toLowerCase()
    .includes(policy.requiredMavenPath.toLowerCase());
  if (invokesBareMaven && !hasRequiredMaven) {
    return `[MAVEN-PATH] 禁止使用裸 mvn;请使用 ${policy.requiredMavenPath}`;
  }
  return null;
}

function checkJavaFile(relativePath) {
  const absolutePath = path.join(repoRoot, relativePath);
  const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/);
  const violations = [];

  for (const rule of policy.javaLineRules || []) {
    if ((rule.allowedPathPatterns || []).some((pattern) => new RegExp(pattern).test(absolutePath))) {
      continue;
    }
    const regex = new RegExp(rule.pattern);
    const skipPatterns = (rule.skipLinePatterns || []).map((pattern) => new RegExp(pattern));
    lines.forEach((line, index) => {
      if (skipPatterns.some((skip) => skip.test(line))) {
        return;
      }
      if (regex.test(line)) {
        violations.push({
          id: rule.id,
          file: relativePath,
          line: index + 1,
          message: rule.message
        });
      }
    });
  }
  return violations;
}

function continueAgent(reason) {
  if (platform === "cursor") {
    emit({ followup_message: reason });
  } else {
    emit({ decision: "block", reason });
  }
}

function formatViolations(violations) {
  const displayed = violations.slice(0, 20);
  const lines = displayed.map(
    (item) => `- ${item.file}:${item.line} [${item.id}] ${item.message}`
  );
  if (violations.length > displayed.length) {
    lines.push(`- 另有 ${violations.length - displayed.length} 项未展开。`);
  }
  return `Hook 静态校验未通过,请修正后重新验证:\n${lines.join("\n")}`;
}

function extractCandidateFiles(value) {
  const text = JSON.stringify(value);
  const candidates = [];
  const patterns = [
    /\*\*\* (?:Add|Update|Delete) File:\s*([^\r\n]+)/g,
    /(?:file_path|path|target_file|filePath)"?\s*[:=]\s*"([^"]+\.(?:java|xml|ya?ml|properties))"/gi,
    /([A-Za-z]:[\\/][^"\r\n]*?\.(?:java|xml|ya?ml|properties))/gi,
    /((?:src|pom\.xml|config)[\\/][^"\r\n]*?\.(?:java|xml|ya?ml|properties))/gi
  ];
  for (const regex of patterns) {
    for (const match of text.matchAll(regex)) {
      candidates.push(match[1]);
    }
  }
  return candidates;
}

function normalizeCandidate(candidate) {
  const cleaned = candidate
    .replaceAll("\\\\", "\\")
    .replace(/^["']|["']$/g, "")
    .trim();
  const absolute = path.isAbsolute(cleaned)
    ? path.resolve(cleaned)
    : path.resolve(repoRoot, cleaned);
  const relative = path.relative(repoRoot, absolute);
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
    return null;
  }
  return relative.replaceAll("\\", "/");
}

function readGitChangedFiles() {
  try {
    const output = execFileSync(
      "git",
      ["diff", "--name-only", "--diff-filter=ACMR", "--", "*.java"],
      { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
    );
    return output.split(/\r?\n/).filter(Boolean).map((item) => item.replaceAll("\\", "/"));
  } catch {
    return [];
  }
}

function findCommand(value) {
  if (!value || typeof value !== "object") {
    return "";
  }
  const direct = value.command
    || value.tool_input?.command
    || value.toolInput?.command;
  if (typeof direct === "string") {
    return direct;
  }
  return "";
}

function findRepoRoot(value) {
  const starts = [
    ...(Array.isArray(value.workspace_roots) ? value.workspace_roots : []),
    value.cwd,
    process.cwd()
  ].filter((item) => typeof item === "string" && item.length > 0);

  for (const start of starts) {
    let current = path.resolve(start);
    while (true) {
      if (fs.existsSync(path.join(current, "pom.xml"))
          && fs.existsSync(path.join(current, "scripts", "agent-hooks", "policy.json"))) {
        return current;
      }
      const parent = path.dirname(current);
      if (parent === current) {
        break;
      }
      current = parent;
    }
  }
  return process.cwd();
}

function getStatePath(root, value) {
  const session = value.turn_id
    || value.conversation_id
    || value.session_id
    || "default";
  const hash = crypto.createHash("sha256")
    .update(`${root}|${session}`)
    .digest("hex")
    .slice(0, 24);
  const stateDir = process.env.AGENT_HOOK_STATE_DIR
    || path.join(os.tmpdir(), "duan-agent-hooks");
  fs.mkdirSync(stateDir, { recursive: true });
  return path.join(stateDir, `${hash}.json`);
}

function resetState() {
  try {
    fs.rmSync(statePath, { force: true });
  } catch {
    // Hook 状态只用于审计循环;清理失败不应阻塞正常任务。
  }
}

function parseHookInput(raw) {
  if (!raw.trim()) {
    return {};
  }
  try {
    return JSON.parse(raw);
  } catch {
    const start = raw.indexOf("{");
    const end = raw.lastIndexOf("}");
    if (start >= 0 && end > start) {
      try {
        return JSON.parse(raw.slice(start, end + 1));
      } catch {
        return {};
      }
    }
    return {};
  }
}

function readJson(file, fallback) {
  try {
    return JSON.parse(fs.readFileSync(file, "utf8"));
  } catch (error) {
    if (fallback !== undefined) {
      return fallback;
    }
    throw error;
  }
}

function writeJson(file, value) {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function emit(value) {
  process.stdout.write(`${JSON.stringify(value)}\n`);
}

async function readStdin() {
  let value = "";
  process.stdin.setEncoding("utf8");
  for await (const chunk of process.stdin) {
    value += chunk;
  }
  return value;
}

policy.json

{
  "version": 1,
  "requiredMavenPath": "C:\\Program Files\\JetBrains\\IntelliJ IDEA 2026.1.1\\plugins\\maven\\lib\\maven3\\bin\\mvn.cmd",
  "forbiddenShellRules": [
    {
      "id": "ENV-PERSISTENT-JAVA-HOME",
      "pattern": "(setx\\s+JAVA_HOME|SetEnvironmentVariable\\s*\\(\\s*['\\\"]JAVA_HOME['\\\"])",
      "flags": "i",
      "message": "禁止永久修改全局 JAVA_HOME;只允许在当前终端会话临时设置。"
    },
    {
      "id": "MAVEN-LEGACY",
      "pattern": "D:\\\\env\\\\maven\\\\apache-maven-3\\.6\\.1",
      "flags": "i",
      "message": "禁止使用 Maven 3.6.1;请使用项目约定的 IDEA 捆绑 Maven 3.9.11。"
    }
  ],
  "javaLineRules": [
    {
      "id": "JAVA-SPRING-GENERIC-UTIL",
      "pattern": "import\\s+org\\.springframework\\.util\\.(StringUtils|CollectionUtils)\\s*;",
      "message": "业务代码禁止使用 Spring StringUtils/CollectionUtils 做通用判空,请改用 Hutool。"
    },
    {
      "id": "JAVA-UUID-STRING",
      "pattern": "UUID\\.randomUUID\\(\\)\\.toString\\(\\)",
      "message": "UUID 字符串请使用 IdUtil.simpleUUID() 或 IdUtil.randomUUID()。"
    },
    {
      "id": "JAVA-INLINE-FQN",
      "pattern": "\\bcom\\.duan\\.agent\\.(?:[a-zA-Z_$][\\w$]*\\.)+[A-Z_$][\\w$]*",
      "message": "禁止内联项目全限定类名;请先 import,再使用短类名。",
      "skipLinePatterns": [
        "^\\s*(package|import)\\s+",
        "Class\\.forName\\s*\\(",
        "\\{@link\\s+"
      ]
    },
    {
      "id": "JAVA-TRACE-HOLDER-BOUNDARY",
      "pattern": "\\bTraceIdHolder\\.get\\s*\\(",
      "message": "业务代码禁止直接调用 TraceIdHolder.get(),请从 RequestContextHolder 获取 traceId。",
      "allowedPathPatterns": [
        "[\\\\/]TraceIdFilter\\.java$",
        "[\\\\/]RequestContextBuilder\\.java$"
      ]
    }
  ],
  "semanticReviewChecklist": [
    "逐项对照需求/设计文档,确认没有遗漏字段、分支、异常路径和验收标准。",
    "检查业务方法的 Step 注释是否覆盖关键阶段且描述业务动作,简单方法不要机械加注释。",
    "检查关键链路日志是否充分、是否泄露敏感信息、是否在循环中产生高频 info。",
    "检查是否真的需要 StopWatch;需要时阶段应粗粒度、lowerCamelCase、成对停止并在 finally 汇总。",
    "检查循环 SQL、批量访问、目标数据库兼容性、事务边界和异常处理。",
    "检查命名、分层、接口注入、JavaDoc、文件头、Hutool 与 TraceId 约定。",
    "检查测试是否覆盖成功路径、失败路径和本次修复的回归场景,并运行与改动范围相称的验证。"
  ]
}

4. 大型代码库的代码查询

4.1 场景

Agent 需要回答“某个请求从 Controller 到数据库经历了哪些调用”“修改这个类会影响哪些调用方”“接口实现在哪里被动态调用”。

4.2 最佳组合

(1)使用代码索引类 MCP

当前项目使用 CodeGraph。Agent 可以通过 MCP 查询符号源码、调用路径和影响范围,不必反复执行文本搜索和逐文件读取。

示例:

查询 AuthLoginService.login 到 TokenService.createToken 的完整调用路径,
同时返回涉及方法源码、调用方和现有测试。

为什么适合 MCP:

  • 它需要访问实时项目索引;
  • 输出具有稳定结构;
  • 多个 IDE 或 Agent 可以复用同一能力;
  • 比把整个仓库塞进 Prompt 更节省上下文。

4.3 限制

  • 索引可能落后于刚写入的文件;
  • 动态调用和反射只能做到尽力解析;
  • MCP 返回“谁调用谁”,不等于证明代码正确;
  • 工具描述和返回内容过大仍会污染上下文;
  • 写操作必须配置权限,不能因为接入 MCP 就默认可信。

5. 特定且重复的业务流程

5.1 场景

例如:

  • 根据 Figma 页面实现前端并截图比对;
  • 从飞书目录读取需求文档、生成汇总并回写;
  • 固定格式处理 PDF、Excel 或发布流程;
  • 每次发布都执行相同的构建、制品和验证步骤。

5.2 最佳组合

(1)优先使用 Skill

Skill 可以同时携带步骤、脚本、模板和参考资料,又只在任务相关时加载。

以飞书为例,lark-cli 命令、身份排查、文档导出和高风险写入确认是一套完整工作流,更适合 Skill。只有“高风险操作必须确认”这种跨流程都成立的要求,才需要放进 Rule 或 Hook。

5.3 限制

  • Skill 的 description 写得太宽会误触发,太窄会无法发现;
  • 下载第三方 Skill 前要审查脚本、依赖、网络与权限;
  • 很少使用不是拒绝 Skill 的理由,按需加载本来就是它的优势;
  • 如果某个 Skill 每次都被触发,说明其中一部分可能应该上移为 Rule 或公共 Tool。

6. 并行研究、测试与审查

6.1 适合 Subagents 的任务

(1)读取密集且可独立
  • 一个 Agent 查安全风险;
  • 一个 Agent 查测试缺口;
  • 一个 Agent 核对框架官方文档;
  • 一个 Agent 分析运行日志;
  • 主 Agent 等待全部结果后统一判断。

这种拆分能降低主线程的上下文噪声,也能缩短墙钟时间。

6.2 不适合直接并行的任务

(1)多个 Agent 同时修改同一模块

如果 A 修改 Service,B 同时修改其测试,C 又重构 DTO,而三者依赖的接口仍在变化,最后的合并和验证成本可能高于单 Agent 顺序完成。

(2)强依赖前序结论

如果第二步必须建立在第一步的精确结果上,就不是真正独立的并行任务。

6.3 最佳实践

  • 先让子 Agent 以只读方式探索和审查;
  • 每个子任务只有一个明确交付物;
  • 要求返回证据、文件位置、假设和未确认项;
  • 主 Agent 不直接相信摘要,要抽查关键证据;
  • 写操作尽量只有一个明确所有者;
  • 小任务不要为了“看起来先进”强行拆 Agent。

当前项目暂不主动引入自定义 Subagent 是合理的。等到安全审查、测试分析或文档核对形成稳定且可独立的工作单元,再逐步加入,收益会更确定。

7. 安全、审计与不可绕过的约束

7.1 场景

例如禁止读取密钥、禁止执行危险删除、禁止向生产环境写入、依赖安装必须经过审查。

7.2 最佳组合

(1)权限与沙箱

从根源限制 Agent 能访问的文件、网络和命令。

(2)PreToolUse Hook

根据具体参数阻止明显不合规调用,并记录原因。

(3)CI 与服务端策略

将真正不能绕过的检查放在版本库、流水线、代码托管平台或目标服务端。不要把安全完全寄托在模型是否听话,或者客户端 Hook 是否成功触发。

7.3 为什么 LLM 审查不能取代脚本

LLM 擅长理解语义,却不是稳定的访问控制器。同一段命令在不同上下文下可能得到不同判断;模型调用也可能超时、失败或增加成本。

最合适的组合是:

  • 确定性边界使用脚本和权限;
  • 复杂语义使用模型审查;
  • 高风险动作仍需人工确认;
  • 最终结果由 CI 或服务端再次验证。

8. 快速选择表

在这里插入图片描述

8.1 按问题选择机制

需求首选机制为什么主要限制
统一命名、分层、沟通方式Rules / AGENTS.md每次任务都需要上下文会淡化;Codex 默认合并上限 32 KiB
特定任务的完整操作流程Skill按需加载,可带脚本与模板依赖触发描述和第三方信任
读取代码、数据库或外部系统Tool / MCP提供实时、结构化能力权限、延迟、工具选择和上下文开销
阻止错误命令PreToolUse Hook执行前确定性拦截事件覆盖并非绝对安全边界
修改后静态检查PostToolUse / Stop Hook不依赖模型主动记得脚本只能判断可编码规则
注释、日志、设计细节复核Stop Hook + 主 Agent在结束前恢复语义注意力仍是模型判断,需要测试佐证
并行代码探索、测试分析Subagents隔离噪声并缩短时间Token 与协调成本,写冲突
跨项目分发整套能力Plugin可版本化安装 Rules、Skills、MCP、Hooks供应链与版本兼容风险
决定代码能否合并CI可重复、可审计、难绕过反馈通常晚于本地 Hook

三、从“模型写代码”走向“Agent 工程”

1. 这几年真正变化的是什么

1.1 第一阶段:补全代码

早期 AI 编程的核心体验是补全一行、一个函数或一段样板代码。人掌握执行过程,模型只是输入法的升级。

1.2 第二阶段:对话式修改

随后模型开始理解多个文件,开发者通过对话要求它解释、重构和修复。此时 Prompt 与上下文管理成为关键。

1.3 第三阶段:单 Agent 闭环执行

Agent 开始自己搜索代码、修改文件、运行测试,并根据错误继续迭代。Rules、Tools、MCP、Hooks、沙箱和长期任务开始进入主流产品。

1.4 第四阶段:多 Agent 与长期自治

当前行业明显在向多 Agent、后台任务和长期运行演进。OpenAI 在 GPT‑5.6 中加入了 Multi-agent beta 与 Programmatic Tool Calling;Cursor 在持续推进长任务、云 Agent、异步子 Agent和插件生态;Anthropic 的研究系统也采用了 orchestrator-worker 结构。

这并不意味着多 Agent 是刚刚发明的。早期软件 Agent、角色协作和多智能体研究早就存在。真正的变化是模型的工具使用、上下文保持、执行可靠性和基础设施成熟到了可以把这些概念产品化的阶段。

2. 为什么行业越来越少谈参数量

2.1 不是参数不重要,而是参数不再足够说明问题

参数规模仍然影响容量与能力上限,但它已经很难单独回答这些更实际的问题:

  • 一个真实需求能否完整做完;
  • 需要多少 Token、工具调用和时间;
  • 长任务中能否保持目标;
  • 能否正确调用代码搜索、浏览器和测试;
  • 失败后能否发现并修正;
  • 每次任务的成本与成功率是多少。

对于 MoE 模型,总参数、激活参数和专家路由本来就是不同概念。只比较一个总参数数字,很容易把模型容量、推理成本和实际能力混为一谈。

2.2 GPT‑5.6 是一个很典型的例子

在这里插入图片描述

OpenAI 对 GPT‑5.6 的公开介绍重点不是参数数量,而是:

  • 单位 Token 能完成更多有效工作;
  • Programmatic Tool Calling;
  • Multi-agent;
  • 更强的长期任务、工具使用与计算机操作;
  • Sol、Terra、Luna 三档能力与成本;
  • 推理强度、缓存、延迟和成功率。

官方甚至直接用更少 Token、更少工具调用、更短时间和更低估算成本描述进步。GPT‑5.6 发布说明模型使用指南共同反映了一个趋势:模型竞争正在从“谁更大”转向“谁能在完整工作流中更稳定、更便宜地完成任务”。

说得直白一点,我并不太关心一个模型到底藏了多少参数。我更关心的是,它改完十几个文件之后,是否还记得最开始那条不起眼但很重要的日志约束;测试失败后,它是解释失败,还是继续把问题修好。

3. Kimi K3 带来的另一种观察

3.1 参数规模仍然有传播力

Kimi K3 官方公布的总参数为 2.8T,原生支持视觉与 100 万 Token 上下文,并采用更稀疏的 MoE 结构,每次激活 896 个专家中的 16 个。它证明开放模型仍然可以继续冲击规模前沿。

但这个例子也恰好说明,参数不是全部。Kimi 官方同时强调了 Kimi Delta Attention、Attention Residuals、长周期编码、知识工作、推理能力,以及不同 Harness 下的评测结果。Kimi K3 官方技术博客还明确说明,完整模型权重计划在 2026 年 7 月 27 日前发布;当把它表述为已经提供产品与 API、正在完成开放权重落地,而不是提前把完整权重发布写成既成事实。

3.2 开放模型会推动 Harness 标准化

当模型可以替换时,真正长期积累价值的部分会逐渐上移:

  • 项目自己的 Rules 与验收标准;
  • 可跨模型复用的 Skills;
  • 标准化 MCP Server;
  • Hooks、权限和审计;
  • 测试集、Evals 与历史任务数据;
  • 团队自己的 Agent 工作流。

闭源模型和开放模型会继续竞争,但工程团队不应该把全部能力锁死在某一个模型名字上。一个成熟的 Agent 工程体系,应当允许在不同任务中选择不同模型,同时保留相同的项目规则、工具和验证标准。

4. 我对后续趋势的判断

4.1 多 Agent 会发展,但不会无限增加 Agent 数量

未来更常见的不会是“几十个 Agent 一起热闹地写代码”,而是少量边界清晰的角色:

  • 主 Agent:维护目标、决策与最终责任;
  • Explorer:只读探索代码;
  • Reviewer:审查正确性、安全和测试;
  • Worker:拥有明确代码修改范围;
  • Domain Agent:通过专用 MCP 或 Skill 处理数据库、前端、飞书等领域任务。

评价多 Agent 系统的标准也不会是 Agent 数量,而是任务是否能独立拆分、结果能否验证、协调成本是否低于收益。

4.2 Rules 会变短,策略会越来越可执行

团队会逐渐发现,把所有经验都写进一个超长 AGENTS.md 并不可靠。稳定趋势应该是:

  • Rules 保留原则和索引;
  • 详细流程进入 Skills;
  • 机械约束进入 Policy as Code;
  • 生命周期检查进入 Hooks;
  • 最终门禁进入 CI;
  • 复杂判断由 Reviewer Agent 或主 Agent完成。

4.3 MCP 会成为基础设施,而不是卖点

现在大家还会特意说“我们支持 MCP”。以后它可能像 HTTP、LSP 一样退到基础设施层。真正形成差异的会是:

  • Tool 设计是否清楚;
  • 数据是否实时可信;
  • 权限是否最小化;
  • 返回内容是否适合模型处理;
  • 是否有稳定的审计、缓存和失败恢复。

4.4 人的角色不会消失,而是向上移动

我自己的体会是,Vibe Coding 最舒服的部分,是可以少写大量重复代码;最危险的部分,也是很容易因为“它看起来已经完成了”而降低警惕。

未来开发者更像是系统导演:

  • 把模糊需求变成可验证目标;
  • 选择合适的模型与 Agent;
  • 设计 Rules、Skills、Tools 和 Hooks;
  • 决定哪些事情可以自动,哪些必须确认;
  • 用测试和 Evals 判断能力,而不是凭一次漂亮演示下结论。

真正高质量的 Vibe Coding,不是把代码全部交给模型,然后祈祷结果正确;而是建立一个即使模型偶尔忘记、误判或跑偏,也能被工程系统拉回来的闭环。

5. 最后的总结

5.1 一套更稳妥的默认方法

(1)长期原则放 Rules

保持短、清楚、可执行,不复制整本手册。

(2)当前需求放 Plan

把文档转成步骤、文件范围和验收清单。

(3)实时能力交给 Tools 与 MCP

让 Agent 查询真实代码和系统,不依赖记忆猜测。

(4)特殊流程封装为 Skills

只在需要时加载,并复用脚本与模板。

(5)机械约束放 Hooks

执行前阻止,执行后记录,结束前检查。

(6)语义问题由主 Agent 或 Reviewer 复核

注释、日志、耗时和架构合理性不能只靠正则。

(7)可独立的读取任务再交给 Subagents

先从探索、测试和审查开始,不急着让多个 Agent 同时写代码。

(8)CI 保留最终否决权

任何 Agent 的“已经完成”,都不应该自动等价于“可以合并”。

如果把这些机制放在正确的位置,Vibe Coding 就不再只是一次灵感驱动的代码生成,而会逐渐成为一种可维护、可审计、可扩展的软件工程方法。

6. 参考资料

6.1 官方文档与延伸阅读

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐