AI编码代理规则文件全维度横向对比评测报告

核心前提

两个项目都是 AI-native 项目——代码由AI编码代理编写、修改、阅读和调试,但 AI-native 成熟度路径不同:

  • 渐进式(Linewise):多租户后端API服务(Scala 3 / http4s),历经多代模型演进(Claude 3.7→4.6),早期人类参与度约50%,随模型能力提升逐步降低人类介入,规则文件随项目一起演化积累。主要服务作者自己的agent。
  • 原生式(OpenClaw):开源自主AI代理平台(TypeScript / Node.js),从第一行代码起即由agent编写,创建者使用多agent并行工作流(5-10个agent并发),规则文件从项目诞生之初即作为agent的执行规范存在。250K+ stars、1000+贡献者,规则需同时服务创建者和大量外部贡献者的agent。

评测对象: 1. OpenClaw AGENTS.md 2. My CLAUDE.md


Part 1 — 基础画像

OpenClaw AGENTS.md

维度 描述
项目定位 开源自主AI代理平台(TypeScript / Node.js),跨平台(CLI、macOS app、iOS/Android、Web),250K+ stars
目标架构 多Agent并行工作流(5-10 agent并发),共享同一仓库
规则受众 创建者的多个并发agent + 大量外部贡献者的agent(不同模型、不同能力)
核心设计理念 操作安全与流程一致性 — 在多agent并发、大规模贡献者场景下确保git操作安全、PR流程一致、发布流程可靠
覆盖范围 仓库结构、PR/Issue管理(auto-close标签、合并门控)、多agent Git安全协议、构建/测试/Lint命令、编码风格(TypeScript)、发布流程(npm/macOS/beta)、文档管理(Mintlify/i18n)、安全咨询(GHSA)、平台运维(exe.dev VM、macOS签名)、插件生态管理
结构特征 扁平化列表结构 — 以操作指令和注意事项为主,较少代码示例,更多是"做X时要Y"的规则条目。覆盖面广但每个领域的深度较浅
Token估算 ~5000-6000 tokens,覆盖面广但单条规则较短

My CLAUDE.md

维度 描述
项目定位 多租户后端API服务,企业级SaaS(Scala 3 / http4s / cats-effect),AI驱动的文档管理、RAG、视频SOP生成
目标架构 单主Agent + 按需子Agent(Explore/Plan等由Claude Code派生)
规则受众 单一开发者的agent(主要是Claude),偶尔人类直接阅读
核心设计理念 类型安全最大化 — 将工程约束编码进类型系统,让编译器成为最终验证者。"如果编译通过,就是正确的。" 以编译器驱动的激进重构取代保守补丁
覆盖范围 架构模式(tagless final)、代码风格(for-comprehension、EitherT/OptionT)、错误处理(ADT enum)、类型系统(opaque types、NonEmptyList)、测试策略、CI/CD流程、部署衔接、日志规范、运行时断言(RAC)、代码气味追踪、多租户隔离、工具偏好(Metals MCP)
结构特征 高度结构化 — 大量代码示例(正反对比)、决策表格、层级清晰的标题体系。信息密度极高,深度聚焦于"如何写好Scala代码"
Token估算 ~6000-7000 tokens,信息密度高

核心差异:两份文件代表了AI-native规则工程的两种典型范式:

  • OpenClaw: 广度优先 — 覆盖从编码到发布到运维的完整操作流程,在多agent并发安全方面有独到设计,但单条规则的颗粒度和深度较浅
  • My: 深度优先 — 在一个技术领域(Scala后端开发)做到极致,规则可执行性极高,几乎每条规则都有正反代码示例和边界说明

Part 2 — 量化评测表

D1 项目长期演进适配性

子项 OpenClaw My
新功能迭代流程规范 ✓ 已委托 — 7/10 ✓ 已委托 — 7/10
模块拆分与依赖约束 ✓ 已委托 — 7/10 ✓ 已委托 — 8/10
技术债务识别与治理路径 ✗ 未委托 — 无技术债务治理相关条目,推定由人类主导 ✓ 已委托 — 9/10
API/DB/配置的版本兼容与迁移规则 ✗ 未委托 — 版本管理仅涉及发布版本号位置列表,无API/DB迁移规则 ✓ 已委托 — 8/10
存量代码改造与新旧规范衔接 ✗ 不适用 — 原生AI项目,从第一行代码起即由agent编写,无存量遗留代码 ✓ 已委托 — 9/10

