ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

不到 40 行的规则里藏了 4 种模式:ECC 如何用 Protocol、Dataclass 与生成器约束 Python 代码

不到 40 行的规则里藏了 4 种模式:ECC 如何用 Protocol、Dataclass 与生成器约束 Python 代码

不到 40 行的规则里藏了 4 种模式:ECC 如何用 Protocol、Dataclass 与生成器约束 Python 代码

【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC

Agent 改写.py文件时,凭什么判断好坏?ECC 给出的答案是 rules/python/patterns.md 这份不到 40 行的规则文件:用 Protocol 约束接口,用 Dataclass 定型数据,用上下文管理器与生成器管好资源和内存。本文以这份 Python 模式规则为主线,结合 src/llm 的真实实现,说明这些约束在仓库中如何生效、又被哪些代码验证过。

规则落在仓库什么位置:路径触发的加载机制

ECC 把编码约束按技术栈拆分存放,Python 专项规则就藏在语言规则目录下。文件头部带有一段 YAML frontmatter:

--- paths: - "**/*.py" - "**/*.pyi" ---

从这段 frontmatter 可以看出:规则生效的条件不是它自己放在哪个目录,而是它命中哪些文件路径——Agent 一旦处理仓库里任意.py或.pyi(类型桩)文件,这份规则就会随之装载,约束其后的编码与评审动作。正文首行直接交代了定位:"This file extends common/patterns.md with Python specific content",即它是在 rules/common/patterns.md 这套跨语言公共模式之上叠加的 Python 专项层,不是独立标准。西班牙语本地化版本 docs/es/rules/python/patterns.md 是同一份文件的文档树镜像。

同一目录 rules/python/ 下还有 4 个配套规则文件,把 Python 开发的四个维度补齐:

  • coding-style.md:风格与 PEP 8 细节
  • testing.md:pytest 测试纪律
  • hooks.md:ECC hook 机制的集成约定
  • security.md:安全约束

patterns 文件本身只干一件事:用最少的篇幅钉住 4 种结构性选择(接口、DTO、资源、迭代),细节全部外推给技能层。这正是它能直接注入 Agent 上下文而不撑爆 token 预算的原因。

数据流视角:从接口契约到对外信封

跟着一个数据对象走完生命周期,规则里的三组模式恰好排成一条流水线:Protocol 钉住接口长什么样,Dataclass 钉住数据长什么样,API 信封钉住对外能看见什么。三层各管一段,字段变化只会在本层引发涟漪。

Protocol 与 ABC 怎么选:结构子类型对比名义继承

规则给出的第一组模式是用 Protocol 定义数据访问接口:

from typing import Protocol class Repository(Protocol): def find_by_id(self, id: str) -> dict | None: ... def save(self, entity: dict) -> dict: ...

从这段代码可以看出:Repository不要求任何类显式继承它,只要对象身上存在find_by_id与save两个方法签名,mypy / pyright 这类静态检查器就判定它兼容该接口。这是typing.Protocol的结构子类型机制,与需要class X(Repository)显式继承的名义类型检查相对。方法名的组合对应父文档 rules/common/patterns.md 中 Repository 模式定义的标准操作集findAll / findById / create / update / delete,业务逻辑只依赖这份抽象,数据库、文件、远端 API 实现可以随时替换。

有意思的是,ECC 自己在抽象 LLM 提供商时选的是经典 ABC,见 src/llm/core/interface.py(L11-L31):LLMProvider用@abstractmethod声明generate、list_models、validate_config三个必须实现的方法,同时给出supports_tools、supports_vision两个共享默认实现,以及一个抛NotImplementedError的占位方法get_default_model。

维度ABCProtocol
满足条件显式继承基类方法签名齐全即可,无需继承
默认行为可给所有实现共享公共实现纯形状契约,没有"实现"概念
适用场景自家实现类,必须逐个补齐关键方法外部传入的数据结构,或只做形状核对

取舍的关键在"约束对象是谁":要管住自家必须补齐、还要共享默认行为的实现类,用 ABC;要约束外部对象且只关心方法形状,用 Protocol,避免继承带来的耦合。

Dataclass 加固三招:default_factory、frozen、post_init

规则的第二组模式是一个请求 DTO:

from dataclasses import dataclass, field @dataclass class CreateUserRequest: name: str email: str age: int | None = None @dataclass(frozen=True) class LLMInput: messages: list[Message] metadata: dict[str, Any] = field(default_factory=dict)

