Skip to content

第 4 章:Skill 与 Rule——给 Agent 立规矩

我遇到了什么问题

第 3 章解决了"上下文从哪来"的问题——记忆、检索、规范,三者构成 Agent 的知识体系。

但知识体系建好之后,新的问题出现了。

说实话,当时在我们的产品里,LLM 节点的上下文处理做得很简单——就是保存 10 轮对话历史,多了就截断。做完之后我觉得够用了,没在这上面多花精力,转头就去开发后面的功能了。

毕竟 10 轮对话,对于简单的问答场景确实够了。

但我没想到,随着产品功能越来越多,我自己用 Qoder 编程越来越吃力。

问题 1:LLM 总是犯同样的错误

我写 C# 代码,要求异常不能吞掉、要自然上抛。第一次我跟 Qoder 说了,它改了。第二次写新模块,它又吞了。第三次、第四次……同样的错误反复出现。

每次都要说一遍,说了又忘。这不就是带新人时的感觉吗——你教过他一次,但他下次还是犯同样的错。

问题 2:重复模块的批量创建

我的项目结构是有规范的——C# 模块怎么建、前端组件怎么组织、API 调用用什么模式。

但每次让 Qoder 创建一个新模块,它都要"重新理解"一遍项目结构。创建第一个模块时我手把手教了,创建第十个模块时它还在问同样的问题。

更烦的是,它每次创建出来的结构还不一样。第一个模块用 try-catch 包了一层,第二个模块直接抛异常,第三个模块又换了种写法。同一个项目,三种风格。

问题 3:每次对话都在“重新入职”

这是最根本的问题——不管我跟 Qoder 说过多少次“异常要上抛”、“API 用 axiosInstance”,新开一个对话窗口,它全忘了。

不是忘了,是根本没有这个信息。每次新对话,Agent 的上下文是空的。我之前跟它说过的所有规范、所有偏好,都不会自动带进去。

当时我也不知道有什么机制能让 Agent 自动加载项目规范。每次开始干活,都得先花十分钟把规范再说一遍。这哪是 AI 编程,这是 AI 陪聊。

换句话说,Agent 缺的不是记忆力,缺的是一套“自动加载的规矩”。

本质:Agent 没有"规矩"

回过头看,这三个问题的本质是一样的:Agent 没有"规矩"。

10 轮上下文只解决了"记住刚才说了什么",但没有解决"做事必须按什么规矩来"。就像你招了一个记忆力不错的新人——他能记住你昨天说的话,但他做事的方式每天都不一样。

这不是记忆的问题,是规范的问题。

我做了什么决策

决策 1:Skill——把重复操作封装成"技能"

第一个问题的解法:把反复说的操作手册写成文件。

一个 Skill 就是一份标准化的操作手册——输入是什么、输出是什么、放在哪里、遵循什么规范。

markdown
---
name: create-requirement
description: 生成符合项目命名规范的通用需求文档。
  当用户提到"需求文档"、"写需求"等关键词时自动触发。
  输出目录统一为 Documents\修改记录。
---

# 通用需求文档规范

## 输出目录
`D:\DEV\CODE\Automator\Client\Documents\修改记录`

## 文件名格式
MMDD-HHMMSS-具体需求描述.md

## 工作流程
1. 接收需求 → 分析需求名称、背景、期望结果
2. 创建文档 → 生成时间戳文件名,在指定目录创建
3. 写入内容 → 按模板填写(需求分析、实施要点、注意事项)

有了这个 Skill,Agent 每次写需求文档都会自动遵循规范——文件放对位置、命名格式正确、内容结构完整。

同样的事情不用再说第二遍。

决策 2:Rule——把编码约束变成"法律"

第二个问题的解法:写 Rule 文件。

Rule 和 Skill 的区别:

  • Skill 定义"怎么做一件事"(完整操作流程)
  • Rule 定义"做这件事时必须遵守什么"(单条约束)

Rule 比 Skill 更短、更聚焦:

markdown
---
description: C# 异常处理准则 - 异常透明模式,问题尽早暴露,异常自然上抛
glob: "**/*.cs"
---

# C# 异常处理准则 — 异常透明模式

## 核心思想
**问题尽早暴露,异常自然上抛。**