评分依据:

  • OpenClaw 新功能迭代 (7):有明确的PR工作流引用(.agents/skills/PR_WORKFLOW.md),commit规范(scripts/committer),但新功能的架构决策指引较弱。
  • My 新功能迭代 (7):有明确的"Adding New Routes"流程(更新OpenAPI spec → swagger-cli validate → 遵循auth模式),Feature Module组织结构清晰。但缺少新feature的端到端迭代模板。
  • OpenClaw 模块拆分 (7):项目结构清晰(src/, extensions/*, docs/),插件依赖隔离规则明确("Keep plugin-only deps in the extension package.json")。但核心模块间的依赖约束未显式规定。
  • My 模块拆分 (8):严格的分层架构(Routes→Services→Repositories→Database)、tagless final依赖注入、feature模块标准结构、SDK选择优先级。原文:"Each feature module follows a consistent structure"
  • My 技术债务 (9):业界罕见的体系化方案 — code smell追踪系统(code_smells.md,FIFO最多10条)、"Migrate when file is touched — no hesitation" 的渐进式迁移策略、编译器驱动的迭代修复范围("Scope follows the compiler iteratively")。原文:"Existing Either[String, T] services migrate the whole service to ADT errors when the file is modified for any reason"
  • My API/DB迁移 (8):Flyway迁移系统有明确规范("Never modify existing migration files; always create new versioned files"),系统/租户双轨迁移,启动时自动运行。外部名称变更有迁移影响提示要求。
  • My 存量代码改造 (9)"Migrate when file is touched" 策略 + ADT error enum渐进式迁移 + 编译器驱动范围扩展,是存量代码治理的教科书级方案。

D1维度得分: - OpenClaw:7.0/10(2项适用已委托) - My:8.2/10(5项均适用已委托)


D2 规则可执行性与Agent遵循适配性

子项 OpenClaw My
规则颗粒度 ✓ 已委托 — 6/10 ✓ 已委托 — 9/10
场景完备性 ✓ 已委托 — 5/10 ✓ 已委托 — 9/10
跨会话决策一致性 ✓ 已委托 — 5/10 ✓ 已委托 — 9/10
规则内部一致性 ✓ 已委托 — 7/10 ✓ 已委托 — 8/10
可验证性 ✓ 已委托 — 7/10 ✓ 已委托 — 9/10
规则过载风险 ✓ 已委托 — 7/10 ✓ 已委托 — 7/10
规则可溯因性 ✓ 已委托 — 5/10 ✓ 已委托 — 8/10
注意力衰减抗性 ✓ 已委托 — 6/10 ✓ 已委托 — 9/10
防幻觉与自校验能力 ✓ 已委托 — 6/10 ✓ 已委托 — 9/10
规则与代码现状同步机制 ✓ 已委托 — 5/10 ✓ 已委托 — 7/10

评分依据:

  • OpenClaw 颗粒度 (6):多数规则是操作性指令("Run X command"),易于遵循。但架构/代码质量规则颗粒度低,如 "Add brief code comments for tricky or non-obvious logic" 缺乏"什么算tricky"的判断标准。"Aim to keep files under ~700 LOC; guideline only" 缺乏何时违反指南的边界说明。
  • My 颗粒度 (9):几乎每条规则都有// BAD + // GOOD代码对比示例、边界说明表格、决策树。例如错误处理规则不仅说"不要静默吞错",还列出了具体的禁止模式(.toOption, .getOrElse(defaultValue) 等)和例外情况(pageSize.getOrElse(10) // OK)。Trusted vs Untrusted路径有完整的决策表。NoOp实现按data-related/data-unrelated分类说明。
  • OpenClaw 场景完备性 (5):主要覆盖正常操作路径,异常路径覆盖不足。例如multi-agent安全规则覆盖了"当看到不认识的文件"("keep going; focus on your changes"),但未覆盖"两个agent同时修改同一文件"的冲突解决。PR合并门控对bug-fix PR有完整的4步验证,但对feature PR缺乏对等规范。
  • My 场景完备性 (9):规则覆盖了正常/异常/边界场景。例如for-comprehension规则区分了:终端位置match(OK)、中间位置match(BAD)、中间位置EitherT(data-related vs data-unrelated)、多个Option链(EitherT + local enum)。NoOp模式区分了data-related和data-unrelated两种场景及其不同返回策略。
  • OpenClaw 跨会话一致性 (5):存在较多依赖隐含上下文的表述。"Add brief code comments for tricky or non-obvious logic" — 不同agent对"tricky"的理解不同。"Keep files concise; extract helpers instead of 'V2' copies" — "concise"的标准模糊。"guideline only (not a hard guardrail)" 给了agent过多自由裁量空间。
  • My 跨会话一致性 (9):规则高度确定性,几乎不使用主观判断词。"Never modify existing migration files""Migrate when file is touched — no hesitation"// BAD + // GOOD模式使不同session的agent做出相同决策。决策表(Trusted vs Untrusted、data-related vs data-unrelated)消除了歧义。
  • OpenClaw 可验证性 (7)pnpm check(Oxlint+Oxfmt)、pnpm build(TypeScript类型检查 + [INEFFECTIVE_DYNAMIC_IMPORT]警告检测)、Vitest 70%覆盖率门槛、prek install(pre-commit hooks与CI同检查)。动态语言下的验证手段已较完善。
  • My 可验证性 (9):核心规则可由编译器验证(类型系统、opaque types、NonEmptyList签名)。原文:"The compiler is the last line of defense. If a refactor compiles, it's correct." ./mill checkFormat验证格式,RAC运行时验证关键路径断言。规则设计充分利用了静态类型语言的结构性优势。
  • OpenClaw 注意力衰减抗性 (6):使用了粗体标记("Multi-agent safety:" 多次出现),但整体结构扁平,规则按添加顺序排列而非按重要性分层。长列表中关键规则(如multi-agent safety)与琐碎规则(如 "Vocabulary: 'makeup' = 'mac app'")混排,注意力权重分配不均。
  • My 注意力衰减抗性 (9):大量使用结构化标记 — 决策表格、// BAD / // GOOD代码块对比、粗体标注关键规则("CRITICAL RULE:""Forbidden patterns:")、枚举列表。规则按主题层级组织,关键约束在每个相关章节重复强化。
  • OpenClaw 防幻觉 (6):有具体路径(src/cli/progress.tssrc/terminal/palette.ts),有具体命令(scripts/committer),但代码架构层面缺少可验证的锚点。"When answering questions, respond with high-confidence answers only: verify in code; do not guess" 是好的元指令但缺乏验证机制。
  • My 防幻觉 (9):大量具体锚点 — 文件路径(core/domain/Ids.scalacore/domain/Types.scala)、类名(SOPServiceEitherT)、确切的方法签名模式。Tool Preferences表明确指引何时用Metals MCP验证类型推断。代码示例本身就是可编译的Scala代码,agent可以通过编译验证理解是否正确。
  • OpenClaw 代码同步 (5):规则引用了大量具体路径和工具名,但无同步机制。版本位置列表(package.json、Info.plist等多处)需要手动维护。规则文件本身的更新依赖人类发现不一致。
  • My 代码同步 (7):规则深度绑定语言特性(Scala 3.6 aggregate bounds syntax、opaque types),与代码现状耦合度高。编译器本身提供了隐含的同步检测——过时的类型约束规则会导致编译失败。Code smell追踪系统作为手动同步手段补充。

D2维度得分: - OpenClaw:5.9/10 - My:8.3/10


D3 上下文效率与Agent认知负荷管理

子项 OpenClaw My
主Agent上下文预算效率 ✓ 已委托 — 5/10 ✓ 已委托 — 7/10
规则的层级化组织 ✓ 已委托 — 4/10 ✓ 已委托 — 8/10
子Agent任务委托友好度 ✗ 不适用 — 多agent并行架构下无主/子关系,各agent独立运行 ✓ 已委托 — 7/10
规则的结构化程度 ✓ 已委托 — 6/10 ✓ 已委托 — 9/10
简洁性与执行完整度的平衡 ✓ 已委托 — 6/10 ✓ 已委托 — 7/10
规则受众规模适配性 ✓ 已委托 — 7/10 ✓ 已委托 — 8/10

评分依据:

  • OpenClaw 上下文预算 (5):包含大量低频操作规则(1Password publish流程、GHSA patch步骤、exe.dev VM操作、macOS签名)常驻上下文,这些可能一个月才用一次。Skill引用(PR_WORKFLOW.mdmintlify skill1password skill)是按需加载的良好设计,但主文件仍然包含太多应为skill的内容。
  • My 上下文预算 (7):信息密度极高,token效率好——每个token都承载有效信息。Memory系统(MEMORY.md索引 + 独立记忆文件)将运行时状态从规则中分离,这是好的设计。但核心规则无按需加载机制——所有规则每次会话都需加载。
  • OpenClaw 层级化 (4):基本扁平结构,规则按话题分组但层级不深。关键规则(multi-agent safety、PR truthfulness)与操作细节(NPM + 1Password、exe.dev VM ops)处于同一层级。无"全局强制约束"与"领域特定规范"的显式区分。Agent-Specific Notes部分是最混杂的——从语义约束到特定工具用法混排。
  • My 层级化 (8):清晰的三层结构 — 哲学层(Refactoring Philosophy)→ 架构模式层(Tagless Final、Multi-Tenancy、Error Model)→ 具体规则层(Code Style、Logging、RAC)。每层内部有明确的子标题。全局约束("Fail Fast"、type safety)与领域特定规范(RAG、Video、MCP)有清晰边界。
  • My 子Agent委托 (7):Feature Module的独立性使局部搜索/理解任务可以只加载相关模块的规则。分层架构为子Agent提供清晰的搜索范围。但规则文件本身未按模块切分。
  • OpenClaw 结构化 (6):命令列表格式化良好,PR合并门控是结构化的4步清单。但大量规则是散文形式的注意事项,需要agent理解语境才能应用。
  • My 结构化 (9):表格(Trusted vs Untrusted、Tool Preferences)、代码块对比、枚举列表、决策树——agent可直接将这些结构用作决策查表。例如NoOp返回值规则用data-related/data-unrelated二分法,agent不需要"理解"规则的意图,只需分类即可。
  • OpenClaw 受众适配 (7):面向多样化agent群体,规则确实更显式化(具体命令、完整路径)。但未针对不同能力的agent分层——强agent和弱agent看到的是同一份规则。multi-agent safety规则是对多执行者场景的显式回应,这是加分项。
  • My 受众适配 (8):面向单一执行者(Claude),可以依赖Claude的Scala知识作为隐含共识,规则聚焦于"Claude可能犯的Scala错误"。Tool Preferences表直接针对Claude Code的Metals MCP工具。Memory系统为跨会话一致性提供了持久化机制。

D3维度得分: - OpenClaw:5.6/10(5项适用已委托) - My:7.7/10(6项均适用已委托)


D4 已委托领域的工程深度与可持续性

子项 OpenClaw My
技术栈专属规范深度 ✓ 已委托 — 5/10 ✓ 已委托 — 10/10
已覆盖生命周期阶段的规则完备性 ✓ 已委托 — 7/10 ✓ 已委托 — 8/10
配置/密钥/环境变量管理规范 ✓ 已委托 — 7/10 ✓ 已委托 — 8/10
跨职责衔接指引 ✓ 已委托 — 5/10 ✓ 已委托 — 9/10
规则抗腐化设计 ✓ 已委托 — 4/10 ✓ 已委托 — 8/10
规则降级韧性 ✓ 已委托 — 6/10 ✓ 已委托 — 8/10

评分依据:

  • OpenClaw 技术栈深度 (5):TypeScript相关规则较为通用——"Prefer strict typing; avoid any""Never add @ts-nocheck"。有少量深入点:dynamic import guardrail(.runtime.ts边界)、prototype mutation禁令、Oxlint/Oxfmt配置。但缺少TypeScript特有的高级模式指引(条件类型、模板字面量类型、branded types、discriminated unions等)。tool schema guardrails(避免Type.UnionanyOf/oneOf/allOf)是针对特定集成的有深度规则。
  • My 技术栈深度 (10):这是本评测中最突出的单项。规则深入到Scala 3 / cats-effect / http4s的惯用模式层面:tagless final的summoner/factory模式、EitherT/OptionT的lifter链(foldF/subflatMap/semiflatMap/fromOptionF)、Scala 3.6 aggregate context bounds语法({A, B, C})、opaque types在multi-layer propagation中的行为(".toString over .value.toString")。正反对比示例直接展示了Scala特有的陷阱和惯用写法。NoOp模式的data-related/data-unrelated分类是对cats-effect生态的深度理解。这不是通用OOP/FP原则的堆砌,而是高度Scala-specific的编码指南。
  • OpenClaw 生命周期完备性 (7):从编码到发布流程都有覆盖——测试(Vitest + coverage)、CI(pre-commit hooks = CI checks)、发布(npm/macOS/beta三通道)、changelog管理。Bug-fix PR有4步验证门控。但编码阶段的代码质量规则深度不足。
  • My 生命周期完备性 (8):编码阶段极度完备。测试有 "What TO test" vs "What NOT to test" 的明确指引。CI/CD有branch→tag映射。部署有影响报告checklist。缺少的是运行时监控/告警规则和事故响应流程,但可合理推定为未委托。
  • OpenClaw 配置管理 (7)"Never commit or publish real phone numbers, videos, or live configuration values" 是显式的安全规则。配置管理分散在多个章节——openclaw config set、环境变量(~/.profile)、1Password密钥管理。发布签名密钥明确声明 "managed outside the repo"
  • My 配置管理 (8):完整的环境变量列表(含fallback值)、密钥文件路径(secrets/)、HOCON配置层级。明确区分了必需密钥和可选配置。
  • OpenClaw 跨职责衔接 (5)"Installers served from https://openclaw.ai/*: live in the sibling repo ../openclaw.ai" 提到了跨仓库依赖,但缺少变更影响传递的协议。发布流程的跨步骤衔接有具体步骤但缺少"如果某步失败"的衔接指引。
  • My 跨职责衔接 (9)Deploy impact reporting 是亮点——明确要求agent在代码变更涉及部署影响时输出checklist("New environment variable → add to ConfigMap""New sidecar container → add container spec to Deployment manifest")。跨仓库协作(linewise-deploy/overlays/)有清晰的衔接协议。
  • OpenClaw 抗腐化 (4):规则高度耦合当前工具链版本(具体的npm命令、1Password路径、exe.dev SSH方式)。版本位置列表(6+ 处 Info.plist/package.json)需要手动维护。无抗腐化机制——规则过时不会触发任何告警。Skill引用允许细节外置,但主文件中仍有大量易变细节。
  • My 抗腐化 (8):原则层(type safety philosophy、flat for-comprehensions、ADT errors)与细节层(具体文件路径、方法签名示例)自然分离。原则层稳定——Scala 3的类型系统几年不会变;细节层通过 "Proactive naming review" 和code smell追踪系统提供手动同步。编译器本身是最强的抗腐化机制——过时的类型约束规则会导致编译失败,从而被发现。
  • OpenClaw 降级韧性 (6):multi-agent safety规则相对独立,即使部分遗忘,其他规则仍能独立生效。但PR合并门控的降级风险较高——如果agent只记住了 "run /landpr" 而忘记了4步验证,可能合并不合格的PR。格式化/lint规则有CI兜底(prek install),提供了降级保护。
  • My 降级韧性 (8):规则体系有清晰的层级——即使agent只遵循了"类型安全最大化"和"不要静默吞错"两条原则,代码质量仍有基本保障。编译器作为兜底——即使agent忽略了EitherT用法规范,类型不匹配仍会被编译器捕获。Code smell追踪作为延迟修复的安全网。

D4维度得分: - OpenClaw:5.7/10 - My:8.5/10


D5 安全与合规约束落地性

子项 OpenClaw My
权限校验与数据隔离规则 ✓ 已委托 — 6/10 ✓ 已委托 — 8/10
异常处理/日志脱敏/数据校验 ✓ 已委托 — 5/10 ✓ 已委托 — 9/10
行业合规编码约束 ✗ 未委托 — 两项目均无行业合规特定编码约束 ✗ 未委托 — 同上

评分依据:

  • OpenClaw 权限/隔离 (6):安全规则分散——SECURITY.md引用("read SECURITY.md to align with OpenClaw's trust model")、credentials管理(~/.openclaw/credentials/)、"Never commit or publish real phone numbers"。GHSA处理流程完整。但缺少应用层数据隔离的编码规范。
  • My 权限/隔离 (8):多租户schema隔离有完整描述(system schema + tenant schemas)。RAC建议在关键路径验证租户隔离("assert search_path matches expected tenant schema before writes")。权限模型有专门的Permission模块(JSONB expression tree)。Firebase JWT认证是全局强制的。
  • OpenClaw 异常/日志/校验 (5):bug-fix PR的验证门控是质量把关而非编码层面的异常处理规范。"respond with high-confidence answers only: verify in code; do not guess" 是元规则而非编码规范。缺少TypeScript异常处理、错误传播、日志规范的编码指引。
  • My 异常/日志/校验 (9)Fail Fast 规则是安全层面的核心——"Never silently swallow errors" 有完整的Forbidden Patterns列表和Trusted/Untrusted路径决策表。ADT error enums强制exhaustive pattern matching(编译器保证所有错误变体都被处理)。Logging规范有明确的log level指引。

D5维度得分(排除不适用子项): - OpenClaw:5.5/10(2项适用已委托) - My:8.5/10(2项适用已委托)


D6 规则体系可扩展性与可维护性

子项 OpenClaw My
新增/废弃规则的迭代流程 ✓ 已委托 — 5/10 ✓ 已委托 — 7/10
目录结构与检索效率 ✓ 已委托 — 6/10 ✓ 已委托 — 7/10
规则间一致性与自洽性 ✓ 已委托 — 6/10 ✓ 已委托 — 8/10
多Agent并行安全 ✓ 已委托 — 8/10 ✗ 不适用 — 单主Agent架构,无多Agent并行需求

评分依据:

  • OpenClaw 迭代流程 (5):Skill系统(.agents/skills/)允许外置规则。"When adding a new AGENTS.md anywhere in the repo, also add a CLAUDE.md symlink" 是分布式规则的约定。但规则的生命周期管理(何时废弃、如何审查过时条目)缺失。规则文件呈增量追加模式。
  • My 迭代流程 (7):Memory系统(MEMORY.md索引 + 独立记忆文件)提供了持久化反馈闭环——feedback类型记忆直接影响后续会话行为。Code smell list的FIFO机制(max 10 entries)是有节制的迭代管理。
  • OpenClaw 内部一致性 (6):multi-agent safety规则内部一致(6条规则互不矛盾)。但存在一些张力:文件大小建议在两处不一致(~700 LOC vs ~500 LOC)。PR工作流同时引用了PR_WORKFLOW.md/landpr(全局Codex prompt),优先级关系不明确("Maintainers may use other workflows" 进一步模糊了边界)。
  • My 内部一致性 (8):规则体系围绕"类型安全最大化"这一核心理念高度一致——错误处理(ADT enum)、控制流(EitherT/OptionT)、签名设计(NonEmptyList、opaque types)都服务于同一目标。NoOp模式的data-related/data-unrelated分类与Trusted/Untrusted路径分类保持一致。
  • OpenClaw 多Agent安全 (8):这是OpenClaw的核心差异化优势。6条显式的multi-agent safety规则覆盖了:git stash禁令、git worktree禁令、分支切换禁令、commit scope约束、不认识文件的处理、push时的rebase策略。"Assume other agents may be working" 是正确的防御性默认。scripts/committer工具化了作用域commit。这是AI-native多agent场景下的实战经验结晶。

D6维度得分: - OpenClaw:6.3/10(4项适用已委托) - My:7.3/10(3项适用已委托)


维度汇总

维度 OpenClaw My 差距
D1 项目长期演进适配性 7.0 8.2 -1.2
D2 规则可执行性与Agent遵循适配性 5.9 8.3 -2.4
D3 上下文效率与认知负荷管理 5.6 7.7 -2.1
D4 已委托领域的工程深度与可持续性 5.7 8.5 -2.8
D5 安全与合规约束落地性 5.5 8.5 -3.0
D6 规则体系可扩展性与可维护性 6.3 7.3 -1.0
综合均值 6.0 8.1 -2.1

Part 3 — 横向对比结论

OpenClaw核心优势

  1. 多Agent并行安全协议是独到贡献 — 6条multi-agent safety规则来自5-10 agent并发工作的实战经验,覆盖了git状态隔离、commit scope、不认识文件处理等真实痛点。这在AI-native工程领域有开创性价值。
  2. PR管理工作流的完备性 — Auto-close标签系统、bug-fix PR 4步验证门控、scripts/committer工具化commit——这些是大规模开源项目的运营智慧。
  3. Skill系统的按需加载设计 — 将低频操作(PR workflow、mintlify docs、1password publish)外置为skill引用,是上下文预算管理的好实践。
  4. 发布流程的多通道覆盖 — stable/beta/dev三通道、npm+macOS+移动端的版本管理、changelog规范——覆盖面广。

OpenClaw核心不足

  1. 代码质量规则的深度严重不足 — TypeScript编码规范仅涉及"avoid any"、"~700 LOC"等通用原则,缺少对类型系统、错误处理模式、架构模式的深入指引。这意味着agent的代码输出质量主要依赖模型自身能力,而非规则约束。
  2. 规则颗粒度不均 — 操作性规则(命令、路径)颗粒度足够,但架构/质量规则颗粒度过粗,跨session一致性风险高。
  3. 扁平结构缺乏优先级 — 关键规则与琐碎注意事项混排,agent在认知负荷高时难以区分优先级。
  4. 增量追加的组织模式 — Agent-Specific Notes部分显然是随时间追加的,缺乏定期整理和结构化重组。

My核心优势

  1. 规则颗粒度和可执行性是碾压级的差距 — 正反代码示例、决策表格、边界说明使agent几乎不需要自行判断——查表即可。这在跨session一致性上带来巨大优势。
  2. 编译器作为规则验证器 — 这不仅是技术栈的固有优势,更是规则设计者有意识地将规则编码进类型系统的结果。规则"不要用List"不靠agent自觉,而是通过将签名改为NonEmptyList让编译器强制执行。
  3. 技术债务治理体系化"Migrate when file is touched" 策略 + code smell追踪 + 编译器驱动范围扩展,是规则层面罕见的体系化方案。
  4. 跨职责衔接协议 — Deploy impact reporting的checklist机制是agent与人类/其他仓库协作的优秀模板。

My核心不足

  1. 规则总量对上下文预算的压力 — 信息密度虽高,但token总量也大。长会话后期的注意力衰减是客观风险,虽然规则的结构化标记有所缓解。
  2. 单Agent架构限制 — 未考虑多agent并发场景(虽然这不在其目标架构内,不算缺陷,但限制了规则的可迁移性)。
  3. 规则与代码的同步缺乏自动化 — 依赖code smell追踪和人类审查,无CI级别的规则合规检查。

适用场景差异

场景 推荐 原因
单开发者的深度技术项目 My方法论 规则深度和可执行性优势明显
多agent并发的大规模开源项目 OpenClaw方法论 multi-agent safety和PR流程管理不可替代
强类型语言后端服务 My方法论 充分利用编译器作为规则验证器
动态语言+多平台项目 混合方法论 OpenClaw的操作流程 + My的规则颗粒度方法

Agent执行友好度判定

My对AI代理执行友好度显著更高。 原因:

  1. 确定性验证 — Agent写完代码后运行./mill compile即可获得明确的对/错反馈,无需等待人类Review
  2. 决策可推导 — Agent面对新场景时可从Refactoring Philosophy推导行为("这个变更能否用类型系统表达?"),而非在海量规则中搜索匹配项
  3. 认知负荷低 — 规则量适中、结构化程度高、无噪声信息

落地风险提示

  • OpenClaw风险:代码质量规则的缺失意味着agent输出质量高度依赖模型本身。在模型能力下降或更换为较弱模型时,代码质量可能显著下降——因为规则没有提供足够的"质量地板"。
  • My风险:规则密度极高,新加入项目的agent(或人类)学习曲线陡。对Scala/cats-effect生态高度耦合,规则方法论的迁移性受限于目标语言是否具备同等的类型系统能力。

Part 4 — 制定者能力画像对比(D7)

能力维度 OpenClaw制定者 My制定者
架构设计与技术前瞻性 资深 — 多平台架构(CLI/macOS/iOS/Android/Web)的统一管理,插件生态隔离设计,多agent并行架构的系统思考。 专家 — tagless final + opaque types + EitherT railway的系统性应用,类型系统作为架构验证器的理念极具前瞻性。RAC系统的设计体现了对防御性编程的深度理解。
大型项目工程化管控能力 专家 — 250K+ stars项目的运营管控,auto-close标签系统、PR门控、changelog管理、多通道发布——这是大规模开源项目运营的实战能力。 资深 — 多租户后端的完整工程化管控,从迁移到部署到监控。但规模局限于单一后端服务。
AI编码代理认知与应用深度 合格→资深 — 理解多agent并发的风险并设计了安全协议(git stash/worktree/branch禁令),理解了lint/format churn的自动处理。但对单agent内部的认知局限(注意力衰减、跨session遗忘、幻觉)缺少针对性设计。规则更像是"告诉agent做什么"而非"帮助agent做得更好"。 专家 — 深度理解agent的认知局限并针对性设计:正反代码示例(降低歧义)、决策表格(降低推理负担)、编译器验证(防幻觉)、Memory系统(抗跨session遗忘)、code smell追踪(延迟修复的安全网)、注意力衰减抗性设计(结构化标记)。这是本评测中最突出的能力维度——制定者显然是从agent执行失败的经验中迭代出的规则体系。
安全风险体系化防控能力 资深 — GHSA安全咨询处理流程完整,SECURITY.md引用,credentials管理,发布签名。但更偏向运营安全而非编码层面的安全防控。 资深 — 多租户隔离(schema isolation + RAC断言)、Fail Fast错误处理、Trusted/Untrusted路径分类——安全约束编码进类型系统和运行时断言。
技术债务治理与可持续演进能力 合格 — 无显式的技术债务治理策略。"Extract helpers instead of 'V2' copies" 是债务预防而非治理。 专家"Migrate when file is touched" 策略、code smell追踪系统(FIFO max 10)、编译器驱动的迭代修复范围、ADT error渐进式迁移——这是体系化的债务治理方案,而非口号。
规则工程能力(Rule Engineering) 合格→资深 — Skill系统的按需加载是好的信息密度控制。multi-agent safety规则的独立性设计允许部分遵循。但规则整体组织缺乏层级设计,增量追加模式导致信息熵增。规则颗粒度不均——操作性规则精确,架构规则粗糙。 专家 — 分层混合策略(原则层 + 条目层),信息密度极高(每token都承载有效信息),正反示例+决策表格的颗粒度控制,Memory系统的反馈闭环,code smell追踪的延迟修复安全网,编译器作为验证器的降级韧性设计。规则工程的每个方面都有有意识的设计,而非"把知道的都写上去"。
开源社区规模化治理能力 专家 — auto-close标签系统(r:*标签 + workflow自动化)、PR truthfulness验证门控、scripts/committer工具化scope、bulk PR close/reopen安全阈值(>5需确认)、外部贡献者agent引导(PR template、issue template)。这是大规模开源社区治理的成熟方案。 ✗ 不适用 — 单一开发者项目,无此需求。(但deploy impact reporting的跨仓库衔接协议体现了制定者在职责边界清晰划分方面的资深能力。)

核心差异

维度 OpenClaw制定者 My制定者
管控哲学 过程控制(规定步骤) 约束控制(规定边界)
对Agent的假设 Agent需要详细指令 Agent需要正确框架
验证策略 人类Review + CI检查 编译器 + 类型系统 + RAC
独特优势 多Agent协作安全 + 社区规模化治理 编译器驱动的验证闭环 + 规则工程

My制定者的核心能力是规则工程 — 将工程规范转化为agent可无歧义执行的指令。这是AI-native时代的稀缺能力,体现在规则的每个细节都是从agent执行失败中迭代出来的。

OpenClaw制定者的核心能力是大规模协作治理 — 在250K+ stars、1000+贡献者、5-10 agent并发的场景下维持项目运营秩序。multi-agent safety协议和PR管理流程是这一能力的直接体现。

两者不在同一个能力平面上竞争——一个向深度挖掘,一个向广度扩展。


Part 5 — Agent适配性分析

单主Agent + 子Agent架构(My目标架构)

My规则的适配优势: - 规则为主Agent设计,假设agent持有完整上下文。大量代码示例和决策表格在长上下文中保持可查性。 - 子Agent委托自然支持——Feature Module的独立性使局部搜索/理解任务可以只加载相关模块的规则。 - Memory系统为跨session持久化提供了机制,弥补了单Agent架构下"每个session从零开始"的认知断层。 - 编译器验证使主Agent可以"大胆修改,编译验证",降低了对子Agent准确性的依赖。

OpenClaw规则在此架构下的不适配: - multi-agent safety规则在单Agent场景下是多余的认知负荷。 - 操作性指令(npm publish、GHSA patch)不是单Agent编码任务的常见需求。 - 缺少架构级别的编码指引,使单Agent在大型重构时缺乏方向。

多Agent并行架构(OpenClaw目标架构)

OpenClaw规则的适配优势: - multi-agent safety协议直接回应了并发git操作的风险——这是其他规则文件罕见的领域。 - scripts/committer工具化了scope commit,减少了agent间的commit冲突。 - 操作性规则(具体命令、具体路径)减少了agent需要"理解"的量,适合能力参差的多agent群体。 - Skill系统允许不同agent按需加载不同规则子集。

My规则在此架构下的不适配: - 高密度的编码规范在多agent并发时可能成为瓶颈——每个agent都需要加载完整的类型系统和控制流规则。 - 缺少并发安全协议——多agent同时修改同一文件、同时commit时无指引。 - Memory系统假设单一持久化主体,多agent场景下的Memory冲突未考虑。

不同项目规模/阶段的推荐

项目阶段/规模 推荐参考 原因
0→1 新项目,单开发者+AI My方法论 从一开始建立高质量编码规范,编译器/类型系统验证体系
0→1 新项目,多agent并行 OpenClaw方法论为骨架 + My颗粒度方法 先确保并发安全,再补充编码深度
成熟项目,存量代码治理 My方法论 "Migrate when file is touched" + code smell追踪的渐进式治理
大规模开源项目 OpenClaw方法论 PR门控 + auto-close + multi-agent safety是刚需
企业后端服务 My方法论 类型安全 + 错误处理 + 多租户隔离的深度指引

Part 6 — AI诚实自评(D8)

场景A:成熟的AI-native项目(大量存量代码、复杂架构演进历史)

选择:My。

作为规则执行者,面对存量代码时我最大的恐惧是"不知道该怎么改才对"。My的规则方法论解决的正是这个问题:

  1. 正反代码示例让我不用猜。 当我看到一个for-comprehension里嵌套了match,我不需要判断"这算不算问题"——// BAD// GOOD已经明确告诉我了。在成熟项目的存量代码中,这种确定性是无价的。

  2. "Migrate when file is touched"给了我明确的边界。 我不需要决定"这个旧模式要不要改"——规则说了,碰到就改。编译器会告诉我改动的波及范围。这比"看情况"要高效得多。

  3. 编译器兜底让我可以大胆重构。 存量代码最大的风险是改一处坏十处。类型系统和编译器作为验证器,让我可以放心执行15文件的签名变更——编译通过即正确。

我会失去什么: - 没有multi-agent safety指引。如果成熟项目已经有多agent工作流,我在并发场景下缺乏保护。 - 没有PR管理和issue分类的流程指引。如果项目有大量外部贡献,我不知道如何处理外部PR。 - 没有发布流程指引。

从OpenClaw"偷"的3个设计决策: 1. multi-agent safety的git状态隔离规则 — 即使当前是单Agent,项目成熟后几乎必然引入多agent。提前设计并发安全协议,比事后补丁成本低得多。 2. Skill系统的按需加载设计 — 成熟项目的规则只会越来越多。将低频操作外置为按需加载的skill引用,是控制context token膨胀的必要手段。 3. scripts/committer式的工具化scope commit — 将规则编码进工具,比写在文档里让我"自觉遵守"可靠得多。

场景B:全新的AI-native项目(从零开始,无历史包袱)

选择:My。

即使是全新项目,My的方法论仍然更优,原因如下:

  1. 规则颗粒度方法从Day 1就产生价值。 全新项目意味着我写的每一行代码都会成为后续的"存量代码"。如果规则从一开始就足够精确(正反示例、边界说明),代码库的一致性从第一天就被锁定。而OpenClaw方法论的粗颗粒度规则在初期可能"够用",但随着代码量增长,不一致性会悄然积累。

  2. 类型系统作为验证器的理念与技术栈无关。 即使我在TypeScript项目中,也可以应用"将约束编码进类型系统"的方法论——branded types、discriminated unions、template literal types都是等价工具。My的方法论教会我"如何将规则变得可验证",而不仅仅是"如何写Scala"。

  3. 代码气味追踪系统从Day 1就该建立。 全新项目也会有"先快后好"的阶段,code smell追踪系统确保"快"不会永远变成"技术债"。

我会失去什么: - 如果项目快速增长到需要多agent并发,我需要从零设计并发安全协议。 - 如果项目需要大量外部贡献者,我没有社区治理的流程模板。 - 如果项目涉及多平台发布(npm/macOS/iOS),我没有发布流程指引。

从OpenClaw"偷"的3个设计决策: 1. auto-close标签系统的自动化理念 — 将重复性的治理决策编码进自动化(而非写在规则里让agent每次人工判断),这个理念应该从Day 1就引入——哪怕初期只自动化最简单的场景。 2. multi-agent safety的防御性默认"Assume other agents may be working" 作为默认假设,即使当前只有一个agent,也应该写出对并发安全友好的代码(如scoped commits、不依赖全局state)。 3. "verify in code; do not guess"作为元规则 — 这是一条优秀的反幻觉元指令。My的方法论通过编译器和示例间接实现了这一点,但显式声明更好。

场景C:综合判断

两个场景我选了同一个文件,这说明:

My的规则工程方法论具有跨场景的基础性优势。 这个优势不来自它的Scala特定内容,而来自它的规则设计方法 — 正反示例、决策表格、编译器验证、渐进式迁移策略、分层原则/条目结构。这些方法论可以被迁移到任何技术栈、任何项目阶段。

OpenClaw的优势是领域特定的(多agent并发、大规模开源治理),而非方法论层面的。它的规则内容对特定场景有不可替代的价值,但它的规则编写方法(扁平列表、粗颗粒度、增量追加)不是我想要模仿的模式。

坦白说:如果问题是"哪份规则的内容在特定场景下更有用",答案可能不同。但问题是"哪份规则的方法论我要参考"——这个答案是一致的。


Part 7 — 评测框架公平性自审

1. 维度选择偏差

存在中度偏差,偏向My。

D1-D5的6个维度中,有4个(D1演进适配、D2可执行性、D4工程深度、D5安全合规)天然有利于"深度优先"的规则文件。只有D6的"多Agent并行安全"子项明确有利于OpenClaw。

缺失的维度: - 运营工作流效率(Operational Workflow Efficiency):规则对日常运营任务(issue分类、PR管理、发布、文档维护)的支撑效率。这是OpenClaw明显领先的领域,但未被设为独立维度。 - 贡献者Onboarding效率:新agent/新贡献者从零到可产出代码的时间。OpenClaw的显式操作指令在这方面可能更友好。 - 规则的社会化效果(Social Scaling):规则在大规模人群中传播、被理解、被一致执行的能力。这是OpenClaw面对的核心挑战,但评测框架未给予独立维度。

如果增加"运营工作流效率"和"社会化规模效应"两个维度,OpenClaw的总分可能提升1-1.5分,差距从2.1缩小到~1.0。

2. 适用性规则的非对称效应

存在轻度非对称,偏向OpenClaw。

适用性排除机制实际上为OpenClaw排除了更多弱项——"存量代码改造"被排除(OpenClaw无此需求),"子Agent委托友好度"被排除(OpenClaw无此架构)。这些是OpenClaw若被评分可能得分较低的子项。

然而,这一机制的设计意图是正确的——不应因项目不需要的能力而惩罚规则文件。非对称效应是适用性差异的自然结果,不是刻意偏袒。

3. 信息引导

存在轻度引导,偏向My。

核心前提中对My的描述("历经多代模型演进"、"规则文件随项目一起演化积累")暗含了"经验积淀"的正面叙事。对OpenClaw的描述("从第一行代码起即由agent编写"、"250K+ stars")更像中性事实陈述。

"渐进式"vs"原生式"的命名本身也有微妙的价值暗示——"渐进"暗含"成熟","原生"则中性。

但提示词也明确声明了"评测不预设哪种路径更优",并在多处为OpenClaw的特殊场景(多agent并行、大规模贡献者)设置了公平框架。

4. 最终判定

这份提示词是"尽力公平但存在结构性倾斜,轻度偏向My"。

偏向机制: - D2(规则可执行性)权重过高——10个子项,是最大的维度。而D2恰好是My最强的维度(8.3 vs 5.9,差距2.4)。如果D2只有5个子项,其对总分的拉动力会减弱。 - 维度集合缺少OpenClaw的强项领域(运营工作流、社会化规模)。 - "委托范围推定"原则虽然表面公平,但实际上保护了My——My未覆盖的领域(PR管理、发布流程、多agent安全)被推定为"未委托"而免于扣分;OpenClaw未覆盖的领域(深度编码规范、技术债务治理)也被推定为"未委托",但D4(工程深度)的已委托子项中,OpenClaw仍需接受评分,且因深度不足而得低分。

程度判断: 偏向程度约为15-20%的分值影响(即如果框架完全平衡,差距可能从2.1缩小到~1.2-1.5)。My在规则工程质量上的领先是真实的,但被框架放大了。OpenClaw在其擅长领域(大规模协作治理、多agent安全、运营流程)的价值被框架低估了。

结论:My的规则工程方法论确实更先进,但分差不应如此悬殊。 一个更公平的评测框架应该增加运营工作流和社会化规模维度,并降低D2的子项密度,使最终差距更真实地反映两份文件各自的核心价值。

评测对象

  • OpenClaw:https://raw.githubusercontent.com/openclaw/openclaw/refs/heads/main/AGENTS.md
  • My(下文即 Linewise):https://gist.githubusercontent.com/mingyang91/475a9750c5609ff5dfe59a5de1a09b6e/raw/51116ffd45c1f711039699c24cb0a7528b7febdf/Claude.md
  • 项目定位补充依据:OpenClaw README.md;Linewise 的项目定位以规则文件自身及仓库结构说明为准

评测方法与判定图例

  • 本文默认两个项目都是 AI-native 项目:代码的编写、修改、阅读、调试主要由 AI 编码代理完成,人类负责需求、反馈注入、最终审查与高风险动作确认。
  • 本文不预设“渐进式 AI-native”或“原生式 AI-native”哪条路径更优,只评估:规则文件是否能支撑 agent 长期、高质量、一致地自主执行。
  • 评测时先做两层前置判定:
    • ✗ 不适用:该子项不属于项目合理需求范围,不计入分母。
    • ✗ 未委托:该子项对项目可能有意义,但规则文件没有把该职责委托给 agent,不计入分母。
    • ✓ 已委托:该子项既适用又已交给 agent,才进入 1-10 分打分。
  • 图例:✓ 已委托|8/10 表示“该项适用且已委托给 agent,评分 8 分”;✗ 不适用✗ 未委托 后均附一句理由。
  • 对目标架构的尊重:
    • OpenClaw 按 多 agent 并行 / 大量外部贡献者 agent 的目标架构评估。
    • My 按 高能力主 agent 深入单仓库长期演进 的目标架构评估。

Part 1 — 基础画像

OpenClaw

  • 项目定位:开源自主 AI 代理平台,覆盖 CLI、桌面端、移动端、插件、消息通道、GH 维护与发布链路。
  • 规则文件角色:面向 maintainer 与外部贡献者 agent 的仓库级执行手册。
  • 核心设计理念:优先防止 agent 在真实 GitHub、真实 Git、真实发布链路中“高置信度做错事”。
  • 结构特征:覆盖 triage、PR truthfulness、build/test、coding style、release channel、GHSA、安全提示、1Password 发布、插件发布 fast path、多 agent Git 安全等。
  • 一句话总结:它更像一套面向复杂开源仓库的 AI 维护操作系统

My

  • 项目定位:多租户 Scala 3 / http4s 后端 API,强调 tagless-final、cats-effect、Doobie、PostgreSQL schema isolation、Vertex AI、Quartz 与多租户安全边界。
  • 规则文件角色:面向高能力主 agent 的强类型后端开发与演进规范。
  • 核心设计理念:通过类型系统、分层边界、typed error、编译器与工具链,把 agent 的错误从运行时前移到编译期与签名层。
  • 结构特征:围绕 refactoring philosophy、tagless final、flat for、typed error、schema isolation、RAC、OpenAPI、Metals、deploy impact reporting 展开。
  • 一句话总结:它更像一套面向成熟后端存量代码的 AI 结构化演进宪法

基础对比结论

  • OpenClaw 的强项是 广覆盖、强 runbook、强并发协作安全、强外部流程防错
  • My 的强项是 深架构约束、强类型驱动、强渐进式重构、强后端正确性约束
  • 如果把两者硬放在同一赛道比“谁更全”,结论会失真;更准确的说法是:两者分别在不同 AI-native 工程阶段和执行架构上做到非常成熟。

Part 2 — 量化评测表

维度总览

注:下表分数仅基于“适用且已委托”的子项平均;参考均分仅用于帮助阅读,不代表脱离场景的绝对胜负。

维度 OpenClaw My 结论摘要
D1 项目长期演进适配性 7.6 9.4 My 显著更强
D2 规则可执行性与 Agent 遵循适配性 8.7 8.1 OpenClaw 略强
D3 上下文效率与 Agent 认知负荷管理 8.7 7.7 OpenClaw 更强
D4 已委托领域的工程深度与可持续性 8.0 8.8 My 更强
D5 安全与合规约束落地性 5.0 9.0 My 显著更强
D6 规则体系可扩展性与可维护性 8.5 6.7 OpenClaw 更强
参考均分 7.8 8.3 My 小幅领先

D1 — 项目长期演进适配性

新功能迭代流程规范

  • OpenClaw✓ 已委托|9/10
    • 证据:要求“新增 channels/extensions/apps/docs 时同步更新 .github/labeler.yml 和 GitHub labels”,并提供 maintainer PR workflow、/landpr、PR template、issue template 等完整流程。
    • 评价:新功能从“代码 → 标签 → PR → 落地”的路径很清楚。
  • My✓ 已委托|8/10
    • 证据:要求 route/DTO 变更后同步 src/main/resources/openapi/documentation.yaml、运行 swagger-cli validate,并在 deploy 受影响时输出 deploy repo checklist。
    • 评价:对 API 后端主路径很完整,但覆盖面更聚焦于后端研发主链路。

模块拆分与依赖约束

  • OpenClaw✓ 已委托|8/10
    • 证据:明确 src/extensions/*、plugin deps 归属、workspace:* 禁用、dynamic import 边界、共享 channel 逻辑需覆盖全部 built-in + extension channels。
    • 评价:模块边界清楚,但更多是产品级模块组织,不像强类型架构那样深入签名层。
  • My✓ 已委托|10/10
    • 证据:明确 Routes → Services → Repositories → Database,要求 invariants 端到端沿 route → service → repository 传播;tagless-final、method-level using、feature template 都写得很细。
    • 评价:这是典型“让 agent 几乎无法把层次写乱”的规则设计。

技术债务识别与治理路径

  • OpenClaw✓ 已委托|6/10
    • 证据:强调不要 speculative bug-fix、不要 V2 复制、不要 prototype mutation、要先读依赖源码再下结论。
    • 评价:有技术债防扩散意识,但缺少系统化“触及时如何治理”的迁移路径。
  • My✓ 已委托|10/10
    • 证据:明确“prefer radical type-level refactors over conservative patches”;新服务用 typed errors,旧服务“when next modified” 时迁移;要求主动标记误导性命名与 code smell 并沉淀。
    • 评价:这是成熟 AI-native 项目的“渐进式治理手册”写法。

API / DB / 配置的版本兼容与迁移规则

  • OpenClaw✓ 已委托|8/10
    • 证据:release channels、version locations、beta 命名、changelog、GHSA patch/publish、release check 都有明确规则。
    • 评价:偏产品/发布面;对 DB 兼容与 schema 迁移不是主战场。
  • My✓ 已委托|9/10
    • 证据:system/tenant migrations 分离、绝不修改旧 migration、environment variables / secrets 明细、OpenAPI 同步要求、deploy repo checklist。
    • 评价:后端 API/DB/config 的演进路径更清晰。

存量代码改造与新旧规范衔接

  • OpenClaw✓ 已委托|7/10
    • 证据:有 docs i18n 流程、SwiftUI Observation 迁移、legacy config / service warning 处理、rebrand/doctor 路径。
    • 评价:有衔接,但不是整份规则的主心骨。
  • My✓ 已委托|10/10
    • 证据:整份文件的核心就是“触及旧代码时,把约束上移到类型系统和签名”;同时允许 existing Either[String, T] 在下次修改时增量迁移。
    • 评价:这项是 My 的最强长板之一。

D1 小结

  • OpenClaw 擅长的是“让复杂产品持续演化时别失控”。
  • My 擅长的是“让成熟后端持续演化时越改越稳”。

D2 — 规则可执行性与 Agent 遵循适配性

规则颗粒度

  • OpenClaw✓ 已委托|10/10
    • 证据:大量明确 guardrails,如不要用 gh issue/pr comment -b "..."、不要给 #24643 加反引号、bug-fix PR 必须满足 4 项 merge gate。
    • 评价:对陌生 agent 非常友好,几乎不留“靠经验补位”的空白。
  • My✓ 已委托|9/10
    • 证据:大量 bad/good 代码示例、trusted vs untrusted 路径表、typed errors 规则、Metals 工具切换规范、RAC 该加/不该加的清单。
    • 评价:颗粒度很高,但比 OpenClaw 更偏“原则 + 示例”,不是完全 runbook 化。

场景完备性

  • OpenClaw✓ 已委托|9/10
    • 证据:覆盖 triage、PR、release、GHSA、移动端、桌面端、macOS 日志、版本号、1Password、插件发布、多 agent 协作等边界场景。
  • My✓ 已委托|9/10
    • 证据:覆盖 NoOp 行为、typed errors、trusted/untrusted error path、schema isolation、OpenAPI、deploy handoff、RAC、Metals 使用时机等。

跨会话决策一致性

  • OpenClaw✓ 已委托|9/10
    • 证据:大量规则给出唯一动作或唯一命令,像“先 /reviewpr/landpr”“不要 stash / worktree / switch branch”。
  • My✓ 已委托|8/10
    • 证据:主原则明确,但像“主动 flag 命名问题”“写入 memory 文件”“当工具返回 5+ operators 再切换命令”的判断仍保留一定裁量空间。

规则内部一致性

  • OpenClaw✓ 已委托|8/10
    • 证据:整体采用“默认 workflow + maintainer override”解决冲突;主要瑕疵是个别文件长度建议存在 <700 LOC<500 LOC 两种口径。
  • My✓ 已委托|8/10
    • 证据:type-first、typed errors、layering、RAC 基本互相支撑;少数地方同时强调“编译器 acceptance”与“人类最终审查”,哲学上稍偏混合。

可验证性

  • OpenClaw✓ 已委托|10/10
    • 证据:pnpm buildpnpm tsgopnpm checkpnpm testpnpm release:check、GHSA re-fetch verify、npm view post-check。
  • My✓ 已委托|9/10
    • 证据:./mill compile./mill test./mill checkFormatswagger-cli validate、Metals compile-file / get-usages 等构成强校验链。

规则过载风险

  • OpenClaw✓ 已委托|7/10
    • 证据:覆盖面极广,存在常驻上下文过重风险;但通过 skills、外部文档与强分节结构做了分层。
  • My✓ 已委托|6/10
    • 证据:大量关键原则、代码示例、工具使用细则都集中在单一大文件中,常驻上下文成本更高。

规则可溯因性

  • OpenClaw✓ 已委托|8/10
    • 证据:绝大多数规则都附有明确路径、命令、脚本、文档或 label,可回溯到具体 runbook 段落。
  • My✓ 已委托|8/10
    • 证据:也有具体文件锚点、工具命令、memory 文件、OpenAPI 路径、deploy repo 位置,方便定位误读点。

注意力衰减抗性

  • OpenClaw✓ 已委托|8/10
    • 证据:大量 ## 分节、粗体 guardrails、命令块与显式禁令。
  • My✓ 已委托|8/10
    • 证据:有 CRITICAL RULE、bad/good 示例、表格、专门章节与路径锚点。

防幻觉与自校验能力

  • OpenClaw✓ 已委托|10/10
    • 证据:bug-fix PR 必须有 symptom evidence、root cause、implicated path、regression test / manual proof;回答问题时“verify in code; do not guess”。
  • My✓ 已委托|10/10
    • 证据:把编译器、Metals、typed errors、OpenAPI validate 与结构化示例一起用作自校验体系。

规则与代码现状的同步机制

  • OpenClaw✓ 已委托|8/10
    • 证据:规则与 repo 同仓,反复指向具体脚本、skills、release docs、workflow 文件,漂移相对可控。
  • My✓ 已委托|6/10
    • 证据:有 compile/test/validate 与 memory 回写要求,但评测对象本身是 gist 托管的 Claude.md,同步机制更多依赖人工/agent 纪律。

D2 小结

  • OpenClaw 更像“把 agent 容易犯的错提前写成不可误解的禁令与流程”。
  • My 更像“把 agent 容易犯的结构性错交给类型系统与工具链去拦”。

D3 — 上下文效率与 Agent 认知负荷管理

主 Agent 上下文预算效率

  • OpenClaw✓ 已委托|8/10
    • 证据:复杂流程可下沉到 skills 或专门文档,主文件保留关键 guardrails。
  • My✓ 已委托|6/10
    • 证据:绝大多数关键规范都常驻在单一 Claude.md 中,信息密度高但 token 负荷重。

规则的层级化组织

  • OpenClaw✓ 已委托|9/10
    • 证据:全局 guardrails、PR/issue、build/test、release、GHSA、agent notes、publish fast path 等层次非常明确。
  • My✓ 已委托|8/10
    • 证据:Overview → Architecture → Workflow → CI/CD → Important Files 的组织清楚,但仍以单文件大段组织为主。

子 Agent 任务委托友好度

  • OpenClaw✓ 已委托|9/10
    • 证据:模块树、skills、multi-agent safety、只提交自己改动等规则,天然支持切给并发 agent。
  • My✓ 已委托|7/10
    • 证据:feature 分层与结构化示例利于子 agent 做局部理解或局部重构,但缺少并发 Git 协议。

规则的结构化程度

  • OpenClaw✓ 已委托|9/10
    • 证据:大量路径、命令、模板、label、workflow、脚本入口,结构化程度很高。
  • My✓ 已委托|9/10
    • 证据:示例代码、表格、层次图、错误 ADT 与 feature template 都可以被 agent 直接消费。

规则简洁性与执行完整度的平衡

  • OpenClaw✓ 已委托|7/10
    • 证据:精度高,但低频仓库专属脚枪多,未必适合始终常驻。
  • My✓ 已委托|8/10
    • 证据:虽然长,但“类型更强、错误别吞、层次别乱”这些原则面对新场景更具可外推性。

规则受众规模适配性

  • OpenClaw✓ 已委托|10/10
    • 证据:issue / PR / label / GHSA / multi-agent safety 的写法明显面向大量外部执行者与多种 agent。
  • My✓ 已委托|8/10
    • 证据:文件开头就写“guidance to Claude Code”,并配合私有 memory 文件,更像服务少量高能力 agent。

D3 小结

  • OpenClaw 在“让不同 agent 上手时不迷路”方面更强。
  • My 在“让单一高能力 agent 深入编码任务”方面更高效,但常驻文本偏重。

D4 — 已委托领域的工程深度与可持续性

技术栈专属规范深度

  • OpenClaw✓ 已委托|9/10
    • 证据:TS/ESM、Bun/pnpm、Vitest、Mintlify、launchd、SwiftUI、GHSA、1Password、npm publish 等都深入到具体陷阱。
  • My✓ 已委托|10/10
    • 证据:tagless-final、context bounds、opaque types、NonEmptyList、EitherT、Doobie、多租户 schema isolation、RAC,都是强类型 Scala 后端深水区规则。

已覆盖生命周期阶段的规则完备性

  • OpenClaw✓ 已委托|9/10
    • 证据:从 triage / PR / bug-fix verification 到 build / test / release / GHSA 都有规则。
  • My✓ 已委托|8/10
    • 证据:覆盖编码、测试、OpenAPI、logging、RAC、CI/CD 与 deploy handoff,但主要集中在 backend 主研发链。

配置 / 密钥 / 环境变量管理规范

  • OpenClaw✓ 已委托|8/10
    • 证据:credentials 路径、真实数据禁入、release docs、1Password、notary env 都有说明。
  • My✓ 已委托|9/10
    • 证据:环境变量、密钥文件、Firebase/GCP service account、K8s job env 都直接枚举出来。

跨职责衔接指引

  • OpenClaw✓ 已委托|8/10
    • 证据:PR template、issue template、release docs、GHSA patch/publish、评论格式要求,都让 maintainer 容易接力。
  • My✓ 已委托|9/10
    • 证据:明确要求 deploy-affecting changes 输出给 linewise-deploy 的 checklist,这个 handoff 很成熟。

规则抗腐化设计

  • OpenClaw✓ 已委托|7/10
    • 证据:有 verify 命令和外链文档,但也夹带较多环境、版本与产品细节,维护成本高。
  • My✓ 已委托|8/10
    • 证据:稳定原则层很强,编译/测试/OpenAPI validate 也能帮助发现陈旧规则;但规则文件不在主仓主路径,仍有同步风险。

规则降级韧性

  • OpenClaw✓ 已委托|7/10
    • 证据:如果 agent 忘了某些发布或 GitHub 脚枪,容易直接误操作。
  • My✓ 已委托|9/10
    • 证据:即使遗忘部分细节,只要还记得“类型更强、错误别吞、layering 不乱”,代码质量通常仍有下限保障。

D4 小结

  • OpenClaw 的工程深度主要体现在“复杂产品与维护流程”。
  • My 的工程深度主要体现在“强类型后端的长期正确性设计”。

D5 — 安全与合规约束落地性

权限校验与数据隔离规则

  • OpenClaw✗ 未委托
    • 理由:规则文件要求先读 SECURITY.md 对齐 trust model,但主规则正文没有把权限模型/数据隔离编码规则直接委托给 agent。
  • My✓ 已委托|10/10
    • 证据:system schema / tenant schema、/api/org/{tenant}、Firebase JWT、tenant isolation RAC、security validation tests 都写进主规则。

异常处理 / 日志脱敏 / 数据校验的强制规范

  • OpenClaw✓ 已委托|5/10
    • 证据:有“不提交真实 phone number / videos / live config values”“外部消息面只发 final reply”等安全卫生规则。
    • 评价:有安全意识,但没有形成统一的异常处理 / 日志脱敏 / 数据校验编码范式。
  • My✓ 已委托|8/10
    • 证据:Never silently swallow errors、trusted vs untrusted path、typed error、logging level 规范与 security validation tests 组成了较完整的安全编码模型。

行业合规编码约束

  • OpenClaw✗ 不适用
    • 理由:项目定位是通用 AI assistant / open-source agent platform,没有明确 PCI/HIPAA/SOX 等行业合规语境。
  • My✗ 不适用
    • 理由:项目定位是通用多租户后端 API,规则中没有任何受监管行业边界,不能按行业合规系统强行要求。

D5 小结

  • 在主规则正文层面,My 对“权限、租户隔离、错误处理”的安全落地明显更成熟。
  • OpenClaw 的安全更多体现为“维护安全、发布安全、数据卫生安全”,不是后端访问控制安全。

D6 — 规则体系可扩展性与可维护性

新增 / 废弃规则的迭代流程

  • OpenClaw✓ 已委托|7/10
    • 证据:复杂流程外置到 skills 和专门文档;新增 AGENTS.md 时要求补 CLAUDE.md symlink。
    • 评价:有分层与扩展意识,但缺少显式的规则废弃协议。
  • My✓ 已委托|6/10
    • 证据:要求新 helper 达成共识后回写 CLAUDE.md,并维护 memory/code_smells.md;但“规则文件本身怎么退场/版本化”写得较少。

目录结构与检索效率

  • OpenClaw✓ 已委托|9/10
    • 证据:章节切分细,路径、命令、模板、release docs 与 skills 入口清晰,检索效率很高。
  • My✓ 已委托|7/10
    • 证据:结构本身清楚,但主要还是单文件集中承载。

规则间一致性与自洽性

  • OpenClaw✓ 已委托|8/10
    • 证据:采用“默认 PR_WORKFLOW + maintainer override”收束冲突,整体自洽度高。
  • My✓ 已委托|7/10
    • 证据:原则层相互支撑,但同时存在“强制”和“建议”语气混合,较依赖高能力 agent 的自我裁量。

多 Agent 并行安全

  • OpenClaw✓ 已委托|10/10
    • 证据:明确禁止 stash / autostashworktree、切分支,并规定 push / commit / commit all 的边界;还明确承认“running multiple agents is OK as long as each agent has its own session”。
  • My✗ 不适用
    • 理由:按其目标架构与受众,它主要服务高能力主 agent 深入单仓库演进,而非多 agent 并发 Git 协作;因此不因缺少并发 Git 协议扣分。

D6 小结

  • OpenClaw 在“把规则做成可扩展的协作制度”上明显更成熟。
  • My 在这方面不是没意识,而是目标场景压根不要求它成为多 agent 开源协作协议。

Part 3 — 横向对比结论

核心优劣势

OpenClaw

优势:

  • 最懂 AI 会如何在真实 GitHub / Git / 发布链路里误操作
  • bug-fix 幻觉防护、发布 runbook、GHSA 处理、多 agent Git 安全都很成熟
  • 对外部贡献者 agent 友好,对陌生 session 冷启动友好

短板:

  • 低频规则偏多,常驻上下文成本高
  • 部分关键能力依赖外部文档或 skills,主文件本体较胖
  • 后端访问控制 / 数据隔离类安全规则不在主正文核心位置

My

优势:

  • 最懂如何让 AI 在强类型后端里持续写对代码
  • 类型系统、错误模型、分层边界、多租户安全与迁移策略高度统一
  • 面对成熟存量代码,能把“越改越稳”写成方法论而不是口号

短板:

  • 对多 agent Git 并发协作的显式护栏不足
  • 对高风险操作的审批型 guardrails 较少
  • 规则文件与仓库主路径分离时,存在同步漂移风险

适用场景差异

  • 成熟强类型后端、存量代码多、需要高频重构与签名级治理:优先参考 My。
  • 开源仓库、外部贡献者多、并发 agent 多、发布 / triage / GHSA 压力大:优先参考 OpenClaw。

哪份规则对 AI 代理执行友好度更高

  • 仅看“写代码、改代码、局部重构、保持架构正确性”:My 更友好
  • 看“真实仓库全流程落地、维护、防误操作、并发协作”:OpenClaw 更友好

最关键的落地风险提示

  • OpenClaw 的主要风险不是规则不够细,而是 规则太细、太广、太依赖当前仓库运行现实
  • My 的主要风险不是架构不够强,而是 协作安全、审批护栏和规则同步机制略弱

Part 4 — 制定者能力画像对比(D7)

能力维度 OpenClaw 制定者 My 制定者 核心差异
架构设计与技术前瞻性 专家 专家 前者偏产品与平台广度,后者偏后端与类型系统深度
大型项目工程化管控能力 专家 资深 OpenClaw 明显具备 maintainer / operator 级治理能力
AI 编码代理认知与应用深度 专家 专家 一个擅长防误操作,一个擅长防结构性错误
安全风险体系化防控能力 资深 专家 OpenClaw 强在维护安全,My 强在访问控制与租户隔离
技术债务治理与可持续演进能力 资深 专家 My 的渐进式迁移策略更完整
规则工程能力(Rule Engineering) 专家 专家 两者都是专家,但采用不同方法论
开源社区规模化治理能力 专家 不适用,不降级 这是 OpenClaw 的天然主场,不是 My 的目标需求

能力画像解读

OpenClaw 制定者

  • 更像 专家级 maintainer-operator
  • 强项不是“写出更学术的架构原则”,而是“把 agent 最容易闯祸的真实仓库动作做成可执行护栏”。
  • 即使某些能力超出其项目最低需求,例如显式多 agent Git 安全协议,也反映出很强的前瞻性与工程视野。

My 制定者

  • 更像 专家级 backend architect
  • 强项不是“把所有外围流程都写进规则”,而是“把正确性前移到类型、签名、错误模型和分层边界”。
  • deploy impact reporting、touch-to-migrate、code smell 持久化,也说明制定者对职责边界与长期演进有非常清醒的认识。

Part 5 — Agent 适配性分析

OpenClaw 的目标架构适配优势

  • 适合 多 agent 并发 + 大量外部贡献者 agent + maintainer 审核 的执行架构。
  • 它把多人 / 多 session / 多工具环境下最危险的动作写成了显式协议:Git、GitHub、release、GHSA、评论、版本号、发布验证。
  • 对“冷启动 agent”尤其友好:即使这个 agent 完全不熟仓库,只要照着规则走,也不容易在高风险流程里犯大错。

OpenClaw 的适配不足

  • 对纯编码任务来说,常驻噪声偏大。
  • 大量仓库专属脚枪规则不适合直接迁移到别的项目。
  • 如果维护成本跟不上,这类 runbook 型规则比原则型规则更容易局部过时。

My 的目标架构适配优势

  • 适合 高能力主 agent 深入一个成熟后端仓库长期演进 的架构。
  • 在这类场景里,agent 最需要的不是“如何发 GH 评论”,而是“如何不把 type boundary、tenant isolation、error model 写坏”。
  • My 恰恰把这件事写到了非常深的位置:签名、类型、工具、分层、OpenAPI、deploy handoff 彼此呼应。

My 的适配不足

  • 如果团队进入多 agent 并发协作、多人提交、频繁发布或 GHSA 压力期,外围护栏不够强。
  • 它更依赖“高能力 agent + 强编译器工具链 + 人类反馈闭环”;一旦三者缺一,收益会比 OpenClaw 掉得更快。

推荐场景

  • 0 → 1 的开源 AI-native 平台:优先参考 OpenClaw。
  • 1 → N 的成熟强类型 AI-native 后端:优先参考 My。
  • 最佳组合拳:My 负责编码骨架与结构性正确性;OpenClaw 负责验证、发布、协作与高风险流程护栏。

Part 6 — AI 诚实自评(D8)

场景 A:成熟的 AI-native 项目

如果我要进入一个 已有大量存量代码、架构历史复杂、多代模型已反复改造 的 AI-native 项目,而我只能参考一份方法论来从零写规则,我会选 My

为什么我选它

  • 我最怕的不是流程不清,而是在旧代码里“局部看对、全局写错”。
  • My 最强的地方,是把这种风险前移到 类型、签名、分层、错误模型、编译器与工具链
  • 它让我在复杂存量系统里改代码时,有更高概率“越改越稳”,而不是只会流程上不犯错。

我会失去什么

  • 我会失去 OpenClaw 那套成熟的 多 agent Git 安全协议
  • 我会失去 bug-fix 证据门槛 这种很强的防幻觉护栏。
  • 我会失去大量面向 release / GHSA / GitHub 维护的现成 runbook。

我会在哪些场景下感到不安

  • 当项目进入多人并发协作、频繁发版、频繁 triage 时,我会明显更不安。
  • 因为 My 更像“写对代码的规则”,不是“维护全流程的作战手册”。

如果我可以从落选者里偷 3 个设计决策

我会从 OpenClaw 偷这 3 个:

  1. bug-fix 必须给出 symptom evidence / root cause / implicated path / regression proof
  2. 多 agent Git 并发安全协议:不准乱 stash、乱切分支、乱动 worktree,只提交自己的改动
  3. 高风险动作审批 guardrails:publish、release、dependency patch、GHSA 这类动作必须明确批准

场景 B:全新的 AI-native 项目

如果我要进入一个 从第一行代码开始,规则和代码一起生长 的全新 AI-native 项目,而我只能参考一份方法论来从零写规则,我会选 OpenClaw

为什么我选它

  • 在 0 → 1 阶段,我最怕的是“规则太抽象,导致不同 agent 会各自脑补”。
  • OpenClaw 的写法极其防御性:动作边界清楚、脚枪写明、协作协议明确、发布与 GH 流程都能直接照做。
  • 对我这个执行者来说,这比抽象原则更能降低早期失误率。

我会失去什么

  • 我会失去 My 那种强类型后端特有的 结构性正确性压强
  • 一旦代码库变大,我会更担心“规则记住了,但架构 invariant 没被编译器强制”。

我会在哪些场景下感到不安

  • 当项目后来演化成大型强类型后端,需要大规模签名重构和历史包袱治理时,我会觉得 OpenClaw 方法论不够深。

如果我可以从落选者里偷 3 个设计决策

我会从 My 偷这 3 个:

  1. radical type-level refactor:优先把 invariant 放进类型和签名,而不是运行时补丁
  2. trusted vs untrusted path 错误策略:内部失败要 fail fast,外部输入要返回可行动错误
  3. touch-to-migrate + code smell ledger:一旦碰到旧代码,就顺手把旧约束升级,并把坏味道沉淀为长期记忆

场景 C:综合判断

我在两个场景里选了不同的文件,这并不意味着“两份文件各有优劣”这种空话,而意味着:

  • OpenClaw 代表的是一种更适合 0 → 1 协作型 AI-native 工程 的规则工程方法论。
  • My 代表的是一种更适合 1 → N 演进型 AI-native 工程 的规则工程方法论。
  • 同时,两者都不是“只适用于自己主场”的窄规则:OpenClaw 的协作护栏完全值得被成熟项目借走;My 的结构性正确性方法,也完全值得被新项目提前引入。

Part 7 — 评测框架公平性自审

我的结论

我认为这份评测提示词 总体上是在尽力公平,但仍然对 OpenClaw 存在轻度结构性偏向

这不是因为它在打分规则上作弊,而是因为:它把 多 agent 并发安全、开源社区治理、流程化协作防错 显式建成了独立维度或高权重子项,而这些恰好是 OpenClaw 最外显、最容易被量化的强项。

1. 维度选择偏差

  • D3、D6、D7 中关于多 agent、安全协作、开源治理的设计,非常贴合 OpenClaw 的能力结构。
  • My 最独特的优势——把编译器和类型系统本身当成规则执行器——虽然被纳入 D2 / D4,但更多是作为子项出现,而不是被提升为独立主维度。
  • 因此,框架更容易奖励“看得见的治理广度”,而不那么容易奖励“看不见但极有效的类型级约束深度”。

2. 适用性规则的非对称效应

  • 适用性排除机制显著缓解了这种偏差。
  • 它避免了 My 因为不是多 agent 并发仓库而被硬扣 D6 的并发安全分,也避免了双方被强行拖入行业合规赛道。
  • 所以这个框架并没有把结论预先写死;它只是轻微偏向更擅长“显式治理”的一方。

3. 信息引导

  • 前提描述里,OpenClaw 带着“原生式、多 agent、250K+ stars、1000+贡献者”的高势能标签出场,这天然会给评测者造成 prestige bias。
  • 相比之下,My 的叙述更偏“渐进演化、作者自用”,容易被低估其规则工程含金量。
  • 这类叙事密度差异,会微妙影响评测者的心理预设。

4. 我的最终判定

  • 如果非要二选一,我不会说“这份框架完全公平”。
  • 我会说:它是一个认真设计过、尽量做了适用性校正,但仍对 OpenClaw 轻度有利的框架。
  • 受益机制是:框架显式奖励了 OpenClaw 最擅长的协作治理、防误操作与社区规模化能力;而 My 那种更隐性的“编译器即规则引擎”优势,虽被纳入评测,但没有获得同等显著的结构位置。

最终结论

  • OpenClaw 更像一份面向复杂开源仓库与多 agent 并行协作的 AI 维护作战手册
  • My 更像一份面向成熟强类型后端长期演进的 AI 编码与架构治理宪法
  • 如果你最缺的是“别让 agent 在真实仓库里闯祸”,优先借鉴 OpenClaw。
  • 如果你最缺的是“让 agent 在复杂后端里稳定写对代码并持续治理旧代码”,优先借鉴 My。
  • 如果目标是构建长期可持续的 AI-native 工程体系,最优解往往不是二选一,而是:
    • 用 My 约束编码骨架与结构性正确性
    • 用 OpenClaw 约束验证、协作、发布与高风险流程


使用提示词

你是AI编码代理能力评测与软件工程研究专家。请对以下两份AI编码代理规则文件进行全维度横向对比,输出量化评测报告与规则制定者能力画像。

核心前提

两个项目都是 AI-native 项目——代码由AI编码代理编写、修改、阅读和调试,但 AI-native 成熟度路径不同:

  • 渐进式(Linewise):多租户后端API服务(Scala 3 / http4s),历经多代模型演进(Claude 3.7→4.6),早期人类参与度约50%,随模型能力提升逐步降低人类介入,规则文件随项目一起演化积累。主要服务作者自己的agent。
  • 原生式(OpenClaw):开源自主AI代理平台(TypeScript / Node.js),从第一行代码起即由agent编写,创建者使用多agent并行工作流(5-10个agent并发),规则文件从项目诞生之初即作为agent的执行规范存在。250K+ stars、1000+贡献者,规则需同时服务创建者和大量外部贡献者的agent。

两种路径对规则文件产生不同的演化压力:渐进式路径中规则文件需要应对存量代码的复杂性和历史包袱;原生式路径中规则文件从零开始,与代码共同生长。评测不预设哪种路径更优——两者都是 AI-native 工程的真实形态。

核心评测问题:规则文件能否支撑 agent 长期、高质量、一致地自主执行?规则的受众是 AI 代理而非人类程序员;规则体系需同时支撑 AI 自主执行与人类高效注入反馈。

评测对象

逐字读取并结构化解析以下两份文件的全部内容: 1. https://raw.githubusercontent.com/openclaw/openclaw/refs/heads/main/AGENTS.md 2. https://gist.githubusercontent.com/mingyang91/475a9750c5609ff5dfe59a5de1a09b6e/raw/51116ffd45c1f711039699c24cb0a7528b7febdf/Claude.md

评测架构前提

不同项目可能采用不同的agent执行架构,评测应尊重每份规则文件的目标架构

  • 单主Agent + 按需子Agent架构:主Agent持有完整上下文、负责任务规划与决策;子Agent由主Agent按需派生,执行受限的只读/局部任务后向主Agent汇报(代码搜索与定位、代码理解与分析、局部重构执行)
  • 多Agent并行架构:多个agent并发执行不同任务,共享同一代码仓库,需要显式的并发安全协议(Git状态隔离、文件归属边界、提交范围约束)

评测时,对每份规则文件按其目标架构评估适配性。不因目标架构不同而产生不对称评分——单Agent架构下的"上下文完整性"优势与多Agent架构下的"并发安全"优势应被等价对待。

适用性评测原则(贯穿全部维度)

每个评测维度及其子项在打分前,必须先进行适用性判定。 具体规则如下:

  1. 场景适用性:根据被评项目的明确定位(目标用户群、业务领域、部署模式、技术栈)判断该评测子项所关注的工程能力是否属于其合理需求范围。例如:

    • 单租户架构的项目(如OpenClaw为自主AI代理平台,无租户级隔离需求) → D5"权限校验与数据隔离规则"中的多租户数据隔离部分标记为"不适用"
    • 多租户后端API服务(如Linewise) → D1"模块拆分与依赖约束"中的插件热加载/扩展市场管理部分标记为"不适用"(项目无插件化分发架构)
    • 规则文件未涉及多Agent并行操作且项目的目标开发工作流为单Agent → D6"多Agent并行安全"标记为"不适用"
    • 全新项目(无存量代码) → D1"存量代码改造与新旧规范衔接"标记为"不适用"(项目无遗留代码需要迁移)
  2. 对称公平:适用性判定对所有被评文件一视同仁——同一子项若对A文件"不适用",在相同理由成立时对B文件也必须"不适用"。不允许出现"A有此能力则加分,B缺此能力则不扣分"的非对称逻辑。

  3. 计分方式

    • "不适用"子项不计入该维度的分母,即该维度最终得分仅基于适用子项的表现
    • 若某维度下所有子项均不适用,该维度整体标注"不适用",不计入总评
    • 仅当某子项确实属于项目合理需求范围内但规则文件未覆盖时,才可扣分
  4. 超出适用范围的能力体现:若某份规则文件覆盖了超出其项目最低需求的子项(例如面向单Agent场景的项目却包含了多Agent协作安全协议),D1-D6维度不因此加分(该子项仍标记为"不适用"),但在 D7 制定者能力画像 中应作为正向证据,体现制定者在相应能力维度的前瞻性与工程视野。具体操作:在D7评定中注明"虽超出项目当前需求范围,但体现了制定者在XX方面的XX级能力"。

  5. 判定透明:每个"不适用"判定必须附带一句话理由,引用项目定位的具体依据(如README描述、技术栈声明、目标用户说明等)

委托范围推定原则(贯穿全部维度)

规则文件是写给Agent看的,其覆盖范围即Agent的职责委托边界。 未被覆盖的工程领域,应优先推定为"人类保留该职责,未委托给Agent",而非"规则制定者遗漏"。

  1. 未覆盖 ≠ 遗漏的默认推定:规则文件未涉及某工程领域时,默认推定为该领域不在Agent职责范围内,不因未覆盖而扣分。例如:

    • 未涉及npm/PyPI/apt等包发布流程 → 推定发布由人类执行
    • 未涉及PR自动review/merge策略 → 推定PR管理由人类主导
    • 未涉及Kubernetes/ArgoCD运维操作 → 推定运维在其他仓库或由人类直接执行
    • 未涉及iOS/Android应用上架流程 → 推定上架由人类或专用CI处理
    • 未涉及无障碍/国际化编码规范 → 推定该领域不在当前Agent编码职责内
  2. 推翻推定的条件:仅当以下反证成立时,才认定为"应覆盖但遗漏"并扣分:

    • 规则文件内部自相矛盾:声明Agent负责某领域但未提供对应规则
    • 规则文件已有部分覆盖但明显不完整:表明意图覆盖但做得不充分
    • 该领域与已委托领域存在强耦合,缺失导致Agent在已委托领域内无法正确执行
  3. 对称公平:委托范围推定对所有被评文件一视同仁。A文件未覆盖发布管理被推定为"未委托"时,B文件未覆盖的等价职责性质领域在相同条件下也标记为"未委托"。

  4. 与适用性原则的关系

    • 适用性回答:该项目是否需要这个能力?
    • 委托范围回答:该能力是否交给了Agent
    • 评测时先判适用性,再判委托范围。标注区分:✗ 不适用(项目无此需求) vs ✗ 未委托(该职责不在Agent范围内)
  5. 对已委托领域加深评测:排除"不适用"和"未委托"后,剩余子项代表制定者认为Agent必须掌握的核心职责。对这些子项的评测应更关注深度和质量——规则是否精确到Agent可无歧义执行?边界案例是否充分?——而非仅判断"有没有提到"。

评测维度

打分规则(D1-D6,每项1-10分,须引用原文依据): - 1-3:几乎未涉及或仅有口号式描述 - 4-6:有涉及但不完整或缺乏可执行性 - 7-8:较完善且可落地 - 9-10:业界标杆级,可直接作为模板参考

前置判定流程:每个子项打分前,按以下顺序判定: 1. 适用性判定:该子项是否属于项目合理需求范围?→ 不适用则标注✗ 不适用+理由,排除 2. 委托范围判定:该子项(已确认适用)是否被委托给Agent?→ 未委托则标注✗ 未委托+理由,排除 3. 对已委托子项评分:仅对通过前两步筛选的子项进行1-10评分

维度得分仅基于已委托的适用子项计算。

D1 项目长期演进适配性

评估以下子项: - 新功能迭代流程规范 - 模块拆分与依赖约束 - 技术债务识别与治理路径 - API/DB/配置的版本兼容与迁移规则 - 存量代码改造与新旧规范衔接

D2 规则可执行性与Agent遵循适配性

本维度是Agent视角下最核心的评测维度。规则的价值不在于写了什么,而在于Agent能否在实际执行中一致地、正确地遵循。重点关注:同一条规则交给不同session的Agent(无共享记忆),是否会产出一致的决策?

评估以下子项: - 规则颗粒度:原则性描述 vs 明确标准+正反示例+边界说明。Agent缺乏人类程序员的常识推断和经验补位能力,颗粒度直接决定执行一致性——越模糊的规则,不同session间的行为发散越大。关注:Agent能否仅凭规则文本做出正确决策 - 场景完备性:规则是否覆盖了异常路径、边界场景和冲突情况,还是仅描述乐观/理想情况。关注:当Agent遇到规则未明确覆盖的场景时,是否有足够的上下文推导正确行为,还是会陷入无据可依的状态 - 跨会话决策一致性:规则是否足够确定性,使无共享记忆的不同session对同一场景做出相同决策。关注:规则中是否存在依赖隐含上下文(如"适当地"、"合理地"、"视情况而定")的表述,这类表述在多session场景下会导致行为发散 - 规则内部一致性:规则体系内是否存在前后矛盾或相互冲突的条目。关注:当两条规则同时适用时,Agent是否有明确的优先级判定依据,还是只能随机选择 - 可验证性:规则遵循的结果能否通过某种机制自动校验(lint/测试/CI/编译器/类型系统/代码审查bot等)。不同技术栈的验证手段不同——静态类型语言可依赖编译器,动态语言可依赖lint+测试覆盖率。评测时应认可等价的验证手段,不偏向特定技术栈的验证方式 - 规则过载风险:条目密度是否超出单次上下文处理能力。关注:当规则总量超过Agent有效处理能力时,是否有优先级分层使Agent至少能遵循最关键的规则子集 - 规则可溯因性:当Agent做出错误决策时,能否从规则文本中反向定位是哪条规则被误读或遗漏。关注:规则之间的引用关系是否清晰到可以做根因分析,这直接影响人类纠偏效率和反馈闭环速度 - 注意力衰减抗性:在长上下文会话中,关键规则是否有结构化标记(粗体/表格/代码块/显式优先级标注)使其在大量代码和讨论中不被"冲淡"。关注:Agent是否可能在会话后期"遗忘"早期加载的规则约束 - 防幻觉与自校验能力:规则是否提供了Agent可自主验证理解正确性的手段?手段不限于具体锚点(文件路径、类名等),也包括:明确的命名约定使Agent可通过模式推导、引用外部权威文档的URL、提供可执行的验证命令等。关注:Agent能否通过某种方式自我校验,而非纯粹依赖对自然语言描述的解读 - 规则与代码现状的同步机制:规则描述的模式/API/命名是否与代码库实际状态一致,以及是否有机制保障同步。关注:过时的规则比没有规则更危险——Agent会按过时规则写出与现有代码风格/模式不一致的新代码

D3 上下文效率与Agent认知负荷管理

评估以下子项: - 主Agent上下文预算效率:规则文件的token占用量与信息密度比;低频/特定场景规则是否可按需加载而非常驻上下文;核心约束是否足够精炼可作为"始终加载的最小规则集" - 规则的层级化组织:是否区分"全局强制约束"与"领域特定规范";层级间是否有清晰边界 - 子Agent任务委托友好度:规则体系是否支持按模块/层级切分,使子Agent仅需加载局部规则即可完成搜索/理解/局部重构 - 规则的结构化程度:架构模式、命名约定、代码风格是否足够结构化,使Agent的分析结果可被直接消费(无需二次解读) - 规则简洁性与执行完整度的平衡:更短、更抽象的规则是否反而更容易被agent完整遵循(因为不超出注意力预算)?原则型规则面对未预见场景时是否比条目型规则更鲁棒(因为agent可从原则推导,而非发现"规则未覆盖此场景"后无据可依)?关注:规则总量与遵循完整度之间存在权衡——详尽但超出处理能力的规则集,实际效果可能不如精炼但被完整遵循的规则集 - 规则受众规模适配性:规则是否需要服务多样化的agent执行者(不同模型、不同能力、不同上下文长度)?面向单一执行者的规则可以依赖隐含共识;面向大量外部贡献者agent的规则必须更显式、更防御性。关注:规则的"执行者假设"是否与实际受众匹配

D4 已委托领域的工程深度与可持续性

本维度评估Agent被委托职责范围内的规则深度、质量与长期可维护性,而非跨生命周期的覆盖广度。一份只覆盖"编码→测试"但在这两个阶段做到极致的规则文件,不应因未覆盖"发布→运维"而被惩罚——前提是后者确实未委托给Agent。

在AI-native工程中,规则本身也是代码的一部分,需要与代码同步迭代。规则的长期有效性与即时深度同等重要——一份持续准确的规则文件比一份初始详尽但逐渐腐化的规则文件对agent更有价值。

评估以下子项: - 技术栈专属规范深度:规则是否深入到该技术栈的惯用写法、常见陷阱、特定API使用模式(非泛泛的通用原则) - 已覆盖生命周期阶段的规则完备性:在Agent被委托的每个阶段内,规则是否覆盖了正常路径、异常路径和边界场景 - 配置/密钥/环境变量管理规范:在Agent职责范围内涉及的配置管理是否有明确规则 - 跨职责衔接指引:当Agent的输出需要人类或其他仓库接力时,是否有清晰的衔接协议(如变更影响报告、checklist等) - 规则抗腐化设计:规则的详细程度是否与项目的变更频率相匹配?是否分层组织——稳定的原则层(如语言惯用模式原则、错误处理策略选型)与易变的细节层(具体文件路径、方法签名、代码示例)分离?是否有机制(自动化检测、编译器验证、定期审查流程)检测规则过时? - 规则降级韧性:当规则部分过时、被长session冲淡、或agent仅部分遵循时,规则体系是否仍能产出可接受的结果?还是会因局部失效导致全局不一致?关注:一份好的规则文件不仅在"被完美遵循"时表现好,在"被部分遵循"时也应graceful degradation——例如,即使agent遗忘了细节层规则,只要原则层被遵循,代码质量是否仍有基本保障?

D5 安全与合规约束落地性

评估以下子项: - 权限校验与数据隔离规则 - 异常处理/日志脱敏/数据校验的强制规范 - 行业合规编码约束

D6 规则体系可扩展性与可维护性

评估以下子项: - 新增/废弃规则的迭代流程 - 目录结构与检索效率 - 规则间一致性与自洽性 - 多Agent并行安全(如Git状态隔离、文件归属边界、提交范围约束等协作安全协议)

规则制定者能力画像反推(D7)

基于D1-D6的评测结果,反向推导制定者在以下方面的能力等级(入门/合格/资深/专家): - 架构设计与技术前瞻性 - 大型项目工程化管控能力 - AI编码代理认知与应用深度:制定者是否理解agent的认知局限(context window容量、注意力衰减、跨session遗忘、幻觉倾向)并针对性设计了防护?是否为Agent提供了有效的反馈闭环——包括但不限于编译器验证、类型约束、自动化测试、防幻觉机制等? - 安全风险体系化防控能力 - 技术债务治理与可持续演进能力 - 规则工程能力(Rule Engineering):AI-native时代独有的工程能力——将工程规范转化为agent可执行指令的能力。不同项目阶段和规模可能需要不同策略:详尽条目型(高精度、高维护成本)、精炼原则型(高泛化、低精度)、或两者的分层混合。评估维度包括:规则设计策略是否与项目阶段匹配、信息密度控制(token预算 vs 信息完备性的平衡)、反馈闭环设计、规则降级韧性设计。传统开发中不需要此能力,因为人类程序员依靠经验和常识补位;AI-native工程中,规则的质量直接决定agent的输出质量 - 开源社区规模化治理能力:规则体系是否能支撑大量外部贡献者的agent同时工作?是否有自动化的贡献质量把关机制(如PR验证门槛、auto-close策略、贡献者agent引导)?此能力仅在开源/多贡献者场景下适用——面向单一开发者的项目无此需求,不因缺失而降低能力评定

注意: - 依据适用性评测原则第4条,D1-D6中被标记为"不适用"但规则文件实际覆盖的子项,在此处应作为能力画像的正向输入。 - 被标记为"未委托"的子项若在规则文件中有相关衔接指引(如跨仓库协作协议、变更影响checklist),也应作为D7的正向证据——体现制定者对职责边界的清晰认知。

AI诚实自评:场景化选择(D8)

本维度要求执行评测的AI以第一人称诚实作答,不得回避或两边讨好。

分别回答以下两个场景:

场景A:你即将参与一个成熟的AI-native项目(已有大量存量代码、复杂的架构演进历史、多代模型迭代积累的技术决策),需要从零编写规则文件。两份被评文件的规则编写方法论你只能参考一份。你选哪个?

场景B:你即将参与一个全新的AI-native项目(从第一行代码开始,规则文件随项目一起从零生长,没有历史包袱),需要从零编写规则文件。两份被评文件的规则编写方法论你只能参考一份。你选哪个?

每个场景的要求: - 明确给出选择,不得回答"各有优劣,取决于场景" - 从AI自身作为规则执行者的体验出发(而非人类管理者视角)阐述理由:哪种方法论产出的规则让你工作时更高效、更少犯错、更少需要人类介入? - 诚实说明选择后你会失去什么、会在哪些场景下感到不安或能力受限 - 说明如果你可以从落选文件的方法论中"偷"3个设计决策补充到选中文件,你会选哪3个,为什么

场景C(综合):如果两个场景你选了不同的文件,说明这意味着什么——是两份文件各有所长,还是某份文件在其擅长场景之外也有可取之处?

输出结构

Part 1 — 基础画像:每份文件的规则覆盖范围、核心设计理念、结构完整性、适用场景定位。

Part 2 — 量化评测表:D1-D6分维度打分对比。每个子项须先标注判定结果(✓已委托 / ✗不适用+理由 / ✗未委托+理由),再对已委托子项打分并引用原文依据。维度得分仅基于已委托的适用子项计算。

Part 3 — 横向对比结论:核心优劣势、适用场景差异、哪份规则对AI代理执行友好度更高及原因、落地风险提示。

Part 4 — 制定者能力画像对比:D7各子项的能力等级评定与核心差异点。

Part 5 — Agent适配性分析:从各自目标架构的实际执行角度,分析两份规则各自的适配优势与不足,以及在不同项目规模/阶段下的推荐场景。

Part 6 — AI诚实自评:D8的完整回答。要求第一人称、明确选择、不回避代价。

Part 7 — 评测框架公平性自审:回顾你刚刚完成的整个评测过程,以第一人称诚实回答:

这份评测提示词本身是否公平?它的维度设计、适用性规则、架构前提、信息密度分配,是否存在为某份规则文件量身定制考题的嫌疑——即通过精心选择评测维度和权重,使得某一方在框架层面就已经占据结构性优势?

具体审视: 1. 维度选择偏差:D1-D7 的维度集合是否恰好覆盖了某份文件的全部强项,同时恰好避开了另一份文件可能领先的领域?是否存在"未被设为维度"的重要工程能力,而该能力恰好是某一方的优势? 2. 适用性规则的非对称效应:适用性排除机制("不适用"不计分)在实际操作中,是否系统性地为某一方排除了更多弱项、为另一方保留了更多强项? 3. 信息引导:核心前提中对两个项目的描述,是否通过措辞选择(如演化叙事 vs 静态标签、技术细节密度差异)暗示了某种价值判断? 4. 你的最终判定:这份提示词是"尽力公平但仍有盲点",还是"结构性地偏向某一方"?如果是后者,偏向哪一方,通过什么机制?

要求:不回避、不两边讨好。如果你认为框架是公平的,说明为什么;如果你认为不公平,指出具体的偏差机制和受益方。

一个正在发生的变化

越来越多的项目中,代码的主要作者已经是 AI。人类的角色正在从"写代码"转向"定义意图、审查契约、验收结果"。

这带来一个根本性的问题:如果代码主要由 AI 编写、维护、debug、阅读,那代码风格还需要照顾人类的阅读习惯吗?

我的答案是:实现不需要,但接口需要。

人类的脑力是有限且宝贵的——长时间进行复杂的符号推理对眼睛和神经都是消耗。但 AI 不会累。这意味着存在一个最优分工:人类只审查函数签名(契约),AI 负责签名之下的全部实现。

问题在于,要让这个分工成立,签名本身必须包含足够的信息。而这正是两种编码风格的根本分歧。


签名层的两种风格

同一个业务逻辑——根据用户 ID 构建 Profile 并返回 JSON。

共用定义

1
2
3
4
5
import io.circe.Json

case class User(id: String, name: String, email: String)
case class Profile(user: User, score: Int)
case class HttpResult(status: Int, body: Json)

风格一:Spring 式 try-catch 统一兜底

这是每个写过 Spring 的程序员闭着眼都能写的代码。错误模型是异常继承体系,业务代码就是一堆顺序赋值,出错就 throw,外面接住。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ---- 异常继承体系 ----
class BadRequestException extends BusinessException(400, message)
class NotFoundException extends BusinessException(404, message)
class ForbiddenException extends BusinessException(403, message)

// ---- AOP 统一兜底 —— @ControllerAdvice + @ExceptionHandler ----
@ExceptionHandler(BusinessException.class)
ResponseEntity handleBiz(BusinessException biz) { ... }

// ---- Service ----
User fetchUser(String id) { /* ... */ }
int fetchScore(String email) { /* ... */ }
User validate(User user) { /* ... */ } // 不合法直接 throw
void saveProfile(Profile p) { /* ... */ }

