开发技巧

用 Windsurf/Devin 项目规则与 AGENTS.md 管理 AI 指令

名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Mem

BINBHO · 图文教程

名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Memories 只适用于 Cascade(旧版智能体),Devin Local 用 Skills。配置目录优先用 .devin/,旧的 .windsurf/ 仍会被读取。本文按当前文档写,界面细节以你安装的版本为准。

这篇给一个项目写一套规则:根目录 AGENTS.md(始终生效)、子目录 api/AGENTS.md(只在改 api/ 时生效),以及 .devin/rules/ 下四种激活方式各一个规则文件。再写一个 lint_rules.py:检查 trigger 是否合法、glob 规则有没有 globs、model_decision 有没有 description、是否超过字数上限,并模拟“编辑某个文件时会带上哪些规则”。

先看清成品

  • 检查脚本在示例项目上真实运行:6 个文件全部通过;在故意写错的项目上找出 5 个错误和 2 条提醒。
  • 发现官方示例 globs: **/*.test.ts 不是合法 YAML(PyYAML 报 ScannerError),所以脚本没有用 yaml 库解析。
  • 4 项 pytest 通过。

合规提醒:用你自己的账号和授权的套餐,不共享账号。规则、工作流和 MCP 配置会把指令和工具交给 AI,不要写进密钥;公司代码先确认组织政策是否允许使用。AI 运行命令前要看清楚,不要开启“自动执行所有命令”。

运行说明:box 是 Linux 服务器,没有图形界面,我没有安装或运行 Devin Desktop / Windsurf 编辑器,所以没有验证编辑器实际加载这些文件的效果。真实运行过的是:本文的检查脚本、示例项目和 MCP 服务器(2026-10-21,UTC+8)。

做完之后你会得到什么?

成果 说明
demo/ 示例项目的规则和 AGENTS.md
broken/ 故意写错的规则
lint_rules.py 规则检查
test_lint_rules.py 4 项测试

需要准备哪些环境?

项目 说明
编辑器 Devin Desktop(原 Windsurf),从 https://windsurf.com 下载
Python 3.11 以上(用到 tomllib),pip install pytest

关键事实

事实 来源
全局规则 ~/.codeium/windsurf/memories/global_rules.md,始终生效,最多 6000 字符 官方 Memories & Rules
工作区规则 .devin/rules/*.md(优先)或 .windsurf/rules/*.md,一个文件一条规则,每个最多 12000 字符;旧的 .windsurfrules 仍会读取 同上
frontmatter 的 trigger:always_on(每条消息都带)、model_decision(只带 description,需要时读全文)、glob(读写匹配 globs 的文件时)、manual(输入 @规则名 时) 同上
AGENTS.md 不需要 frontmatter:根目录 = 始终生效,子目录 = <目录>/** 的 glob 规则 官方 AGENTS.md
自动生成的 Memories 只在本机、只对旧版 Cascade 智能体有效;需要长期保留的知识建议写成规则或 AGENTS.md 官方 Memories & Rules

文件结构

89-windsurf-A-src/
├── demo/
│   ├── AGENTS.md
│   ├── api/AGENTS.md
│   └── .devin/rules/
│       ├── security.md          # always_on
│       ├── python-style.md      # glob
│       ├── db-migrations.md     # model_decision
│       └── release-notes.md     # manual
├── broken/                      # 故意写错
├── lint_rules.py
└── test_lint_rules.py

第 1 步:根目录 AGENTS.md

# 项目约定(根目录 AGENTS.md:始终生效)

- Python 3.11,依赖用 `uv` 管理,不要用 pip 直接装到系统里。
- 改完代码先跑 `uv run pytest -q`,全部通过再说“完成”。
- 不要读取或修改 `.env`、`secrets/` 下的任何文件。

第 2 步:api/AGENTS.md

# api/ 目录约定(子目录 AGENTS.md:只在读写 api/ 下的文件时生效)

- 所有接口返回 JSON,错误统一为 `{"error": {"code": ..., "message": ...}}`。
- 新接口必须同时写测试,放在 `api/tests/`。

第 3 步:四个规则文件

<!-- security.md -->
---
trigger: always_on
---

- 不要在代码、日志、提交信息中写入任何密钥或 Token。
- 运行会删除文件、改 git 历史或访问生产环境的命令前,先停下来问我。
<!-- python-style.md -->
---
trigger: glob
globs: **/*.py
---