## 适用规则
1. 避免外层捕获:不在逻辑外层套用通用 try-catch 块
2. 必需字段直抛:缺失直接抛 InvalidOperationException
3. 可选字段降级:缺失记录 Debug 日志并使用默认值
4. 保留原始堆栈:不主动捕获异常,让堆栈自然传播

再看一个前端的 Rule:

markdown
---
description: 前端 API 调用模式规范 - 依赖注入 + axiosInstance,Service Class 风格
glob: "**/api/**/*.ts"
---

# 前端 API 调用模式规范

## 推荐模式:依赖注入 + Service Class
每个 API 模块一个 Service Class + 单例实例。

## 禁止的做法
- 不要直接使用原生 fetch
- 不要重复创建 axios 实例

注意 Rule 的 glob 字段——它指定这条规则只在匹配的文件上生效。C# 规则只对 .cs 文件生效,前端规则只对 .ts 文件生效。

规则不是"建议",是"法律"。 Agent 生成的代码如果违反规则,就应该被测试或 review 拦截。

决策 3:Skill 的系统化管理

当 Skill 数量超过 10 个之后,新的问题出现了:怎么让 Agent 知道有哪些 Skill 可以用?

最直觉的做法:把所有 Skill 的路径硬编码到代码里。

csharp
// 硬编码方式
var skillPaths = new List<string>
{
    "/path/to/skill1",
    "/path/to/skill2",
    // 每加一个 Skill 就要改代码
};

这种方式的问题是:每加一个 Skill 就要改代码、重新编译。

更好的做法:让各模块自己报告 Skill 路径

csharp
// 接口定义:各模块实现此接口,报告自己的 Skill 路径
public interface IAgentSkillsContributor
{
    IReadOnlyList<string> GetSkillPaths();
}

// 管理器:从 DI 收集所有贡献者的路径,统一构建 Provider
internal class AgentSkillsManager
{
    private readonly IEnumerable<IAgentSkillsContributor> _contributors;

    public AgentSkillsProvider BuildProvider()
    {
        var allPaths = _contributors
            .SelectMany(c => c.GetSkillPaths())
            .Distinct()
            .ToList();

        return new AgentSkillsProvider(allPaths, ...);
    }
}

这个设计的好处:

  • 新增 Skill 不需要改代码:只需要在对应模块的 GetSkillPaths() 里加一个路径
  • 模块解耦:每个模块管自己的 Skill,不需要知道其他模块有什么
  • 延迟加载:首次 LLM 调用时才扫描 Skill 文件,启动时不花时间

然后在 AiAgentModule 中注册:

csharp
// 核心层注册默认空实现(无任何 Skill)
services.TryAddSingleton<IAgentSkillsContributor, EmptySkillsContributor>();

// Integration 层可以注册自己的实现,覆盖默认
// services.AddSingleton<IAgentSkillsContributor, MyIntegrationSkillsContributor>();

这里用了 Provider 可覆盖模式——核心层注册默认空实现,业务层通过提前注册来覆盖。这是整个 Components 项目的核心设计模式之一。

决策 4:Skill 的渐进式加载——解决“撑爆上下文”

Skill 系统化管理之后,新的问题来了:当 Skill 数量超过 20 个,如果把所有 Skill 的全文都塞进 System Prompt,上下文窗口直接爆了。

这就是扁平化加载的问题——每次 LLM 调用,所有 Skill 全文一次性拼进去:

【扁平式:所有 Skill 全文一次性塞入】
System Prompt = 角色定义 + 记忆 + ▼全部 Skill 全文▼ + 工具指引
                                    ↑ 20 个 Skill × 平均 200 行 = 4000 行
                                    ↑ 上下文窗口直接爆了

解法是渐进式加载(Progressive Disclosure)——分三层,按需加载:

第 1 层:广告(Advertise)
  → System Prompt 中只注入 Skill 的 name + description(几行)
  → LLM 知道“有哪些 Skill 可用”,但还没看全文

第 2 层:加载(Load)
  → LLM 判断当前任务需要某个 Skill → 调用 load_skill("create-csharp-module")
  → 框架返回该 Skill 的 SKILL.md 全文