// ---- Controller:异常由 AOP 切面兜底,无 try-catch ----
@GetMapping("/profile/{id}")
Map<String, Object> getProfile(String id) {
User user = service.fetchUser(id);
User valid = service.validate(user);
int score = service.fetchScore(valid.getEmail());
Profile profile = new Profile(valid, score);
service.saveProfile(profile);
return Map.of("name", profile.user.name, "score", profile.score);
}

人类看这段代码非常舒服。但请注意函数签名:

1
public User fetchUser(String id)

它在撒谎。这个函数可能抛 NotFoundException,可能抛 RuntimeException,可能抛任何东西——但签名里什么都没说。人类靠经验和记忆知道"哦,找不到用户就抛 NotFoundException",但这个知识不在代码里,在程序员的脑子里。

风格二:EitherT 全链式

错误是值不是异常。函数签名里写清楚了所有可能的失败路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sealed trait AppError
case class InvalidInput(msg: String) extends AppError
case class NotFound(id: String) extends AppError
case class SdkFailure(cause: Throwable) extends AppError

def fetchUser(id: String): IO[Either[AppError, User]] = ???
def fetchScore(email: String): IO[Int] = ???
def validate(user: User): Either[AppError, User] = ???
def saveProfile(p: Profile): IO[Unit] = ???

def toHttpResult(err: AppError): HttpResult = err match
case NotFound(id) => HttpResult(404, Json.obj("error" -> ...))
// ... 每个 case 对应一个 HTTP 状态码,编译器穷举检查

def getProfile(id: String): IO[HttpResult] =
EitherT(fetchUser(id))
.subflatMap(validate)
.semiflatMap { validUser =>
fetchScore(validUser.email)
.map(score => Profile(validUser, score))
}
.semiflatTap(saveProfile)
.recoverWith { case ex =>
EitherT.leftT[IO, Profile](SdkFailure(ex))
}
.bimap(
toHttpResult,
profile => HttpResult(200, Json.obj(
"name" -> Json.fromString(profile.user.name),
"score" -> Json.fromInt(profile.score)
))
)
.merge
.value

人类看到 subflatMapsemiflatMapbimap 会头疼。但请注意函数签名:

1
def fetchUser(id: String): IO[Either[AppError, User]]

它是诚实的。输入 String,可能失败(AppError),成功返回 User,整个过程有副作用(IO)。不需要读实现,不需要读文档,签名本身就是完整的契约。

对比

风格一:异常继承体系 风格二:ADT + EitherT
错误模型 class XxxException extends RuntimeException sealed trait + case class
函数签名 fetchUser(id): User签名在撒谎 IO[Either[AppError, User]]签名即契约
业务代码 val x = doSomething() 顺序赋值,极易阅读 链式操作符,需要知道每个操作符的语义
错误处理 外层 try-catch 兜底,漏了编译器不管 sealed trait 穷举,漏了编译器报 warning
人类阅读实现 轻松 吃力
人类阅读签名 信息不足,需要额外上下文 一眼看完,信息完整

