Tips & Tricks (更新: 2026/7/21)

Harness Engineering 是什么?用 Codex 案例搭建可验证的 AI 代理护栏

面向初学者讲清 Harness Engineering:从 OpenAI 的 Codex 案例,到文件权限、人工审批和可执行测试。

Harness Engineering 是什么?用 Codex 案例搭建可验证的 AI 代理护栏

你让 AI 代理“顺手整理一下仓库”,它却改了无关的配置;你让它“跑完测试再汇报”,最后只得到一句“测试通过”,却找不到命令和退出码。团队刚开始把开发任务交给 AI 时,这两类问题很常见。

问题不只在提示词。代理能看到什么、能调用哪些工具、遇到什么情况必须停下,以及什么才算完成,如果这些边界没有写清楚,模型再强也只能猜。Harness Engineering(可理解为“给代理搭工作护栏”),就是设计这套外围环境,让代理可以动手,又不能越过关键边界。

本文先用 OpenAI 公开的 Codex 案例解释这个概念,再实现一个最小文件护栏。示例会限制读取范围、禁止覆盖已有文件,并用不消耗 API 额度的 Node.js 测试验证关键边界。

本文要点

  • Harness 不是一个包装脚本,而是仓库知识、工具、权限、测试、日志、恢复方式和人工审批的组合。
  • OpenAI 的 Codex 案例表明,要让代理持续工作,必须让仓库结构、应用状态和质量规则既可读取又可执行。
  • AI 适合搜索、起草和重复执行;删除、生产变更、对外发送和付费操作应由人判断。
  • 只检查路径字符串并不等于安全沙箱。符号链接、覆盖写入、进程权限和操作系统隔离都要考虑。
  • “已验证”必须说明命令、结果和验证范围。代理自己说“成功”不能当作证据。

Harness Engineering 到底是什么

提示词说明“这一次想做什么”,Harness 则规定“这次任务在什么环境里执行”。可以把它拆成六层:

层级要回答的问题最小例子
上下文代理能了解什么?AGENTS.md、限定目录、版本化规格
工具代理能做什么?读取、测试、创建草稿
权限代理在哪里必须停下?删除或发送前要求人工批准
验证什么才算完成?npm test 以状态码 0 退出
可观测性出错后怎样追查?保存命令、diff 和关键报错
恢复坏结果怎样撤回?小提交、dry run、回滚步骤

模型只是这个闭环中的一个部件:

人的目标与审批规则
        ↓
仓库规则 → AI 代理 → 获准使用的工具
    ↑                    ↓
规格与文档 ← 测试、日志和差异
    └──── 失败后修复;需要判断时升级给人 ────┘

如果某个设计决定只存在于同事脑中,换一个更强的模型也读不到。反过来,只要把相关知识放到代理能找到的位置,并把验收条件写成可执行检查,普通任务也会稳定很多。

为什么这个词在 2026 年受到关注:OpenAI 的 Codex 案例

一个重要背景是 OpenAI 在 2026 年 2 月 11 日发布的文章:Harness engineering: leveraging Codex in an agent-first world

OpenAI 披露,这个实验从空的 Git 仓库开始。大约五个月后,由三名工程师驱动 Codex,累计打开并合并了约 1,500 个 Pull Request;仓库规模约为一百万行。文章还说明,应用逻辑、测试、CI、文档、可观测性和内部工具都由 Codex 生成,产品已有内部日常用户和外部 alpha 测试者。

这里值得学习的不是单独追求“1,500 个 PR”,更不是认为任何团队接入 Codex 后都会得到相同产出。OpenAI 明确说明,这种能力依赖该仓库专门设计的结构和工具,不能直接推广到没有类似投入的项目。

他们公开的方法包括:

  • 把计划和设计决定保存在有版本记录的仓库文件中;
  • 让代理可以直接查看 UI、日志、指标和链路追踪;
  • 用结构测试和自定义 lint 强制执行依赖方向等不变量;
  • 失败时寻找缺失的工具、规则或抽象,而不是只要求模型“再努力一次”;
  • 定期扫描过期文档和结构漂移,把清理工作做成持续流程。