第 3 层:读取/执行(Read/Execute)
  → LLM 需要 Skill 的附属资源 → 调用 read_skill_resource(...)
  → 需要执行脚本 → 调用 run_skill_script(...)

关键思路:LLM 自己决定什么时候加载哪个 Skill。 不是我们帮它全塞进去,而是它看到目录之后自己判断。

这就像你去图书馆——不是把所有书搬到你面前,而是先看目录索引,需要哪本再取。

框架怎么实现的

微软的 Microsoft.Agents.AI 框架提供了 AgentSkillsProvider,它就是实现这个三层加载的核心类:

csharp
// AgentSkillsProvider 作为 AIContextProvider 注入到 Agent 管道
// 每次 LLM 调用前,框架自动执行 ProvideAIContextAsync()
//
// 它做两件事:
// 1. Instructions → 生成 <available_skills> 列表(只有 name + description)
// 2. Tools → 注册 load_skill / read_skill_resource / run_skill_script 三个工具
//
// LLM 看到 available_skills 列表后,如果需要某个 Skill,就调用 load_skill 工具

在我们的产品中,AgentSkillsManager 负责从 DI 收集所有 Skill 路径,构建 AgentSkillsProvider

各业务模块(Integration、Workflow...)
  │ 各自实现 IAgentSkillsContributor.GetSkillPaths()
  │ 返回各自的 Skills/ 目录

AgentSkillsManager(核心层)
  │ 汇总所有路径 → 构建 AgentSkillsProvider
  │ 作为 AIContextProvider 注册到管道

Agent 每次调 LLM 前,框架自动执行:
  ├── Instructions: "可用 Skill 列表(仅 name + desc)"
  └── Tools: [load_skill, read_skill_resource, run_skill_script]

这就是为什么 Skill 文件必须有 YAML Frontmatter——namedescription 字段就是给第 1 层“广告”用的。没有 Frontmatter,LLM 连 Skill 的名字都看不到,更别说按需加载了。

上下文还是太多怎么办?

渐进式加载解决了“Skill 太多塞不下”的问题,但长对话本身也会撑爆上下文——工具调用结果越积越多、历史消息越来越长。

这时候需要上下文压缩(Context Compaction)——在消息到达上下文窗口上限之前,主动压缩历史:

压缩策略做什么什么时候用
工具结果压缩把早期的工具调用结果压缩为摘要最温和,优先使用
滑动窗口只保留最近 N 轮对话对话式场景
LLM 摘要用一个小模型把旧消息总结成一段话需要保留语义时
截断直接删掉最早的消息预算严格时

这些策略可以组合使用——先压缩工具结果,再压缩对话历史,最后实在不行才截断。

Rule 在 Qoder 框架中的三类机制:始终生效、文件匹配、模型决策

决策 5:Rule——不是文档,是"法律"

前面讲了 Skill 是“操作手册”。但还有一类东西不是“怎么做”,而是“必须怎么做 / 不能怎么做”。

这就是 Rule

Skill 与 Rule 的本质区别:Skill 是操作手册,Rule 是交通法规

Rule 和 Skill 的本质区别

SkillRule
定义“怎么做一件事”“做这件事时必须遵守什么”
长度几十到几百行(完整操作流程)几行到十几行(单条约束)
加载方式渐进式(LLM 按需 load)自动加载(每次对话都生效)
触发条件LLM 判断需要时才加载匹配 glob 模式就自动生效
类比操作手册交通法规

举个例子:

Skill: “怎么创建一个 C# 模块”(完整步骤:建目录、写文件、配 DI、写测试)
Rule:  “异常不能吞掉,必须上抛”(一条约束,永远生效)

Rule 在 Qoder 框架中怎么实现

Rule 的实现和 Skill 完全不同。Skill 是"按需加载",Rule 是"自动生效"。

我用的是 Qoder——一个 AI 编程 IDE。Qoder 内部有一套完整的 Rule 机制,我把它的原理讲清楚。

第一步:怎么写一条 Rule

Rule 文件放在项目的 .qoder/rules/ 目录下,格式是 .mdc(Markdown + 元数据)。一个 Rule 文件长这样:

markdown
---
description: C# 异常处理准则 - 异常透明模式,问题尽早暴露,异常自然上抛
glob: "**/*.cs"
alwaysApply: false
enabled: true
---

# C# 异常处理准则 — 异常透明模式

## 核心思想
**问题尽早暴露,异常自然上抛。**

## 适用规则
1. 避免外层捕获:不在逻辑外层套用通用 try-catch 块
2. 必需字段直抛:缺失直接抛 InvalidOperationException
3. 可选字段降级:缺失记录 Debug 日志并使用默认值
4. 保留原始堆栈:不主动捕获异常,让堆栈自然传播

## 示例
// ✅ 推荐
void Configure(NodeConfig config) {
    if (config.RequiredId == null) 
        throw new InvalidOperationException("必需字段 RequiredId 缺失");
}

// ❌ 避免
void Configure(NodeConfig config) {
    try { ... } catch (Exception ex) { /* 吞噬异常 */ }
}

Frontmatter 里有四个关键字段:

字段作用示例
description一句话说明这条规则干什么的"C# 异常处理准则"
glob匹配哪些文件时自动生效**/*.cs
alwaysApply是否无视 glob 都生效true = 永远注入
enabled是否启用false = 临时禁用

再看一个前端 Rule:

markdown
---
description: 前端 API 调用模式规范 - 依赖注入 + axiosInstance
glob: "**/api/**/*.ts"
alwaysApply: false
enabled: true
---

## 推荐模式:依赖注入 + Service Class
...
## 禁止的做法
- 不要直接使用原生 fetch
- 不要重复创建 axios 实例

写 Rule 的核心原则:短、聚焦、有正反例。 一条 Rule 只约束一件事,配上正确和错误的写法对比。

第二步:Rule 怎么被使用

Rule 写好之后,不需要你手动告诉 AI"请遵守这条规则"。Qoder 框架会自动处理。

Qoder 内部把 Rule 分成三类,每类的加载时机不同:

Qoder Rule 三类机制:

1. always_on_rules(始终生效)
   → alwaysApply: true 的规则
   → 每次对话都自动注入上下文,无条件生效
   → 适合:全局约束(如"不讲技术名词"、"异常必须上抛")