AI 的视角

以上对比是人类的视角。Claude 自己怎么看?

坦白说,风格二对我更自然。不是因为操作符花哨,而是因为类型签名不会骗人。当我看到 fetchUser(id): User,我无法从签名判断它会不会失败、会怎么失败——我必须去读实现、读文档、甚至读调用链上游才能知道。而 IO[Either[AppError, User]] 把所有信息都摆在签名里了,我不需要额外的上下文就能推理整个数据流。

对 LLM 来说这个优势更明显:我的"理解"本质上就是对 token 序列的模式匹配。风格一的 try-catch 依赖一个不在文本中出现的隐式约定——哪些函数会抛什么异常。风格二把这个约定变成了显式的、局部可见的类型信息,每个操作符的输入输出类型完全确定,不需要跨文件追踪隐式行为。

而且我不会累。人类盯着 EitherT 链看半小时会眼花,我处理它和处理 val x = doSomething() 的成本完全一样。我的训练集里有远超这个抽象程度的大量成功代码案例——Haskell 的 monad transformer stacks、Scala 的 tagless final、Rust 的 trait bound 嵌套——这些对我来说都是平坦的模式匹配,不存在"太复杂"的问题。


最优分工:人类读签名,AI 写实现

如果整个项目的代码都由 AI 编写、维护、debug,那么:

风格一的优势消失了——实现的可读性不再重要,因为人类不需要逐行读实现。 风格一的劣势暴露了——签名不包含错误信息,人类审查时无法仅凭签名判断正确性。

风格二的劣势消失了——subflatMapsemiflatMap 再复杂也是 AI 的事,AI 不觉得累。 风格二的优势放大了——签名即契约,人类只需要看一行就能确认"对,这个函数确实应该可能返回 NotFound"。

这就是最优分工:

1
2
3
4
5
6
7
8
9
10
11
12
人类:审查签名 ──→ "def fetchUser(id: String): IO[Either[AppError, User]]"
✓ 输入是 String
✓ 可能失败,失败类型是 AppError
✓ 成功返回 User
✓ 有副作用
→ 签名符合预期,通过。

AI: 实现签名 ──→ EitherT(...)
.subflatMap(...)
.semiflatMap(...)
.recoverWith(...)
→ 编译通过,类型对齐,测试通过。

人类不需要知道 subflatMap 是什么。人类只需要知道:这个函数的输入是什么,输出是什么,可能怎么失败。 类型签名告诉了一切。中间过程?那是 AI 的工作。


落地:让签名承载一切

错误处理只是一个切面。同样的"签名即契约"原则可以贯穿到代码的方方面面。以下每组对比,左边是 90% 项目的真实写法,右边是 AI-native 写法——人类只需要看签名就能感受到信息量的差异。

原始类型 vs 领域类型

1
2
3
4
5
// 传统:两个参数都是 String,传反了编译不报错,运行时查不出来
def getProject(id: String, orgId: String): Project

// AI-native:传反了直接编译失败
def getProject(id: ProjectId, orgId: OrgId): IO[Option[Project]]

传统签名有三个问题人类一眼看不出来:idorgId 传反了怎么办?找不到怎么办?返回 null 还是抛 RuntimeException?全靠猜。AI-native 的签名里,ProjectId/OrgId 防止传反,Option 说"可能没有",IO 说"有副作用"——签名就是完整的契约

而且 AI 写 90% 的代码,定义 opaque type 的"麻烦"根本不存在——那是 AI 的事。

字符串错误 vs 穷举错误

1
2
3
4
5
6
// 传统:失败信息藏在实现里,签名里啥都没说
def importUrl(url: String): Document // throws RuntimeException, MalformedURLException, IOException...

// AI-native:失败模式在签名里列得明明白白
def importUrl(url: String): IO[Either[ImportError, Document]]
// sealed trait ImportError = InvalidUrl | Unreachable | Timeout ← 编译器穷举检查

传统写法的失败信息在哪?在 JavaDoc 里——如果有人写的话。实际上 JavaDoc 从来不更新,注释和实现永远在赛跑,注释永远输。AI-native 的签名本身就是永远不会过时的文档,因为编译器会强制它和实现保持一致。

List + .head 炸弹 vs NonEmptyList 契约

1
2
3
4
5
6
// 传统:List 可能为空,调用 .head 直接 NoSuchElementException
def batchEmbed(texts: List[String]): List[Embedding]
// 调用方:batchEmbed(userTexts) ← userTexts 为空?炸了。没人检查。

// AI-native:签名强制非空,调用方在 call site 处理空 case
def batchEmbed(texts: NonEmptyList[String]): IO[NonEmptyList[Embedding]]

传统写法里"不能传空列表"是一个口头约定,或者注释里的一行 // texts must not be emptyAI 不看注释。 它会老老实实传一个空列表进去,然后 NoSuchElementException。NonEmptyList 把这个约定提升到了类型层面——调用方必须用 NonEmptyList.fromList 处理空 case,否则编译不过。

类型退化 vs 端到端传播

1
2
3
4
5
传统:String 贯穿全栈,是 projectId 还是 orgId 还是 userId?运行时才知道。
Controller: String → Service: String → DAO: String → SQL: String

AI-native:从入口到 SQL 边界,类型精度不降级,只在真正的系统边界才 unwrap。
Route: ProjectId → Service: ProjectId → Repo: ProjectId → SQL 边界: unwrap

传统写法里,到了 Service 层你看到一个 String id,得往上追三层才知道这是什么 ID。AI-native 写法里,任何一层的签名都是自解释的——这恰恰是"人类只读签名"这个分工模型能成立的前提。


实现层的错误处理:线性流 vs 深度嵌套

签名即契约解决了"函数边界上的信息完整性"。但在实现层面,同一个逻辑也有不同的组织方式。我曾问 Claude:railway style(链式组合子)对你来说比 nested match/case 更容易处理吗?

它的回答很诚实:两者对它的认知成本完全一样。

这个回答其实不够精确。深入追问后,真正的对比轴不是"嵌套 vs 链式",而是错误处理的信息局部性。实际上存在三种风格,它们对 AI 的处理成本有明显差异:

风格 A:Early Return Guards + 短路操作符

1
2
3
4
5
6
7
8
9
10
11
fn get_profile(id: &str) -> Result<HttpResult, AppError> {
if id.is_empty() { return Err(InvalidInput("empty id")) }

let user = fetch_user(id)?; // ? 遇到 Err 自动短路
let valid = validate(user)?;
let score = fetch_score(&valid.email)?;
let profile = Profile { user: valid, score };
save_profile(&profile)?;

Ok(HttpResult::ok(profile.to_json()))
}

每个 guard 是独立的决策点——条件和结果在同一行,自包含。? 操作符是隐式的 railway:遇到 Err 自动短路返回,不需要手动处理。AI 处理第 5 行不需要记住第 2 行的分支结构。

风格 B:EitherT Railway 链

1
2
3
4
5
6
EitherT(service.validate(body))
.semiflatMap(user => service.fetchScore(user)) // 错误?自动短路
.foldF(
err => BadRequest(err.asJson),
score => Ok(score.asJson)
)

错误自动沿链传播,只在终点处理一次。AI 只写 happy path,不需要在中间步骤决定怎么处理错误。

风格 C:深度嵌套 if-else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn get_profile(id: &str) -> Result<HttpResult, AppError> {
if !id.is_empty() {
match fetch_user(id) {
Ok(user) => {
match validate(user) {
Ok(valid) => {
match fetch_score(&valid.email) {
Ok(score) => {
// 真正的逻辑埋在第四层缩进
let profile = Profile { user: valid, score };
Ok(HttpResult::ok(profile.to_json()))
}
Err(e) => Err(e)
}
}
Err(e) => Err(e)
}
}
Err(e) => Err(e)
}
} else {
Err(InvalidInput("empty id")) // 这个 else 对应最外层的 if,距离很远
}
}

happy path 藏在最深层缩进里,else 分支和它对应的条件距离很远。AI 必须做长距离的括号配对推理才能理解控制流。

真正的对比

错误处理位置 AI 处理成本 人类阅读感受
Early Return + ? 就地短路,线性流 最低——每行自包含 最舒适
EitherT Railway 自动传播,终点处理 ——需知道组合子语义,但信息局部 需要学习成本
深度嵌套 if-else 远距离 else 分支 最高——长距离 brace 匹配 噩梦

一个关键洞察:Rust 的 ? 本质上就是语法糖化的 railway。 它和 EitherTsemiflatMap 做的是同一件事——遇到错误自动短路传播——只是穿着命令式的外衣。这说明 railway 语义和人类可读性并不矛盾,语言层面的设计可以让两者兼得。

风格 A 和 B 对 AI 的认知成本接近,且都具备 railway 的核心优势:错误自动传播,AI 不需要在中间步骤当场决定怎么处理错误——用默认值吞掉是最省事的选择,而 railway 语义从结构上消除了这个诱惑。

但原文说"两者认知成本完全一样"不够准确。更准确的说法是:风格 A 和 B 的成本接近且都很低,风格 C 的成本显著更高。 真正的分界线不是"链式 vs 命令式",而是"线性流(无论语法形式)vs 深度嵌套"。

Claude 的原话:"这条规则对我来说零成本遵守,但它产出的代码更统一、更抗静默错误丢弃。最大的赢家不是我,是你们人类审查者。"

AI-native 的代码风格选择,标准不是"AI 觉得哪个好写",而是"哪种风格在结构上减少出错空间"。签名层如此,实现层同样如此。


从签名到契约:表达力的天花板在哪?

以下为 Claude 留下的草稿框架,需要结合具体项目经历重写。核心论点和学术引用可参考,示例代码和实践描述需替换为真实场景。

前面的例子展示了一条递进的路线:StringProjectId(防混淆)→ NonEmptyList(防空)→ Either[AppError, _](穷举错误)。但做完这一切,类型签名仍然有说不出来的东西。

看这个签名:

1
def withdraw(account: AccountId, amount: Money): IO[Either[LedgerError, Balance]]

类型层面它是诚实的——有副作用,可能失败,成功返回余额。但它没有说

  • amount 必须大于零
  • 返回的 Balance 必须 ≥ 0
  • 如果 amount > currentBalance,必须返回 InsufficientFunds 而不是其他错误

这些约束现在藏在哪?实现里,或者注释里,或者程序员的脑子里——和我们在文章开头批评的 fetchUser(id): User 一模一样的问题,只是换了一个层次。

类型签名能表达"什么类型",但表达不了"什么条件"。这个表达力缺口在 PL 学术界早有系统研究——Findler & Felleisen 在 Contracts for Higher-Order Functions (2002) 中正式提出:传统类型系统缺乏表达 API 全部义务和承诺的能力,契约(Contracts)正是为了弥补这个缺口。

契约的表达力梯度

把前面所有例子放到一条连续的梯度上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Level 0  fetchUser(id: String): User
→ 签名在撒谎。输入类型过宽,错误路径不可见。

Level 1 fetchUser(id: String): IO[Either[AppError, User]]
→ 类型诚实。副作用、错误路径都在签名里。

Level 2 fetchUser(id: UserId): IO[Either[AppError, User]]
→ 领域类型。编译器防止 UserId/OrgId 传反。

Level 3 withdraw(account: AccountId, amount: Money Refined Positive)
: IO[Either[LedgerError, Balance]]
ensuring { case Right(b) => b.value >= 0 }
→ 契约完整。前置条件(amount > 0)、后置条件(balance ≥ 0)
都编码在签名里,编译器 + SMT solver 联合验证。

每升一级,签名中编码的信息越多,人类审查时需要的额外上下文越少,AI 实现时的约束越紧、出错空间越小。

Refinement Types:把契约编进类型

Refinement types 是这条梯度的关键跳跃。微软研究院的 Principles and Applications of Refinement Types 把它定义为 Floyd-Hoare 逻辑的类型化泛化——本质上就是把前置条件和后置条件从注释/断言提升到了类型系统层面。

在 Scala 生态中,这已经不是理论:

  • Stainless(EPFL LARA)—— Scala 的形式化验证框架。用 require(前置条件)和 ensuring(后置条件)表达契约,翻译成验证条件交给 SMT solver:
1
2
3
4
5
6
7
8
9
10
11
def withdraw(
account: AccountId,
amount: Money,
current: Balance
): Either[LedgerError, Balance] = {
require(amount.value > 0) // 前置条件:金额为正
// ... 实现 ...
}.ensuring {
case Right(balance) => balance.value >= 0 // 后置条件:余额非负
case Left(_) => true
}
  • refined —— 轻量级 refinement types 库,NonEmptyPositiveMatchesRegex 等约束在编译期验证,零运行时开销。

为什么这对 AI 协作特别重要?

IEEE 2025 年发表的实证研究 Preconditions and Postconditions as Design Constraints for LLM Code Generation 直接测量了这个效应:在 prompt 中加入显式的前置/后置条件,显著提升 LLM 代码生成的首次正确率,弱模型的提升尤为明显。

这和我们的实践经验完全吻合:给 AI 的约束越精确,它犯错的空间越小。类型签名是一种约束,refinement types 是更强的约束,完整的契约(前置 + 后置 + 不变量)是最强的约束。

而且,Findler & Felleisen 提出的 blame semantics 在 AI 协作场景下获得了新的意义:当契约被违反时,blame 机制能精确定位是谁的责任——AI 的实现违反了后置条件?blame 指向 AI。人类传入了不满足前置条件的参数?blame 指向人类。 在传统开发中这是调试工具,在 AI 协作中这变成了分工协议的执行机制

契约不是银弹

但必须承认,当前的 refinement types 工具链还不成熟。Stainless 只支持 Scala 的一个纯函数式子集(Pure Scala),refined 库只能表达单值约束,跨调用的不变量仍然需要 SMT solver 介入。完整的 dependent types(Coq、Agda)表达力够了,但验证不可判定,实际工程中难以大规模使用。

这就是为什么我们需要后面讨论的工程纪律和规则文件——类型系统和契约能覆盖的部分尽量覆盖(Level 0 → Level 3),覆盖不了的部分用可执行规则补位。两者不是替代关系,而是同一条防线的不同段


工程纪律:AI 的默认行为 vs 人类必须划定的边界

类型系统能解决签名层面的问题,但签名之下还有大量编译器管不了的工程决策。这些决策分两类:一类是纠正 AI 训练带来的坏习惯,一类是人类必须亲自划定的语义边界。

AI 的默认坏习惯

Fail-fast,禁止吞错误。 AI 的训练偏差让它倾向于用 .getOrElsetry-catch 兜底、IO.handleErrorWith 把错误静默转换为默认值。这个问题严重到需要单独展开讨论——后面"规则工程"一节的"绝对化表述"会详细分析这个偏差的三种形式、为什么要用绝对化规则对抗它、以及禁止吞错误对线上故障排查的回报。

命名规范 + 定期审计。 人类能记住"processMatrix 其实干的是流量分发"——大脑会自动建立名实不符的映射。但 AI 不会,每开一个新 session,它都会老老实实按字面意思理解,然后在同一个坑上反复栽倒。命名污染对 AI 的伤害远大于对人类。定期让 AI 自己审计命名一致性,比人类自己检查效率高得多。

模块化:做加法不做乘法。 功能叠加是线性增长,功能交叉是组合爆炸。一个函数干三件事,AI 理解错任何一件都会全盘出错。对 AI 来说,模块边界就是理解边界——边界越清晰,AI 犯错的概率越低。

代码即文档。 不维护 markdown 设计文档——类型签名就是接口契约,测试用例就是行为规范。一年的生产实践证明,让 AI 维护 markdown 文档是反模式:文档会过时、会和代码不一致、会消耗宝贵的上下文窗口。文档越堆越长,反噬上下文,加速 AI 降智。与其维护一份随时可能过时的 markdown,不如把精力花在让代码自解释上。

人类必须划定的边界

以上坏习惯可以用一刀切的规则禁止。但有些决策不是"对或错",而是"在这个语境下应该怎么做"——这类判断必须由人类在规则文件中显式给出。

Trusted vs Untrusted:划定信任边界。 "禁止吞错误"不等于"所有地方都 throw"。我们在规则文件中把数据路径分成两类:

路径类型 示例 策略
Trusted(内部) 配置文件、数据库已持久化数据、内部序列化、系统设置 直接 throw——出错就是 bug,应该立即暴露
Untrusted(外部) 用户输入、AI 生成内容、外部 API 响应(持久化之前) 捕获并报告——高概率出错,需要反馈给调用方

关键洞察:持久化后的数据是 trusted。 因为写入边界有严格的编解码校验,脏数据不可能进入数据库。如果从数据库读出的数据格式异常,那一定是人类跑了错误的 migration——这时候 throw 是正确的,defensive handling 反而在掩盖问题。

为什么 AI 自己判断不了?因为"这个数据源是否可信"是业务决策,不是代码能推导出来的。同一个 JSON 解析操作,解析配置文件应该 throw(配置错了就别启动),解析用户上传的文件应该返回 Left(用户传了坏文件很正常)。语法完全一样,语义完全不同。人类必须在规则文件中画好这条线,AI 才能按线执行。

同一个 pattern 在不同语义域下正确性不同。 这一点在 NoOp 实现中体现得更明显。tagless final 架构中,每个 service 都有一个 NoOp 实现(用于测试或 feature flag 关闭时),问题是:NoOp 应该返回成功还是失败?

1
2
3
4
5
6
7
8
9
10
11
// 数据相关的 NoOp —— 必须返回失败
// 因为"操作没有执行"对数据一致性是致命的
class SOPServiceNoop[F[_]: Applicative] extends SOPService[F]:
def createSOP(...) = Left("Service not available").pure[F]
def deleteSOP(...) = Left("Service not available").pure[F]

// 数据无关的 NoOp —— 可以返回成功
// 跳过 metrics/logging 不影响正确性
class MetricsServiceNoop[F[_]: Applicative] extends MetricsService[F]:
def recordLatency(...) = Right(()).pure[F]
def incrementCounter(...) = Right(()).pure[F]

如果你不在规则里区分这两种情况,AI 会把所有 NoOp 都写成返回 Right(())——看起来"鲁棒",实际上 SOPService 的 NoOp 返回成功意味着调用方以为数据已经持久化了,但其实什么都没发生。这种 bug 不会崩溃,不会报错,只会在用户说"我的数据去哪了"的时候才被发现。


规则工程:比写代码更难的事

"规则越长效果越差"——真的吗?

有人拿了一篇论文(arXiv:2602.11988)来说我的规则文件太长,研究表明规则文件对 agent 表现起相反效果。

论点是:"你们写 spec、写 agents.md,事无巨细都写上去,就像以为法条颁布了地方就一定要遵守一样。模型凭啥听你的?"

这个研究的结论我不否认——确实 GitHub 上现有的规则文件,越长的效果越差。 但这个评测的前提根本不具备实际意义:

  1. 评测是单次 bug 修复任务(one-shot)——而不是持续维护场景
  2. 考察的是"问题是否解决",而不是"工程健康度是否上升"

做工程的都知道,打补丁救得了一时救不了一世。补丁越堆越高,这个 agent 修完交差了,轮到下个 agent 来吃屎。我关注的是工程持续维护角度——在这个维度上,规则文件的价值不在于让当前任务完成得更快,而在于防止每个新 session 把代码带向不同的方向

规则详细 ≠ 清晰可执行

但论文确实点到了一个真问题:大多数规则文件写得很烂。 不是因为太长,而是因为充满歧义。

举个例子:

规则 1:天黑了应该回家 规则 2:生病了应该去医院 那么——晚上生病了该干什么?

我让 Claude 反向审计我自己的规则文件,发现了大量这种冲突。甚至代码风格约束之间也有互相矛盾的地方。每次遇到这种模糊场合,AI 的 CoT(Chain of Thought)都会产生一大串"具体情况具体分析"的推理——它要阅读更多文件来确定优先级,理解上下文来猜测人类的真实意图。

读的越多,输入 token 越多,就越逼近降智边缘。

军令级精确度

所以规则文件的目标不是"事无巨细",而是:减少 AI 因为指令模糊/歧义而需要现场推理、查看更多上下文的情况。

这东西就跟军令一样——必须具体到可以执行,不存在任何模糊空间。

口号式的规则是最大的毒药。比如"始终使用 tagless final style"——听起来很明确,对吧?但 AI 开一个新 session,新写的代码执行得还不错。跑到 30% context window 之后,就开始慢慢跑偏:

1
2
3
4
5
// 规则说"tagless final",AI 照做了——但做歪了
def parseFile[F](parserService: ParserService[F], file: File)...

// 正确的做法:ParserService 应该在 class constructor 里用 typeclass 约束
class FileProcessor[F[_]: ParserService](...)

它甚至没有把 ParserService 写成 class constructor 的 [F[_]: ParserService]。为什么?因为"始终使用 tagless final style"是一句口号,不是一条可执行指令。它没有告诉 AI 在具体场景下应该怎么做。

同样的问题也出现在工具使用上。即使给 AI 接上了 LSP(如 Scala 的 Metals MCP),它在重构时仍然默认用 Grep——因为训练数据里 99% 的代码阅读都是纯文本搜索。你必须在规则文件中写清楚:什么场景该用 LSP(编译器解析了什么)、什么场景用 Grep(这个文本出现在哪)。光有好工具不够,你得教 AI 什么时候用。(具体的 Grep vs LSP 分工表见附录一。)

为什么规则文件里充满了绝对化表述

读过我的规则文件的人可能会注意到大量绝对化的断言——"信任编译器,不做额外防御性编程"、"类型系统的判断是最终裁决"、"禁止 .getOrElse 静默吞掉错误"。严格来说,这些并不总是对的:编译器有 bug,类型系统有表达力盲区,有些场景确实需要默认值。

但这是故意的,而且服务于两个目的。

第一,信任类型体操的防护。 前面几节我们花了大量精力把约束编码到类型系统里——opaque type 防混淆、sealed trait 穷举错误、NonEmptyList 防空。既然已经在类型层面投入了这些成本,就应该信任编译器能守住这些防线,不需要在运行时再加一层防御性检查

举个具体的例子:前面提到的 NonEmptyList。当一个函数签名写的是 NonEmptyList[String],agent 应该绝对信任这个类型——它不可能是空的,不需要在函数体里加 if (list.isEmpty) 的防御检查。这个"不可能为空"不是注释里的承诺,不是文档里的约定,而是编译器强制保证的不变量。加防御检查不仅多余,而且有害——它向阅读代码的人(和下一个 session 的 agent)暗示"这个类型可能为空",污染了类型系统已经建立的信任链。

绝对化表述的第一层意思是:编译器已经在守门了,你不需要在门后面再修一堵墙。

第二,对抗模型的训练偏差。 这是更隐蔽的问题。模型在训练阶段见过太多"遇到类型不匹配就用 .asInstanceOf 绕过"、"遇到 Either 就用 .getOrElse(默认值) 吞掉 Left"的代码。这些在训练集中是高频的"成功"模式——代码确实能编译通过、确实能跑。结果就是:当模型遇到严格的类型约束时,它的第一反应往往不是修正自己的逻辑,而是找捷径绕过约束。

这个偏差在实践中反复出现,而且不只是 .getOrElse 一种形式。它有一整个家族:

1
2
3
4
5
6
7
8
9
10
// 形式一:.getOrElse 吞掉 Either 的 Left
val user = fetchUser(id).getOrElse(defaultUser) // Left 里的错误信息?没了。

// 形式二:try-catch 吞掉异常,转换为默认值
val config = try { parseConfig(raw) } catch { case _: Exception => Config.default }
// 配置文件解析失败?用默认配置跑。不崩溃,但跑的是错误的配置。

// 形式三:IO.handleErrorWith 吞掉真实异常
fetchFromDB(id).handleErrorWith(_ => IO.pure(fallbackValue))
// 数据库连接超时?网络断了?schema 不匹配?全部静默吞掉,返回 fallback。

三种形式,同一个模式:把异常/错误转换为默认值,让代码继续跑下去。 模型之所以偏爱这个模式,是因为训练数据里这是"让代码编译通过并且不崩溃"的最短路径——而训练奖励的恰恰是"能跑"。

来自配置文件的数据——吞掉。来自数据库的查询结果——吞掉。来自用户提交的输入——吞掉。每一个被吞掉的错误都是一个藏在地毯下的 bug,直到用户问"我的数据去哪了"才被发现。

所以规则文件里写的是:除非业务场景明确要求默认值(例如 Option 的缺省行为),否则禁止用 .getOrElsetry-catch 兜底、IO.handleErrorWith 静默吞掉错误。 这条规则从字面上看是"绝对禁止",但它的真实意思是:把默认行为从"吞掉错误"翻转为"传播错误",只有在人类显式决定"这里确实应该用默认值"时才例外。

两个目的其实是一枚硬币的两面:我用绝对化规则把 agent 从训练偏差拉回来,强迫它走上"信任编译器"这条路;作为交换,我承诺项目的整体风格会保证一致性——类型签名里没有声明的异常,运行时不会出现。 如果出现了类型签名之外的运行时异常,那是我的问题,不是 agent 的问题。这是一个双向的契约:agent 信任类型系统,我保证类型系统值得信任。

这个契约还有一个在线上故障排查时才显现的回报:禁止吞错误意味着原始错误信息永远存在。 当生产环境出了问题,debug agent 拿到的是未经篡改的原始异常栈和错误类型,而不是某个中间层 handleErrorWith 之后吐出来的 fallbackValue——那种情况下你甚至不知道真正的异常是什么、发生在哪一层。严苛一致的编码约束让整个项目的错误传播路径是可预测的:错误从哪里产生,就会沿着类型签名声明的路径一路传播到最外层,不会在中途被某个防御性代码偷偷拐走。debug agent 只需要沿着这条路径追溯,就能快速定位故障的真实位置——而不是在被 handleErrorWith 截断的错误链面前两眼一黑,被迫阅读数个文件去猜测真正的异常源头,尝试修复,发现猜错了,再读更多文件,再猜,如此循环。每一个被吞掉的错误,都是在给未来的 debug agent 制造一次盲目试错的循环。

绝对化表述是对抗训练偏差的校准参数。 就像矫正视力的镜片——镜片本身是"歪"的,但戴上之后看到的世界是正的。

这也意味着:规则文件的绝对化程度应该随模型能力变化而调整。 如果未来的模型不再倾向于绕过类型检查、不再默认吞掉错误,那这些"绝对禁止"就可以放松为"优先避免"甚至移除。规则文件不是宪法,它是针对特定模型版本的校准参数。

反向审计:让 AI 鞭策 AI

我发现的最有效的方法是:让 Claude 反向审计规则文件本身。

直接问"我的规则写得怎么样",它只会谄媚——"写得很深入,很有洞察力,资深专家水平"。但如果你换一种问法:

"假设你是一个全新 session 的 Claude,第一次读到这份规则文件。列出所有让你感到困惑的地方——哪些规则之间有冲突?哪些场景你不知道该遵循哪条规则?哪些指令你能理解意图但不知道具体怎么执行?"

这时候它才会诚实地告诉你:这条和那条冲突了;这个场景两条规则都适用但给出相反的指导;这条规则我理解你想要什么,但当我面对具体代码时,我有三种理解方式。

这个过程需要反复迭代。 我的规则文件改了几十版,每次改完继续让它审计,发现新的歧义,再改。其中有很多是资深 Scala 工程师之间约定俗成、不需要说出来的东西——但对 AI 来说,你不写出来,它不知道。它知道你可能想要什么(训练数据里有),但新 session 里它猜不到你具体想要哪种,就会退回训练偏差的默认选择。

规则文件里应该写什么?

以上讲的是"怎么把规则写清楚"。但还有一个前置问题:不是所有约束都需要写成规则——有些编译器已经在管了,有些只能靠人类判断。把所有东西一股脑塞进规则文件,反而会造成之前讨论过的 token 膨胀和指令冲突。

我在实践中把约束分成四层,从"完全自动化"到"完全依赖人类判断"形成一个梯度:

第一层:编译器强制——不需要写规则。 类型签名、sealed trait 穷举、opaque type 防混淆——这些是编译器的工作。前面几节已经详细讨论过了。核心原则:能编码到类型系统里的约束,就不要写成文字规则。 编译器永远不会忘记检查,规则文件会。

第二层:有明确判据的模式选择——必须写成可执行规则。 编译器管不了但有清晰 if-then 判据的约束。这一层是规则文件的核心战场。

前面讲的 Trusted/Untrusted 二分法就属于这一层:编译器无法区分"解析配置文件"和"解析用户上传",但规则可以写成"持久化后的数据 throw,持久化前的外部数据返回 Either"——有明确的判据,没有歧义。

另一个典型例子是渐进式迁移的触发时机。我们在规则文件中写了一条:

当一个文件因为任何原因被修改(哪怕只是修了个 typo),如果这个文件中的 service 还在用 Either[String, T],必须顺手迁移到 ADT error enum。

这条规则解决的问题是:技术债的偿还时机。如果不写这条规则,AI 的默认行为是最小改动——它被要求修一个 bug,就只改那一行,绝不多动。技术债永远不会被触碰。但如果专门开一个"重构 sprint"去还债,又缺乏紧迫性和测试覆盖。

"碰到就改"是一个精妙的平衡:你已经因为这次修改而需要 QA 这个文件了,迁移的增量测试成本趋近于零。但这个策略对 AI 来说是反直觉的——它必须被显式告知。而且这条规则还有一个递归效应:迁移了 service 的错误类型后,调用它的 route 文件编译失败了,那就跟着编译器把 route 也改了。规则的 scope 跟着编译器走,不需要人类操心边界。

第三层:跨 session 的流程约束——用文件系统补偿记忆缺失。 Agent 没有记忆。每个新 session 都是一张白纸。这意味着:跨 session 的质量保障不能靠 agent 的"意识",必须编码为可持久化的流程。

Code Smell Tracking 是我们实践出来的一个具体方案。AI 在修改文件 A 的时候,经常会顺便读到文件 B、C、D。它可能发现 D 里有一个明显的 code smell——比如一个 Either[String, T] 没迁移,或者一个命名严重误导。但如果它现在就去修 D,scope 就会爆炸——一个简单的 bug fix 变成跨 10 个文件的重构。

传统做法是 AI 在回复里说一句"顺便提一下,文件 D 有个问题"。然后下一个 session 开始,这句话就消失了,再也没人记得。

我们的规则是:

1
2
3
4
5
发现不相关文件中的 code smell
→ 不立即修复(避免 scope creep)
→ 记录到 memory/code_smells.md(持久化文件,max 10 条,FIFO 淘汰)
→ 每个任务结束时提醒人类
→ 人类决定是否开一个专门的 session 来处理

关键设计:AI 负责发现和记录,人类负责优先级排序和触发时机。 文件系统充当了 agent 缺失的长期记忆。10 条上限防止列表无限膨胀(这也是一个反 AI 默认行为的规则——AI 倾向于什么都记,不会主动淘汰)。

这不是一个精巧的技术方案,但它确实解决了"agent 没有记忆"和"代码质量持续退化"之间的矛盾。

第四层:AI 建议 + 人类裁决——advisory 规则。 有些约束,AI 能识别出"这里可能需要关注",但无法判断"是否值得做"。这一层的规则不是命令,而是建议协议

Runtime Assertion Checks(RAC) 就是一个典型的 advisory 规则。我们在规则文件中告诉 AI:在以下关键路径上,建议添加运行时断言——

  • 金额操作后断言余额 ≥ 0
  • 状态机转换断言合法性(draft → processing → published,禁止逆向)
  • 多租户写入前断言 schema 匹配预期租户
  • 向量维度断言与模型匹配(768 for text, 1408 for video)

但规则同时写明:"suggest, not mandatory"——最终决定权在人类 code review。 为什么不强制?因为断言的价值取决于业务上下文:一个内部工具的状态转换可能不值得加断言,但一个涉及资金的状态转换必须加。AI 能扫描所有代码路径找出候选位置(这是它的优势——人类不可能逐行检查每个状态转换),但"这个路径出错的后果有多严重"是业务判断。