从这段代码可以看出:name与email是必填字段,age采用"可空加缺省"的组合写法,3.9+ 语法下无需typing.Optional;@dataclass自动补出__init__、__eq__等样板,把 DTO 写得尽可能薄。在此基础上,skills/python-patterns/SKILL.md 给出三招加固:

  1. 可变默认值走field(default_factory=...)——metadata: dict = field(default_factory=dict)就是tags: list = []的反面写法,避免多个实例共享同一个容器对象;
  2. 不可变 DTO 加frozen=True——冻结后字段被改写会立刻报错,层间传递的数据无法被中途篡改;
  3. 用__post_init__做构造期校验——技能库的示例在其中校验邮箱格式与年龄区间,把非法数据拦在对象诞生之前。

仓库的 LLM 抽象层是这套写法最直接的落地处。src/llm/core/types.py 里Message(L26)、ToolDefinition(L48)、LLMInput(L96)、LLMOutput(L121)这批跨层数据对象全部是@dataclass(frozen=True),有三个细节值得注意:

  • name: str | None = None的可空写法贯穿全文件,与规则示例的语法完全同构;
  • 序列化方法随 DTO 内聚:每个对象自带to_dict(),ToolDefinition更进一步提供to_openai_tool()与to_anthropic_tool()两个适配方法(L63-L79)——同一份数据转成不同后端协议的方式被封在对象内部,新增后端时调用方一行不用改;
  • 扩展字段统一收尾:LLMInput.metadata用field(default_factory=dict)(L104)承接,扩展属性不污染强类型主字段。

对外信封:DTO 与 API 响应格式的分工

数据流到 HTTP 边界时,父规则 rules/common/patterns.md 的 API Response Format 约定接管:所有响应使用统一信封——成功指示位、可空的数据负载、可空的错误信息、分页元数据total/page/limit。分工很明确:DTO 负责内部传输的形状,信封负责对外暴露的形状。分层之后,接口漂移与内部字段泄漏被挡在两层边界之外,每一层只需维护一份契约。

资源与内存的两条硬纪律

规则文件用短短两行还钉死了两条硬性纪律:用上下文管理器(with语句)管理资源,用生成器做懒求值与内存高效的迭代。每一条背后都是 Python 机制的一块缺口。

with 语句管理资源:上下文管理器的两种写法

为什么必须是with:Python 没有 C++、Rust 那样的 RAII 析构时机保证,文件句柄、数据库连接、锁都必须显式释放。手写try/finally正确但冗长、还容易漏掉某条分支;with把"获取 → 使用 → 必然释放"固化成语法结构,让释放成为默认行为而不是依赖自觉。

函数式写法借助contextlib.contextmanager,适合给一段逻辑包一层前后处理的轻量场景:

from contextlib import contextmanager @contextmanager def timer(name: str): start = time.perf_counter() yield print(f"{name} took {time.perf_counter() - start:.4f}s") with timer("data processing"): process_large_dataset()

从这个示例可以看出:yield是分界线——之前是前置处理,之后是收尾处理,with块内的代码恰好在生成器挂起于yield时执行,几行函数就换来了完整的上下文管理器语义,不用手工维护状态。

类式写法通过实现__enter__/__exit__拿到完全控制权,特别适合"依据是否出异常决定提交还是回滚"的事务场景:

class DatabaseTransaction: def __enter__(self): self.connection.begin_transaction() return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.connection.commit() else: self.connection.rollback() return False # 不吞异常,继续向上抛

(取自技能库的DatabaseTransaction示例,构造函数持有 connection 对象)

从这段节选可以看出:__exit__会拿到异常类型参数,用exc_type is None区分提交与回滚两条路径;末尾的return False才是关键——它表示异常不被吞掉、继续向上传播,只有刻意要"消化"某个异常时才应返回True。

生成器管内存:从百万元素到逐个产出

为什么用生成器:列表推导式会把全部元素物化进内存,数据量上去之后内存随元素数线性增长;生成器函数每次只产出下一个值,任意时刻内存里只有一份数据。对百万级求和,sum(x * x for x in range(1_000_000))只保留当前值,而sum([...])会先建出完整列表。

技能库的典型示范是逐行读大文件:

def read_large_file(path: str) -> Iterator[str]: with open(path) as f: for line in f: yield line.strip()