- 函数和类都写 docstring(中文即可)。
- 能提前 return 就提前 return,少嵌套。
- 不要 `except Exception: pass`,至少记录日志。
<!-- db-migrations.md -->
---
trigger: model_decision
description: 修改数据库表结构、写迁移脚本或改 models.py 时使用
---

<migrations>
- 迁移只能新增,不能修改已经合并的迁移文件。
- 删除列分两步:先停止读写并发版,下一个版本再删除。
- 每个迁移都要能回滚,写好 downgrade。
</migrations>
<!-- release-notes.md -->
---
trigger: manual
---

写发布说明时:按“新增 / 修复 / 变更”三类列出,每条一句话,面向用户而不是开发者,不写内部工单号。

第 4 步:检查脚本

"""检查 Devin Desktop(原 Windsurf)工作区规则:.devin/rules/*.md、旧的 .windsurf/rules/*.md 和 AGENTS.md。
规则来自 docs.devin.ai/desktop/cascade/memories 与 agents-md:
- 每个规则文件的 frontmatter 用 trigger 声明激活方式:always_on / model_decision / glob / manual;
- glob 需要 globs,model_decision 靠 description 让模型决定是否读取全文;
- 工作区规则每个文件最多 12000 字符,全局 global_rules.md 最多 6000 字符;
- AGENTS.md 不需要 frontmatter:根目录始终生效,子目录相当于 <目录>/** 的 glob 规则。
注意:官方示例写的是 `globs: **/*.test.ts`(不加引号),这不是合法的 YAML(** 会被当成别名),
所以这里按“一行一个 key: value”解析,不用 yaml 库。"""
import fnmatch, sys
from pathlib import Path

TRIGGERS = {"always_on", "model_decision", "glob", "manual"}
WS_LIMIT, GLOBAL_LIMIT = 12000, 6000


def frontmatter(text):
    if not text.startswith("---\n"):
        return None, text
    end = text.find("\n---", 4)
    if end < 0:
        return None, text
    meta = {}
    for line in text[4:end].splitlines():
        if ":" in line:
            k, v = line.split(":", 1); meta[k.strip()] = v.strip()
    return meta, text[end + 4:].lstrip("\n")


def lint_rule(path):
    text = Path(path).read_text(encoding="utf-8"); errs, warns = [], []
    if len(text) > WS_LIMIT:
        errs.append(f"{len(text)} 字符,超过工作区规则上限 {WS_LIMIT}")
    meta, body = frontmatter(text)
    if meta is None:
        errs.append("没有 frontmatter(--- trigger: ... ---)"); return errs, warns, None
    t = meta.get("trigger")
    if t not in TRIGGERS:
        errs.append(f"trigger={t!r},应为 {sorted(TRIGGERS)} 之一"); return errs, warns, t
    if t == "glob" and not meta.get("globs"):
        errs.append("trigger: glob 但没写 globs")
    if t == "model_decision" and not meta.get("description"):
        errs.append("trigger: model_decision 但没写 description,模型无从判断何时读取")
    if t != "glob" and meta.get("globs"):
        warns.append(f"写了 globs 但 trigger 是 {t},globs 不会起作用")
    if not body.strip():
        errs.append("正文为空")
    if t == "always_on" and len(text) > 3000:
        warns.append("always_on 规则每条消息都会占上下文,内容较长时考虑改成 model_decision 或 glob")
    return errs, warns, t


def agents_scope(root, path):
    rel = Path(path).parent.relative_to(root)
    return "always_on" if str(rel) == "." else f"glob {rel.as_posix()}/**"