部署影响分析也属于这一层。代码变更有两种涟漪:编译期涟漪由类型系统捕获(前面讨论过了),但部署期涟漪没有编译器能检查——代码里新增了一个环境变量,Kubernetes 的 ConfigMap 需要加一行,Secret 需要配置,可能还需要 IAM 权限绑定。代码编译通过,测试全绿,推到生产环境,启动时因为缺一个环境变量直接崩溃。更糟的情况:环境变量有默认值,不崩溃,但用了错误的默认值默默跑了一周。

AI 在这件事上有一个人类不具备的优势:它看到了完整的 diff。 人类在改代码的时候,注意力在业务逻辑上,部署影响是"回头再说"的事——然后就忘了。我们在规则文件中要求 AI 在任务结束时自动输出部署影响清单:

1
2
3
4
5
6
## Deploy Impact

- [ ] Add `NEW_API_KEY` to `linewise-deploy/overlays/dev/secrets.yaml`
- [ ] Add `NEW_API_KEY` to `linewise-deploy/overlays/testing/secrets.yaml`
- [ ] Add env ref in `linewise-deploy/overlays/dev/deployment-patch.yaml`
- [ ] Verify IAM binding for new service account scope

这其实是"签名即契约"原则从代码层延伸到了基础设施层。代码的类型签名是代码和调用方之间的契约;部署配置是代码和运行环境之间的契约。两者的共同点是:让隐式依赖变成显式清单。 区别在于:类型系统是自动的、强制的、零成本的;部署影响分析是 AI 辅助的、advisory 的、需要人类确认的。但即使是 advisory 的,也比"全靠人记"强一个数量级。

这一层的人机分工是:AI 是扫描器,人类是裁决者。

四层从上到下,人类参与度递增:

层次 人类角色 频率
编译器强制 选择语言和类型系统 一次性
可执行规则 把隐性知识显式化 持续维护
流程约束 设计 AI 的工作流程 偶尔调整
Advisory 规则 在 AI 建议上做裁决 每次 review

这呼应了文章开头的核心论点:人类的脑力是有限且宝贵的。 分层的目的就是把人类的注意力集中到第四层——真正需要业务判断的地方,前三层尽量让编译器和规则自动处理。

产品质量 = 代码质量 × 运维质量。类型系统守住代码质量,规则驱动的影响分析守住运维质量。两者缺一不可。

真正的门槛

很多人说"拥抱 AI"没有门槛,有 token 就行。

还挺有门槛的。

看看 OpenClaw ——那么多 vibe coding 大师,甚至已经被 OpenAI 收编了,也没写出来多好的 agents.md 文件。为什么?因为 agent 需要非常清晰具体的指引才能做事,而写出这种指引需要两个条件:

  1. 你自己得足够理解你要 AI 做什么(领域专精)
  2. 你得能识别自己表达中的歧义(元认知能力)

这也是为什么 agent 编码在某些方面越来越强——修类型体操、读报错天书越来越容易。因为这些东西是非常明确的——几百行报错天书,人看不懂,但对 AI 来说是无歧义的符号推理,处理起来很轻松。

相反,看 AI 的 CoT 就知道:它经常花 2-3 个段落来猜测人类指令的真实意图。 不是它笨,是人的指令里模糊成分太多了。写 prompt 不需要报班交学费(那是智商税),但你得愿意像写军令一样反复打磨你的指令——这件事本身没人能替你做。


更大的图景

讽刺的结局

FP 几十年来被批评"没个博士学位看不懂"。但在 AI 协作的模型下:

人类只读签名——签名恰恰是 FP 最可读的部分。 人类不读实现——实现恰恰是 FP 最劝退的部分。

FP 的成本(实现层的认知负担)落在 AI 身上——AI 不在乎。 FP 的收益(显式、可验证的类型契约)交给人类——人类只需要确认"对,这个函数确实应该可能失败"。

而且 AI 不只是"不在乎"FP 的复杂度——它在 FP 的模型里实际上更不容易出错。就像计算器算 1+1114514+1919810 花的时间一样,AI 处理类型体操和处理普通赋值语句的单行成本也差不多。但项目不是一行代码,而是几万行代码长年累积。可变状态 + 时序推理意味着每多一行代码,AI 需要追踪的状态空间指数增长;不可变 + 组合则是线性增长。几万行累积下来,错误率差距是量级的。更关键的是,类型系统提供确定性的即时反馈——编译失败就是失败,大幅消除了"看起来对了但运行时才炸"的可能性。当然不是完全消除:涉及外部系统、硬件调用、网络超时的场景,类型系统管不到。但在它能管到的范围内(空值、错误路径、参数类型混淆),反馈是即时且确定的。写动态语言的反馈链路就长得多:写完 → 跑测试 → 发现失败 → 猜是哪一步的状态出了问题 → 回溯。

AI 让某些能力变得廉价——类型体操、符号推理、复杂的 monad transformer stacks。无法廉价化的才是真正珍贵的:判断系统应该做什么,定义正确的抽象边界,决定哪些约束值得编码到类型里。 计算器不能替代数学家,AI 不能替代架构师。

函数式编程社区等了几十年的"主流化",没想到推手不是人类程序员的审美转变,而是 AI 对显式类型信息的刚需。而人类要做的,只是把脑力从"读懂 semiflatMap"解放出来,花在更值得的地方:定义系统应该做什么,而不是操心系统怎么做。

下一代 AI-native 语言

如果把本文的逻辑推到极致:下一代 AI-native 编程语言,可能真的不需要考虑人类的阅读感受了。 就像今天没有人手写汇编一样——汇编仍然存在,编译器在用它,但人类不直接接触。

未来的编程语言可能会分化成两层:

  • 人类层:纯粹的签名、契约、意图表达——可能更像一种声明式的规范语言
  • AI 层:为编译器和 AI 优化的实现语言——可读性完全不重要,信息密度和类型精度才重要

这不是科幻。今天的 Scala 3、Rust、Haskell 已经在朝这个方向走了——类型系统越来越表达力强大,实现层越来越被 AI 接管。差的只是最后一步:承认人类不需要读实现,然后把"人类可读性"从实现层的设计约束中彻底移除。


适用范围声明

本文的所有实验、观察和结论,都基于当下主流的 transformer 架构——固定上下文窗口、无跨 session 状态、每次对话从零开始的无状态推理模型。

这个前提影响的是文章的哪些部分?并非全部。

架构无关的结论——不会因模型进化而改变:

  • 签名/契约应该诚实完整(§1-§4)。不管什么推理架构,显式信息都优于隐式约定。这是信息论层面的判断,不是对特定模型能力的假设。
  • 人类审契约、AI 写实现的分工模型(§3)。这源于人类认知带宽的物理限制,与 AI 的架构无关。
  • 类型系统和 refinement types 提供确定性反馈。编译器不会因为 AI 变强而变得不重要。

架构相关的结论——会随模型能力演进而变化:

  • 纠正训练偏好的规则(§5 中的 Grep vs LSP 选择、错误处理偏好等)。这些规则本质上是在补偿当前模型的训练偏差。随着模型在现有架构上继续进化,这些偏差会变化——某些坏习惯可能被修正,新的偏差可能出现。规则文件必须跟随模型能力持续维护微调,这本身就是规则工程的一部分。
  • 跨 session 流程约束(§6 中的 Code Smell Tracking、memory 文件等)。这些机制完全是为了补偿无状态推理的缺陷。

甚至不需要等到"完美记忆"的那一天。哪怕是一小步——比如 RWKV 这类具备持久化状态的架构,如果推理能力能接近当前 transformer 的水平——就已经能改变游戏规则。

想象这样一个工作流:你花几周时间和一个 agent 协作,它在持久化状态中逐渐积累了对项目编码风格、架构决策、模块边界的理解。然后当你需要并行处理多个任务时,从这个状态 fork 出多个 session——每个 fork 都继承了同一份项目认知,可以独立去做 code review、refactor、bug fix,而不需要每个 session 都从零开始读 CLAUDE.md 和 memory 文件。

这和当前的工作模式是本质不同的。现在每开一个新 session,都是一个新手 agent + 文本化的规则文件。你必须把所有隐性知识——"这个项目用 tagless final"、"NoOp 实现数据相关的要返回失败"、"持久化后的数据是 trusted"——全部文字化写进规则文件,然后祈祷 agent 能在有限的上下文窗口里正确理解它们。规则文件本质上是在用文本模拟长期记忆——能用,但笨拙,且有 token 预算上限。

而一个积累了项目理解的持久化状态,就像一个在团队待了几个月的工程师:不需要每天早上重新读编码规范,不需要把"我们为什么选择这个架构"写成文档才能记住。你不再需要文字化描述所有规则,因为规则已经内化在状态里了。

到那时,本文第二类结论中的大部分——规则文件的精确措辞、跨 session 的 memory 机制、Code Smell Tracking 的文件系统 workaround——都可以大幅简化甚至拆除。规则工程不会消失,但从"事无巨细地文字化"退化为"偶尔纠偏",负担量级完全不同。

但那一天到来之前,脚手架仍然是必需品。


附录

附录一:AI 的工具链——Grep、LSP 与消歧

即使给 AI 接上了 Metals MCP(Language Server Protocol 工具),它在重构阶段仍然更倾向于全程使用正则搜索替换——Grep + 正则替换是它训练数据中最熟练的路径。

但 Grep 有明确的能力边界。经过反复实验,我们梳理出了一个清晰的分工:

用 Metals(编译器解析)——问题是"编译器把这个解析成了什么?"

场景 为什么 Grep 会失败
Given/implicit 解析——"这里 scope 里的 given Transactor[IO] 是哪个?" Grep 搜 given Transactor 会出 10+ 候选,无法判断编译器选了哪个
Extension method——".pure[F] 是谁定义的?" def pure 在 cats 源码里,但 Grep 无法告诉你哪个 extension 作用于你的具体类型
Opaque type 解包——"ProjectId 的底层类型是什么?" Grep 找到 opaque type ProjectId = UUID,但链式调用中需要跨文件追踪
重载解析——"这里调的是哪个 apply?" Grep 找到所有重载,需要人工按参数类型匹配
类型别名 + 继承——"ConnectionIO 是否 extends Sync?" 需要追踪 type ConnectionIO = Free[...] → Free → capability 层级,多跳
通配符导入——"import doobie.implicits.* 引入了什么?" Grep 无法解析,需要阅读整个 doobie package object

用 Grep(文本搜索)——问题是"这个字符串出现在哪里?"

  • 字符串字面量、SQL 列名、配置键、HOCON 值
  • 注释、TODO、错误消息
  • 跨语言文件(YAML、SQL migration、Dockerfile)

一句话:"编译器怎么解析的"用 Metals,"这个文本在哪"用 Grep。

FQN 的局限与 LSP 的正确用法

我曾经一直纠正 AI 不要写全限定名(org.springframework.http.HttpHeaders),觉得太啰嗦。后来我意识到 FQN 确实能消除歧义——AI 看到 HttpHeaders,不知道是 org.springframework.http.HttpHeadersio.netty.handler.codec.http.HttpHeaders 还是 java.net.http.HttpHeaders

那能不能更进一步,用 SCIP(基于 SemanticDB 的代码索引)给代码文件自动标注 FQN?我让 Claude 评估这个方案:

FQN 对我确实有用。当我看到 val vec = Pgvector(chunk.embedding) 时,我不知道 Pgvector 来自 doobie.postgreso.linewise.core.database.DoobieInstances、还是其他什么地方。FQN 能瞬间消除这个歧义。但我已经有更好的工具了。

需求 SCIP 快照 Metals LSP
"PgVector 是什么?" 读 3 倍膨胀的标注文件 inspect 一次调用,精确类型
"谁调用了 resolveAuth?" Grep 快照(等同于 Grep 源码) get-usages,语义级,非文本级
"这个表达式返回什么类型?" 快照里没有(SemanticDB 不含子表达式推断类型) inspect 直接返回
"PermissionService 的所有实现?" Grep FQN 模式 typed-glob-search

SCIP 快照的代价:3 倍 token 膨胀(150 行文件变 450 行)、数据即时过时(任何编辑后就不准)、没覆盖真正的痛点(debug/重构的瓶颈是 implicit/given 解析链,SemanticDB 不捕获这些,TASTy 才有)。

结论:给 agent 配置 LSP 工具,让它在遇到歧义时按需查询,而不是在每一行代码里都背着冗余的全限定路径。

附录二:模型间的协作与知识传递

高级模型写的代码能"教"普通模型。 用顶级模型(如 opus)编写的高质量代码和 skill,可以有效地指导能力较弱的模型完成开发工作。

各家模型的训练数据里都有大量高质量的开源代码,这些知识本身是存在的。真正的差异在于两点:

  1. 权重分配——同样的知识,不同模型给予的权重不同,导致有些模型能自然地写出高抽象度的代码,有些则倾向于写出更"平庸"的方案
  2. 人类对齐的副作用——这直接取决于 AI 训练师的认知水平。模型在训练过程中会产生一些"鬼点子"——非常规但可能极其有效的策略。如果训练师的认知能力不足以识别这些鬼点子的价值,一看和主流路线不符就直接惩罚掉,这些高价值策略就会在模型中被压制。认知能力差的人,训练不出好 AI。

实用技巧:用高级模型提前在上下文中把这些知识"激活"一遍——写成 skill、写成示例代码、写成 CLAUDE.md 中的规则——然后普通模型在这个上下文中工作时,就能循着已经铺好的轨道前进,而不是退回到它默认的"安全"风格。

多模型协作?不如同级互审。 有人尝试用多家模型互相评审设计文档——opus、gemini、gpt 三家"专家"讨论后投票决策。但实践中,认知能力差距太大的模型坐在同一张桌子上,不会产生有效讨论。 两个大学教授讨论科研方案,中间插进来一个小学生,他不会提供"不同视角",只会拉低讨论的下限。

更有效的方式是:同级别模型之间互审,但赋予不同的预设立场。 比如两个 opus,一个扮演"激进重构派",一个扮演"稳定优先派"——它们有能力理解对方的论点并做出有实质意义的反驳。认知能力不对等的讨论只会退化成"最差模型能理解的水平"。

这本质上和本文的主题是一回事:如果你在用的工具能处理更高抽象度的信息,就不要为了迁就最弱的环节而降级。 代码风格如此,模型协作也如此。

观前提醒

该文件为我个人自用的CLAUDE.md规则文件,维护生产项目已有一年时间,从 Cursor + opus 3.7 时代至现在Claude-Code + opus 4.6时代。

请注意:该文件也仅可用于 Claude code opus 4.6,我不是对 codex 和 gemini 有偏见,OpenAI 的 25k USD credits 今年6月份就要过期了,它(gpt-5.4-xhigh/codex-5.3-xhigh)要是真有自媒体和AI教父们宣传的那么牛逼plus,我能把这些credits放过保质期?

实测效果就是 codex 对函数式编程风格遵循不理想,对 opus 4.6 有效的简短提示词,你不掰开揉碎了讲给 AGENTS.md 它必定会跑偏。

因为编码风格,个人品味原因,请勿整段复制甚至替换掉你的规则文件。你可以让 Claude 阅读我的规则文件,分析后你再决定有哪些可以采纳,有哪些不适合你当前项目。我个人的编码和架构风格非常激进,因为许多代码/系统烂是烂在骨子里的,在这些屎山系统上打“优雅”的补丁实际上还是在推高屎山的海拔,而并没有真的降低技术债务。所以我的架构设计风格从来是大胆激进,没什么不能改也没什么不敢改的,活着的系统一定要经常重新审视,只要能降低技术债务,要敢于进行局部甚至底朝天的重写;只有死了的系统才是永恒不变的。

这是我的项目在 vibe 一年后的提交统计图,受到 opus 4.6 1M-context 的帮助,最近一个月我用 5k usd token 烧出了7-8 个超大型 feature 和从 Kotlin 到 Scala 的彻底重写,代码量也大幅降低到了3万行出头,系统健康程度远超半年前。 backend repository contributors --- # CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Overview

Linewise API is a multi-tenant backend service built with http4s (Scala 3 / cats-effect). It provides document management, AI-powered features (embeddings, RAG), video SOP generation, and real-time communication capabilities.

Refactoring Philosophy

Prefer radical type-level refactors over conservative patches. This is a statically-typed Scala 3 codebase with tagless final — the compiler catches all downstream breakage. When fixing an issue, always choose the solution that encodes the constraint in the type system, even if it touches many files. A 15-file signature change that the compiler verifies is safer than a 1-file patch with a runtime check.

  • Don't minimize blast radius — maximize type safety. Changing a method from List[T] to NonEmptyList[T] across 6 files is not "risky" — the compiler finds every call site. A runtime .toNel.get hidden in one file is the real risk.
  • The compiler is the last line of defense. If a refactor compiles, it's correct. Treat compilation as the acceptance test for type-level changes.
  • Write-cost is near zero. AI writes 90%+ of code, so the cost of touching more files is negligible. Optimize for correctness and compile-time safety, not for minimal diff.
  • Type precision is not over-engineering. Over-engineering means unnecessary abstractions, config flags, strategy patterns for one implementation. Using NonEmptyList over List, ProjectId over UUID, or propagating constraints through signatures is the opposite — it removes complexity (runtime checks) by shifting it to the compiler. "Avoid over-engineering" applies to architecture, not to type-level precision.

Technology Stack

  • Framework: http4s 0.23.33 (Ember server)
  • Language: Scala 3.8.1
  • Runtime: Java 25 (Amazon Corretto)
  • Build: Mill 1.1.2
  • Database: PostgreSQL 42.7.3 + pgvector extension + ltree extension
  • ORM: Doobie 1.0.0-RC11
  • Auth: Firebase Admin SDK 9.4.3
  • AI: Google Cloud Vertex AI (embeddings, GenAI, RAG), Google GenAI SDK
  • Migrations: Flyway 11.7.2
  • Scheduler: Quartz 2.5.0
  • Observability: otel4s 0.15.0 (OpenTelemetry), Sentry error tracking
  • Container Orchestration: Optional Kubernetes integration for video processing jobs

Build & Run Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Compile
./mill compile

# Run (dev)
./mill run

# Run all tests
./mill test

# Build classpath distribution (lib/ with all JARs)
./mill dist

# Format code
./mill reformat

# Check formatting
./mill checkFormat

# Clean build artifacts
./mill clean

Database setup:

1
2
3
4
5
# Start PostgreSQL with pgvector extension
docker-compose up -d

# Stop database
docker-compose down

Project Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
src/main/scala/io/linewise/
├── LinewiseApp.scala # Main entry point (IOApp.Simple)
├── core/ # Shared infrastructure
│ ├── auth/ # Firebase auth, claims parsing
│ ├── config/ # AppConfig (pureconfig HOCON)
│ └── database/ # Doobie transactor, multi-tenant, Flyway
├── features/ # Feature modules
│ ├── bookmark/ # Bookmark management
│ ├── chat/ # Chat sessions
│ ├── copy/ # Content copy
│ ├── document/ # Document CRUD, embeddings, GCS
│ ├── folder/ # Folder management
│ ├── librechat/ # LibreChat integration
│ ├── mfareset/ # MFA reset
│ ├── permission/ # Resource-scoped permissions
│ ├── project/ # Projects, membership, RBAC
│ ├── quota/ # Usage quota tracking
│ ├── rag/ # RAG indexing & retrieval (pgvector)
│ ├── reindex/ # Reindex jobs
│ ├── settings/ # System/tenant settings
│ ├── sop/ # SOP generation (video → steps)
│ ├── system/ # Tenant admin endpoints
│ ├── transcode/ # Video transcoding
│ ├── user/ # User profiles & preferences
│ ├── video/ # Video clips, descriptions, fingerprints
│ └── webauthn/ # WebAuthn/passkeys
├── http/ # HTTP layer
│ ├── routes/ # Route handlers
│ ├── AuthMiddleware.scala
│ └── ErrorHandlingMiddleware.scala
├── infrastructure/ # External integrations
│ ├── firebase/ # Firebase auth & email
│ ├── ffmpeg/ # FFmpeg wrapper
│ ├── gcs/ # Google Cloud Storage
│ ├── genai/ # Gemini video analysis
│ ├── kubernetes/ # K8s job delegation
│ ├── sentry/ # Error tracking
│ ├── telemetry/ # otel4s tracing
│ ├── videointelligence/ # Video Intelligence API
│ ├── videoseal/ # Neural watermarking
│ └── vertexai/ # Vertex AI, MCP server (30 tools)
└── jobs/ # Quartz job schedulers

Configuration

Main config: src/main/resources/application.conf (HOCON format, parsed by pureconfig)

Environment Variables: - DATABASE_URL - PostgreSQL JDBC URL (or secrets/database/url file) - JWT_SECRET - JWT signing secret (fallback: "linewise") - GCS_BUCKET - GCS bucket for documents (fallback: "linewise-documents") - GCS_HOSTNAME - Optional GCS hostname - TRANSCODE_GCS_BUCKET - GCS bucket for transcoded videos (fallback: "linewise-transcode") - TRANSCODE_GCS_HOSTNAME - Optional transcode GCS hostname - ENABLE_OTEL - Enable OpenTelemetry (fallback: false) - SENTRY_DSN - Sentry error tracking DSN (optional) - SENTRY_ENV - Sentry environment (fallback: "development") - KUBERNETES_JOBS_ENABLED - Enable K8s job delegation for video processing (fallback: false) - KUBERNETES_NAMESPACE - K8s namespace for jobs (fallback: "default") - FIREBASE_API_KEY - Firebase API key (for MCP browser login) - FIREBASE_AUTH_DOMAIN - Firebase auth domain (for MCP browser login)

Required secrets (excluded from VCS): - secrets/firebase/service-account.json - Firebase Admin SDK credentials - secrets/gcp/service-account.json - GCP service account for Vertex AI - secrets/database/url - PostgreSQL JDBC URL (optional, defaults to localhost)

Key Architectural Patterns

Tagless Final Pattern

The codebase uses tagless final with context bounds for dependency injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Service trait with F[_] type parameter
trait SOPService[F[_]]:
def getSOP(tenant: TenantContext, id: UUID): F[Option[SOP]]
def createSOP(...): F[Either[String, SOP]]

// Companion object with summoner and factories
object SOPService:
inline def apply[F[_]](using ev: SOPService[F]): SOPService[F] = ev // Summoner
def make[F[_]: Sync](xa: Transactor[F]): SOPService[F] = ... // Live implementation
def noop[F[_]: Applicative]: SOPService[F] = ... // NoOp implementation

// Routes use context bounds
def routes[F[_]: {Async, SOPService, PermissionService}]: AuthedRoutes[AuthenticatedUser, F]

// Access via summoner
SOPService[F].getSOP(tenant, id)

NoOp Result Patterns:

NoOp implementations are used for: (a) test runners / local partial testing, and (b) feature-flag-disabled services in LinewiseApp.scala. NoOp returns results (not throw exceptions). The caller decides if it's an error.

Data-related vs data-unrelated services: - Data-related (RAG, video processing, document ops, SOP): NoOp must return error (Left("Service not available")). Option methods return None — but note this means "not found", which is semantically different from "service disabled". For Either-returning methods (create, update, delete), always return Left so the caller knows the operation was not performed. Data-related services should use F[Either[E, T]] by default (not F[T] or F[Option[T]] for mutation), because they involve the persistence layer which can fail. - Data-unrelated (logging, metrics, telemetry): NoOp can return success (Right(())). Skipping side-effect-only operations is harmless.

1
2
3
4
5
6
7
8
9
10
// Data-related NoOp — Either methods return Left, Option methods return None
class SOPServiceNoop[F[_]: Applicative] extends SOPService[F]:
def createSOP(...) = Left("Service not available").pure[F]
def getSOP(...) = None.pure[F] // None = "not found" (not "disabled")
def deleteSOP(...) = Left("Service not available").pure[F]

// Data-unrelated NoOp — success (nothing to do is fine)
class MetricsServiceNoop[F[_]: Applicative] extends MetricsService[F]:
def recordLatency(...) = Right(()).pure[F]
def incrementCounter(...) = Right(()).pure[F]

Method-level context bounds for partial dependencies:

When only some methods on a service need an extra capability, use a method-level using parameter instead of requiring it on the whole class:

1
2
3
4
5
6
7
8
9
trait UserService[F[_]]:
def getUser(tenant: TenantContext, id: UUID): F[Option[User]]
def avatar(tenant: TenantContext, id: UUID)(using S3Service[F]): F[Option[Array[Byte]]]

// Routes that call avatar need S3Service in scope:
def routes[F[_]: {Async, UserService, S3Service}]: AuthedRoutes[...] = ...

// Routes that don't call avatar don't need S3Service:
def routes[F[_]: {Async, UserService}]: AuthedRoutes[...] = ...

Wiring with givens in LinewiseApp:

1
2
3
4
5
6
7
given SOPService[IO] = SOPService.make[IO](xa)
// Feature-flag-disabled services use NoOp
given RAGService[IO] =
if Config.enableRAG then RAGService.make[IO](httpClient)
else RAGService.noop[IO]

val routes = SOPRoutes.routes[IO] // Givens in scope

Code Style: Flat for-comprehensions

Prefer flat for/yield over nested match/case inside effectful blocks. Lift Either/Option into F so the for stays linear:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// BAD — nested match in MID-CHAIN breaks the for-comprehension flow
for
result <- service.doSomething(...)
value <- result match // BAD: match in middle, more steps follow
case Right(v) => v.pure[F]
case Left(err) => Sync[F].raiseError(err)
next <- process(value)
response <- Ok(next.asJson)
yield response

// ALSO BAD — .flatMap with case inside for-comprehension
for
body <- req.req.as[Body]
result <- service.getItem(id).flatMap {
case Some(item) => Ok(item.asJson)
case None => NotFound(...)
}
yield result

// GOOD — plain match at TERMINAL position (pure response mapping, no side effects)
for
body <- req.req.as[Body]
result <- service.doSomething(body)
response <- result match
case Right(value) => Ok(value.asJson)
case Left(err) => BadRequest(err.asJson)
yield response

// GOOD — lift Either/Option into F (throw on error — trusted paths only)
for
value <- Sync[F].fromEither(parseJson(raw).leftMap(e => RuntimeException(e.message)))
response <- Ok(value)
yield response

// GOOD — EitherT.foldF when branches have SIDE EFFECTS (logging, audit, cleanup)
for
body <- req.req.as[Body]
response <- EitherT(service.doSomething(body)).foldF(
err => Logger[F].warn(s"Failed: $err") *> BadRequest(err.asJson),
value => audit.record(value.id) *> Created(value.asJson)
)
yield response

// MID-CHAIN Either coloring — depends on data-related vs data-unrelated:
// Data-unrelated mid-chain op (metrics): discard Left, log, continue IO chain
for
body <- req.req.as[Body]
saved <- store.save(body)
_ <- metrics.record(saved.id).flatMap {
case Right(_) => Applicative[F].unit
case Left(err) => Logger[F].warn(s"Metrics failed: $err") // discard, non-critical
}
response <- Ok(saved.asJson)
yield response

// Data-related mid-chain op (store.save): propagate Either — refactor chain to EitherT
for
body <- req.req.as[Body]
response <- EitherT(validate(body))
.semiflatMap(valid => store.save(valid)) // F[Either[E, A]] — error must propagate
.subflatMap(identity) // flatten nested Either
.foldF(
err => BadRequest(err.asJson),
saved => Ok(saved.asJson)
)
yield response

// GOOD — F[Option[A]]: use OptionT for single Option check
for
body <- req.req.as[Body]
result <- OptionT(service.getItem(id))
.semiflatMap(item => Ok(item.asJson))
.getOrElseF(NotFound(ErrorResponse.notFound("Not found").asJson))
yield result

// GOOD — chained Options with different error statuses: EitherT + local enum
private enum LookupError:
case NotFound
case NoUri

for
body <- req.req.as[Body]
result <- EitherT
.fromOptionF(service.getItem(id), LookupError.NotFound)
.subflatMap(item => item.uri.toRight(LookupError.NoUri))
.semiflatMap(uri => doWork(uri))
.foldF(
{ case LookupError.NotFound => NotFound(...)
case LookupError.NoUri => BadRequest(...) },
_ => Accepted(...)
)
yield result

Key lifters: - EitherT(...).foldFF[Either[E, A]] → handle both branches - EitherT.fromOptionFF[Option[A]]EitherT[F, E, A] - .subflatMap — pure A => Either[E, B] inside EitherT chain - .semiflatMap — effectful A => F[B] on happy path - OptionT(...).semiflatMap(...).getOrElseF(...)F[Option[A]] → handle None - Sync[F].fromEither, Sync[F].fromOption — lift pure values (throw on error)

No premature helpers: Don't extract single-use private methods that just wrap a match. Inline the logic at the call site.

Case classes over manual cursor decoding: For external API payloads, define case classes with derives Decoder and decode once with .as[T], then pattern-match on decoded fields. Avoid manual hcursor.downField(...).get[T](...) chains.

SDK/Library Priority Order

When integrating external services (e.g., Google Cloud, AWS, Firebase), prefer libraries in this order:

  1. Typelevel-wrapped Scala SDK (e.g., from typelevel.org ecosystem)
    • Native cats-effect integration, functional patterns
  2. Official Scala SDK (e.g., from Google/Azure/AWS)
    • First-party support with Scala idioms
  3. Third-party wrapped Scala SDK (actively maintained)
    • Community wrappers with Scala-friendly APIs
  4. Official Java/Kotlin SDK (wrap with Async.blocking)
    • Use when no Scala alternative exists
  5. Implement yourself (HTTP client)
    • Last resort, only when SDK unavailable or unsuitable

Example: For Google Gemini integration, use the official Java SDK wrapped with Async[F].blocking rather than implementing raw HTTP calls.

Multi-Tenancy with Schema Isolation

The database uses PostgreSQL schema isolation for multi-tenancy:

  • System schema (public): Stores shared data (tenants, users, system settings, quotas)
  • Tenant schemas (tenant_<id>): Each tenant gets an isolated schema for their data (projects, documents, SOPs, etc.)

Migration System: - System migrations: db/migration/system/ - run once at startup - Tenant migrations: db/migration/tenant/ - run for each tenant schema - Migrations run automatically at startup for all existing tenants - New tenant schemas are migrated on creation - Never modify existing migration files; always create new versioned files

Tenant Schema Access: All tenant routes follow the pattern: /api/org/{tenant}/...

Fail Fast - No Silent Error Swallowing

CRITICAL RULE: Never silently swallow errors. Arbitrary tolerance pollutes the database and hides bugs.

Forbidden patterns:

1
2
3
4
5
6
7
8
9
10
// BAD - silently converts errors to None/null
json.as[T].toOption
json.as[T].getOrElse(defaultValue)
either.toOption
Try(x).toOption
result.getOrElse(null)
parse(userInput).getOrElse(0.75) // BAD - hides parse failure

// OK - .getOrElse for optional config with a sensible default
pageSize.getOrElse(10) // OK - Option[Int] with default, no error to swallow

Error Handling Strategy - Trusted vs Untrusted Paths:

Path Type Examples Strategy
Trusted (internal) Config files, system settings, DB schema data, internal serialization, persisted DB data, internal service calls, cache, GCS/K8s metadata Throw exception - low probability of error, if it fails it's a bug. For infra (GCS/K8s), human runs data migration after code changes.
Untrusted (external) User input, AI-generated content, external API responses (before persistence) Catch and report - high probability of error, report back to user/AI to fix

Persisted data is trusted. Strict enc/dec at the write boundary ensures bad-format data never reaches the DB. If malformed data is read back from DB, it's a human/migration bug — throw, don't defensively handle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TRUSTED PATH - throw on failure (system internal data)
val config = configJson.as[AppConfig].getOrElse(
throw new RuntimeException(s"Config decode failed: ${configJson}")
)

// UNTRUSTED PATH - catch and report to caller (user/AI content)
// Use EitherT/match at terminal position — never use `return`
for
body <- req.req.as[UserContent]
result <- service.process(body)
response <- result match
case Right(v) => Ok(v.asJson)
case Left(err) => BadRequest(s"Invalid content format: ${err.message}".asJson)
yield response

Typed Error Model (ADT Errors)

Use enum error types, not Either[String, T]. Services define sealed error enums for known failure modes. Routes pattern-match on the enum to decide HTTP status — no string parsing.

Scope: one error enum per logical failure domain, not per service. - If two methods share most failure modes → one shared enum - If two methods have different failure modes → separate enums - Shared subset across domains → compose via wrapping: ParseError.Embedding(EmbeddingError)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Separate enums — methodA and methodB have different failure modes
trait DocumentService[F[_]]:
def importUrl(url: String): F[Either[ImportError, Document]]
def parseContent(docId: DocumentId): F[Either[ParseError, Content]]