文章还有一个很实用的观点:给代理一张地图,而不是一本一千页的说明书。简短的入口文件负责指路,具体规则放进分主题、可检查、可维护的文档。这样既节省上下文,也更容易发现过期内容。

Claude Code 也可以采用相同思路。官方的 Claude Agent SDK hooks 文档说明,hook 可以检查工具请求、拒绝操作、修改输入或记录审计信息。产品接口不同,但“在代理外部建立边界和反馈闭环”这一原则相同。

AI 可以做什么,人必须判断什么

不要从“完全自治”开始。先自动化容易撤销的动作,对影响客户、资金和生产环境的动作保留审批。

适合先自动化满足条件后再委托必须由人决定
搜索文件修改已有文件删除生产数据
运行测试添加依赖发送客户邮件
总结差异部署到预发布环境修改账单或合同
创建草稿推送分支处理敏感个人信息

判断一个动作能否自动执行时,先问两个问题:出错后能否低成本撤销?会不会影响团队外部的人? 初期只开放读取和临时输出。等成功与失败都能被观察、记录和复现后,再逐步增加权限。

实现一个最小 Harness

下面只给模型两个能力:

  1. 读取 sandbox 中的文本文件;
  2. sandbox 中创建一个新的文本文件。

示例没有删除、覆盖、Shell 或网络工具。本文验证时使用 Node.js 22,并固定了当时验证的 SDK 版本。

mkdir harness-demo
cd harness-demo
npm init -y
npm install @anthropic-ai/[email protected]
mkdir sandbox
echo "# meeting notes" > sandbox/note.md

创建 policy.json

{
  "workspace": "./sandbox",
  "maxSteps": 6,
  "maxToolResultChars": 4000
}

1. 用代码守住文件边界

创建 safe-files.mjs。只写 candidate.startsWith(root) 并不够:名称相近的目录可能误匹配,工作区里的符号链接也可能指向外部。下面的读取逻辑会检查解析后的真实路径,写入则仅允许创建新文件。

import { open, readFile, realpath } from "node:fs/promises";
import path from "node:path";

function assertInside(root, candidate) {
  if (candidate !== root && !candidate.startsWith(root + path.sep)) {
    throw new Error(`outside workspace: ${candidate}`);
  }
}

export async function createFileGate(workspace) {
  const root = await realpath(path.resolve(workspace));

  async function readText(relativePath) {
    const requested = path.resolve(root, relativePath);
    assertInside(root, requested);
    const actual = await realpath(requested);
    assertInside(root, actual);
    return readFile(actual, "utf8");
  }

  async function createText(relativePath, content) {
    const requested = path.resolve(root, relativePath);
    assertInside(root, requested);
    const actualParent = await realpath(path.dirname(requested));
    assertInside(root, actualParent);

    let handle;
    try {
      handle = await open(requested, "wx", 0o600);
      await handle.writeFile(content, "utf8");
    } catch (error) {
      if (error.code === "EEXIST") {
        throw new Error(`refusing to overwrite: ${relativePath}`);
      }
      throw error;
    } finally {
      await handle?.close();
    }
    return "created";
  }

  return { readText, createText };
}

这只是应用层护栏,不是完整的安全边界。需要更强隔离时,还要使用容器、虚拟机、操作系统权限或产品自带的沙箱。应用代码无法抵消管理员级进程权限。

2. 只向模型开放两个工具

创建 agent.mjs。模型名通过 ANTHROPIC_MODEL 提供,而不是写死在文章里,因为不同账户可用的模型会变化。

import Anthropic from "@anthropic-ai/sdk";
import { readFile } from "node:fs/promises";
import { createFileGate } from "./safe-files.mjs";

const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("Set ANTHROPIC_MODEL to a model available to your account.");

const policy = JSON.parse(await readFile("./policy.json", "utf8"));
const gate = await createFileGate(policy.workspace);
const client = new Anthropic();