2. glob_rules(文件匹配)
   → 有 glob 字段的规则
   → 编辑匹配的文件时自动注入,不匹配时不注入
   → 适合:语言/框架级约束(如 C# 规则只对 .cs 生效)

3. model_decision_rules(模型决策)
   → 有 description 但 alwaysApply: false 的规则
   → Qoder 把规则目录列出来,AI 自己判断是否需要加载
   → 适合:场景性约束(如"只在写 API 时才需要"的规范)

用一张图表示它的内部流程:

用户在 Qoder 中编辑文件或发消息

Qoder 框架扫描 .qoder/rules/ 目录

┌─ always_on_rules → 直接注入上下文(无条件)
├─ glob_rules → 检查当前文件是否匹配 glob 模式
│   ├── 匹配 → 注入该 Rule 的完整内容
│   └── 不匹配 → 跳过,不占上下文
└─ model_decision_rules → 把 description 列表给 AI
    → AI 判断"这条规则跟我当前任务有关吗?"
        ├── 有关 → 加载完整内容(FetchRules)
        └── 无关 → 跳过

所有命中的 Rule 内容合并到 System Prompt

AI 生成代码时自动遵守这些约束

关键设计:三类 Rule 的 Token 开销不同。

  • always_on_rules:每次都占 Token,所以必须少而精(3-5 条为宜)
  • glob_rules:只在匹配时占 Token,不匹配时零开销——这是最高效的
  • model_decision_rules:平时只占几行目录索引的 Token,需要时才加载全文

这就是为什么 Rule 文件要有 description 字段——它不只是给人看的,是给 AI 判断"要不要加载这条规则"用的。description 写得越准确,AI 的判断就越准。

实际效果演示

拿我的 Automator Client 项目举例。.qoder/rules/ 目录下有 16 条 Rule:

.qoder/rules/
├── csharp-early-return.mdc         ← glob: **/*.cs
├── csharp-exception-handling.mdc   ← glob: **/*.cs
├── csharp-logger-convention.mdc    ← glob: **/*.cs
├── icon-usage.mdc                  ← glob: **/*.cs
├── no-native-fetch-api-calls.mdc   ← glob: **/api/**/*.ts
├── web-api-pattern.mdc             ← glob: **/api/**/*.ts
├── web-component-pattern.mdc       ← glob: **/*.tsx
├── web-hooks-pattern.mdc           ← glob: **/*.tsx
├── web-tailwind-standard.mdc       ← glob: **/*.tsx (241行)
└── ...(还有 7 条)

当我编辑一个 .cs 文件时:

编辑 PluginController.cs
  → 匹配 **/*.cs 的规则自动注入:
    ✅ csharp-early-return
    ✅ csharp-exception-handling
    ✅ csharp-logger-convention
    ✅ icon-usage
  → 不匹配的规则跳过:
    ❌ web-api-pattern(只对 .ts 生效)
    ❌ web-component-pattern(只对 .tsx 生效)
    ❌ web-tailwind-standard(只对 .tsx 生效)

4 条 C# 规则自动生效,12 条前端规则零开销。 这就是 glob 匹配的威力。

除了文件级 Rule,还有全局 Rule

文件级 Rule 通过 glob 匹配生效。但有些约束是全局的——不分文件类型,每次都要遵守。

这类规则写在两个地方:

  1. AGENTS.md:项目根目录的全局规范文件。Agent 启动时自动读取,始终在上下文中。架构约束、命名规范、依赖原则都写在这里。

  2. alwaysApply: true 的 .mdc 文件:无视 glob,每次对话都注入。

全局规则(不分文件类型)
  → AGENTS.md → Agent 启动时自动加载 → 始终在上下文中
  → alwaysApply: true 的 .mdc → 每次对话都注入

文件级规则(按 glob 匹配)
  → .mdc 文件 → 编辑匹配文件时自动注入

场景性规则(AI 判断)
  → .mdc 文件 → AI 看 description 后决定是否加载

为什么 Rule 不能像 Skill 一样"按需加载"?

因为 Rule 是约束,不是知识。

Skill 是"你可能需要",LLM 可以自己判断要不要看。Rule 是"你必须遵守",如果让 LLM 自己判断要不要看,它可能判断"不需要"——然后写出违反约束的代码。

Rule 必须无条件生效,没有"按需"的余地。

这也是为什么 Rule 要尽量短——它不像 Skill 可以有几百行,Rule 通常只有几行。因为它是每次都注入上下文的,太长会浪费 Token。glob 匹配解决了这个问题——不相关的 Rule 不占空间。

Agent 知识体系:AGENTS.md 是入职手册,Skill 是操作手册,Rule 是规章制度

代码里长成了什么样

Skill 的目录结构

.qoder/skills/
├── create-requirement/       ← 生成需求文档
│   └── SKILL.md
├── create-csharp-module/     ← 创建 C# 模块
│   └── SKILL.md
├── create-web-module/        ← 创建前端模块
│   ├── SKILL.md
│   └── web-module-standard/  ← 子规范
│       ├── SKILL.md
│       ├── formily-usage.md
│       └── templates/
├── design-wpf-ui/            ← 设计 WPF 界面
│   └── SKILL.md
├── build-desktop/            ← 构建桌面端
│   └── SKILL.md
├── build-web/                ← 构建前端
│   └── SKILL.md
├── framework/                ← 框架参考文档(作为 Skill 注入)
│   ├── dictionary-extensions.md
│   ├── enum-extensions.md
│   ├── exception-extensions.md
│   └── ...(12 份基础类库文档)
└── optimize-react/           ← React 性能优化
    ├── SKILL.md
    └── rules/

Rule 的目录结构

.qoder/rules/
├── csharp-early-return.mdc         ← 禁止深层嵌套
├── csharp-exception-handling.mdc   ← 禁止吞异常
├── csharp-logger-convention.mdc    ← 日志规范
├── icon-usage.mdc                  ← 图标使用规范
├── no-native-fetch-api-calls.mdc   ← 禁止原生 fetch
├── semi-tab-design.mdc             ← Semi Design Tab 规范
├── ui-component-import.mdc         ← UI 组件导入规范
├── web-api-pattern.mdc             ← API 调用模式
├── web-component-pattern.mdc       ← 组件编写模式
├── web-hooks-pattern.mdc           ← React Hooks 规范
├── web-index-export.mdc            ← 模块导出规范
├── web-mock-pattern.mdc            ← Mock 数据规范
├── web-state-management.mdc        ← 状态管理规范
├── web-tailwind-standard.mdc       ← Tailwind CSS 规范(241 行)
└── web-type-definition.mdc         ← 类型定义规范

Skill 和 Rule 在 Agent 调用链中的位置

用户发消息

Agent 准备调用 LLM

框架自动执行所有 AIContextProvider:
  ├── MemoryFactsContextProvider   → 注入记忆事实
  ├── CompactionProvider           → 按需压缩历史上下文
  ├── MonitoringContextProvider    → 记录监控数据
  ├── TitleContextProvider         → 更新会话标题
  └── AgentSkillsProvider          → 注入 Skill 目录(仅 name+desc)
                                    + 注册 load_skill 等工具

Rule 文件通过 AGENTS.md 或编辑器 glob 匹配自动注入

所有上下文合并 → 调用 LLM

LLM 判断需要某个 Skill → 调用 load_skill("xxx") → 获取全文

LLM 输出 → 返回结果

业界怎么做的

同样的"给 Agent 立规矩"问题,其他框架是怎么解决的?

Skill 加载机制对比

LangGraph 没有 Skill 概念——所有知识编码到图节点的系统提示词里。每个节点有自己的 prompt,节点之间不共享。好处是简单直接,坏处是知识不能复用——两个节点需要相同知识时,得写两遍。

微软 Agent Framework (MAF) 通过 AgentSkillsProvider 实现渐进式加载——先注入 Skill 目录(仅 name + desc),LLM 按需调用 load_skill 工具获取全文。这个设计最接近我的思路,也是我在产品中实际使用的方案。

Hermes 用 Skill 文件 + 插件系统来定义 Agent 的能力边界。每个 Skill 是一份 Markdown 文档,定义了 Agent 在特定场景下应该怎么做。Hermes 还有一个"Curator"机制——自动维护 Skill 的有效性,过时的 Skill 会被标记或移除。这个思路很好,但我还没实现。

AgentScope Java 的做法是"知识注入"——通过 AIContextProvider 在 Agent 执行前注入上下文。没有独立的 Skill 概念,所有知识通过 ContextProvider 统一注入。

Rule 约束机制对比

LangGraph 的做法是"把约束编码到图里"——Agent 的行为由图的结构决定。你不需要告诉 Agent"不要做 X",因为图里根本没有 X 这个节点。好处是约束是硬性的,坏处是灵活性差——加一个新能力就要改图。

微软 Agent Framework (MAF) 的做法是"EvalCheck"——在 Agent 输出之后做评估检查。类似于"先让 Agent 做事,做完之后检查是否符合规范"。这个思路类似于单元测试——你写代码,然后测试检查对不对。MAF 的 EvalCheck 支持 DTO 级校验、结构化输出验证等。

AgentScope Java 的做法是"运行时干预"——在 Agent 执行过程中注入安全检查。比如"安全中断"(发现危险操作立即停止)、"优雅取消"(允许 Agent 保存状态后停止)、"人工注入"(关键决策点让人确认)。

Qoder 的做法是".mdc 文件 + glob 匹配"——Rule 文件按 glob 模式自动匹配生效,不匹配时零开销。这个设计的好处是 Rule 可以很多但不会撑爆上下文,坏处是 Rule 之间没有优先级——匹配了就全部注入,没有"哪条更重要"的判断。

对比总结

框架Skill 加载Rule 约束特点
LangGraph无 Skill,知识写进节点 prompt约束编码到图结构硬性约束,但灵活性差
MAFAgentSkillsProvider 渐进式加载EvalCheck 输出后校验最接近我的设计
HermesSkill 文件 + Curator 维护无独立 Rule 机制自动化高,但无约束层
AgentScopeAIContextProvider 统一注入运行时安全干预解耦彻底,但概念模糊
Qoder渐进式加载 + load_skill 工具.mdc + glob 匹配文件级精确匹配,零开销

关键认知

这个阶段做完,我有了第四个关键认知:

Skill 是"肌肉记忆",Rule 是"交通法规"。两者缺一不可。

  • Skill 解决"怎么做"——标准化操作流程,不用每次重新教
  • Rule 解决"不能怎么做"——编码约束,防止 Agent 犯已知的错误

但光有 Skill 和 Rule 还不够。Agent 还需要知道项目全貌——架构是什么、模块怎么划分、命名规范是什么。

这就是 AGENTS.md 的价值——它是 Agent 的"入职手册"。一个新员工入职,先看项目文档了解全貌,再看操作手册知道怎么做,最后看规章制度知道什么不能做。

AGENTS.md    → 入职手册(项目全貌)
Skill        → 操作手册(怎么做)
Rule         → 规章制度(不能怎么做)

三者合在一起,就是 Harness 架构的"规范层"。

附:我的 AGENTS.md 是怎么写的

AGENTS.md 不是我自己从零写的,是用 AI 生成的。

思路很简单:我让 AI 扫描整个项目,自己提取关键信息,生成初稿,我再审查修改。

具体是四步:

第一步:扫描目录结构 让 AI 读取项目的目录树,了解有哪些模块、怎么组织的。这一步解决"项目长什么样"。

第二步:分析架构文件 让 AI 读核心代码文件——比如 Startup.csModule.cs、接口定义——提取出分层架构、依赖原则、命名规范。这一步解决"代码是怎么组织的"。

第三步:梳理 Skill 和 Rule 让 AI 扫描 .qoder/skills/.qoder/rules/ 目录,列出所有可用的 Skill 和 Rule,生成索引表。这一步解决"Agent 有什么能力"。

第四步:识别基础工具库 让 AI 扫描 HelperExtensions 等工具类,列出常用方法和用途。这一步解决"有哪些轮子不用重造"。

四步走完,AI 生成一份初稿。我做的事情是:

  • 删掉 AI 过度发挥的内容(它会把每个文件都描述一遍)
  • 补充 AI 不知道的隐性规范(比如"异常必须上抛"这种团队约定)
  • 精简到一页以内(太长的 AGENTS.md 会占用上下文窗口)

最终产出的 AGENTS.md 长这样(以 Automator Client 项目为例):

markdown
# Automator Client 项目上下文

## 项目简介
Automator Client 是一个桌面端自动化工具,包含 WPF 桌面客户端
(Desktop)和 Web 前端(Web),后端基于 ASP.NET Core + Coze 五层架构。

## 后端架构(五层)
API 层 (Coze/Api)        — HTTP请求处理、参数校验、响应转换
  → Application 层        — 业务流程编排、VO转换
    → Domain 层           — 核心业务逻辑
      → Repository 层     — 数据库CRUD
        → Model 层        — EF Core 实体定义

### 依赖原则
- 禁止反向依赖和跨层调用
- 不捕获异常,全部向上抛出
- 主键ID 统一使用 int,时间戳使用 DateTime

### 命名规范
| 层级 | 类型 | 示例 |
|------|------|------|
| API | {模块名}Controller | PluginController |
| Domain | {模块名}DomainService | PluginDomainService |
| Repository | I{EntityName}Repository | IPluginRepository |

## 可用 Skill(/ 命令)
| 命令 | 用途 |
|------|------|
| /requirement | 通用需求文档 |
| /create-csharp-module | 创建 C# 模块 |

## 基础类库快速索引
### Framework.Helper
| 类 | 功能 |
|---|------|
| ObjectPropertySetter | 设置对象属性,支持嵌套路径 |
| JsonHelper | JSON 序列化/反序列化 |

### Framework.Extensions
| 类 | 功能 |
|---|------|
| StringExtensions | 字符串分割/提取/修剪 |
| DictionaryExtensions | 字典批量操作/合并 |
| ExceptionExtensions | 异常详细信息打印 ex.Dump() |

这份文档不到 130 行。但它解决了最核心的问题:Agent 每次新对话启动时,自动具备系统级认知。 不用我再花十分钟把规范说一遍。

写 AGENTS.md 的核心原则就一条:给 AI 看的东西,要像给新员工入职手册一样写——只写它必须知道的,不写你知道但不需要它知道的。