跳到正文

项目上下文

你的Harness是通用的。它涉及的每个项目都不是。

有一个项目使用bun test。下一个用的是vitest。第三种用npm run check来做typechecklint组合,完全不符合你在提示词中设定的任何惯例。Agent不应该需要猜测,你也不应该需要一个项目一个项目地教它。

大多数生产Harness使用的技巧和团队对人类贡献者用的一样:在仓库里放一个 markdown 文件,解释项目的工作原理。Agent会读取并调整。

学习成果

如果工作目录中存在AGENTS.md文件,Harness会读取该文件并将其内容注入系统提示词中,作为“项目指令”部分。没有文件时,提示词只回退到基础指令。

快速路径

  1. 检查AGENTS.md里有没有工作目录
  2. 如果有,读成字符串
  3. 把它当作buildSystemPrompt传给projectContext

动手练习 3.4

发现并注入工作目录AGENTS.md

要求:

  1. index.ts时,检查cwd/AGENTS.md是否存在
  2. 如果有,就读作UTF-8
  3. 将内容(或undefined)作为buildSystemPrompt传递给projectContext
  4. 确认提示词构建器现有的projectContext处理在有该部分时添加,缺席时省略

实现提示:

  • node:fsexistsSync在这里没问题。检查只在启动时进行一次,而不是在热循环中
  • 使用 path.join(cwd, "AGENTS.md"),而非模板文字,以保持路径处理的一致性
  • 暂时不要添加单仓库目录巡回。下面的挑战涵盖了这一点,模块4的沙箱抽象无论如何都会改变文件发现的工作方式

发现经过

整个故事只有几句话:

ts
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

const agentsPath = join(cwd, "AGENTS.md");
const projectContext = existsSync(agentsPath)
  ? readFileSync(agentsPath, "utf-8")
  : undefined;

把它传给构建器:

ts
const instructions = buildSystemPrompt({
  workingDirectory: cwd,
  sandboxType: "local",
  toolNames: Object.keys(tools),
  projectContext,
});

就这些。约定胜于配置。没有插件系统,没有注册,没有事件总线。一个已知位置的文件。

AGENTS.md里会有什么

这个文件是用于 harness 无法从代码中推断出的项目特定事实:

markdown
# Project Instructions

## Commands
- `bun test` runs the test suite
- `bun run build` builds for production
- `bun run lint` checks code style

## Architecture
- Monorepo, packages live in `packages/`
- Each package has its own `tsconfig.json`
- Shared types in `packages/shared/`

## Style
- Functional components, no classes
- Named exports, not default
- Error messages must be user-facing

## Lessons learned
- Auth middleware must run before rate limiting
- Don't modify migration files directly, generate new ones

Agent读到这些后做出调整。它知道指令。它熟悉架构。它了解项目中反复出现的错误。现在,这种Harness在React项目中表现得像React项目Agent,在CLI项目中像CLI项目Agent,因为项目本身告诉Agent自己是哪种项目。

**注意:一个熟悉的模式,文件名不同**

这和Cursor用在.cursorrules、Codex用AGENTS.md、Claude用代码CLAUDE.md、用π用自己约定的技巧一样。文件名会有所不同。模式没有。发现一个 markdown 文件,注入为指令。

为什么现在一个文件就足够了

真正的Harness会在目录中父 Agent单存储库中寻找AGENTS.md文件。它会把它们合并。它也可能支持.cursorrules.github/copilot-instructions.md~/.agents/default.md

我们还没有构建这些。工作目录里有一个文件涵盖了教学点:项目上下文来自文件,Harness在启动时发现,提示词吸收。行走和合体叠加在上面,形状不变。

动手试试

在工作目录中输入一个AGENTS.md,Agent无法从代码中推断出具体指令:

markdown
# Project Instructions
- All commits must use the format `feat(scope): message`
- The verification step is `bun test`, not `npm test`

运行Agent:

bash
bun run index.ts . "What command do I use to run the tests in this project?"

Agent应该回答,因为档案里已经说明了bun test。移除AGENTS.md,继续运行同样的提示词。Agent会猜(大概率npm test)。

bash
npx tsc --noEmit

提交

bash
git add index.ts
git commit -m "feat(prompt): inject AGENTS.md as project context"

完成标准

  • [ ] index.ts检查AGENTS.md的工作目录
  • [ ] 如果存在,其内容会作为buildSystemPrompt传递给projectContext
  • [ ] 在有AGENTS.md的情况下,Agent会从文件中回答项目相关的具体问题
  • [ ] 没有AGENTS.md,Harness仅使用基础指令运行,没有错误
  • [ ] npx tsc --noEmit

**注意:单存储库的目录行进**

真正的单仓库在根节点有一个AGENTS.md,每个包内部也有另一个。从cwd开始,走到仓库根点(找.git),沿途收集所有AGENTS.md。合并它们,最深的文件覆盖或扩展根节点。现在想想冲突:当根节点显示use npm而包显示use pnpm时会发生什么?不同的Harness解决这个问题的方式不同。 Pi会合并所有找到的东西。Cursor只用最深的。Codex将根和 cwd 连接起来。试试一种策略,注意它哪里出了问题。

参考实现

ts
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { buildSystemPrompt } from "./src/system";

const cwd = resolve(process.argv[2] || process.cwd());

const agentsPath = join(cwd, "AGENTS.md");
const projectContext = existsSync(agentsPath)
  ? readFileSync(agentsPath, "utf-8")
  : undefined;

const tools = { read, grep, bash };

const agent = new ToolLoopAgent({
  model: "anthropic/claude-haiku-4-5",
  instructions: buildSystemPrompt({
    workingDirectory: cwd,
    sandboxType: "local",
    toolNames: Object.keys(tools),
    projectContext,
  }),
  tools,
  stopWhen: stepCountIs(10),
});

非官方简体中文翻译 · 原课程来自 Vercel Academy