从这个函数可以看出两条硬纪律的组合形态:with保证文件在迭代结束后必然关闭,yield保证任意时刻内存中只有一行——资源纪律与内存纪律在同一段四行函数里同时生效。适用边界同样清晰:生成器适合"顺序处理、不回看"的数据流;如果需要随机访问、索引或查询长度,就该先物化成列表,不要为了省内存硬用生成器。技能库还附了一条可读性提醒:像[x * 2 for x in items if x > 0 if x % 2 == 0]这样叠了多层条件的推导式会伤害理解,应回退成普通for循环。

短约束 × 长配方:python-patterns 技能库全景

规则文件结尾的 Reference 小节把读者指向python-patterns技能,实体是 skills/python-patterns/SKILL.md(751 行),其 frontmatter 写明了四个激活时机:编写新 Python 代码、评审既有代码、重构、设计 Python 包/模块。二者的关系可以概括为:规则文件是跟着文件路径自动装载的短约束,技能库是按需打开的长配方。

主题适用时机与正文的关联
类型注解(TypeAlias、TypeVar 泛型、Protocol)新函数/类的签名与泛型设计第二节 Protocol 的完整展开
错误处理(具体异常、raise ... from e)DTO 校验失败的抛出策略对应__post_init__的异常路径
装饰器三种写法(均用functools.wraps)计时、计数、重试等横切关注点与函数式@contextmanager同源
并发(线程池、进程池、asyncio.gather)Agent 工具调用等多路 I/Owith语句多资源场景的延伸
包组织(src-layout、__all__、import 分组)Python 包/模块设计src/llm 就是分层包结构示例
内存与性能(__slots__、join替代循环拼接)大数据读取、高频字符串操作生成器纪律的延展
工具链(black/isort/ruff/mypy/pytest/bandit/pip-audit)本地预提交与 CI 检查与根目录 pyproject.toml 对齐

技能库末节给出一份反模式清单,与正文的正面模式正好组成"该做 / 不该做"的一对:

  • 可变默认参数:def f(items=[])在多次调用间共享同一个列表,应改为items=None再建新列表
  • 用type(obj) == list判型:应改用isinstance,否则子类对象匹配不到
  • 用== None比较:应改用is None
  • from module import *:应显式导入具体符号
  • 裸except吞异常:应捕获具体异常类型并记录日志

落地清单:让 Python 规则真正生效的六项检查

  • 确认目标.py/.pyi文件被 frontmatter 的 paths 命中,同目录风格、测试、安全规则随之加载(验证:手动比对 frontmatter 的 paths 与目标文件后缀)
  • 数据访问约束用 Protocol、库内 Provider 抽象用 ABC,业务代码只依赖抽象接口(验证:mypy .运行后不新增不兼容报错)
  • 跨层数据对象全部为frozen=Truedataclass 且自带to_dict()序列化方法(验证:评审字段类型,pytest覆盖序列化分支)
  • 所有带副作用的资源(文件、连接、锁)走with,__exit__返回值与异常策略一致(验证:ruff check .加人工核查资源异常路径)
  • 大数据迭代走生成器,热路径不构造百万级临时列表(验证:评审时检索sum([、list(...)等一次性物化写法)
  • pyproject.toml的[tool.ruff]、[tool.mypy]、[tool.pytest.ini_options]段落与技能库模板对齐(根目录 pyproject.toml 已有真实配置可参照)(验证:pytest --cov与ruff check .正常跑通)

回看这份不到 40 行文件的设计初衷:它不是一份孤立文档,而是一张三层知识网络的最上层索引——第一层 rules/common/patterns.md 回答跨语言架构怎么搭(Repository、API 信封),第二层 rules/python/patterns.md 回答 Python 里怎么落地(Protocol、Dataclass DTO、with、生成器),第三层 skills/python-patterns/SKILL.md 回答更复杂的问题用什么配方(装饰器、并发、包组织、工具链)。src/llm/core 中frozen dataclass层层传递、ABC钉住 Provider 边界、序列化方法随 DTO 内聚的写法,说明这三层约束是被仓库真实代码验证过的工程实践。当你把这套规则注入自己项目的 Agent,并让代码严格遵守 Protocol 解耦、Dataclass 定型、with管资源、生成器控内存的纪律,得到的将不只是"能跑"的代码,而是可被静态检查、可被测试、可长期演进的结构化工程资产。

【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进