def lint_tree(root):
    root = Path(root); report, bad = [], 0
    files = sorted(list(root.rglob(".devin/rules/*.md")) + list(root.rglob(".windsurf/rules/*.md")))
    names = {}
    for f in files:
        errs, warns, t = lint_rule(f); bad += len(errs)
        report.append((f.relative_to(root).as_posix(), t, errs, warns))
        names.setdefault(f.name, []).append(f.relative_to(root).as_posix())
    for n, ps in names.items():
        if len(ps) > 1 and any(".devin/" in p for p in ps) and any(".windsurf/" in p for p in ps):
            report.append((n, None, [], [f"同名规则同时在 .devin 和 .windsurf 下:{ps},.devin 优先,旧文件可能已失效"]))
    for f in sorted(list(root.rglob("AGENTS.md")) + list(root.rglob("agents.md"))):
        t = f.read_text(encoding="utf-8"); errs = []
        if t.startswith("---\n"):
            errs.append("AGENTS.md 不需要 frontmatter,作用范围由所在目录决定")
        if len(t) > WS_LIMIT:
            errs.append(f"{len(t)} 字符,超过 {WS_LIMIT}")
        bad += len(errs)
        report.append((f.relative_to(root).as_posix(), agents_scope(root, f), errs, []))
    if (root / ".windsurfrules").exists():
        report.append((".windsurfrules", "always_on", [], ["旧的单文件规则仍会被读取,建议拆分到 .devin/rules/"]))
    return report, bad


def which_apply(root, file_path):
    """模拟:编辑某个文件时,哪些 glob 规则和子目录 AGENTS.md 会被带上(always_on 总是带上)。"""
    out = []
    for f in sorted(Path(root).rglob(".devin/rules/*.md")):
        meta, _ = frontmatter(f.read_text(encoding="utf-8"))
        if meta and meta.get("trigger") == "glob":
            pats = [p.strip() for p in meta.get("globs", "").split(",") if p.strip()]
            if any(fnmatch.fnmatch(file_path, p) or fnmatch.fnmatch("x/" + file_path, p) for p in pats):
                out.append(f.name)
    for f in Path(root).rglob("AGENTS.md"):
        rel = f.parent.relative_to(root).as_posix()
        if rel != "." and file_path.startswith(rel + "/"):
            out.append(f"{rel}/AGENTS.md")
    return out


def main(argv=None):
    argv = argv if argv is not None else sys.argv[1:]
    root = argv[0] if argv else "."
    report, bad = lint_tree(root)
    for p, t, errs, warns in report:
        print(f"{'✗' if errs else '✓'} {p:<36} {t or ''}")
        for e in errs: print("    错误:", e)
        for w in warns: print("    提醒:", w)
    for fp in argv[1:]:
        print(f"编辑 {fp} 时额外生效:{which_apply(root, fp) or '无'}")
    print(f"共 {len(report)} 项,{bad} 个错误")
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())

第 5 步:测试

import lint_rules as L

def test_demo_clean():
    report, bad = L.lint_tree("demo")
    assert bad == 0 and len(report) == 6

def test_broken_found():
    report, bad = L.lint_tree("broken")
    msgs = " ".join(e for _, _, errs, _ in report for e in errs)
    assert bad == 5
    for k in ("description", "12000", "'always'", "没写 globs", "不需要 frontmatter"):
        assert k in msgs

def test_frontmatter_unquoted_glob():
    # 官方示例写法,yaml 库会报错,这里要能解析
    meta, body = L.frontmatter("---\ntrigger: glob\nglobs: **/*.test.ts\n---\n\n正文")
    assert meta == {"trigger": "glob", "globs": "**/*.test.ts"} and body == "正文"

def test_which_apply():
    assert L.which_apply("demo", "api/routes.py") == ["python-style.md", "api/AGENTS.md"]
    assert L.which_apply("demo", "main.py") == ["python-style.md"]
    assert L.which_apply("demo", "web/app.js") == []

第 6 步:运行