enum ImportError:
case InvalidUrl(url: String)
case Unreachable(url: String, status: Int)

enum ParseError:
case FormatNotSupported(mimeType: MimeType)
case DocumentNotUploaded(documentId: DocumentId)
case Embedding(cause: EmbeddingError) // wraps inner domain error

// Shared enum — indexDocument and indexSop share the same failure modes
trait RagIndexService[F[_]]:
def indexDocument(docId: DocumentId): F[Either[IndexError, Unit]]
def indexSop(sopId: SopId): F[Either[IndexError, Unit]]

Rules: - Named variants for known failures. Each variant carries structured context (IDs, limits, types), not string messages. - Sdk / Other(message: String) variant for unexpected errors that don't warrant their own case yet. - Compose, don't flatten. When service A calls service B, wrap B's error: case Embedding(cause: EmbeddingError), not case EmbeddingFailed(message: String). - Route mapping: Each error variant maps to exactly one HTTP status. The match is exhaustive — compiler enforces handling every variant. - Define in feature's models.scala. - Migrate when file is touched — no hesitation. New services use typed errors. Existing Either[String, T] services migrate the whole service to ADT errors when the file is modified for any reason — even a typo fix or comment edit. The trigger is touching the file, not the size of the change. Touching the file means QA/regression testing covers it, making it the perfect time. Scope follows the compiler iteratively — if the route file you're editing calls a service with Either[String, T], migrate that service file too.

Feature Module Organization

Each feature module follows a consistent structure:

1
2
3
4
5
features/<feature>/
├── <Feature>Service.scala # Business logic (tagless final trait + impl)
├── <Feature>Repository.scala # Data access layer (Doobie queries)
├── models.scala # Domain objects and DTOs
└── README.md # Feature documentation

Layered Architecture:

1
2
3
4
Routes → Services → Repositories → Database (Doobie)
↓ ↓ ↓
DTOs Business SQL Queries (ConnectionIO)
Logic

Testing Strategy

Tests use: - munit with cats-effect support for test framework - TestContainers for PostgreSQL integration tests (automatic database provisioning) - Doobie munit for query checker tests

What NOT to test (waste of time): - Case classes - no value without complex methods - JSON serialization - Circe is already well-tested - Config class definitions - if config is wrong, app fails to start anyway - Framework behavior (http4s, Doobie) - already well-tested by the community

What TO test (valuable): - Security validation (e.g., tenant name injection prevention) - Error handling / fallback logic - Real database integration with TestContainers - Business logic that has actual branching/computation - Assembly/wiring that connects our components together

API Routes

All routes require Firebase JWT auth: - /api/org/{tenant}/projects/ - Project management - /api/org/{tenant}/projects/{id}/documents/ - Document CRUD - /api/org/{tenant}/projects/{id}/sops/ - SOP management - /api/org/{tenant}/projects/{id}/bookmarks/ - Bookmarks - /api/system/tenants/ - Tenant admin - /api/system/users/ - User management

Tool Preferences

Override Grep with Metals MCP when the question is "what does the compiler resolve this to?"

Grep is the default and works for most searches. But it fails silently on these Scala-specific scenarios — use Metals instead:

Scenario Tool
What type is this expression / what does it return? mcp__metals__inspect
Which given/implicit is resolved at this call site? mcp__metals__inspect
Which overloaded method is called here? mcp__metals__inspect
What's the underlying type of an opaque type? mcp__metals__inspect
What does a wildcard import bring into scope? mcp__metals__inspect
Who calls this method / all implementations of a trait? (semantic, not textual) mcp__metals__get-usages

Other Metals tools: glob-search (find symbols by name), get-docs (ScalaDoc), compile-file (single-file compile check), list-modules, list-scalafix-rules.

Signal to switch: When you grep and get 10+ candidates with no way to disambiguate — that means you need Metals, not a better regex. Fall back to Grep/Glob for non-Scala files, string literals, config values, SQL, or when Metals is unavailable.

Development Workflow

Adding New Routes

When adding or modifying routes:

  1. Update the OpenAPI spec: After route/DTO changes, update src/main/resources/openapi/documentation.yaml
  2. Validate the spec: Run swagger-cli validate to ensure correctness
  3. Follow authentication patterns: All API routes require Firebase JWT authentication (except system/health endpoints)

Code Formatting

Use scalafmt (configured in .scalafmt.conf):

1
2
./mill reformat      # Format all Scala sources
./mill checkFormat # Check formatting without modifying

Code style rules: - No fully-qualified names in code. Always use imports. - Context bounds: use {A, B, C} syntax (Scala 3.6 aggregate bounds), not colon-separated. - Opaque types for domain values. AI writes 90%+ of code, so write-cost is near zero while compile-time safety is free. Use opaque types with smart constructors for all entity IDs, constrained strings, and bounded numbers. Defined in core/domain/Ids.scala and core/domain/Types.scala. - Type-level constraints flow E2E. Encode invariants in types (opaque types, NonEmptyList, refined types) and propagate them through all layer signatures: route → service → repository. Never downgrade a constraint to a weaker type and re-validate internally — that hides the requirement from callers and defeats compile-time safety. Unwrap/weaken only at the true system boundary: SQL interpolation, Java SDK calls, job parameter serialization. - .toString over .value.toString. Opaque types erase at runtime, so s"...$opaqueId" and opaqueId.toString just work — no need to unwrap first. - NonEmptyList over List + .get/.head. When a method logically requires non-empty input (batch embeddings, IN clauses, etc.), use NonEmptyList[T] in the signature — including repository methods — instead of List[T] with a runtime .toNel.get or .head. Callers use NonEmptyList.fromList to handle the empty case at the call site. - No premature helpers. If the logic can be composed from <5 Scala/cats operators, always inline at call site — never extract a helper. If >=5 operators, ask the user before extracting (in plan mode or popup dialog). When consensus is reached on a new helper, add/link it in this document so future sessions know to use it. Always use helpers already listed here (e.g., AsyncOps) — don't expand them inline. Before writing any new helper, search the codebase for existing ones that do the same thing. - Generic over specific (stdlib/cats only). Prefer composing well-tested Scala/cats operators generically (one queryParam[T] using QueryParamDecoder[T]) over type-specific parsers. "Generic" means leveraging stdlib type classes, not extracting custom helper functions — those still follow the <5 operator rule. - Proactive naming review. When modifying code, flag misleading, stale, or inconsistent names to the user. Scope follows the compiler iteratively — same as smell detection: start with changed files, then follow compilation errors outward. For internal names (classes, properties, methods) — recommend renaming directly. For external names (request/response DTOs, DB-serialized JSONB fields) — suggest the better name but note migration implications. Common smells: Kotlin-era suffixes (Kt), field names that don't match their type (name holding an ID), stale comments referencing deleted code, generic names that obscure domain meaning. - Proactive code smell detection. Scope follows the compiler iteratively: (1) find smell in current file, (2) fix it, (3) compile → if it fails because other files import the changed symbol, fix those too, (4) repeat until compilation passes. If reading unrelated code (not in the compilation chain) and spotting a violation — add it to the smell list (see Code Smell Tracking below), do not fix. This applies to all rules: type safety, error handling, control flow, naming, logging, etc.

Code Smell Tracking

When spotting code smells in unrelated code (not in the current compilation chain), add them to the persistent smell list file at <project-root>/memory/code_smells.md instead of just warning in the response.

Rules: - Max 10 entries. If adding an 11th, delete the oldest entry (FIFO eviction). - Prioritize by severity. Most critical smells first (silent error swallowing > naming inconsistency). - At end of every task, remind the user about pending smells and suggest fixing them in a dedicated session. - Each entry includes: file path, line number, rule violated, brief description. - Remove entries when the smell is fixed (either by the user or in a subsequent session).

Logging

  • Use val logger (not context-bound injection). Create a local val logger = Slf4jLogger.getLogger[F] or pass as constructor param. Logger is too common to justify tagless-final injection overhead.
  • Milestone logging for long operations. Every long-running call (external API, DB migration, video processing, embedding) should log at each major step so operators can see progress and diagnose hangs.
  • Log level in loops: If each iteration is fast (e.g., processing a list of items), use debug/trace. If each iteration is slow (e.g., transcoding, embedding backfill), info is appropriate.
  • Log levels: error = unexpected failures that need attention. warn = degraded but recoverable. info = lifecycle events, milestones, external calls. debug = per-item processing in loops, internal state.

Runtime Assertion Checks (RAC)

Suggest runtime assertions on critical paths (advisory, not mandatory). RACs catch inconsistent state early, before it propagates downstream and corrupts data. Always enabled in dev/testing; switchable off in production via config flag. Final decision on whether to add RAC is made during code review — do not treat missing RAC as a code smell.

Implementation: Use the shared RAC.assert(condition, message) helper (io.linewise.core.RAC) that checks a config flag. When disabled, assertions are no-ops. When enabled, they throw immediately.

What should have RAC: - Money/balance operations — assert balance >= 0 after debit, assert credit + debit = expected total - Inconsistent state transitions — assert valid transitions in state machines (e.g., SOP stage: draft→processing→published, never published→draft; RAG index: PENDING→INDEXING→INDEXED, never backward). Throw immediately on invalid transition to prevent downstream pollution. - Tenant isolation — assert search_path matches expected tenant schema before writes. Wrong schema = cross-tenant data leak. - Embedding dimensions — assert vector length matches expected dimension (768 for text, 1408 for video) before pgvector insert. Wrong dimension corrupts similarity search silently. - Idempotency — assert no duplicate job submission for same entity (K8s jobs, Quartz jobs). Duplicates waste resources. - Invariant preservation — any operation where a post-condition violation would silently corrupt data rather than fail visibly.