const tools = [
  {
    name: "read_file",
    description: "Read a UTF-8 text file inside the workspace",
    input_schema: {
      type: "object",
      properties: { path: { type: "string" } },
      required: ["path"],
      additionalProperties: false
    }
  },
  {
    name: "create_file",
    description: "Create a new UTF-8 file; existing files cannot be overwritten",
    input_schema: {
      type: "object",
      properties: {
        path: { type: "string" },
        content: { type: "string" }
      },
      required: ["path", "content"],
      additionalProperties: false
    }
  }
];

async function runTool(name, input) {
  if (name === "read_file") return gate.readText(input.path);
  if (name === "create_file") return gate.createText(input.path, input.content);
  throw new Error(`unknown tool: ${name}`);
}

const prompt = process.argv.slice(2).join(" ") ||
  "Read note.md and create summary.md with a three-line summary.";
const messages = [{ role: "user", content: prompt }];

for (let step = 0; step < policy.maxSteps; step += 1) {
  const response = await client.messages.create({
    model,
    max_tokens: 1200,
    system: "Use only the supplied tools. Never claim a file was created unless the tool succeeded.",
    tools,
    messages
  });
  messages.push({ role: "assistant", content: response.content });

  const calls = response.content.filter((block) => block.type === "tool_use");
  if (calls.length === 0) {
    console.log(response.content.find((block) => block.type === "text")?.text ?? "done");
    process.exit(0);
  }

  const results = [];
  for (const call of calls) {
    try {
      const value = await runTool(call.name, call.input);
      results.push({
        type: "tool_result",
        tool_use_id: call.id,
        content: String(value).slice(0, policy.maxToolResultChars)
      });
    } catch (error) {
      results.push({
        type: "tool_result",
        tool_use_id: call.id,
        is_error: true,
        content: error.message
      });
    }
  }
  messages.push({ role: "user", content: results });
}

throw new Error(`step limit exceeded: ${policy.maxSteps}`);

关键点不是提示词写得多长,而是模型根本拿不到删除和覆盖工具。即使它请求覆盖 note.md,文件门禁也会返回错误,代理必须在限定步骤内换一种方案或停止。

3. 调用模型之前先测试门禁

最重要的文件边界可以在本地测试,不必消耗 API 额度。创建 safe-files.test.mjs

import assert from "node:assert/strict";
import test from "node:test";
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createFileGate } from "./safe-files.mjs";

test("file gate blocks traversal, overwrite, and outside symlinks", async () => {
  const base = await mkdtemp(path.join(tmpdir(), "harness-test-"));
  const root = path.join(base, "sandbox");
  const outside = path.join(base, "outside.txt");

  try {
    await mkdir(root);
    await writeFile(path.join(root, "note.md"), "hello", "utf8");
    await writeFile(outside, "secret", "utf8");
    const gate = await createFileGate(root);

    assert.equal(await gate.readText("note.md"), "hello");
    await assert.rejects(() => gate.readText("../outside.txt"), /outside workspace/);
    await assert.rejects(() => gate.createText("note.md", "replace"), /refusing to overwrite/);

    try {
      await symlink(outside, path.join(root, "outside-link.txt"), "file");
      await assert.rejects(() => gate.readText("outside-link.txt"), /outside workspace/);
    } catch (error) {
      if (error.code !== "EPERM") throw error;
    }

    assert.equal(await gate.createText("summary.md", "safe"), "created");
  } finally {
    await rm(base, { recursive: true, force: true });
  }
});

先执行离线检查:

node --test safe-files.test.mjs
node --check agent.mjs

这两个命令通过后,再设置 ANTHROPIC_API_KEYANTHROPIC_MODEL,然后运行 node agent.mjs。凭据不要写入源码或 policy.json

三个 Use case:怎样落到真实工作中

1. 软件团队:实现并验证一个 Pull Request

给代理一个范围明确的问题、相关目录和测试命令。验收条件不能只是“代码写好了”,而应包括:先得到可以复现的失败,再确认修复后的测试通过,并留下可读的 diff。生产部署和数据库迁移仍由人批准。

