3749 字
约 12 分钟
3
第四讲. 把指令拆分到不同文件里

第四讲. 把指令拆分到不同文件里

你开始认真对待 harness 了,这很好。你建了个 AGENTS.md,把能想到的所有规则、约束、历史教训都塞了进去。一个月后这个文件膨胀到了 300 行,两个月 450 行,三个月 600 行。然后你发现 agent 的表现反而变差了:改一个小 bug,agent 花大量上下文处理无关的部署指令;关键的安全约束埋在第 300 行,被直接忽略了;文件里有三条互相矛盾的代码风格规则,agent 每次随机选一条。

这就是"巨型指令文件"陷阱。觉得什么都有用,什么都往里装,结果想找一条具体规则得把整个文件翻一遍。写了 600 行,但真正跟当前任务相关的可能只有三分之一。

问题的根源:一个恶性循环

最常见的恶性循环是这样的:agent 犯了个错,你说"加条规则防止这个",加到 AGENTS.md,暂时管用。然后 agent 又犯了另一个错,再加一条。重复下去,文件膨胀到不可控。

这其实是很自然的反应,每次出问题就"加条规则"感觉很合理。但累积效应是灾难性的。让我们看看具体出了什么问题。

上下文预算被吃掉了。 Agent 的上下文窗口是有限的。假设你的 agent 有 200K tokens 的窗口(Claude 的标准),一个膨胀的指令文件可能占掉 10-20K。看起来还有不少余量?但一个复杂的任务可能需要读几十个源文件,工具执行的输出也占上下文,对话历史也在累积。到真正需要理解代码的时候,预算已经不够了。

中间迷失。 "Lost in the Middle"这篇论文(Liu et al., 2023)清楚地证明了:LLM 对长文本中间部分的信息利用效率显著低于两端。你的 AGENTS.md 有 600 行,第 300 行写的是"所有数据库查询必须用参数化查询",这是安全硬约束。但它被埋在中间,agent 几乎一定会忽略它。

优先级冲突。 文件里混合了不可违反的硬约束("不得使用 eval()")、重要的设计指导("优先使用函数式风格")、和某个特定场景的历史教训("上周修了一个 WebSocket 内存泄漏,注意类似的模式")。这三条规则的重要性完全不同,但在文件里看起来一模一样。Agent 没有可靠的信号来区分哪个是红线,哪个只是建议。

维护衰减。 大文件天生难维护。指令过时了没人删,因为删除的后果不确定("也许别的地方依赖这条规则?"),但加新指令是无成本的。结果文件只增不减,信噪比持续下降。这和软件里的技术债务积累是同一个问题。

矛盾累积。 不同时期加的指令之间开始出现矛盾:一条说"用 TypeScript 严格模式",另一条说"某些遗留文件允许用 any"。Agent 每次随机选一条遵循。

核心概念

  • 指令膨胀:指令文件一旦占到上下文窗口的 10-15%,就开始挤占代码阅读和任务推理的预算。一个 600 行的 AGENTS.md 可能占用 10,000-20,000 tokens,对 128K 的窗口来说就是 8-15%。
  • 长文本中间信息容易被忽略:Liu 等人 2023 年的研究表明,LLM 对长文本中间部分的信息利用效率明显低于两端。埋在 600 行文件第 300 行的关键约束,被忽略的概率非常高。
  • 指令信噪比(SNR):文件中与当前任务相关的指令占总指令的比例。做 bug 修复时被要求读 50 行部署指令,SNR 就很低。
  • 入口文件:短小的入口文件,作用是引导 agent 去找更详细的文档,而不是自己包含所有内容。50-200 行就够了。
  • 按需展开:先给概要信息,需要的时候再给详细信息。好的 harness 设计和好的 UI 设计一样,不把所有选项一次性砸到用户脸上。
  • 分不清轻重:当所有指令以相同格式和位置呈现时,agent 分不清哪些是不可违反的硬约束,哪些只是建议性的软约束。

指令文件架构

flowchart LR
    Mono["一个超长 AGENTS.md"] --> MonoLoad["改一个小 bug<br/>也得把部署说明和历史备注全读一遍"]
    MonoLoad --> MonoRisk["关键规则埋在中间<br/>很容易漏掉"]

    Router["短 AGENTS.md"] --> Topics["按任务去读 API / 数据库 / 测试文档"]
    Topics --> RoutedResult["把更多上下文留给代码阅读<br/>和验证"]