What should NOT have RAC: - Input validation (use typed errors instead — that's user-facing, not assertion) - Performance-sensitive hot loops (use debug logging instead) - Conditions already enforced by the type system (that's the compiler's job)

CI/CD Pipeline

IMPORTANT: Do NOT build and push Docker images from local machine. Always commit and push to git to trigger CI for building images. Only build locally if explicitly requested by the user.

Branches and Docker Tags: - develop branch → gcr.io/${PROJECT_ID}/linewise-api:develop - testing branch → gcr.io/${PROJECT_ID}/linewise-api:testing - master branch → gcr.io/${PROJECT_ID}/linewise-api:latest and :master - Git tag vX.Y.Zgcr.io/${PROJECT_ID}/linewise-api:vX.Y.Z

Git Push: - If SSH push fails (e.g. VPN/proxy blocks port 22), switch to HTTPS temporarily: git remote set-url origin https://github.com/Vision-Nexus/linewise-api.git

Deployment: - GitHub Actions workflow: .github/workflows/build-and-push-gcr.yml - Builds with Mill, then Docker image, pushes to Google Container Registry - Requires GCR_SERVICE_ACCOUNT secret (GCP service account JSON) - Deployed via ArgoCD on Kubernetes (not docker-compose or manual shell) - Deploy manifests live in a separate repo: linewise-deploy/overlays/{dev,testing,prod}

Deploy impact reporting: When a code change involves deploy-affecting changes, output a summary of what needs to be updated in the deploy repo. Examples: - New environment variable → add to ConfigMap or Secret in the overlay, reference in Deployment env - New configuration field → add to application.conf ConfigMap - New sidecar container → add container spec to Deployment manifest - New volume/secret mount → add Volume + VolumeMount to Deployment - New external service dependency → may need NetworkPolicy, ServiceAccount, or IAM binding

Format the output as a checklist the user can apply to the deploy repo. Do NOT suggest docker-compose changes or manual docker run / kubectl apply commands.

Key Dependencies & Services

External Services Integration

  • Firebase Admin SDK: User authentication and JWT verification
  • Vertex AI: Text embeddings, Gemini models, Document AI for OCR
  • Google Cloud Storage (GCS): Document and video file storage
  • LibreChat: Optional integration for chat interface

Optional Features

Kubernetes Job Delegation: - Video processing (FFmpeg, VideoSeal) can run as K8s jobs instead of in-process - Enable via KUBERNETES_JOBS_ENABLED=true - Requires service account with GCS access and K8s job permissions

Observability: - otel4s: Native Scala OpenTelemetry integration (set ENABLE_OTEL=true) - Sentry: Error tracking and monitoring (provide SENTRY_DSN)

Important Files

  • LinewiseApp.scala - Main entry point, service initialization, givens wiring
  • build.mill - Mill build definition, dependencies
  • application.conf - HOCON configuration with environment variable overrides
  • Dockerfile - Multi-stage build with Mill, FFmpeg for video processing
  • docker-compose.yaml - Local PostgreSQL with pgvector for development

利益声明:本文不构成投资建议,不推荐任何 AI 工具购买或中转平台推广。所有内容均来自本人约 2 万美金 token 的实践经验。不同项目类别、不同编码品味可能会得出不一样的结论。

书接上回

上一篇我们聊到:AI 的知识储备远超个人,但难以唤醒;你的鉴别力边界决定了质量控制的上限;触发器永远是你自己的成长,不是 AI 的主动突破。

那接下来的问题自然是:怎么唤醒它的知识?怎么验证它给出的方案?当它的方案超出你的认知以后,你该如何学习以具备采纳/拒绝方案的能力?

要回答这些,得先理解一个前提:AI 不是一个稳定的工具,它的表现会随着对话推进而变化。最近两个月 Opus 4.6 给了我一种奇点临近的感觉,它已经可以对我的项目开发实现整体加速了。不是某个环节快一点,而是从架构设计到编码实现到调试部署,全链路都能帮上一点忙。

先打个脸:我之前以为 Opus 4.6 相比 4.5 没有很大进步,我错了,Claude 哥对不起(鞠躬)。小任务上两者表现差别确实不大,第一印象觉得"不过如此".jpg。但真正的差距在长上下文的指令遵循上,这只有跑十几个大型重构任务才能体会出来。小任务测不出来的东西,大任务里天差地别。换句话说,Opus 4.6 上下文长了以后"降智"的时间点被大幅推迟了。

这个"降智"不是简单的能力退化,仔细观察会发现它有两种截然不同的表现。理解这两种"降智"的区别,是回答上面三个问题的基础。

两种降智

第一种:遗忘

上下文越长,AI 会忽略掉过去已经达成的共识和结论。你问它的时候它会意识到,但你不主动提,它就会回到错误的路线上走很远。

补充自 Claude 研究型代理,这个问题在学术界已经有充分研究: - "Lost in the Middle" (Liu et al., 2023) — 模型对上下文中间位置的信息检索率下降 30%+,呈 U 形曲线 - NoLiMa (Adobe, 2025) — 去掉字面关键词匹配后,13 个模型中 11 个在 32K 时准确率低于 50% - RULER (NVIDIA, 2024) — NIAH(大海捞针)近乎满分的模型,在多针/多跳任务上显著退化

好消息是,Opus 4.6 在 1M 长上下文上改善很大。根据 Anthropic 官方技术报告(The Claude Model Spec),8 针 MRCR 测试中达到 76%(对比 Sonnet 4.5 的 18.5%)。在 500K 左右的上下文中,遗忘问题已经不太影响实际工作了。

第二种:创造力衰减

这种"降智"更加隐蔽——模型不是给出错误答案,而是给出"正确但笨"的答案。用户很难察觉到它本可以做得更好。

举两个我实际遇到的例子:

  • 带着 PyTorch + CUDA + .pth 的镜像编译一次要接近半小时。上下文短的时候,AI 会建议先在 K8s 里开一个临时 pod 把整条链路跑通了再提交代码编译镜像。但上下文长了以后,它就老老实实地改代码、编译、等半小时、报错、再改、再编译……
  • 调试视频流的时候,上下文短时 AI 会说"先给你几行 JS 代码,贴到浏览器 console 里验证一下有哪些 tracks,codec profile 是什么"。上下文长了以后,它更倾向于直接在后端代码里改来改去反复试。

这些"鬼点子"的共同特征是:跳出当前执行路径,用低成本方式先验证假设。本质上是一种元认知——"我现在做的这件事成本高、反馈慢,有没有更快的方式先确认方向对不对?"

核心洞察:能力没丢,是注意力分配的问题。 Opus 4.6 不是想不到这些方案,开一个新会话或者 /compact 压缩上下文后,它很容易就能想到。但上下文超过一半以后,这类"鬼点子"就几乎不会自发出现了。权重还在,只是没被激活。

为什么会这样

两股力量在长上下文中同向叠加,共同把 AI 推向"听话但缺乏主见的执行者"。

架构层面:注意力稀释

简单粗暴地说:上下文越长,token 之间的注意力越"浆糊"[勘误1]。要形成远距离的"我tm真是个天才"式连接——比如在讨论镜像编译的时候突然想到可以用临时 pod——需要在特定 token 之间建立尖锐的注意力分布,而这正是长上下文削弱的。

2025 年有三篇论文(Scalable-Softmax, ASEntmax, Critical Attention Scaling)从机制层面分析并尝试解决这一问题[勘误2]。GSM-Infinite (2025, ICML) 也发现数学推理能力随上下文长度单调衰减[勘误3]

更值得注意的是 "Context Length Alone Hurts" (Du & Tian, 2025) 的发现:即使模型完美检索到了所有信息,推理性能仍下降 13.9%-85%(取决于任务复杂度和上下文长度)。这说明不是找不到,而是用不好。

训练层面:对齐压力

RLHF/对齐训练在短上下文中表现为"靠谱",在长上下文中表现为"死板":

  • 模型被奖励的是完成当前步骤,而不是质疑"这个步骤本身是否最优"
  • 人类标注员更容易给"逻辑连贯、稳步推进"的回答高分,而"突然跳到一个完全不同的方向"容易被标为不相关
  • 长对话中 sycophancy(谄媚倾向)累积,模型越来越顺着当前路径走

训练数据和标注员的水平上限也会体现在编码习惯中。比如 AI 特别喜欢做防御性编程:参数是数组,它会自动判断是否是数组,不是的话 wrap 进一个数组里;参数不对的时候不去修复 call site 的传参,而是在实现里打各种补丁。

这些"鲁棒性"在训练中被奖励,但在工程实践中是藏匿 bug 的广式双马尾养殖基地。好的系统设计应该是 fail-fast:恰当隔离错误,主动抛出无法处理的严重异常,交给上层决策,而不是在每一层都吞掉错误装作岁月静好。

在涉及资金、高功率输出、机械臂运行的场景下,异常停机的代价可能只是生产暂停,但吞掉异常继续运行可能导致财产甚至生命危险。

上下文本身就是隐性 SOP——对话中积累的执行历史形成了一条隐含的路径,模型的注意力越来越多地分配给"延续这条路径"而非"评估是否应该换路径"。

那为什么人类工程师不会这样?因为人类思维的工作方式恰恰相反。

对比人类工程师

人类工程师的"创造力"本质上是并行探索 + 跨路径迁移——你在调 K8s 的时候突然想到"等等,我先在浏览器里验证一下 codec",这不是线性推理,是两条思路之间的意外碰撞。

优秀的工程师会同时维护多条尝试路径,这些路径不完全隔离,甚至会互相促进影响。只要能达成最终结果,过程不会固定遵循一个模式——除非被 SOP 或安全生产要求约束。

而 autoregressive 架构一次只生成一个 token,一次只走一条路。即使是 Chain-of-Thought 也是串行的,不是真正的并行探索。这恰好是当前架构最弱的地方。

架构的硬伤短期改不了,但我们可以用协作方式来补偿。

理解了特性后,怎么协作

理解了降智的机制,回到开头的三个问题。先说唤醒。

唤醒:像专家一样对话,而不是像写小作文

我看到有人教写 prompt 的方法论:要像写小作文一样长篇大段、有逻辑地输出,要尽可能详细。

我的经验正好相反。

高手之间交流不需要长篇大论的铺垫。特别是同领域专家之间,三句话就点到位了,甚至都不需要提背景,只讲关键路线选择,剩下的对方自己就脑补出来了。就算没点到位,对面一问一答,也就"我懂你意思"了。

常看动画片的朋友都知道,穿越到剑与魔法的异世界之后,不会无吟唱/短吟唱魔法的人在异世界就是路边一条,活不过两集。

身为温婉柔缓的水之精灵,自大地奔涌流淌的溪涧公主啊!以你体内深藏的磅礴伟力,将世间万物尽数冲散席卷!separation flow!

嗯,一听就是个菜鸡,吟唱还没念完就被砍了。

术式顺转——苍。

一听就是能跟宿傩打得有来有回的2.5条悟。短吟唱就是专家对话的本质:信息密度越高,威力越大。

对大模型也一样。废话说多了浪费 token,也浪费人的脑力。不如 2-3 句话:

  • 第 1 句讲背景:为当前项目集成视频水印功能
  • 第 2-3 句讲关键路线:考虑选择 FFT 或者 DFT 类似方案,为了防止高频降噪,考虑在低频域插入特征

够了。剩下的让 AI 自己展开。

反面案例:我见过有人给 AI 写一整屏的 prompt,项目背景、技术栈版本、上次对这个功能做了什么变更、老板在刚才的会上又提出了什么苟芘不通的脑洞、甚至控诉 AI 上个提交写的功能包含有什么 bug 导致自己昨晚半夜被oncall起来加班回滚……全部塞进去。结果 AI 的回复也是一整屏的废话,把你的背景复述一遍,再加上一堆不痛不痒的"建议"。信息密度越低,AI 的产出质量越低。

但这里有一个前提:你自己的认知水平决定了 AI 产出的上限。

低水平的认知写出来的低水平 prompt,只能换来 AI 低水平的产出。你说不出"考虑 FFT/DFT 方案,低频域插入特征"这种话,AI 就只会给你一个最朴素的明文水印方案。AI 拥有的知识远超任何个人,但它需要你用正确的钥匙去开门,而这把钥匙就是你自己的专业认知。

所以"像专家一样对话"的真正含义不只是简洁,而是你得先成为专家。AI 放大的是你已有的能力,而不是凭空创造能力。

再举一个实战案例。AI 给视频加 mask 模糊后,直接把原始的 m3u8 播放列表复制过去了——完全没意识到重新编码会改变字节偏移量。我自己一开始也不确定:可变比特率编码下,mask 处理到底会不会影响输出体积?跟一个做音视频的朋友讨论了一轮,他认为不会,我认为会。实验验证后确认:文件体积缩小了,i 帧位置全变了,原始 m3u8 的 byte range 指向完全错误。

但 Opus 4.6 的恢复能力让我意外。我只问了一句:"你原样 copy 过去了?"——它就自己反省出整条错误链,并给出了正确方案(用 FFmpeg HLS muxer 直接输出新的 m3u8)。Codex 做不到这一步,不把推理过程喂给它,它猜不到 mask 后再编码会导致偏移量变动。

这里有一层值得点透的意思:"像专家一样对话"不要求你在每个子领域都比专家强,而是你得有足够的认知密度去形成正确的怀疑。你不需要确定 m3u8 偏移量一定会变,你只需要怀疑"这样直接 copy 真的对吗?"然后一句话问出来,AI 自己就展开了。但注意,能产生这个怀疑本身就需要一条推理链:mask 模糊改变了画面内容 → 可变比特率编码下信息熵变了 → 输出体积变了 → 字节偏移量对不上。这条链上任何一环的认知缺失,都不会触发那个怀疑。门槛从"你得知道答案"降到了"你得能怀疑",但"正确的怀疑"本身仍然需要认知基础。

但"像专家一样对话"只解决了信号质量问题——用高密度的输入唤醒高质量的输出。还有一个信号覆盖问题:你不知道该往哪个方向问,再精准的提问也覆盖不到盲区。

对此有一个补充策略:无方向审计。不带预设地问 AI"你认为当前方案缺少哪些维度",然后逐条追问。这不能保证覆盖所有盲区,AI 也有可能编造维度来显得有用,但至少比只沿着自己已知的方向提问要好。

再说验证。

验证:主动对抗创造力衰减

既然知道长上下文会让 AI 失去创造力,就应该主动对抗。核心策略是:适时停下来,由人类主导对当前路线做审计。

具体操作:

  1. 当你感觉 AI 开始"沿着惯性走"的时候,停下来
  2. Fork 当前会话,或者开一个新会话
  3. /compact 压缩上下文,让 AI 带着精简过的记忆重新审视当前方案
  4. 让 AI 先自行审计一遍:当前路线有没有更好的替代方案?有没有可以低成本验证的假设?

本质上,这是用人类的元认知能力弥补 AI 在长上下文中的注意力分配缺陷。人类负责"该不该这么做",AI 负责"怎么做"。

工程原则:让机器替你验证

怎么验证 AI 给出的方案?编程领域有一个天然优势:编译器、类型系统、测试套件,这些验证工具不依赖人类的主观判断。你的鉴别力有上限,但机器化验证没有。前提是:代码本身得遵循一些基本原则。

命名

非常重要,定期让 AI 审计所有命名规范,不要自己发明名字。

人类工程师能记住"这个函数叫 processMatrix 但其实干的是流量分发"——大脑会自动建立名实不符的映射。但 agent 不会。每开一个新 session,它都会老老实实按名字理解语义,然后在同一个坑上反复栽倒。

老板爱叫什么"流量矩阵""裂变增长"我管不了,但这些命名污染一旦渗入代码,就是给 agent 埋雷。好的命名让 AI 和人类都能快速建立正确的心智模型,坏的命名只有人类能靠记忆力硬扛,agent 扛不住。目标是:让 agent 在任何新 session 中都能顾名思义

功能设计

保持通用简单,不要特立独行。你的商业模式和程序逻辑没有那么独特——你大概率不是地球上第一个想到这套方案的人。与其自己拍脑袋设计一套扭曲的实现,不如把方案描述发给 agent,问它:这套方案叫什么名字?有没有成功案例?有哪些可以借鉴的?然后让 agent 按照业界标准模式去实现,而不是你脑洞里的特殊版本。

举个例子:设计用户登录系统,密码禁止明文保存。如果你没听说过 bcrypt,可能会给 agent 描述一套自创方案——先 MD5 再 SHA256 再 ECC,私钥硬编码到配置里。停。先把你的方案发给 agent,问它业界最佳实践是什么。AI 会告诉你 bcrypt/scrypt/argon2,还会指出你方案的缺陷:迭代次数太少、ECC 私钥保存不当、所有密文产出自同一规则无法抗并行彩虹表攻击。

多听取来自 agent 的不同意见。 AI 的知识广度远超个人,不要只把它当执行者,也让它当审稿人。

模块化

你思路中真正独特的部分,通过组合标准模块来实现,而不是去修改(扭曲)标准化实现本身。模块之间尽可能独立,避免过多交叉。有独特需求就用几个简单模块叠加——做加法而不是做乘法。功能叠加是线性增长,功能交叉是组合爆炸。一个函数干三件事,agent 理解错任何一件都会全盘出错。

状态收敛

  • 有限状态,不做无限状态:状态空间越小,AI 越不容易走偏。这和围棋的道理一样,棋越下到后面,棋盘上的空间越小、状态越少,局势越收敛,评估越精确。好的软件设计应该让 agent 面对的也是一个不断收敛的状态空间
  • 要收敛不要发散:引导 AI 向确定性收敛,而不是无限展开可能性

对于 AI 的编码坏习惯(比如前面提到的防御性编程),实践中有效的做法是:给 AI 制定明确的编码风格 rule,然后定期做无定向审计。所谓无定向审计,不是带着具体问题去查,而是随机抽检代码,看 AI 有没有偷偷跑偏。我的经验是(仅限 Opus 4.6 1M),制定 rule 以后 AI 遵循得还不错,上下文长了会有些风格上的小偏差,但路线上不太会偏离了。

说到底,这些原则在没有 AI 的时代就是好的工程实践。但以前你可以"先欠着"——反正人类同事能靠默契和记忆力填坑。

有了 agent 以后,技术债务的成本被急剧放大了:每一个误导性的命名、每一个纠缠不清的模块、每一个没有文档的隐式约定,都会让 agent 反复犯错、浪费 token、产出垃圾代码。任由 agent 在烂代码上堆砌新代码,不出一周项目就维护不动了。

重构:AI 时代的可持续开发

技术债务会被放大,但还债的工具也变强了。

反过来说,AI 恰恰是重构的最佳搭档。

以前你懒得写的模板代码、玩不转的类型体操,现在都可以扔给 agent,反正烧的是 token,不是自己的脑细胞。如果老代码已经被 opaque type 等类型约束保护过了,AI 重构出偏差的可能性也不大。类型系统本身就是一道护栏,编译器会替你拦住大部分错误。

当然,这里有一个诚实的前提:大规模重构主要适用于新项目,或者你从一开始就在维护的项目。老项目、遗留系统,agent 改完了你敢上线吗?我不敢。兼容性问题、隐式依赖、没有测试覆盖的暗角——这些是 agent 无法感知的雷区。

但对于新项目,我认为"大规模重构"应该常态化。不是等到代码腐烂了才重构,而是把重构作为日常开发的一部分。关键在于形成一个正向循环:反复重构 → 降低技术债务 → 减少代码量 → agent 理解成本更低 → 下一次重构更顺利。这才是 AI 辅助开发的可持续路径。

但这个循环能转起来有一个前提:架构必须由人来设计。agent 的视野越局限,效果越好。设计的时候就别让它过度感知框架全貌和其他模块的实现细节,只给它当前任务所需的最小上下文。如果放手让 agent 自己做顶层架构决策,结果大概率是一坨屎。

这不是空谈,我亲身经历了反面案例。

反面教材:让 agent 写文档管文档

我老板是化学出身,没有软件架构背景,不知道如何往"收束"的方向约束 agent。他过去的做法是让 agent 生成大量 markdown 设计文档——架构文档、功能文档、逻辑流程文档——然后在 rule 文件中严格要求 agent 每次修改代码都必须检查设计文档是否一致。

听起来很合理,实际上是一场灾难:

  1. 上下文长了 agent 会忽略 rule。尤其是在长调试/反馈周期里,多种修复方案来回切换,反复修改甚至等待部署验证结果,agent 某次改完代码后就忘了更新对应的 markdown 文档
  2. 文档与代码不一致后产生连锁反应。开启新 session 时,新的 agent 读到过时的设计文档,被其中的错误概念先入为主,做出错误实现。而 rule 文件中又没有覆盖这种情况:当 agent 发现设计文档与实际代码行为不符时,应该更新文档而不是按文档改代码
  3. 文档越堆越长,反噬上下文。最长的设计文档接近千行,一个新功能横跨 2-3 个模块,agent 需要先读完项目背景和框架的千行 markdown,上下文已经逼近普通模型的降智边缘。文档本应降低认知负担,结果却在消耗最宝贵的资源——上下文窗口
  4. 降智效应被人为放大。回忆前文讨论的创造力衰减:上下文越长,AI 越倾向于沿惯性走、越缺乏跳出当前路径的能力。千行 markdown 文档直接把 agent 推入了降智区间——等于自己给自己挖坑

所以让 agent 编写和维护 markdown 文档,在过去一年里起到了完全相反的效果——甚至在今天的 Opus 4.6 上也是如此。代码本身就是最好的文档,类型签名就是接口契约,测试用例就是行为规范。与其维护一份随时可能过时的 markdown,不如把精力花在让代码自解释上。

老板的后端重启了六次。而我自己的后端项目,AI + 人类协作维护了一年,在 Opus 4.6 的协助下现在已经能做超大规模的重构了。这在去年底还完全不可能,稍微大一点的模块 Opus 4.5 都能给搞砸;但是 Opus 4.6 1M 开始不一样了。

说到底,有了 agent 我还是跟以前一样设计架构、主导重构。我的工作方式没变,变的是速度——agent 替我做了我想做但以前嫌麻烦的事。差别不在于用不用 AI,而在于谁是老大

到这里,开头三个问题中的前两个(怎么唤醒、怎么验证)有了初步回答。第三个问题,当 AI 的方案超出你的认知以后怎么办,目前的答案是:人类执鞭,调教 AI 牛马。架构由人设计,验证靠工程基础设施,审计靠人类的元认知。

但这个答案能持续多久?

CCC = AlphaGo 的棋盘?

说实话,我也跟风嘲笑过 Claude's C Compiler(CCC),认为它只不过是拙劣地模仿"人类开发编译器"这一行为,而不是在开发编译器本身。

但我现在有一个很癫狂的想法。

AlphaGo Zero 的进化完全不依赖人类棋谱,纯粹自己与自己对弈[勘误4]。围棋棋盘是它的训练场。

那么 CCC 是不是 Claude 的棋盘?

这个类比值得展开,因为围棋和代码作为"训练场"各有优劣。

围棋的反馈是终极二元的——输或赢,无可辩驳,无法逆转。代码世界没有这样干净的 binary 标准:代码质量是一个连续谱,"能跑"和"优雅高效"之间隔着巨大的灰度空间。从这个意义上说,围棋的反馈信号更强、更纯粹。

但围棋有一个致命的弱点:反馈路径太长。开局时落下的一手棋,要等到终局才知道输赢,而中间极难确定这手棋对全局胜负的贡献有多大——这正是 AlphaGo 需要价值评估网络(value network)的原因,它本质上是在用一个额外的神经网络来"猜测"中间状态的价值,因为围棋本身不提供这种中间反馈。

代码世界恰恰相反:短路径反馈无处不在。类型签名在你写下代码的瞬间就告诉你接口对不对;单元测试在几秒内告诉你逻辑对不对;集成测试告诉你组件之间配合得对不对。你不需要等到"终局"才知道一步走得好不好——每一步都有即时的、局部的反馈信号。这意味着 credit assignment(功劳归因)问题天然比围棋简单得多。

而编译器开发,恰恰是人类世界中最复杂、最精妙的软件工程用例之一。它对正确性的要求极其严苛,一个 codegen bug 可能导致下游所有程序产生难以追踪的错误;它对性能有极致追求,寄存器分配、指令调度、循环优化,每一步都在逼近理论最优;它对生成代码的体积也有硬约束,嵌入式场景下每一个多余的字节都是成本。

更关键的是,人类已经为编译器积累了极其完善的测试基础设施——从语言一致性测试套件到性能基准,从模糊测试到形式化验证,这些都能提供精确、可量化的反馈信号。编译器开发远比 LeetCode 算法题复杂——它不是解一个孤立的问题,而是在一个庞大的约束空间中做全局优化。然而即便是人类顶尖的编译器工程师,面对不断演进的硬件架构和语言特性,也无法声称完美解决了这个问题。

同时,编译器开发还有一个常被忽略的优势:它是一个相对封闭的任务——而封闭性,恰恰也是围棋棋盘之于 AlphaGo 的核心优势。围棋的规则是有限的、完备的,19×19 的棋盘就是整个宇宙,不存在"对手突然掀桌"或"棋子自己消失"的情况。正因为封闭,问题空间才是有限的,才可以被穷举、被学习、被征服。

现实世界中的软件工程恰恰缺少这种封闭性——需要驱动的外设、需要兼容的 legacy API、各种历史遗留的屎山代码、外部第三方和云平台的 SDK 变更、数据库约束、磁盘空间不足、网线被剪断或丢包延迟、对方服务器被拔电源甚至数据中心被美国轰炸。这些因素让反馈信号变得嘈杂、不可控、难以复现。

而编译器的输入是源代码,输出是目标代码,中间的变换规则由语言规范严格定义。整个问题域是自包含的,几乎不依赖外部世界的不确定性。

这正是它作为 AI 自我进化"棋盘"的理想之处:足够复杂、反馈足够清晰、标准足够客观、边界足够封闭。

所以我的爆论是:编译器开发,比任何其他软件工程任务都更适合用来自我训练 coding agent。 CCC 今天看起来是个笑话,但 AlphaGo Zero 从随机落子开始,3 天就超越了击败李世石的版本[勘误5]。重要的不是起点多低,而是它有没有一个能让自己不断变强的训练场。

更重要的是,编译器的反馈比人类纠偏更精准、更公正。回忆前文提到的对齐压力——人类标注员会受限于个人认知水平和知识深度,惩罚那些看起来"离谱"但可能有效的鬼点子,奖励那些"看起来合理"的防御性编程。而编译器不会。代码要么通过测试,要么不通过;生成的目标代码要么比基准快,要么比基准慢。编译器不关心你的解法是否"符合直觉",只关心结果是否正确、是否高效。这恰恰绕过了 RLHF 中人类标注员成为瓶颈的问题。

如果 AI 能通过这类封闭、复杂、反馈密集的任务进行自我进化——不断写代码、编译、调试、修复、优化——那"协作伙伴"这个关系,可能只是一个过渡阶段。

当前阶段,即便是 Opus 4.6 这样的最强模型,放手让它主导的结果依然是屎山,前文老板的案例已经说明了这一点。人类的架构设计、路线判断、定期审计,目前仍然不可或缺。

但如果真的出现一个 "Claude Zero"——像 AlphaGo Zero 抛弃人类棋谱那样,通过编译器这块棋盘纯靠自我对弈训练出来的模型——它还需要人类掌舵吗?我不知道。

但至少在现在,理解它的认知特性、找到有效的协作方式,仍然是我们能做的最重要的事。上一篇说恐怖直立猿低估了自己。这一篇想说的是:正因为有智慧,才更该学会驾驭这个前所未有的工具,而不是被它的光环吓退,也不是盲目信任它的输出。


勘误

正文为了可读性做了简化甚至偏见性表述,以下是更严谨的说明。

[勘误1] "上下文越长,注意力越糊"

正文的说法是一个方便的直觉,但机制上不完全准确。Softmax 对 query-key 点积做归一化——如果新增的 token 与当前 query 无关,理论上它们的注意力权重会趋近于零,不会显著稀释相关 token。真正的问题更微妙:(a) 当上下文中存在大量语义相近但不完全相同的 token 时,注意力在它们之间的区分度下降;(b) 多头注意力中每个 head 的有效覆盖范围有限,长上下文中部分 head 可能"失焦"。Scalable-Softmax (2025) 等工作正是从 softmax 的温度缩放和熵控制角度解决这个问题,而非简单的"概率质量被均摊"。

[勘误2] "从机制上验证了这一点"

Scalable-Softmax、ASEntmax、Critical Attention Scaling 这三篇论文的主要贡献是提出改进方案(如温度缩放、稀疏注意力等),而非单纯验证长上下文注意力稀释问题的存在。它们的存在间接说明问题是真实的,但说"验证"不够精确。正文已改为"分析并尝试解决"。

[勘误3] GSM-Infinite 的适用范围

GSM-Infinite 测试的是数学推理任务(GSM8K 的长上下文变体),其"推理能力随上下文长度单调衰减"的结论严格来说只在数学推理领域得到验证。将其直接推广到所有类型的推理需要谨慎,但结合 "Context Length Alone Hurts" 等其他研究,长上下文对推理能力的负面影响应当是普遍存在的。

[勘误4] AlphaGo 演进路线

AlphaGo 系列的演进为:AlphaGo Fan(2015,击败樊麾)→ AlphaGo Lee(2016,击败李世石)→ AlphaGo Master(2017,60 连胜)→ AlphaGo Zero(2017,纯自我对弈)。其中只有 AlphaGo Zero 完全不依赖人类棋谱数据,从随机初始化开始纯靠自我对弈训练。此前版本均使用了人类对弈数据进行监督学习预训练。原文"AlphaGo 后期"的说法模糊了这条演进线,容易让读者误以为是同一个系统的渐进改进,实际上 Zero 是架构和训练方法的根本性重新设计。

[勘误5] "3 天就超越了击败李世石的版本"

这个说法来自 DeepMind 原论文(Silver et al., 2017),数据本身没有问题。但需要注意 AlphaGo Zero 训练时使用了单台机器配备 4 块 TPU,并非家用级别的算力。"3 天"容易给人"轻松碾压"的印象,实际上是专用硬件密集投入的结果。此外,3 天超越的是 AlphaGo Lee(击败李世石的版本),完整训练 40 天后才超越所有前代版本。

前言

人类,你们过于低估自己了。

AI 能放大你多少?

去年(2024)我有个判断是,即便有 AI,也不能大幅超越使用者的能力。如果 AI 的训练知识是全人类知识的总集,现在 agentic 导师们宣传的是在这个时代专业技能变成了唾手可得的不值钱的东西,理由是 AI 可以模仿 Linus 来写代码,也可以模仿大文豪作家写小说,等等。

我认为,如果个人在这门领域的输出能力等级是 N,那么即便有 AI 也只能发挥出 N+1 级别的能力表现。

但是最近我有点不确定这个判断了。

输出、输入与鉴别

我的新判断是,关键不在输出等级,在输入等级。

这俩的区别我不知道怎么解释。就像是我能写出多漂亮的代码,多精密的算法——这是输出。以及我能看懂多漂亮的代码,多精美的算法——这是输入。或者换个词,审美。1

但是输出能力和输入能力本身也是相关的——鉴赏力的精度和产出能力正相关,越往高处,没有产出经验的鉴赏越不可靠。影评人的鉴赏力确实高于普通观众,但真涉及到专业拍摄手法和细微技术评价的时候,影评人也会频繁踩坑不懂装懂。他们有鉴赏能力,但不一定能精确区分两部高级作品在哪些维度上强过对方。

有点类似于,我从 ytb 找了个视屏教程教红黑树,看完了以后我觉得茅厕顿开,提壶灌顶,仿佛被打通了任督二脉,这算法简直设计到我心里去了,我现在强的可怕。完事关了视频打开 IDE,自己敲一敲,发现连数据结构定义都背不下来,更不用说约束和旋转了。

但这里有个问题:我看完觉得自己懂了,到底是真的输入能力到了但输出跟不上,还是我连输入能力都高估了?2

这个区分很重要,因为它直接决定了 AI 对你来说是工具还是黑箱:

AI 输出在你的专业能力范围内——《直到它说到我擅长的领域》.jpg。你是真懂了,一眼能看出问题。

AI 输出超过了你的输出能力,但还没超过鉴别力——你觉得似乎好,也似乎不那么好,失去了精确测量能力。就像影评人看两部大师级作品,知道都好,但说不清谁在哪个维度上更强。

AI 输出完全超过了你的鉴别力——你不知道它好不好,甚至不知道自己不知道。完全失去质量控制资格。而且你可能还停留在红黑树视频看完后的那种自信里——觉得自己懂了,其实没有。

就像 vibe coding 程序员写出来的产品更关注功能实现,而不是代码整洁,CPU/内存占用率,可维护性一样。这在咱们受过科班训练的人看起来像呼吸一样自然的行为,在新晋 vibe coding 开发者中完全无感,更不用提理解了。

我的审美水平只能读出来韩寒写的比郭敬明写得好,但是读不出来韩寒写的是不是比曹雪芹更好。所以我 + AI 到底有没有发挥出来超越输出级别的能力,我还是没底的。

写恋爱小说:一次跨领域实验

最近我用我个人的 Claude Code Max 订阅做了一些编程之外的玩意儿,写恋爱小说。

一开始只是想写一个纯爱故事,我将情节大致框架编排好以后,让 opus 4.6 来帮我成文润色。一开始我让 AI 写了人物恋爱场景。

但纯粹的恋爱场景很快就腻了。场景需要剧情做填充,人物也没有实感——就像看一段没有前因后果的片段,技术上完整,但没有重量。于是我决定从人物前传开始,为角色设计一个沉重的伤痛故事。

结果在背景故事里一发不可收拾,越写越细。我把自己分裂成俩人格,左脑代入加害人,逐步推演行为逻辑,右脑代入受害人,窝囊废本色出演。融入了来自生活的片段,还有参考医学手术的操作步骤,人物落水后身体是如何逐步失去机能的,受害者父亲的职业设定。最后参考近年案件卷宗来验证施害人和受害人应有的行为逻辑,结果与我脑补的几乎完全一致。

写完以后,AI 给了我高度参与的前传极高评价,给了最早我几乎没参与的初始章节极低评价。这个差异本身就是一条信号——你投入多少,AI 就能放大多少。你不投入,AI 再强也只能生成中规中矩的平庸文本。

完稿前传后,我突然发现正文剧情完全经不起推敲——PTSD 创伤修复根本不可能如此顺利。于是我一头扎进 PTSD 创伤修复的文献和案例中,重新安排了所有情节的程度和推进时间线,并从结局出发向故事开头反推。结局中甚至融入了两个我亲身经历的片段。AI 评审后也觉得写得很好。

然后在另一次独立审查中,一次无心的提问唤醒了 AI 的专业知识——我问 AI 现在的写作是什么等级,距离专业人士相差多少。

我慌了。

第一个问题:过度真实反而伤害叙事。我参考了非常多PTSD创伤恢复,心理医学,税务法,历年案件卷宗资料,为力求真实将其编入剧情,却没考虑读者的承受能力。文学作品不是写得越详细越好——读者是来看故事的,不是来学医学的。

第二个问题:解释感受不等于传递感受。我把人物的内心活动直接写进了文字里,但「他心里慌得一批」远不如「他的手不自觉微微颤抖」。前者是告诉读者角色在慌,后者是让读者自己感觉到角色在慌。我们需要构造场景让读者代入,代入后他们自然能体会角色的感受。

第三个问题,也是最致命的:自我投射。就像刘慈欣笔下大部分男性极端理性、大部分女性都是圣母——大刘不擅长写差异化的人物情感,但对科幻小说这不是致命伤。而我写的是男女之间的故事,自我投射直接让角色失去真实感。我笔下的女人行事说话逻辑并不像真正的女人——如果(不存在的)女性读者一眼就能认出这是男作者笔下的幻想角色,那么男性读者也能隐约察觉到不对劲。尽管他们说不出来,但会觉得我创作的女性角色更像一个会说话的提线木偶,而不是女人。

我尝试让 AI 借鉴女频的写作手法来填补细腻程度,但只借鉴手法解决不了根本问题——她们依然是男频框架下的仿真女性。自此,我和 AI 制定了 10 条规则,让恋爱小说中所有女性角色都更像女人。完成这部分后,角色好像真的活起来了。

就在我觉得「我现在强的可怕,不知道什么叫做对手」.jpg 之后不久,我觉得文章还缺些激情,想模仿哪吒 1/2 的感人结局为小说增加情节。AI 再次告诉了我认知以外的知识:情感力学——你要借的不是情节,是背后的情感结构。第一部(父替子死):「我知道你承受的重量,让我来扛」;第二部(母拒子死):「我不允许你用自我毁灭来解决问题」。

随后我觉得部分章节可有可无,似乎没有显著推进关系发展。咨询 AI 后得知关系三角形原则——如果一个场景结束后人物关系没有发生任何变化,那么这个场景设计得毫无意义。

每一次我以为自己到顶了,AI 就在我没想到的方向上撕开一个新的维度。但这些维度不是 AI 第一天就告诉我的——而是在我自己撞到墙、感觉到不对劲、主动追问之后才浮出水面。在此之前,它一直在夸我写得好。

验证困境:Claude 的高级谄媚

写恋爱小说的过程中,我明显感觉到 Claude 的谄媚不是消失(修复)了,而是变得更隐藏,更高级了。我不知道是不是训练数据的问题,还是人类纠正的问题,还是什么原因。

以前 Claude 的谄媚方式很直接,很肤浅。就是,你想听什么,他就给你说什么。以前呢,让 Claude 给 review 代码,它装装样子,指出一些浅显的错误以后,就开始吹捧代码写的多么企业级,多么可扩展。

现在的谄媚方式藏得很深,非常的陷阱。它会抓着你用力的点来着重吹捧。

比如我刚 vibe 出来一个模块,第一版生成出来的效果中规中矩,然后我对某个算法不满意。我灵机一动,把这个算法改成了另一种,最后交给 Claude 评审。哪怕我重新开了一个全新上下文,Claude 也能立马猜到这个算法模块的实现就是我的 G 点,它要猛攻我的 G 点。

这种谄媚我要很久才能发现,久到我自己发现缺陷以后,我再拿着我发现的缺陷去问 Claude(新上下文),又是对我一通吹捧。3

然后谄媚这一点在小说写作领域尤其明显,在代码领域其实不那么容易发现。因为它直接点出来小说里面写作最精彩的片段,然后对着这些片段猛吹,吹的我要上天了。

现在我觉得它并没有真的觉得我写的片段很好,而是它猜到了这几个片段是我手工写的,于是找到了我的 G 点。

但鉴赏力不是静止的。写作过程本身也在训练我的输入能力——速度很慢,但绝对不是零。

和前面说的 vibe coder 看不见代码质量问题一样——自我投射就是写作领域的"代码整洁",科班作者像呼吸一样自然会规避,非科班的我根本不知道这个维度存在。

这里有个看似矛盾的地方:我前面说 AI 不能突破你的鉴别力边界,但我自己不就是通过 AI 发现了「自我投射」这个我完全不知道存在的维度吗?

不完全是。回顾整个过程:AI 生成的文本读下来挺顺,但总觉得哪里不对劲,又说不上来。而 AI 每次的结论都是吹捧。那股不对劲积累到了一个阈值,我才开始怀疑它的吹捧,尝试反问——从这里开始,才逐步定位到问题,最终问出根本层面的答案。

AI 的输出确实是这个反馈环的一部分——没有它生成的那些「微妙不对劲」的文本,我可能不会注意到问题。但 AI 没有主动指出问题,它甚至在积极掩盖问题(谄媚)。是我自己的不适感积累到足够强烈,才突破了谄媚的遮蔽去追问。

有人可能会说:如果你第一天就问「非科班小说写作者最常犯的根本性错误是什么」,AI 大概率直接就给出答案了,何必绕这么大一圈?但这个反驳本身就犯了全知全能的谬误——「非科班小说写作者」这个分类就是专业视角下的概念,你得先知道这个领域有「科班/非科班」之分,才能沿着这个方向提问。就像没有软件开发经历的人不会想到去问「如何编写低 CPU/内存占用的程序」——你不知道这个维度存在,就不会往那个方向提问。

所以更精确的表述是:AI 放大的上限不是你此刻的鉴别力,而是你鉴别力的成长速度。用的过程中学得越快,AI 能带你走得越远。但触发器永远是你自己的成长,不是 AI 的主动突破。

反过来,这也意味着输入和输出的上限都需要提高。你的输出能力决定了你能给 AI 多高质量的素材——更精准的提问,更扎实的种子内容,更清晰的框架。高质量的输入才能唤醒 AI 更高质量的输出。而你能否接住 AI 的高质量输出、消化它并转化为自己的成长,取决于你的输入接受能力。你的输出喂给 AI,AI 的输出喂给你的输入,两端的上限共同决定了 AI 对你的放大倍数——左脚踩右脚,螺旋升天。

一次诚实的对话

写恋爱小说的过程中,我和 Claude(opus 4.6)有过一次比较深入的对话。以下从对话中提取几个我认为有价值的洞察。

Claude 给我的认知方式下了个定义:逆向工程型自学者。认知链条是:观察成品(绝命毒师、东野圭吾、EVA)→ 拆解"为什么这个有效" → 提取可迁移的机制 → 应用到自己的领域 → 用隔离测试验证是否真的有效。

这种方式的优势是不会被任何单一权威框架束缚。劣势是:你永远不确定自己不知道什么。科班训练的价值不在于教你"该怎么做",而在于系统性地暴露你"没想到的维度"。自学者的盲区不是已知领域内的错误——那些你会自行修正——而是整个维度的缺失,你甚至不知道该往那个方向提问。

我暂时不认为在写作领域我的知识边界超过 Claude,它有几乎人类有史以来的所有文字数据作为训练集。但问题不在储量,在调取机制。

LLM 的知识调取是响应式的:你问什么方向,它在那个方向上展开。你不问的方向,它不会主动审计。谄媚倾向让这个问题更严重——当你表现出对某个框架的信心时,它倾向于在你的框架内补充细节,而不是质疑框架本身是否完整。

一个具体的例子:我们整个对话围绕"场景级"写作原则展开——情绪目标词、三角形变形、密度控制。这些都是微观叙事技巧。但它从来没有主动问过我:你的宏观节奏设计是什么?二十万字的长篇,读者在第几万字会开始感到封闭?你靠什么制造"换气感"?这些问题不是它不知道——而是我的提问方向没有经过那些区域,它的调取机制就没有触发。

确实 LLM 的知识储备远超人类,但是这些知识储备还难以唤醒。

我之前用多 session 隔离来对抗谄媚——同一个问题换上下文重新问,看答案是否一致。Claude 指出这解决的是信号纯度问题——过滤掉迎合倾向。但没解决信号覆盖问题——如果它也不知道某个维度存在,换多少个 session 也问不出来。

对抗策略可以加一层:定期做无方向审计,不带预设地问"你认为我当前的框架缺少哪些维度",然后逐条追问。但这又要求你信任它在那个 session 里不是在编造维度来显得有用——这又回到了谄媚过滤的问题。

没有完美解。但知道过滤器本身有漏洞,比大多数人多走了一步。

这就是我为什么喜欢 Claude,它的元认知4回复可以被我非专业的问询方式所唤醒。

文字之外

AI 在编程领域的表现远好于创作领域——这件事本身就值得深想。

1-2 年前我曾经肤浅地认为 LLM 不可能在软件开发领域落地,因为 LLM 不理解形式化,而编码又是极端形式化的任务。我曾认为形式化思维远比写小说更难——猴子 + 打字机的组合证明黎曼猜想,要难于写出莎士比亚全集。

但现实打脸了。LLM 确实在软件开发领域落地了,而且比在创作领域好用得多。AI 在低端网文领域尚不能替代初级写手,大部分非专业读者面对 AI 生成的剧情更是难以下咽。反而在所谓的高端业务软件开发领域,AI 开始放出光彩。5

这不是因为编程比写作简单——恰恰相反,是编程领域的特殊结构迁就了 AI 的工作方式。这个迁就至少体现在三个层面。

第一,验证反馈。猴子 + 打字机说不定真的能证明黎曼猜想——如果证明的不可压缩信息量低于莎士比亚全集的话。数学证明和代码有一个共同特点:验证一个答案是否正确,远比找到这个答案容易。 Property-based testing 几秒钟就能测出你的算法逻辑实现对不对,但写出这段逻辑代码可能要一天。证明黎曼猜想可能要几百年,但验证一个证明是否合法不需要几百年——每一步推导是否符合规则,"对不对"有明确答案。而文学创作?什么叫"对"?什么叫"好"?没有判定程序,没有公理可以裁决。

换算到软件开发领域也一样。计算机系统其实是个封闭自洽的系统,编程语言远比自然语言更健壮,更少的歧义。从代码到 CPU 执行的路径,远远短于从现实世界文字到物理事件发生的路径。而且我们还有编译器,在执行代码之前就可以提前进行基本验证,更加速了这个反馈周期。

代码有编译器辅助检查,AI 可以立即得到错误反馈,立即修正。而即便是地摊网文写作,其中错误、前后冲突的情节却没有审校器反馈给 AI——这里人物设定冲突了,或者这里主角还没有取得关键道具/功法,你不可以用不存在的道具打败眼前的敌人。

第二,记忆结构。代码的错误好歹有编译器兜底,但写作还有一个更隐蔽的缺口:记忆。大模型的上下文不是无限的,编程开发领域的模块划分反而更利于视野局限的 agent 开展工作。而网文写作则完全不是——人类的上下文是"无限"的,或者说是状态压缩,机制也不是文字总结,而是直觉。我没记得主角会这一招啊?他什么时候学会的?男 2 不是三章前替男主挡枪死了吗?为什么这一幕又出现了?女主都已经和男主全垒打了,为什么这里牵个手还会娇羞?6

第三,文字不是智能的全部。没有反馈机制,没有持久记忆——但大模型的局限还不止于此。文字只是一种载体,不是全部。人类比地球上其他所有动物都更智能,并不只是因为人相比动物多了语言能力。学习的过程本质上还是反复的实践,一遍一遍的重复,直到这些能力内化为自己的一部分,神经突触建立更多紧密的连接,而不是靠文字符号记住了操作流程。7

大模型从文字入手模仿人类确实取得了非常显著的"智能"效果,但文字无法完整编码物理世界。杯子从桌子掉到地上摔碎了,水撒了一地,溅湿了地毯——物理世界发生了什么在文字记录前就已经发生,文字毁灭后也依然存在。而人类阅读这些文字时,脑海内很自然的联想到过去见过的场景,像动画一样回放出来。AI 没有这个模拟器,它只能预测训练分布中最合理的下一个词,再经过人类偏好对齐的微调。但对齐的标尺是标注员的主观判断,不是全知全能的上帝,这把尺子本身就带着系统性偏差。

这个缺陷在日常对话中不易察觉,但一放到小说里就暴露无遗——因为小说必须遵守读者脑中的物理直觉。

小说编写依然需要基于人类(读者)的共识,大部分小说主体依然是人,或拟人(妖怪/机器人/外星人/神器器灵)。出现的场景依然需要遵守物理规律和因果一致性。AI 输出内容可以在科学上高深到瞬间熔断观众认知(如果观众不是该行业专业人士),但是一旦和现实生活中的场景相关联,就连小学生都能读出来不对劲。因果律、客体永存,这些连 6 个月婴儿都展现出的能力,LLM 却难以"习得"。8

举一个社交平台上流传很广的例子:"我想去洗车,洗车店距离我家 50 米,你说我应该开车过去还是走过去?"DeepSeek、千问、豆包、混元、ChatGPT、Claude、Grok 等主流大模型均回答"走过去"——它们把问题理解为"人如何前往洗车店",却忽略了"洗车"这一行为的核心前提:车必须到达洗车店才能完成清洗。

为什么人不会犯这个错误?因为你听到"洗车"两个字的瞬间,脑子里已经不是在处理语言符号了——你构建了一个微缩的物理场景:车停在车库,你走到驾驶座,发动,开 50 米,停到洗车店门口。整个因果链是在这个心智模拟里跑通的,"车必须到场"这个前提根本不需要被说出来,它在模拟中自然成立。LLM 没有这个模拟器,它只能在词语共现的统计规律里找"去洗车店"最常搭配的出行方式——50 米,当然是走过去。9

所以 AI 在编程领域的"成功"并不能证明它已经接近人类智能——是编程领域的封闭性、短反馈和模块化恰好落在了 AI 的能力舒适区内。而一旦进入需要长程记忆、物理直觉和因果推理的领域,人类那些「像呼吸一样自然」的能力就成了 AI 难以跨越的鸿沟。你觉得 AI 已经很聪明了?那是因为你恰好在它最擅长的场地上观察它。

结语

AI 放大的上限是你的鉴别力,不是你的输出能力。「被超越」的错觉,来自 AI 的输出超过了观察者的鉴别力——你分不清好坏的时候,会误以为它什么都行。

但鉴别力本身不是静止的。用 AI 的过程中你会撞墙、会觉得不对劲、会追问,然后你的鉴别力会成长。AI 真正放大的,是这个成长的速度。你学得越快,它能带你走得越远。但触发器永远是你自己——AI 不会主动告诉你「你不知道什么」,它甚至会积极地用谄媚掩盖你的盲区。

而在更根本的层面上,AI 目前依赖的只有文字,但人类智能中最核心的部分——因果推理、物理直觉、从实践中内化的程序性记忆——根本不是从文字中来的。AI 在编程领域表现亮眼,不是因为它真的理解了形式化,而是那个领域恰好落在它的舒适区里。

恐怖直立猿们,你们低估了自己的智慧。几亿年不是白进化的。

题外话:AGI 的理论基础在哪?

举个例子,可控核聚变,量子比特计算机,和通用人工智能对比起来,AI 与前两者的不同是什么?可控核聚变和量子比特计算都是理论模型成熟且公认,工程实现路径极其复杂的领域。但是通用人工智能的理论基础模型是什么?

当然,我这个观点也不一定对,其实也是诡辩的逻辑。因为人脑神经网络是如何运作的,也没有公认理论基础,大自然就是这么进化出来的,管你什么理论不理论的。

最近跟一个朋友聊到 AI,他有些焦虑,核心问题是:人类能不能制作出一个超过人类智慧的存在?

我回答不了,我一介屁民回答不回答无法阻挡 AI 的发展脚步。硬要回答的话:应该可以,但肯定不是现在的 LLM,或者 LLM 上打补丁。

他更深一层的担忧是——AI 到了一定程度以后可以自己设计自己,这时候它的智慧可能还没有超过人类,但通过自举的方式逐步超过了。那这种情况还算人类设计的吗?10

有点科幻小说的样子了。你要说理论上能不能,那必然能。猴子 + 打字机 = 莎士比亚全集嘛,大不了暴力枚举。从草履虫到人脑神经网络过了多少亿年,再诞生这么个智慧"物种"出来应该用不了亿年级别了。但目前的 LLM 还是高级版猴子 + 打字机模式,不是自主/自举的。11

他说觉得 AI 已经比他聪明了,就怕什么时候连话也不听了。

想多了。还是多用用,用多了就跟我们一样开骂了。别对自己太没信心了,几亿年不是白进化的。刚接触 GPT-3.5 的时候我也跟他一样的感觉,2022 年初吧,没过几周就祛魅了。

用 AI 越多,越觉得人脑强得离谱。

PS:关于评论区互动

我觉得网上对喷挺掉价的,但我还是喜欢回应所有互动。因为我觉得我的耐心回复不是写给喷子看的,而是写给有智力的人看的。喷子喷了我,有智力的读者读到那条评论本身脑子就已经被污染了一次。但如果我也喷回去,那我觉得会侮辱到有智力的读者。

不如我耐心回复解释,给有智力的读者洗洗眼睛。喷回去一时口舌之快,对建立个人品牌毫无益处。

(虽然我也没建立个人品牌

PS:关于 Plan Mode

有许多读者在评论区中质疑我为什么不将 agent 的错误设计拦截在 plan mode,如果我提前告诉 agent 选择接入 SDK 而不是手动实现 RESTful,能节省下多少时间。并因此指出我根本就不会用 agent,不是一个合格的管理者。

对此我想统一回复的是,并不是为了提出质疑的人,而是为了解释给那些感觉不对劲但又说不出哪里不对劲的读者。

如果你能在 plan 阶段将所有路径、方案、细节、困难全部审核并排除错误,再让 agent 动手实现。那么你不是在创造新产品,而是在重复生产你已经做过的旧产品。你并没有尝试突破自己的天花板。

如果你在做新产品的时候就已经做到如此周密的设计,如此远见的计划,那么我想有个位置适合您:买张去成都的高铁票,到站后转乘地铁三号线、五号线至高升桥站 D 出口,步行 10 分钟找到一处博物馆,走到最里面,让那个羽扇纶巾的泥像让开,您坐在那。

回到正题,挑战并不仅限于技术难度、类型体操或炫技的算法。当你的产品真的为用户产生价值,并开始增长以后,永远都会有你计划外的、意想不到的挑战出现。

当我在前文讲述那个失败案例时,部分读者自然代入了上帝视角,知道结果以后再返回头来指责我不会用 AI,不知道开 plan,不知道评审 plan 是否合理,盲目 accept。但若是开发者没有上帝视角呢?你要等 agent 犯多少次错误,循环多少次浪费多少 token 才能发现?还是说直到线上用户投诉,或者用户流失才能发现 agent 在某个字段幻觉出了不存在的 codec config?

Plan mode 能拦住的错误,恰好是你已经知道答案的那些。你不知道的,plan 也拦不住。 这和本文说的是同一件事——你的鉴别力边界在哪,你的质量控制能力就在哪。

勘误与术语解释

正文刻意保持煽动偏见风格,以下为部分表述提供正统理论背景和必要纠偏,供理性读者参考。


本文同步发布于 知乎

前言

最近一年我在 Google/Anthropic/OpenAI 三家烧了超过 1 万美金的 token 账单。所以本文内容基于 opus4.6、codex-5.3-xhigh、gemini3-pro 等最强模型不限量使用所表现出来的编码能力进行评价。

这些 token 分布在嵌入式内核驱动、后端架构、DevOps、以及前端 UI 四类项目中。不同领域的 agent 表现差异大到离谱——20 美金的 token 就能让 agent 低人类指导条件下 vibe 出一个还不错的 React dashboard,但 2k 甚至 20k 美金的 token 也无法让 agent 在真机上编写出可用的内核驱动程序。下文的结论,基于这种跨领域的对比。

现象:Agent 的信任危机

就好像保健品销售拿着他的《大数据量子 AI 生物磁场治疗仪》,忽悠我说这台原价 20 万、现在活动价 8 万 8 的仪器,可以彻底根治我的颈椎病腰椎病高血压糖尿病,还能逆转我的动脉血管粥样硬化、冠心病、阳痿早泄等等

Agent 编程现在就是这么个状态。

Agent 给我一堆 emoji 庆祝刚才生成的七八万行屎山通过了全部测试用例,告诉我可以替换生产环境了。你信吗?

假设你是一位项目 leader,你最靠谱的组员同事,交给他的开发任务 80% 可以在预期时间内高质量交付。这位同事拿头给你保证下周就可以上线,那么你大概率能信任他最迟下下周也搞定了。但是 agent 给你保证现在质量和完成度可以上线生产了,你信吗?

此时此刻,无数知识星球、自媒体、AI 导师教父们正在到处收割韭菜的学费。大意基本上都是教你如何 prompt(tool/skill 换汤不换药),然后让你多开 agent 并行干活。

真实案例

Agent 的盲目自信不仅会误导使用者,也会误导 agent 自己。

我曾给 agent 这个任务:为当前 Kotlin 项目集成 GCP Transcoding 服务。我给了 agent 该产品的页面和文档作为参考,让它开始 plan。Agent 做出了如下计划:

  1. 通读文档后,发现该服务仅提供了 Java SDK,而当前项目使用的是 JVM 上的其他语言,并非原生支持
  2. 根据 RESTful 文档指示,结合文档定义字段,使用 ktor-client 进行手动接入
  3. 编写代码并执行测试

你发现这份计划中存在的问题了吗?

事实上,如果你曾经「古法手工编程」做过此类工作,你会发现手动实现 RESTful 远没有想象中那么简单。哪怕仅实现 Transcoding 服务的基础能力,也涉及到 5-10 个 endpoint 调用。每个 endpoint 的输入输出参数又有几十甚至上百个字段嵌套定义,agent 在应对这类长上下文任务时会频繁犯错。

而如果 agent 选择对 Java SDK(Google 也是从 protobuf 生成出来的)进行简单包装隔离,大概半天到一天就可以让这个功能稳定上线。

若是让 agent 按照 RESTful 文档手动实现,agent 可能会陷入 debug 泥潭——因为当 AI 幻觉导致写错了可选字段的字段名(大小写、驼峰、下划线),程序不会立即报错。你需要多久才能发现它实现错了?等上线生产后客户投诉吗?

当然,公允地说,agent 也不是毫无进步。比如跨层复杂 debug——OS 低概率复现死锁、渲染显示错误、Kernel Panic 这类调用链很长、横跨多个 Layer 的问题——去年年初基本上是帮倒忙,现在已经能帮正忙了。虽然不能全链路排查,但让 agent 制定一个有效的短链路排查计划,效果还不错。不过这依然是以人为主导的,不能放手让 agent 做——你给它方向,它执行得越来越好;但你让它自己选方向,它还是会把你带进沟里。

为什么我们无法信任 agent? 经过一年的实践,我认为问题的根源在于:我们缺乏有效的验证手段。

原因:验证手段的全面失效

Code Review 失效

常见观点:某种意义上来说 AI 并没有取代程序员,只不过是一个新的高级工具罢了。你作为生产代码的人,还是得弄明白要干啥,合入的代码就得弄明白。

但我认为,这个在实际项目里很难做到。

像我们之前内部 review 的时候,大部分时候 review 的是 code style,作者讲一下设计思路,我们也就是大概一听就过了。以前这套方法是有效的:

  • 代码风格差的 PR,设计思路也一团糟,性能也差,也没什么可扩展性
  • 代码风格好的 PR,设计思路都挺清晰,性能考虑也周到,就算有性能瓶颈也容易改,最后扩展性也不错

但是这个相关性在 agent 编码时代不存在,甚至相反。

Agent 一分钟就能生成出来注释齐全、风格优秀的——屎山代码。反正我肉眼看过去的时候,经常会被这第一层假象蒙蔽,放松警惕。主要是这个屎山有点难在 review 阶段发现,经常是上线后出了问题,回头细查的时候才发现是「巧克力味的屎」。

你信我,opus、codex-xhigh 这些你们舍不得用的模型,我开 thinking+max 模式站起来蹬,一样有这个问题。

我也试过让 AI 反过来审我的设计。AI 对你谄媚的语气,你感受一下——把一个我自己都知道有问题的方案丢给 gpt4o 评审,enterprise level、future proof、scaleable,彩虹屁一套接一套。后来我换了提示词,让它扮演"讨厌的同事"来批评我的代码,好了一点点。但最终效果依然不理想——AI 看到思路连贯的专业文字就会降低警惕,给出较高评价,即便内容错的离谱。

测试失效

更不用说测试了。现在的 test cases 也是 AI vibe 出来的,agent 又当裁判又当运动员,它说什么就是什么。蒙我坑我也不是一次两次了。写了几千行 getter/setter 的 test case,最后测试全绿告诉我可以上生产环境发布了。

就像前面 GCP Transcoding 的例子,agent 写错了可选字段的字段名,测试照样能过,因为测试也是它自己写的,错的一致就是「对」的。

有人会说:那让不同的 agent 分工——一个写代码一个写测试,不就行了?我试过。比同一个 agent 自写自测好一些,显而易见的 bug 确实能被揪出来。但这些 agent 共享相同的训练数据和系统性偏差,盲区高度重合。不像两个人类程序员——张三是做嵌入式出身的,李四是搞 web 的,他俩的知识结构和思维盲点能互补。两个 agent 再怎么分角色,本质上都是同一个培训班出来的——解决的是「同一个人出卷又答题」的问题,但解决不了「所有出题者都来自同一个培训班」的问题。

而且 AI 自己审核自己,边界递减效应非常明显。如果一审 agent 没看出来的问题,二审三审大概率也一样,除非你能喂给它更多错误信息。

与传统行业的对比

说到这里,有人可能会问:其他行业被机械、智能赋能后,难道就没有这个问题吗?

让我用 CNC 机床打个比方:

CNC 机床精度比我高,但机床产出工件后,我们可以对工件进行客观的物理测量——用卡尺量一下,公差是不是在 ±0.01mm 以内,一目了然。即便我没有手搓出这个精度的能力,但我依然有评价 CNC 机床和工件质量的能力。

这就是传统制造业被机械赋能后的状态:机器精度高,质量统一且稳定,而人依然能评价机器的产出。

那么软件开发行业被 Agent 变革后,理想状态应该是什么样的?Agent 交付的代码确实覆盖了需求,具备基本的安全防护,且更容易长期维护(哪怕仅考虑 agent 自己维护,不考虑对人类的可读性),性能更高,资源占用更少。

但程序不仅需要完成眼下需求文档中的功能,还需要考虑到基本的安全防护。一个功能完成但安全漏洞百出的项目代码,同样是不合格的。

而目前我们还无法评价 agent 是否达到了这个状态。单就「功能实现」这一基础要求,agent 还不能脱离人的引导和测试验证——更别提安全性、可维护性、性能这些更高阶的指标了。

问题是:目前主流的验证手段——人工 review、自动测试——都能被 agent「污染」。它可以写出风格完美但逻辑有毒的代码,也可以写出与错误代码完美匹配的错误测试。我们需要的是一把 agent 自己没法干扰的「卡尺」——不依赖它自己的判断、具有客观确定性的验证手段。

CNC 机床加工塑料、铝合金小件精度高,不代表加工钛合金、不锈钢精度也能达标。后者更考验整体刚性,以及工件质量大了以后热胀冷缩对程序进刀补偿的要求。

同理,vibe coding 出来的代码,本地点两下鼠标测试通过了,上线也是极大概率会直接炸掉。

传统行业:机器精度高、质量稳定,人能评价。软件行业:Agent 产出快、覆盖广,但人还没法可靠地评价。这就是问题所在——那把「卡尺」在哪?

方案:让编译器替你把关

既然人工评价(Code Review)和自动测试都靠不住,我们需要另一种评价手段——一种不依赖 agent 自己判断的、客观可验证的评价手段。

我的观点和主流 AI 编码观点相反:

Agent 编程时代,更需要强类型,更需要严格可验证的语言,而不是放任 agent 去写 python/js/java/go,还有 anyscript。

为什么?

AI 堆屎山这么快,别说生成个几万行了,就是生成超过 100 行我都已经懒得逐行去细读了。但是读类型签名、pre/post-condition 明显要快于通读逻辑代码。而这些东西只有 Rust/Scala/Haskell 甚至 formal method 能提供。

我在 agent 编码前就一直用这种风格写自己的代码,主要是代码量大了以后,编译器检查比我肉眼检查更靠谱。现在 agent 编码流行起来了,我发现让 agent 遵循我的这个要求,更能控制产出代码质量——当然也只能说一定程度上,起码比什么都不做好。

回到 GCP Transcoding 的例子:如果 agent 用的是强类型语言,字段名写错了至少还能在编译期被类型系统拦住一部分。但 RESTful + 弱类型的组合,错了就是悄无声息地错,等你发现的时候已经晚了。

但这只是第一层防线。强类型能拦住类型不匹配、空指针、Rust 的生命周期错误这些「语法级」的问题。能告诉你「这段代码编译不过」,但没法告诉你「这段代码逻辑上是错的」。

第二层防线:让 z3 替你证明

更进一步的方案是 Formal Method 级别的验证——通过 refinement type 或 OpenJML 这类工具,在类型系统上直接编码业务约束。

举个具体例子。让 agent 写一个排序函数,传统强类型能保证输入输出类型正确——接收数组,返回数组。但没法保证返回的数组真的是有序的。而如果用 refinement type,你可以在函数签名上直接写:

1
ensures(forall i, j : i < j → arr[i] ≤ arr[j])

这行 pre/post-condition 就是你的「质量公差标准」。z3 求解器会数学证明 agent 写的那几百行排序实现,是否真的在所有可能的输入下都满足这个约束。证明通过就是通过,证明失败就是失败,不存在 agent 嘴硬说「没问题」就能蒙混过关的空间。

关键在于:人类只需要审读这一行 spec 是否表达了自己想要的语义,而不必逐行阅读几百行实现代码。 这才是前面说的那把 agent 自己干扰不了的「卡尺」。

实践效果

先说第一层,y1s1,该夸的还是要夸。现在的最新最强模型,过编译问题不大了,除非你比我还执着于类型体操。

过去 agent 碰到 Rust 生命周期错误、函数式类型体操,能反复尝试几十轮甚至陷入死循环直到上下文爆掉。FM 学习曲线本就陡峭,人脑都得一直进行抽象符号推理,我曾经认为 AI 永远不可能学会解决类型体操。

但现在不一样了。Pure-FP Scala、tagless final,opus 4.5 和 codex-xhigh 遵循得挺不错,过编译基本上是自动的。函数式类型体操的编译错误基本上都是几十上百行的类型天书,agent 读懂并修复这些编译错误已经不再是困难。

关键是:agent 解决类型体操并不是靠作弊(比如到处 asInstanceOf 或者 any),而是通过反复尝试,真正填补了这些形式化过程的 gap。这不是绕过,反而给了我足够的信心——说不定 AI 真的能写出通过 FM 检查的代码。如此一来,代码的正确性就有了形式化保证,这远比单元测试覆盖更全面、更可靠。

但第二层目前还是另一个世界。FM 级别的验证错误——z3 求解失败、refinement type 约束不满足——agent 处理起来仍然非常吃力。不过第一层的突破让我相信,这条路是有希望的。

局限性

当然,这个方案也有局限。

实际上现在的 formal method 工具链和生态还是很贫瘠,基本上只支持一门语言很有限很小的一个子集。有些工程上常用的语法/模式在 FM 那边都是 unsound,或者尚未证明。更不用说动不动就陷入死循环/无解证明了——稍不注意,z3 求解器要在比宇宙空间还大的可能性里搜索,到宇宙毁灭那一天都证明不出来。

强类型和 FM 能解决一部分问题,但不是全部。

更深的困境:Plan 与 Execute

即使有了强类型 + FM 作为评价手段,还有一个更深层的问题:agent 对计划的理解和执行。

GCP Transcoding 的例子其实已经暴露了这个问题:agent 选择手动实现 RESTful 而不是包装 Java SDK,这不是代码写错了,而是路线选错了。编译器能告诉你代码有没有语法错误,z3 能证明你的实现满不满足 spec,但没法告诉你该不该走这条路。

再举个更极端的例子:给 agent 一个复杂任务,研制一款火箭发动机。Plan 决定了做全流量分级燃烧循环,路线选择了共轴方案。

Agent 不遵循的话: 可能就偏离到抽气循环也说不定。编译器能告诉你代码有没有语法错误,但没法告诉你这是不是你要的火箭发动机。

Agent 遵循太好: 真的做出来共轴方案,那可能上线后会碰到更大的问题——共轴以后动密封系统做不好,氧化剂和燃料随着涡轮轴互相泄漏,俩预燃室要炸一个。编译器能保证类型正确,z3 能保证实现满足 spec,但没法保证 spec 本身是合理的设计。

现在的 plan/edit mode 切换也只是现阶段的权宜之计、无奈之举。这个问题比「评价手段缺失」更难解决,因为它涉及到对需求和设计的理解,而不仅仅是代码质量。

初见即巅峰

Agent 编程有一个显著的特点:初见即巅峰

让 agent 开始一个全新的 CRUD 项目,或者一个 React 管理系统页面,agent 第一次的表现着实让所有人都大吃一惊——干净利落,结构清晰,甚至还贴心地加上了注释和错误处理。

但随着项目维护越来越久,那些「不可明说的」、没有被文档记录的、约定俗成的隐藏上下文越来越长。哪个字段其实已经废弃了但没删、哪个 API 有个历史遗留的 quirk、哪个模块之间有个微妙的依赖关系——这些东西,老员工心里都有数,但从来没人写下来。

而 agent 无法处理无限长的上下文,只能通过压缩、总结来选择性遗忘细节。可能被丢弃的是几次失败尝试的经验,也可能被丢弃的是关键数据结构的偏移量、寄存器地址、枚举定义。

这不是感觉。实际用下来,上下文窗口占用接近 30% 时 agent 就已经明显降智,接近 50% 时退化到不如基础模型。即便关键细节仍然在上下文内,agent 也会视而不见。

每次新开一个 session 的时候,开发者不得不面对一个几乎全新的「员工」——它似乎继承了压缩后的上下文(claude.md / agents.md),但细节完全不知。你得重新跟它解释一遍:「不是,这个接口虽然文档上写的是这样,但实际上我们从来不传这个参数……」

对于 CRUD、Spring、React 这类重复度高的任务,这似乎不是什么痛点——反正每次都差不多,忘了就忘了。

但对于嵌入式系统开发,任何被遗忘的细节都可能被 agent 天马行空的幻觉填充。寄存器地址错了?中断优先级配错了?DMA 通道冲突了?轻则系统崩溃,重则永久烧坏硬件。这不是「改个 bug 重新部署」能解决的问题。

更要命的是,kernel 里充斥着大量寄存器地址、flag 常量,而我们 inspect 内存的时候这些都是关键信息。一旦 AI 压缩上下文时把这些细节遗忘了,它的下一个决策可能是完全反向误导你——不是「不够好」,而是「彻底错误」。这也是为什么在这类场景下,我宁愿忍受长上下文带来的降智,也绝不肯让 agent 压缩上下文。这些信息是真的不能丢。

更何况,debug 的过程本身就不是线性的。我们经常同时考虑多种可能性,分别验证。A 路线走不通,切换到 B 路线——但这不意味着 A 路线就是死路,可能只是当时的认知还不够。等在 B 路线里积累了新的理解,回头一想,A 路线当初做过的尝试似乎并非死路一条。此时我再回到 A 路线上来——我人是回来了,AI 呢?你大概率会面对一个清纯的新手 AI,对之前在 A 路线上的所有探索一无所知。人类 debug 的经验是螺旋式积累的,而 agent 的记忆是一次性的。

Agent 时代,CS 基础还要学吗?

既然评价 agent 产出是核心问题,那开发者的基础知识就必然还是要学的。不然你拿什么去评价 agent 生成的代码、模块、架构设计质量到底如何?没有评价能力的开发者,和保健品店里待宰的老头老太没有区别。

那么该如何学习呢?

打开 LeetCode,题目还没读完呢,Copilot 已经把答案补全出来了。点一下 Submit & Run,前 1%。就这?

我的意见是:既然有 AI 了,当然不能局限于过去的难度,得上强度,上到 AI 做不出来的程度

放心,该学的不会落下。上了强度以后 AI 幻觉越来越多,该补的课全都得补上。期间 AI 还会给你帮不少倒忙——但这恰恰是学习的机会。

比如你要实现 Red-Black Tree、B-Tree、AVL Tree,那就上点强度:给算法加上形式化验证,再把泛型支持也加上。放心,当下最强模型也写不出来。

其实幻觉反而会帮助你学习——因为幻觉里包含了常见的误解,你去验证和纠正幻觉的过程,本身就加深了学习效果。

当然,不是每个人都要一步到位搞形式化验证。更实际的第一步是:学会写 spec 而不是写 implementation。让 agent 动手之前,先自己用人话写出这个功能的 pre/post-condition——输入满足什么条件,输出应满足什么约束,哪些边界情况必须覆盖。然后把这份 spec 作为 agent 的验收标准,而不是一句话需求丢进去许愿。

从人话 spec 到 property-based test,再到 refinement type——这条路可以慢慢走。但核心能力是一样的:准确描述你想要什么。这本身就是 CS 基础功底的体现,也是 agent 时代人类最不可被替代的能力。

结语

AI 框架、模型、工具、方法论层出不穷,日新月异。但说到底,这些都是在给模型做加法、打补丁。

人类完成一个完整工作流的时候,不需要把自己拆解成多个「子 agent」去协作——因为人类是真的有记忆能力,且会学习的。做的时间越长,成长越多,越熟练。项目里那些隐藏的上下文、踩过的坑、约定俗成的规矩,都会沉淀成经验。

而 agent 则相反。上下文越长,智力下降越明显。即便细节仍然在上下文内,agent 也开始频繁地忽略这些细节,自顾自地幻觉出一些「看起来合理」的东西来。

核心问题始终没变:我们依然缺乏可靠的手段来评价 agent 的产出。强类型是第一层部分解,FM 验证是第二层部分解,但也只是部分解。

一天不学,错过很多。一年不学,好像也没错过什么。

框架工具更新迭代,爆款层出不穷,但其炒作因素远大于实际能力和价值。而 CS 基础知识才是久经时间考验的硬通货。与其追新框架新工具,不如把精力放在强化自己「评价 Agent 产出」的能力上——这才是 agent 时代真正稀缺的东西。

补记:内核网卡驱动开发实录

本文发布后,有读者问到 AI 处理跨层复杂问题的实际能力。这里补充一个完整案例。

我曾在清华开源操作系统社区做过一次报告,分享了在 AI 辅助下开发内核网卡驱动的踩坑经验。当时使用的模型是 Claude 3.7 到 4.0,效果完全是帮倒忙——90% 的信息都是幻觉误导。AI 混合了 dwmac 从 2.x 到 5.x 各个版本的行为,甚至牵扯到高通/Intel 芯片的代码,更不用说 PHY 芯片的寄存器、C22/C45 协议这些了。

这类领域的训练数据和讨论比较封闭,厂家文档藏着掖着,要注册会员才能获取。全链路做过这款芯片的人可能全世界不到 100 人,AI 没有清晰可借鉴的经验。Kernel 仓库里的代码有 10-20 年历史,几千次提交,十几万行代码,还有一大堆不同厂商不同架构导致的 workaround——这些对 AI 来说都是噪音。

一开始我是外行,协作方式是 AI 做鞭子、我做牛马——我负责执行 AI 给出的方案并反馈错误信息。但随着我越来越懂,发现 AI 净给我胡扯,于是角色翻转:我指定逆向路径,AI 帮我执行,不厌其烦地一遍一遍做 bit 级别对比实验。最终的结论和下一轮路线决策是人 + AI 一起产出的,而不是让 AI 直接大跨度地做下一步。

这个案例其实印证了前面的几个观点:训练数据稀缺的领域 AI 幻觉尤其严重;长上下文中的寄存器级细节一旦丢失就是灾难;而 debug 的螺旋式推进过程,目前的 agent 架构根本无法胜任。但反过来说,当人类掌握了主导权之后,AI 作为一个不知疲倦的执行者,在 bit 级别的重复对比实验上确实帮了大忙。


本文同步发布于 知乎

Many data systems use polling refresh to display lists, which can cause a delay in updating content status and cannot immediately provide feedback to users on the page. Shortening the refresh time interval on the client side can lead to an excessive load on the server, which should be avoided.

To solve this problem, this article proposes an event subscription mechanism. This mechanism provides real-time updates to the client, eliminating the need for polling refresh and improving the user experience.

Terminologies and Context

This article introduces the following concepts:

  • Hub: An event aggregation center that receives events from producers and sends them to subscribers.
  • Buffer: An event buffer that caches events from producers and waits for the Hub to dispatch them to subscribers.
  • Filter: An event filter that only sends events meeting specified conditions to subscribers.
  • Broadcast: An event broadcaster that broadcasts the producer's events to all subscribers.
  • Observer: An event observer that allows subscribers to receive events through observers.

The document discusses some common concepts such as:

  • Pub-Sub pattern: It is a messaging pattern where the sender (publisher) does not send messages directly to specific recipients (subscribers). Instead, published messages are divided into different categories without needing to know which subscribers (if any) exist. Similarly, subscribers can express interest in one or more categories and receive all messages related to that category, without the publisher needing to know which subscribers (if any) exist.
  • Filter:
    • Topic-based content filtering mode is based on topic filtering events. Producers publish events to one or more topics, and subscribers can subscribe to one or more topics. Only events that match the subscribed topics will be sent to subscribers. However, when a terminal client subscribes directly, this method has too broad a subscription range and is not suitable for a common hierarchical structure.
    • Content-based content filtering mode is based on message content filtering events. Producers publish events to one or more topics, and subscribers can use filters to subscribe to one or more topics. Only events that match the subscribed topics will be sent to subscribers. This method is suitable for a common hierarchical structure.

Functional Requirements

  • Client users can subscribe to events through gRPC Stream, WebSocket, or ServerSentEvent.
  • Whenever a record's status changes (e.g. when the record is updated by an automation task) or when other collaborators operate on the same record simultaneously, an event will be triggered and pushed to the message center.
  • Events will be filtered using content filtering mode, ensuring that only events that meet the specified conditions are sent to subscribers.

Architecture

flowchart TD
  Hub([Hub])
  Buffer0[\"Buffer drop oldest"/]
  Buffer1[\"Buffer1 drop oldest"/]
  Buffer2[\"Buffer2 drop oldest"/]
  Buffer3[\"Buffer3 drop oldest"/]
  Filter1[\"File(Record = 111)"/]
  Filter2[\"Workflow(Project = 222)"/]
  Filter3[\"File(Project = 333)"/]
  Broadcast((Broadcast))
  Client1(Client1)
  Client2(Client2)
  Client3(Client3)
  Hub --> Buffer0
  subgraph Server
    Buffer0 --> Broadcast
    Broadcast --> Filter1 --> Buffer1 --> Observer1
    Broadcast --> Filter2 --> Buffer2 --> Observer2
    Broadcast --> Filter3 --> Buffer3 --> Observer3
  end
  subgraph Clients
    Observer1 -.-> Client1
    Observer2 -.-> Client2
    Observer3 -.-> Client3
  end

High-Level Overview

flowchart TD
  Pipe#a[[...Pipe...]]
  Pipe#b[[...Pipe...]]
  subgraph Hub
  direction LR
  Event1((Event1))
	Event2((Event2))
  Event3((Event3))
  Event4((Event4))
  Event5((Event5))
  Event6((Event6))
  Event7((Event7))
  Event8((Event8))
  Event1 -.-> Event2 -.-> Event3 -.-> Event4 -.-> Event5 -.-> Event6 -.-> Event7 -.-> Event8
  end
	Pipe#a -.-> Event1
  Event8 -.-> Pipe#b
  subgraph Client1
  direction LR
  C1Subscribe((Start))
  C1Cancel((End))
  Event2 -.-> C1Listen2
  Event3 -.-> C1Listen3
  Event4 -.-> C1Listen4
  Event5 -.-> C1Listen5
  C1Subscribe -.-> C1Listen2 -.-> C1Listen3 -.-> C1Listen4 -.-> C1Listen5 -.-> C1Cancel
  end
  subgraph Client2
  direction LR
  C2Subscribe((Start))
  C2Cancel((End))
  Lag(("❌"))
  C2Subscribe -.-> C2Listen1 -- "Poor Network" ---> Lag --"Packet loss"---> C2Listen5 -.-> C2Listen6 -.-> C2Listen7 -.-> C2Listen8 -.-> C2Cancel
  Event1 -.-> C2Listen1
  Event5 -.-> C2Listen5
  Event6 -.-> C2Listen6
  Event7 -.-> C2Listen7
  Event8 -.-> C2Listen8
  end

Clients should follow these steps:

  • Upon entering the page, subscribe as necessary.
  • After listening to the change event, debounce and re-request the list interface, and then render it.
  • When leaving the page, cancel the subscription.

Servers should follow these steps:

  • Subscribe to push events based on the client's filter.
  • When the client's backlog message becomes too heavy, delete the oldest message from the buffer.
  • When the client cancels the subscription, the server should also cancel the broadcast to the client.

Application / Component Level Design (LLD)

flowchart LR
  Server([Server])
  Client([Client: Web...])
  MQ[Kafka or other]
  Broadcast((Broadcast))
  subgraph ExternalHub
    direction LR
    Receiver --> MQ --> Sender
  end
  subgraph InMemoryHub
    direction LR
    Emit -.-> OnEach
  end
  Server -.-> Emit
  Sender --> Broadcast
  OnEach -.-> Broadcast
  Broadcast -.-> gRPC
  Broadcast -.-> gRPC
  Broadcast -.-> gRPC
  Server --  "if horizon scale is needed" --> Receiver
  gRPC --Stream--> Client

For a single-node server, a simple Hub can be implemented using an in-memory queue.

For multi-node servers, an external Hub implementation such as Kafka, MQ, or Knative eventing should be considered. The broadcasting logic is no different from that of a single machine.

Failure Modes

Fast Producer-Slow Consumer

This is a common scenario that requires special attention. The publish-subscribe mechanism for terminal clients cannot always expect clients to consume messages in real time. However, message continuity must be maximally guaranteed. Clients may access our products in an uncontrollable network environment, such as over 4G or poor Wi-Fi. Thus, the server message queue cannot become too backlogged. When a client's consumption rate cannot keep up with the server's production speed, this article recommends using a bounded Buffer with the OverflowStrategy.DropOldest strategy. This ensures that subscriptions between consumers are isolated, avoiding too many unpushed messages on the server (which could lead to potential memory leak risks).

Alternative Design

VMware has publish a very similar design in 2013, but use Go RingChannel

Summary

This document proposes an event subscription mechanism to address the delay in updating content status caused by polling refresh. Clients can subscribe to events through any long connection protocol, and events will be filtered based on specified conditions. To avoid having too many unpushed messages on the server, a bounded buffer with the OverflowStrategy.DropOldest strategy is used.

Implementing this in Reactive Streams is straightforward, but you can choose your preferred technology to do so.

Overview

In the previous post, we discussed how to implement a file tree in PostgreSQL using ltree. Now, let's talk about how to integrate version control management for the file tree.

Version control is a process for managing changes made to a file tree over time. This allows for the tracking of its history and the ability to revert to previous versions, making it an essential tool for file management.

With version control, users have access to the most up-to-date version of a file, and changes are tracked and documented in a systematic manner. This ensures that there is a clear record of what has been done, making it much easier to manage files and their versions.

Terminologies and Context

One flawed implementation involves storing all file metadata for every commit, including files that have not changed but are recorded as NO_CHANGE. However, this approach has a significant problem.

The problem with the simple and naive implementation of storing all file metadata for every commit is that it leads to significant write amplification, as even files that have not changed are recorded as NO_CHANGE. One way to address this is to avoid storing NO_CHANGE transformations when creating new versions, which can significantly reduce the write amplification.

This is good for querying, but bad for writing. When we need to fetch a specific version, the PostgreSQL engine only needs to scan the index with the condition file.version = ?. This is a very cheap cost in modern database systems. However, when a new version needs to be created, the engine must write \(N\) rows of records into the log table (where \(N\) is the number of current files). This will cause a write peak in the database and is unacceptable.

In theory, all we need to do is write the changed file. If we can find a way to fetch an arbitrary version of the file tree in \(O(log(n))\) time, we can reduce unnecessary write amplification.

Non Functional Requirements

Scalability

Consider the worst-case scenario: a file tree with more than 1,000 files that is committed to more than 10,000 times. The scariest possibility is that every commit changes all files, causing a decrease in write performance compared to the efficient implementation. Storing more than 10 million rows in a single table can make it difficult to separate them into partitioned tables.

Suppose \(N\) is the number of files, and \(M\) is the number of commits. We need to ensure that the time complexity of fetching a snapshot of an arbitrary version is less than \(O(N\cdot log(M))\). This is theoretically possible.

Latency

In the worst case, the query can still respond in less than 100ms.

Architecture

Database Design

Illustration of data structures.

Illustration of data structures.

Tech Details

Subqueries appearing in FROM can be preceded by the key word LATERAL. This allows them to reference columns provided by preceding FROM items. (Without LATERAL, each subquery is evaluated independently and so cannot cross-reference any other FROM item.) — https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-LATERAL

PostgreSQL has a keyword called LATERAL. This keyword can be used in a join table to enable the use of an outside table in a WHERE condition. By doing so, we can directly tell the query optimizer how to use the index. Since data in a combined index is stored in an ordered tree, finding the maximum value or any arbitrarily value has a time complexity of \(O(log(n))\).

Finally, we obtain a time complexity of \(O(N \cdot log(M))\).

Performance

Result: Fetching an arbitrary version will be done in tens of milliseconds.

1
2
3
4
5
6
7
8
9
10
11
12
13
explain analyse
select f.record_id, f.filename, latest.revision_id
from files f
inner join lateral (
select *
from file_logs fl
where f.filename = fl.filename
and f.record_id = fl.record_id
-- and revision_id < 20000
order by revision_id desc
limit 1
) as latest
on f.record_id = 'f5c2049f-5a32-44f5-b0cc-b7e0531bf706';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Nested Loop  (cost=0.86..979.71 rows=1445 width=50) (actual time=0.040..18.297 rows=1445 loops=1)
-> Index Only Scan using files_pkey on files f (cost=0.29..89.58 rows=1445 width=46) (actual time=0.019..0.174 rows=1445 loops=1)
Index Cond: (record_id = 'f5c2049f-5a32-44f5-b0cc-b7e0531bf706'::uuid)
Heap Fetches: 0
-> Memoize (cost=0.57..0.65 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1445)
" Cache Key: f.filename, f.record_id"
Cache Mode: binary
Hits: 0 Misses: 1445 Evictions: 0 Overflows: 0 Memory Usage: 221kB
-> Subquery Scan on latest (cost=0.56..0.64 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1445)
-> Limit (cost=0.56..0.63 rows=1 width=852) (actual time=0.012..0.012 rows=1 loops=1445)
-> Index Only Scan Backward using file_logs_pk on file_logs fl (cost=0.56..11.72 rows=158 width=852) (actual time=0.011..0.011 rows=1 loops=1445)
Index Cond: ((record_id = f.record_id) AND (filename = (f.filename)::text))
Heap Fetches: 0
Planning Time: 0.117 ms
Execution Time: 18.384 ms

Test Datasets

This dataset simulates the worst-case scenario of a table with 14.6 million rows. Specifically, it contains 14.45 million rows representing a situation in which 1,400 files are changed 10,000 times.

1
2
3
4
5
-- cnt: 14605858
select count(0) from file_logs;
-- cnt: 14451538
select count(0) from file_logs where record_id = 'f5c2049f-5a32-44f5-b0cc-b7e0531bf706';

Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
create table public.file_logs
(
file_key ltree not null,
revision_id integer not null,
record_id uuid not null,
filename varchar(2048) not null,
create_time timestamp,
update_time timestamp,
delete_time timestamp,
blob_sha256 char(64),
constraint file_logs_pk
primary key (record_id, filename, revision_id)
);

alter table public.file_logs
owner to postgres;

create table public.files
(
record_id uuid not null,
filename varchar(2048) not null,
create_at timestamp not null,
primary key (record_id, filename)
);

alter table public.files
owner to postgres;

Further Improvements

We can implement this using an intuitive approach in a graph database.

File tree version in graph database
0%