例如,代理可以自动搜索报错来源、补测试、修改实现并创建分支;人负责确认产品行为是否符合需求,以及迁移是否会影响真实数据。这样既减少机械工作,也不会把业务判断交给模型猜。

2. 内容团队:给文章加发布前质量门

把“写文章”和“允许发布”分开。代理负责草稿和修改,检查程序负责重复主题、内容深度、代码语法、链接状态和移动端显示。任何一项失败都停止发布,并返回具体修复原因。

例如,代码块必须先通过语法检查,页面必须完成移动端截图检查,文中的“已测试”还要对应一条真实命令。发布按钮仍可由自动流程执行,但前提是所有门禁都有证据。

3. 客户运营:分类咨询并生成回复草稿

代理可以判断咨询类别、提取要点并生成回复草稿,同时说明分类理由。客户资料变更和实际发送由人确认。输入中只保留完成分类所需的个人信息,也不要把完整邮件正文长期写入日志。

这个场景的重点不是让 AI “代替客服”,而是缩短整理时间。只要发送动作不暴露给代理,即使草稿判断错误,也不会直接触达客户。

ROI:先算节省的复核时间

不要用生成了多少 Token 衡量收益。更有用的指标是人工复核时间、返工率和上线前发现的问题数。

假设团队每周有 15 个重复任务,每个任务需要 20 分钟人工检查,总计 5 小时。搭建首版 Harness 用 6 小时,之后每周维护 1 小时,那么每周净节省约 4 小时,初始投入大约一周半可以收回。

这只是计算示例,不是收益承诺。上线前后各记录两周:

  • 每个任务的人工作业分钟数;
  • 返工比例;
  • 进入生产前被门禁拦住的缺陷数;
  • 升级给人工审批的次数。

如果审批请求过多,可以把已经验证过的低风险动作进一步缩小后自动化。如果缺陷或返工上升,应增加检查或补充上下文,而不是扩大代理权限。

Pitfall:常见错误和修正办法

把路径前缀检查当成沙箱

某个路径看起来在工作区内,符号链接解析后却可能指向外部。应检查真实路径、拒绝覆盖,并把操作系统权限作为第二道边界。处理敏感代码时,进一步放进容器或隔离环境。

只在提示词里写“不要做危险操作”

文字只能提供判断依据,不能强制执行。危险工具不要交给代理,或者在工具执行前的 hook 中拒绝。具体配置可以参考本地化的 Claude Code 权限设置指南

把代理的“测试通过”当成验收证据

至少要记录命令、退出状态和验证范围。涉及 UI 时,还需要实际操作或截图。可以结合 验证回执工作流保存证据,让下一位人员知道究竟检查了什么。

每次都塞入所有文档

过长的上下文会淹没真正重要的限制。使用一个短入口链接到分主题、带版本记录的资料,并记录文档的更新时间和验证状态。入口负责导航,机械检查负责阻止关键规则被忽略。

总结与下一步

Harness Engineering 不是把提示词写得更长,而是让必要知识可发现、把工具限制在最小范围、为敏感动作设置审批、用命令判断结果,并把失败沉淀为新规则和测试。

第一次实践时,只选一个流程,写下四项:输入、允许的操作、验收命令、必须人工批准的操作。需要把权限、验证和评审护栏落到团队真实仓库时,主入口是 Claude Code 培训与实施咨询

实际验证结果

2026 年 7 月 21 日,本文中的 safe-files.mjssafe-files.test.mjs 代码块被提取到临时目录,并使用 Node.js 执行。测试覆盖正常读取、创建新文件、拒绝 ../ 路径穿越和拒绝覆盖已有文件;在测试进程允许创建符号链接的系统上,也会验证指向工作区外部的链接被拒绝。agent.mjs 通过了语法检查。

本次验证没有调用 Anthropic API,因为模型权限和费用因账户而异。这里刻意区分“已发布”“语法已检查”“离线测试通过”和“实际调用付费外部 API”四种说法;可靠的 Harness 必须记录当前证据属于哪一种。

#claude-code #codex #harness-engineering #AI代理 #初学者
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。