flowchart TB
    File["600 行指令文件"] --> Top["顶部<br/>快速开始 + 硬约束"]
    File --> Mid["中部<br/>第 300 行的安全规则"]
    File --> Bot["底部<br/>明确的结束检查清单"]
    Top --> Seen["高概率被记住"]
    Bot --> Seen
    Mid --> Missed["高概率被稀释或忽略"]

拆分思路

核心原则:常用信息放手边,偶尔用的收起来,用不上的别带。

入口文件 AGENTS.md 控制在 50-200 行,只放最常用的东西:项目概览(一两句话说清楚这是什么)、首次运行命令(make setup && make test)、全局硬约束(不超过 15 条不可违反的规则)、指向专题文档的链接(一行描述加适用条件)。

# AGENTS.md

## 项目概览
Python 3.11 FastAPI 后端,PostgreSQL 15 数据库。

## 快速开始
- 安装:`make setup`
- 测试:`make test`
- 完整验证:`make check`

## 硬约束
- 所有 API 必须走 OAuth 2.0 认证
- 所有数据库查询必须用 SQLAlchemy 2.0 语法
- 所有 PR 必须通过 pytest + mypy --strict + ruff check

## 专题文档
- API 设计规范 (`docs/api-patterns.md`) — 添加新端点时必读
- 数据库操作约束 (`docs/database-rules.md`) — 涉及数据库修改时必读
- 测试标准 (`docs/testing-standards.md`) — 编写测试时参考

每个专题文档 50-150 行,按主题放在 docs/ 目录下或对应模块目录旁。Agent 只在需要时才去读。用收纳袋整理行李的思路:内衣一个袋,洗漱一个袋,充电器一个袋,找东西不用翻整个箱子。

还有些信息直接放在代码里更合适,比如类型定义、接口注释、配置文件里的说明。Agent 读代码的时候自然能看到,不用再在指令里重复一遍。

每条指令都应该标明来源("为什么加这条规则?")、适用条件("这条规则在什么时候需要?")、过期条件("什么情况下可以删掉这条规则?")。定期审计,删掉过时的、冗余的、矛盾的条目。像管理代码依赖一样管理你的指令,用不上的依赖就该删掉,不然它们只会拖慢系统。

如果某条指令必须在入口文件里,放顶部或底部,不要放中间。"中间迷失"效应告诉我们,LLM 对长文本中间部分的信息利用效率显著低于两端。但更好的做法是把指令放到专题文档里,让 agent 按需加载。

OpenAI 和 Anthropic 都隐性支持拆分的做法。OpenAI 说入口文件应"短小且以路由为导向",Anthropic 说长运行 agent 的控制信息应"简洁且高优先级"。两家都在说同一件事:别把什么都塞进一个文件里。

实际案例

一个 SaaS 团队的 AGENTS.md 从最初的 50 行膨胀到 600 行。内容混合了技术栈版本、编码规范、历史 bug 修复笔记、API 使用说明、部署流程、和团队成员的个人偏好,什么都有,但很难快速找到跟当前任务相关的部分。

Agent 表现开始明显下降:简单 bug 修复任务中 agent 花大量上下文处理无关的部署指令;安全约束"所有数据库查询必须用参数化查询"埋在第 300 行,经常被忽略;三条矛盾的代码风格规则导致 agent 随机选择。

团队做了一次拆分重构:

  1. AGENTS.md 裁剪到 80 行:只保留项目概览、运行命令、15 条全局硬约束
  2. 创建专题文档:docs/api-patterns.md(120 行)、docs/database-rules.md(60 行)、docs/testing-standards.md(80 行)
  3. 入口文件添加指向专题文档的链接
  4. 历史笔记要么转成测试用例,要么删除

重构后:同一任务集的成功率从 45% 提升到 72%。安全约束遵循率从 60% 提升到 95%,因为规则从文件中间移到了入口文件顶部,不再被"中间迷失"了。

核心要点

  • "加条规则"是短期的止痛药,长期的毒药。每次加规则前想想,这条规则放专题文档是不是更合适。
  • 入口文件是路由器,不是百科全书。50-200 行,只放概览、硬约束和链接。
  • 利用"中间迷失"效应:重要信息放文件顶部或底部,不重要的移到专题文档。
  • 像管理技术债一样管理指令膨胀。定期审计,每条指令要有来源、适用条件和过期条件。
  • 拆分之后信噪比提升,agent 把更多上下文预算花在实际任务上,而不是处理无关指令。