$ python lint_rules.py demo api/routes.py web/app.js
✓ .devin/rules/db-migrations.md        model_decision
✓ .devin/rules/python-style.md         glob
✓ .devin/rules/release-notes.md        manual
✓ .devin/rules/security.md             always_on
✓ AGENTS.md                            always_on
✓ api/AGENTS.md                        glob api/**
编辑 api/routes.py 时额外生效:['python-style.md', 'api/AGENTS.md']
编辑 web/app.js 时额外生效:无
共 6 项,0 个错误

$ python lint_rules.py broken
✗ .devin/rules/deploy.md               model_decision
    错误: trigger: model_decision 但没写 description,模型无从判断何时读取
✗ .devin/rules/huge.md                 always_on
    错误: 15427 字符,超过工作区规则上限 12000
    提醒: always_on 规则每条消息都会占上下文,内容较长时考虑改成 model_decision 或 glob
✗ .devin/rules/lang.md                 always
    错误: trigger='always',应为 ['always_on', 'glob', 'manual', 'model_decision'] 之一
✗ .devin/rules/tests.md                glob
    错误: trigger: glob 但没写 globs
✓ .windsurf/rules/tests.md             manual
✓ tests.md                             
    提醒: 同名规则同时在 .devin 和 .windsurf 下:['.devin/rules/tests.md', '.windsurf/rules/tests.md'],.devin 优先,旧文件可能已失效
✗ docs/AGENTS.md                       glob docs/**
    错误: AGENTS.md 不需要 frontmatter,作用范围由所在目录决定
共 7 项,5 个错误
$ echo $?
1
$ python -m pytest -q
4 passed

怎么检查结果?

  • 把 demo/ 里的文件放进你的项目,在编辑器的 Customizations → Rules 面板里应能看到 4 条工作区规则,激活方式和文件里写的一致。
  • 在对话里问一个 Python 文件的问题,再问一个 JS 文件的问题,对比 Python 风格规则是否只在前者生效。
  • which_apply 用 Python 的 fnmatch 模拟 glob,和编辑器的匹配实现不一定完全一样,只用来粗查。

调试示例

1. 官方示例的 frontmatter 不是合法 YAML。 文档里的例子是:

---
trigger: glob
globs: **/*.test.ts
---

我一开始想用 PyYAML 解析 frontmatter,结果:

>>> yaml.safe_load('trigger: glob\nglobs: **/*.test.ts')
ScannerError while scanning an alias
>>> yaml.safe_load('globs: "**/*.test.ts"')
{'globs': '**/*.test.ts'}

在 YAML 里 * 开头表示别名,所以不加引号会报错。编辑器显然不是用严格的 YAML 解析的(文档就是这么写的)。我的检查脚本因此按“一行一个 key: value”解析,并加了测试。影响:如果你的仓库里有 pre-commit 的 YAML 检查或其他工具扫描 frontmatter,可能会对这些规则文件报错。加引号能不能被编辑器正确识别,我没法在 box 上验证,所以示例保持和官方一致不加引号。

2. 同名规则同时出现在 .devin 和 .windsurf。 从旧版升级的项目可能两个目录都有。文档说 .devin/ 优先、.windsurf/ 作为兼容。脚本发现同名文件时会提醒,避免你改了旧目录里的规则却发现没生效。

常见问题

现象 原因 处理
规则没生效 trigger 写错(如 always) 用 lint_rules.py 检查
规则太长被截断 超过 12000 / 6000 字符 拆成多个文件,或改成 model_decision
每条消息都很慢、上下文不够 always_on 规则太多太长 只把真正全局的放 always_on
改了规则没变化 改的是 .windsurf 下的旧文件 以 .devin/rules 为准

模板规则

  • always_on 只放安全和全局约定。
  • 目录专属约定用子目录 AGENTS.md。
  • model_decision 一定写好 description。
  • 规则提交到仓库,和代码一起评审。

项目资源

用途 地址
Devin Desktop 文档(原 docs.windsurf.com) https://docs.windsurf.com
Memories & Rules(官方) https://docs.devin.ai/desktop/cascade/memories
Devin Local 智能体(官方) https://docs.devin.ai/desktop/devin-local
AGENTS.md(官方) https://docs.devin.ai/desktop/cascade/agents-md
Devin Desktop 官网(原 windsurf.com) https://windsurf.com

规则写得少而准,比写一大篇更有用。