第 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——name 和 description 字段就是给第 1 层“广告”用的。没有 Frontmatter,LLM 连 Skill 的名字都看不到,更别说按需加载了。
上下文还是太多怎么办?
渐进式加载解决了“Skill 太多塞不下”的问题,但长对话本身也会撑爆上下文——工具调用结果越积越多、历史消息越来越长。
这时候需要上下文压缩(Context Compaction)——在消息到达上下文窗口上限之前,主动压缩历史:
| 压缩策略 | 做什么 | 什么时候用 |
|---|---|---|
| 工具结果压缩 | 把早期的工具调用结果压缩为摘要 | 最温和,优先使用 |
| 滑动窗口 | 只保留最近 N 轮对话 | 对话式场景 |
| LLM 摘要 | 用一个小模型把旧消息总结成一段话 | 需要保留语义时 |
| 截断 | 直接删掉最早的消息 | 预算严格时 |
这些策略可以组合使用——先压缩工具结果,再压缩对话历史,最后实在不行才截断。

决策 5:Rule——不是文档,是"法律"
前面讲了 Skill 是“操作手册”。但还有一类东西不是“怎么做”,而是“必须怎么做 / 不能怎么做”。
这就是 Rule。

Rule 和 Skill 的本质区别
| Skill | Rule | |
|---|---|---|
| 定义 | “怎么做一件事” | “做这件事时必须遵守什么” |
| 长度 | 几十到几百行(完整操作流程) | 几行到十几行(单条约束) |
| 加载方式 | 渐进式(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 匹配生效。但有些约束是全局的——不分文件类型,每次都要遵守。
这类规则写在两个地方:
AGENTS.md:项目根目录的全局规范文件。Agent 启动时自动读取,始终在上下文中。架构约束、命名规范、依赖原则都写在这里。
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 不占空间。

代码里长成了什么样
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 | 约束编码到图结构 | 硬性约束,但灵活性差 |
| MAF | AgentSkillsProvider 渐进式加载 | EvalCheck 输出后校验 | 最接近我的设计 |
| Hermes | Skill 文件 + Curator 维护 | 无独立 Rule 机制 | 自动化高,但无约束层 |
| AgentScope | AIContextProvider 统一注入 | 运行时安全干预 | 解耦彻底,但概念模糊 |
| 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.cs、Module.cs、接口定义——提取出分层架构、依赖原则、命名规范。这一步解决"代码是怎么组织的"。
第三步:梳理 Skill 和 Rule 让 AI 扫描 .qoder/skills/ 和 .qoder/rules/ 目录,列出所有可用的 Skill 和 Rule,生成索引表。这一步解决"Agent 有什么能力"。
第四步:识别基础工具库 让 AI 扫描 Helper、Extensions 等工具类,列出常用方法和用途。这一步解决"有哪些轮子不用重造"。
四步走完,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 看的东西,要像给新员工入职手册一样写——只写它必须知道的,不写你知道但不需要它知道的。