延伸阅读

练习

  1. 信噪比审计:拿你当前的入口指令文件,列出所有指令条目。选 5 个不同的常见任务类型,标注每条指令是否跟该任务相关。计算每个任务类型的 SNR。那些对大多数任务都是噪声的指令,移到专题文档里。
  2. 按需展开重构:如果你有一个超过 300 行的指令文件,把它拆成:(a) 不超过 100 行的入口文件,(b) 3-5 个专题文档。重构前后各跑同一组任务(至少 5 个),对比成功率。
  3. 中间迷失验证:在一个长指令文件里,把一条关键约束分别放在顶部、中间、底部各跑一组任务(每组至少 5 次),看遵循率有没有差别。你可能会惊讶于位置效应有多大。

代码示例

AGENTS.md

AGENTS.md

从这里开始

  • 阅读 docs/ARCHITECTURE.md
  • 阅读 docs/PRODUCT.md
  • 使用 npm run dev 启动应用
  • 在标记工作完成之前使用 npm run check

严格规则

  • 在阅读 docs/ARCHITECTURE.md 之前,不要更改 Electron 主进程/预加载/渲染器的边界
  • 未经验证不要将功能标记为已完成
  • 为下一个会话留下干净的状态

指令文件反模式

指令文件反模式

  • 将所有仓库知识放入一个文件中
  • 在多个地方重复同一条规则
  • 编写从未被审查的过时规则
  • 编写过于具体的条件指令,以至于很少适用
  • 在启动上下文中嵌入冗长的工具手册

split-vs-monolithic.ts

/**
 * split-vs-monolithic.ts
 *
 * Creates a monolithic instruction file (~200 lines) and then shows how
 * splitting into 4 focused files dramatically reduces the context needed
 * for any single query. Simulates an "agent" searching for a specific rule
 * and measures how many lines it must read in each approach.
 *
 * Run: npx tsx docs/lectures/lecture-04-why-one-giant-instruction-file-fails/code/split-vs-monolithic.ts
 */

// ---------------------------------------------------------------------------
// Simulated monolithic instruction file (200 lines of rules)
// ---------------------------------------------------------------------------

const monolithicInstructions: { lineNumber: number; section: string; content: string }[] = [];

// Section 1: Project Overview (lines 1-50)
for (let i = 1; i <= 50; i++) {
  monolithicInstructions.push({
    lineNumber: i,
    section: "Project Overview",
    content: i === 25 ? "This project uses React 18 with TypeScript strict mode." : `Overview detail line ${i}`,
  });
}

// Section 2: Code Style Rules (lines 51-100)
for (let i = 51; i <= 100; i++) {
  monolithicInstructions.push({
    lineNumber: i,
    section: "Code Style",
    content:
      i === 72
        ? "RULE: Always use explicit return types on exported functions."
        : i === 78
          ? "RULE: Use const assertions for immutable arrays."
          : `Style rule detail line ${i}`,
  });
}

// Section 3: Testing Standards (lines 101-150)
for (let i = 101; i <= 150; i++) {
  monolithicInstructions.push({
    lineNumber: i,
    section: "Testing",
    content:
      i === 120
        ? "RULE: Every new endpoint must have integration tests."
        : i === 135
          ? "RULE: Test files must mirror the source file structure."
          : `Testing detail line ${i}`,
  });
}

// Section 4: Deployment Rules (lines 151-200)
for (let i = 151; i <= 200; i++) {
  monolithicInstructions.push({
    lineNumber: i,
    section: "Deployment",
    content:
      i === 175
        ? "RULE: Never deploy on Fridays. Deploy window is Tue-Thu 10am-3pm."
        : `Deployment detail line ${i}`,
  });
}

// ---------------------------------------------------------------------------
// Split instruction files (4 focused files)
// ---------------------------------------------------------------------------

const splitInstructions: Record<string, { lineNumber: number; section: string; content: string }[]> = {
  "01-project-overview.md": monolithicInstructions.filter((l) => l.section === "Project Overview"),
  "02-code-style.md": monolithicInstructions.filter((l) => l.section === "Code Style"),
  "03-testing.md": monolithicInstructions.filter((l) => l.section === "Testing"),
  "04-deployment.md": monolithicInstructions.filter((l) => l.section === "Deployment"),
};

// ---------------------------------------------------------------------------
// Simulated queries -- the agent needs to find specific rules
// ---------------------------------------------------------------------------

interface Query {
  description: string;
  targetRule: string;
  relevantSection: string;
}

const queries: Query[] = [
  {
    description: "Find the rule about return types",
    targetRule: "explicit return types",
    relevantSection: "Code Style",
  },
  {
    description: "Find the deployment window rule",
    targetRule: "deploy on Fridays",
    relevantSection: "Deployment",
  },
  {
    description: "Find the integration test rule",
    targetRule: "integration tests",
    relevantSection: "Testing",
  },
  {
    description: "Find the test file structure rule",
    targetRule: "mirror the source file",
    relevantSection: "Testing",
  },
];

// ---------------------------------------------------------------------------
// Search simulation
// ---------------------------------------------------------------------------

function searchMonolithic(query: Query): { linesRead: number; found: boolean } {
  // Agent must scan from the top, reading each line until it finds the rule.
  // In the worst case it reads all lines.
  let linesRead = 0;
  let found = false;

  for (const line of monolithicInstructions) {
    linesRead++;
    if (line.content.toLowerCase().includes(query.targetRule.toLowerCase())) {
      found = true;
      break;
    }
  }

  return { linesRead, found };
}

function searchSplit(query: Query): { linesRead: number; found: boolean; fileAccessed: string } {
  // Agent knows which file to look in based on the section.
  // It only reads lines from that one file.
  const fileMap: Record<string, string> = {
    "Project Overview": "01-project-overview.md",
    "Code Style": "02-code-style.md",
    Testing: "03-testing.md",
    Deployment: "04-deployment.md",
  };

  const targetFile = fileMap[query.relevantSection];
  const lines = splitInstructions[targetFile];
  let linesRead = 0;
  let found = false;

  for (const line of lines) {
    linesRead++;
    if (line.content.toLowerCase().includes(query.targetRule.toLowerCase())) {
      found = true;
      break;
    }
  }

  return { linesRead, found, fileAccessed: targetFile };
}

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

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

function run(): void {
  console.log("\n" + "=".repeat(90));
  console.log("  MONOLITHIC vs SPLIT INSTRUCTION FILES");
  console.log("=".repeat(90));

  console.log("\n  Monolithic file: 1 file, " + monolithicInstructions.length + " lines total");
  console.log("  Split files:     4 files, ~" + Math.round(monolithicInstructions.length / 4) + " lines each\n");

  const header = `| ${pad("Query", 42)}| ${pad("Monolithic (lines)", 20)}| ${pad("Split (lines)", 15)}| ${pad("File Accessed", 22)}| Savings`;
  const sep = `|${"-".repeat(44)}|${"-".repeat(22)}|${"-".repeat(17)}|${"-".repeat(24)}|${"-".repeat(10)}`;
  console.log(header);
  console.log(sep);

  let totalMono = 0;
  let totalSplit = 0;

  for (const q of queries) {
    const mono = searchMonolithic(q);
    const split = searchSplit(q);
    totalMono += mono.linesRead;
    totalSplit += split.linesRead;

    const savings = Math.round(((mono.linesRead - split.linesRead) / mono.linesRead) * 100);
    console.log(
      `| ${pad(q.description, 42)}| ${pad(String(mono.linesRead), 20)}| ${pad(String(split.linesRead), 15)}| ${pad(split.fileAccessed, 22)}| ${savings}%`
    );
  }

  console.log(sep);
  const avgMono = Math.round(totalMono / queries.length);
  const avgSplit = Math.round(totalSplit / queries.length);
  console.log(
    `| ${pad("AVERAGE", 42)}| ${pad(String(avgMono), 20)}| ${pad(String(avgSplit), 15)}| ${pad("(targeted file)", 22)}| ${Math.round(((avgMono - avgSplit) / avgMono) * 100)}%`
  );

  console.log("\n" + "=".repeat(90));
  console.log("  KEY INSIGHT");
  console.log("=".repeat(90));
  console.log("  With a monolithic file, the agent must scan up to " + monolithicInstructions.length + " lines for every query.");
  console.log("  With split files, it reads only the relevant " + Math.round(monolithicInstructions.length / 4) + "-line file.");
  console.log("  This means less context window usage, fewer hallucinations, and faster execution.\n");
}

run();

第四讲. 把指令拆分到不同文件里
http://www.clxhxhhr.top/posts/269/
作者
clxstart
发布于
2026-07-26
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。