乐于分享
好东西不私藏

渐进式文档加载:防止上下文溢出的 Skill 设计

渐进式文档加载:防止上下文溢出的 Skill 设计

系列:屎山项目 AI Coding 实战:从梳理到安全改造(第3篇,共5篇)

前置阅读:第1篇:屎山诊断——在改之前先读懂它 | 第2篇:知识抽取——把散落的文档变成AI可消费的结构


开篇:那次把我搞懵的翻车经历

2024年初,我在接手一个运行了八年的支付网关项目时,第一次系统性地遇到了这个问题。

项目有大约23万行Java代码,文档分散在六个地方:Confluence有300多页、代码里有大量块注释、有两个版本的README、有一个从未更新的架构图(.vsdx格式,2019年最后一次修改)。我花了一周时间按第2篇的方法把文档抽取整理,生成了一个叫KNOWLEDGE.md的文件,大概4.2万字,然后把它放进CLAUDE.md作为系统提示。

第一天,效果非常好。AI准确引用了PaymentGatewayOrchestrator.java里的接口签名,知道TransactionStateManager有一个”三阶段提交的简化变体”,知道不能直接修改legacy_fee_calculator模块(因为下游有七个系统在用它的副作用)。

第三天,出问题了。

我让AI帮我给RefundService增加一个幂等检查逻辑。AI生成的代码引用了一个叫IdempotencyRegistry的类——这个类不存在。我翻遍了整个代码库,没有。AI说它在PaymentGatewayOrchestrator.java的第847行——实际上那一行是个空行。然后AI开始信誓旦旦地说”根据之前讨论的架构,IdempotencyRegistry应该在core包下”——我们根本没讨论过这个东西。

这就是上下文溢出之后的典型表现:AI开始在窗口的噪声里脑补出不存在的东西,并且用高度自信的语气描述它们。

那次我回头检查,对话已经进行了大约14轮,累计注入的token大概在15万左右(包括那个4.2万字的KNOWLEDGE.md被多次引用的副本),而Claude Sonnet的上下文窗口是20万。在最后几千token里,AI已经在半随机地组合它窗口里残存的碎片信息了。

这篇文章就从那次翻车经历出发,系统性地解决屎山项目的上下文管理问题。


第一层:问题特征诊断——上下文溢出的精确症状

上下文溢出不是一个二进制事件。它不像内存溢出那样会抛出异常、停止运行,而是一个渐进的质量退化过程。很多人第一次遇到时完全不知道发生了什么,只是感觉”AI越来越蠢”。

1.1 三种典型症状

症状一:幻觉注入——AI开始引用不存在的东西

这是最隐蔽也最危险的症状。AI会以完全正常的语气描述不存在的函数、类、字段,甚至会给出具体的文件路径和行号。关键在于,这些幻觉不是随机的——它们通常是窗口里真实代码片段的”创意重组”,看起来非常合理,甚至能通过初步的代码审查。

典型表现:

  • “调用UserContextHolder.getCurrentTenant()可以获取当前租户” — 这个方法不存在,但UserContextHolder确实存在,getCurrentUser()也存在
  • “参考OrderService.java第234行的实现” — 第234行是一个不相关的注释
  • “这与我们之前讨论的IdemKey规范一致” — 根本没有讨论过IdemKey

症状二:约束遗忘——AI开始违反早期确立的设计决策

在对话初期,你花了很多精力建立了一些关键约束:禁止修改某个模块、必须走某个接口、不能引入新的外部依赖。这些约束被窗口早期的token承载。随着对话深入,这些token被”挤出”了注意力中心,AI开始忘记它们。

典型表现:

  • 对话第2轮:AI确认”知道不能直接修改legacy_fee_calculator,只能通过FeeCalculatorAdapter访问”
  • 对话第8轮:AI生成的代码直接importlegacy_fee_calculator的内部类

症状三:质量阶梯式下降——任务越到后面生成质量越差

这是最系统性的症状。如果你在同一个长对话里完成5个相关任务,你会发现第4、第5个任务的生成质量明显低于前两个。代码开始变得冗余,解释变得模糊,对问题的理解开始出现偏差。

1.2 量化阈值:什么规模的屎山会触发

基于实践经验,给出一个粗略的触发阈值(以Claude Sonnet 20万token窗口为基准):

项目特征
安全区
警戒区
危险区
KNOWLEDGE.md大小
<5000字
5000-20000字
>20000字
单次对话轮数
<8轮
8-15轮
>15轮
代码总量(注入到上下文)
<3万token
3-10万token
>10万token
并行处理模块数
1
2-3
>3

注意这些是交叉叠加的。一个5000字的KNOWLEDGE.md,加上15轮对话历史,加上每轮都注入大量代码片段,照样会溢出。

1.3 诊断工具:如何在Claude Code里观察到溢出

Claude Code没有直接显示token使用量的界面,但有几个间接观察手段:

方法一:关注响应速度的变化。当上下文接近满载时,推理需要遍历更长的序列,首token延迟会明显增加。如果你感觉”AI变慢了”,这有时是溢出的早期信号。

方法二:测试一致性问题。在对话中途,重新问一个对话开始时确认过的约束。如果AI的回答开始模糊或出现偏差,说明早期上下文已经被挤压。

方法三:观察引用准确性。让AI引用一个你明确放在KNOWLEDGE.md里的具体细节(比如一个具体的接口签名),如果AI引用错误或说”不太确定”,说明溢出已经影响到了关键知识层。

方法四:使用Claude Code的/usage命令(如果项目配置了对应的MCP工具)。

1.4 诊断决策树


第二层:根因分析——屎山项目为什么特别容易上下文溢出

普通项目用AI Coding也会遇到上下文问题,但屎山项目有几个独特的放大因素,让溢出来得更早、更严重、更难诊断。

2.1 根因传递链:R1→R4

屎山项目的上下文溢出是四个根因依次传递的结果:

2.2 屎山项目的独特放大效应

放大因素1:文档含大量冗余噪声

一个正常新项目的文档可能有90%的有效信息密度。屎山项目的文档可能只有40%:剩下的60%是过时的接口描述、已经删除的类的注释、被推翻的架构决策的历史痕迹。当你把这样的文档全量注入时,AI会尝试理解所有内容,包括那60%的噪声,这些噪声会干扰AI对真实状态的判断。

放大因素2:单模块理解需要更长的调用链

在一个设计良好的新项目里,理解OrderService.createOrder()可能只需要看这一个类加上它直接依赖的2-3个接口。在屎山项目里,理解相同功能的方法可能需要追踪:LegacyOrderProcessor → OrderStateMachineV2 → OrderStateMachineV1Compat → LegacyDatabaseAdapter → OldOracleConnectionPool,跨越5-8个文件,其中每个文件还有大量内联的业务逻辑注释。AI需要注入的上下文是正常项目的3-5倍。

放大因素3:开发者对文档边界缺乏感知

面对一个陌生的屎山,开发者自己也不知道”改这个功能到底需要了解哪些文档”。于是自然的反应是:都放进去,保险。这种”有备无患”的心理导致了全量注入的普遍性。

2.3 上下文污染的具体机制:一个代码示例

下面这个例子展示了上下文污染如何导致AI生成错误代码。场景是:KNOWLEDGE.md里包含了一段过时的接口描述(来自2019年的版本),和当前真实的接口描述(2023年版本)。由于文档太长,AI在窗口满载时错误地以2019年版本为准生成了代码:

// ── 2019年版本的接口(KNOWLEDGE.md中的旧文档,已废弃)──
// TransactionValidator.validate(String transactionId, String currency)
// 返回:boolean,true表示合法

// ── 2023年实际接口(代码库中的真实接口)──
// TransactionValidator.validate(TransactionRequest request)  
// 返回:ValidationResult(包含状态码、错误描述、审计日志ID)

// ── AI在上下文溢出后生成的错误代码 ──
// AI"记住"了2019年的接口签名(因为它在KNOWLEDGE.md前面部分,更早被注入)
// 而忘记了在代码库分析阶段看到的2023年真实接口
publicvoidprocessRefund(String txId) {
// 这行代码使用了废弃的接口签名
booleanisValid= transactionValidator.validate(txId, "CNY");  // ❌ 编译错误
if (isValid) {
// 这里也丢失了对ValidationResult中审计日志的处理
        refundService.execute(txId);
    }
}

// ── 正确的代码应该是 ──
publicvoidprocessRefund(String txId) {
TransactionRequestrequest= TransactionRequest.of(txId, Currency.CNY);
ValidationResultresult= transactionValidator.validate(request);  // ✅
if (result.isValid()) {
        auditLogger.record(result.getAuditLogId());  // 这里需要记录审计日志
        refundService.execute(txId);
    }
}

这个例子的关键在于:错误非常隐蔽。transactionValidator.validate(txId, "CNY")看起来完全合理,能通过快速代码审查,只有在编译时才会报错——而如果你的项目有动态代理或者泛型擦除,甚至可能在运行时才暴露。

2.4 为什么”开新对话”不是根本解法

很多人的应对策略是”发现AI开始乱说就重开对话”。这确实能清除当前的溢出状态,但有三个问题:

问题一:知识重建成本高。每次新对话都需要重新注入背景知识,如果KNOWLEDGE.md是4万字,这个成本从对话第一轮就开始消耗窗口。

问题二:重建不完整。你很难在新对话开始时完整地复现上一个对话的”当前状态”——那些隐性的约定、那些”我们已经讨论清楚了”的事项,会有遗漏。

问题三:治标不治本。下一个对话依然会在同样的位置溢出,因为根本的注入策略没有改变。

真正的解法是在设计层面解决这个问题:从源头控制每次对话的上下文密度。


第三层:策略对比——三种加载策略的本质差异

从上下文管理的角度,有三种截然不同的策略。它们不只是”好坏”的差异,而是设计哲学的差异。

3.1 策略A:全量加载

这是绝大多数人的第一反应,也是最常见的错误做法。

具体做法:建立一个KNOWLEDGE.md(或者CLAUDE.md里的<knowledge>块),把项目所有的相关文档都放进去,包括架构文档、接口说明、业务规则、已知坑点、历史决策等。然后在每次对话开始时通过系统提示全量注入。

为什么这样做:直觉上”信息越完整,AI理解越准确”。这个直觉在文档量小的时候是对的。

本质问题:这把上下文管理的问题转移给了模型,而不是解决它。你给AI提供了10000行文档,但AI在这次对话里可能只需要其中的500行。其余9500行不但没有帮助,还在消耗窗口空间,并且引入了干扰信息。

适用边界:仅适用于文档总量在5000字以内、项目代码量在5万行以内的小型项目,且对话深度不超过8轮。

3.2 策略B:手动分段

当开发者意识到全量加载有问题时,通常会转向手动控制。

具体做法:将文档拆分成多个文件,在每次开始任务前,手动选择这次任务相关的文档片段注入。比如要改RefundService,就只把refund-module.mdpayment-gateway-overview.md放进系统提示。

优点:精确控制是真实的优点。一个有经验的开发者确实能判断某次任务需要什么文档,这样的精确注入效率最高。

核心缺陷一:高心智负担。每次开始任务前都需要花时间思考”这次需要哪些文档”,这个决策本身就需要对项目有足够深的了解,而对于屎山项目,这种了解往往是不完整的。

核心缺陷二:不可复用。手动分段是个人化的、即兴的决策,无法被团队共享,无法被自动化,也无法被下一个接手项目的人复用。

核心缺陷三:遗漏风险。你”觉得”这次只需要refund-module.md,但实际上RefundService有个隐性依赖在payment-state-machine.md里。遗漏的代价可能是AI因为缺少关键信息而生成错误代码——这比全量加载还糟糕,因为全量加载至少”文档都在”。

3.3 策略C:渐进式Skill设计

渐进式Skill设计的核心思路是:把”加载哪些文档”这个决策从即兴的人工判断变成系统性的规则配置。

具体做法:定义一套Skill文件,每个Skill对应一种任务类型(或者一个触发条件),每个Skill规定了该场景下需要加载的文档层级和具体文件。当AI面对某个任务时,Skill框架自动匹配并加载对应的文档集合。

设计原则:最小必要原则(只加载完成任务所必须的文档)+ 渐进原则(随着任务深入,按需追加更详细的层级)+ 历史窗口原则(对于连续性任务,只保留最近的相关历史,而非整个对话)。

成本结构:前期有设计成本(需要花时间定义Skill规则和文档分层),但一旦建立,团队所有成员都能使用,且会随项目积累持续优化。

3.4 三策略对比

维度
策略A:全量加载
策略B:手动分段
策略C:渐进式Skill
上下文占用
极高(始终注入全量)
低(精确控制)
低到中(按层级递进)
心智负担
低(一次配置永久使用)
极高(每次任务前人工判断)
低(规则配置一次,自动执行)
可复用性
中(文档可复用,但策略不可移植)
低(个人化决策,不可共享)
高(Skill文件可版本化、共享、迭代)
适用场景
小型项目、文档量<5000字
个人项目、极高掌控需求
中大型项目、团队协作、屎山改造
首次设置成本
极低(几分钟)
极低(不需要设置)
中(需要设计文档分层和Skill规则)
长期维护成本
中(文档需要保持更新)
高(每次任务都需要判断)
低(规则稳定,按需迭代)
溢出风险
高(文档增长则风险增加)
低(但遗漏风险高)
中低(设计合理则可控)
遗漏风险
低(全量注入不会遗漏)
中高(依赖个人判断)
低(规则覆盖了主要场景)

3.5 策略选择决策树


第四层:系统性解决方案——渐进式四层Skill架构

渐进式Skill架构的核心是把”上下文”这个概念分解成四个不同性质的层次,每层有明确的内容边界、大小约束和触发条件。

4.1 四层架构全貌

4.2 Layer 0:宪法层——最难写对的那一层

宪法层是最重要也是最容易写错的层。大多数人第一次写宪法层时,会把它写成项目百科全书。这是错的。

宪法层的本质是约束清单,而不是知识库。它的问题不是”AI需要了解什么”,而是”AI在任何情况下都必须遵守什么”。

正确的宪法层内容

  • 技术栈约束(必须使用哪些框架、禁止使用哪些库)
  • 禁止操作清单(哪些模块不能直接修改、哪些接口不能改签名)
  • 核心架构原则(分层规则、模块边界、数据流方向)
  • 关键术语定义(防止AI因为术语歧义生成错误代码)

不应该放进宪法层的内容

  • 模块的具体实现细节(放L2)
  • 函数签名和参数说明(放L2)
  • 历史决策的来龙去脉(放L3或者文档注释)
  • “建议”和”最佳实践”(这不是约束,AI可以选择性遵守)

下面是一个支付网关项目的宪法层示例:

# .claude/skills/constitution.yaml
# Layer 0:宪法层 | 目标大小:≤500 tokens | 触发:始终加载

layer:0
name:"constitution"
description:"技术栈约束与核心架构原则——任何任务下必须遵守"
always_load:true
max_tokens:500

# ──────────────────────────────────────
# 禁止操作清单(PROHIBITED ACTIONS)
# 违反任何一条都必须在代码中显式标注并请求确认
# ──────────────────────────────────────
prohibited:
-module:"legacy_fee_calculator"
reason:"7个下游系统依赖其副作用,任何改动需要跨团队同步"
allowed_access:"通过FeeCalculatorAdapter接口访问"

-pattern:"直接操作数据库连接"
reason:"所有DB操作必须通过DataAccessLayer,不能绕过连接池管理"
allowed_access:"通过RepositoryFactory获取Repository"

-action:"修改任何public接口的方法签名"
reason:"下游系统通过反射调用,签名变更会静默失败"
exception:"新增重载方法是允许的,不能修改已有方法签名"

# ──────────────────────────────────────
# 核心架构约束(ARCHITECTURE CONSTRAINTS)
# ──────────────────────────────────────
architecture:
layers:
-name:"API层"
packages: ["com.company.gateway.api"]
allowed_deps: ["Service层"]

-name:"Service层"
packages: ["com.company.gateway.service"]
allowed_deps: ["Domain层""Infrastructure层"]

-name:"Domain层"
packages: ["com.company.gateway.domain"]
allowed_deps: []  # 领域层不依赖任何外部层

rule:"禁止跨层直接依赖,只允许相邻层依赖"

# ──────────────────────────────────────
# 关键术语定义(GLOSSARY)
# 防止AI对业务术语产生歧义
# ──────────────────────────────────────
glossary:
"Transaction":"一次支付请求的全生命周期记录(从创建到终态)"
"Order":"业务层面的订单,一个Order可能对应多个Transaction(重试)"
"Settlement":"资金清算,与Transaction是多对一关系,不要混用"

为什么≤500 tokens是硬约束:宪法层在每次对话中都会被加载,如果它超过500 tokens,就意味着每次对话都有超过500 tokens被宪法层占用。一个20轮的对话,宪法层会被多次引用,累积消耗可能超过1万tokens。宪法层应该是精炼的约束集,不是项目文档的摘要。

4.3 Layer 1:模块索引层——降低AI的寻路成本

模块索引层解决的问题是:当任务涉及多个模块时,AI需要知道”哪个模块负责什么”以及”模块之间如何协作”,但不需要知道每个模块的具体实现。

关键设计点:接口摘要而非完整实现

为什么不放完整实现?举个具体数字:一个500行的Java类,代码本身大约需要3000-5000 tokens来描述。如果你有8个主要模块,把所有实现都放在索引层,就是2.4-4万 tokens——已经超过了宪法层+索引层的总预算。

接口摘要只需要:类名、主要职责(一句话)、关键公共方法签名(不含实现)、主要依赖(哪些接口/类)。一个500行的类用接口摘要描述,大约只需要200-300 tokens。8个模块加起来1600-2400 tokens,完全在2000 tokens的预算内。

<!-- .claude/skills/module-index.md -->
<!-- Layer 1:模块索引层 | 目标大小:≤2000 tokens | 触发:涉及多模块时 -->

## 模块索引层(Module Index)

### 核心模块清单

#### PaymentGatewayOrchestrator
-**职责**:支付流程总编排,负责协调各子服务
-**关键方法**
  -`initiate(PaymentRequest) → PaymentContext`:发起新支付
  -`resume(TransactionId) → PaymentContext`:恢复中断的支付
  -`cancel(TransactionId, CancelReason) → void`:取消支付
-**依赖**:TransactionStateManager, FeeCalculatorAdapter, RiskService
-**注意**:这是整个支付流的入口,修改此类需要完整的回归测试

#### TransactionStateManager  
-**职责**:管理Transaction的状态流转,是一个"简化的三阶段提交"
-**关键方法**
  -`transition(TransactionId, TargetState) → TransitionResult`
  -`getCurrentState(TransactionId) → TransactionState`
-**依赖**:TransactionRepository, EventBus
-**注意**:状态流转有严格的前置条件,违反前置条件会抛出StateTransitionException

#### RefundService
-**职责**:退款申请和执行,依赖TransactionStateManager做状态变更
-**关键方法**
  -`requestRefund(RefundRequest) → RefundContext`
  -`executeRefund(RefundId) → RefundResult`
-**依赖**:TransactionStateManager, PaymentChannelRouter, AuditLogger
-**注意**:执行退款前必须通过TransactionValidator验证,否则会违反对账规则

#### FeeCalculatorAdapter
-**职责**:封装legacy_fee_calculator,提供类型安全的接口
-**注意**:⚠️ 这是对禁止模块legacy_fee_calculator的唯一合法访问入口

### 模块依赖关系图

PaymentGatewayOrchestrator     ├── TransactionStateManager ──→ TransactionRepository     ├── FeeCalculatorAdapter ──→ [LEGACY] legacy_fee_calculator     ├── RiskService     └── PaymentChannelRouter

RefundService     ├── TransactionStateManager     ├── PaymentChannelRouter
    ├── AuditLogger     └── TransactionValidator


4.4 Layer 2:实现详情层——按模块拆分的深度文档


实现详情层是实际工作中使用最频繁的层。它包含了具体的函数签名、业务逻辑说明、已知坑点和边界条件。


关键原则:每个模块一个独立的文件,每个文件≤5000 tokens。不要把所有模块的实现细节合并成一个大文件——这又回到了全量加载的问题。




<!-- .claude/skills/impl/refund-service.md -->
<!-- Layer 2:RefundService实现详情 | 目标大小:≤5000 tokens -->
<!-- 触发条件:任务涉及退款逻辑时加载 -->

## RefundService 实现详情

### 退款状态机

退款有独立的状态机,与Transaction状态机是嵌套关系:

PENDING_VALIDATION     ↓ (TransactionValidator.validate通过) PENDING_APPROVAL     ↓ (金额>10000元时需要人工审批,否则自动通过) APPROVED     ↓ (调用PaymentChannelRouter.initiateRefund) PROCESSING     ↓ (渠道回调通知结果) COMPLETED / FAILED


### requestRefund 方法

```java
// 方法签名
public RefundContext requestRefund(RefundRequest request) 
    throws RefundValidationException, InsufficientFundException

// 参数说明
// request.transactionId: 原始Transaction的ID(不是OrderId!)
// request.amount: 退款金额,单位分,不能超过Transaction金额
// request.reason: 退款原因,必须是RefundReason枚举中的值

// ⚠️ 重要:退款金额校验使用的是Transaction的已结算金额
// 不是Transaction的申请金额,差值是手续费,不可退
// 错误示例:用transaction.getAmount()做退款上限校验
// 正确示例:用transaction.getSettledAmount()做退款上限校验

已知坑点和边界条件

坑1:重复退款检查RefundRepository.findByTransactionId(txId)可能返回空,也可能返回已FAILED的记录。 只有COMPLETED状态的退款才算”已退款”。FAILED的退款应该允许重新申请。

坑2:异步退款的幂等executeRefund是异步的,渠道回调可能多次到达(网络重试)。RefundCallbackHandler已经做了幂等处理,但依赖refund_id而不是channel_refund_id作为幂等键。 如果你需要在callback处理里增加逻辑,确保你的逻辑也是幂等的。

坑3:退款金额的货币精度系统内部用long存储分,但PaymentChannelRouter的部分渠道接口用BigDecimal存储元。MoneyConverter工具类处理这个转换,但注意它默认用HALF_EVEN舍入—— 如果退款金额是0.005元,会被舍入到0元,然后渠道会拒绝这个退款请求。 最小退款金额是1分。


### 4.5 Layer 3:历史上下文层——滑动窗口机制

历史上下文层是四层中机制最复杂的一层。它需要解决一个矛盾:在连续性任务中,之前对话的决策很重要(不能丢失);但历史累积会导致窗口膨胀(不能全保留)。

解决方案是滑动窗口:只保留最近N次(默认5次)相关操作的摘要,而不是完整的对话记录。

```mermaid
sequenceDiagram
    participant Dev as 开发者
    participant Skill as Skill框架
    participant History as 历史层文件
    participant AI as Claude

    Note over Dev,AI: 第1次对话:分析RefundService依赖
    Dev->>Skill: 开始任务:分析退款模块依赖
    Skill->>AI: 注入 L0+L1+L2(refund)
    AI-->>Dev: 分析结果:发现TransactionValidator依赖
    Dev->>History: 记录摘要:发现RefundService依赖TransactionValidator\n具体在validate()调用前

    Note over Dev,AI: 第2次对话:修改退款幂等逻辑
    Dev->>Skill: 开始任务:增加退款幂等检查
    Skill->>History: 读取历史(窗口内:1条记录)
    History-->>Skill: 返回第1次对话摘要
    Skill->>AI: 注入 L0+L1+L2(refund)+L3(1条历史)
    AI-->>Dev: 生成幂等逻辑,正确引用TransactionValidator
    Dev->>History: 追加摘要:增加了idempotency check\n基于refund_id作为幂等键

    Note over Dev,AI: 第6次对话:触发窗口滑动
    Dev->>Skill: 开始任务(第6次相关操作)
    Skill->>History: 读取历史(窗口内:5条记录)
    History-->>Skill: 返回第2-6次对话摘要(第1次被滑出)
    Skill->>History: 将第1次摘要归档到archive/
    Skill->>AI: 注入 L0+L1+L2+L3(5条历史)
    AI-->>Dev: 基于最近历史做出决策

    Note over Skill,History: 窗口滑动:第7次操作时,第2次被归档

历史层的文件结构:

.claude/skills/history/
├── refund-module/
│   ├── current/                    # 滑动窗口内的历史(最多5条)
│   │   ├── 001-dependency-analysis.md    # 最早的记录(即将被滑出)
│   │   ├── 002-idempotency-check.md
│   │   ├── 003-fee-precision-fix.md
│   │   ├── 004-callback-handler.md
│   │   └── 005-state-machine-review.md  # 最新的记录
│   └── archive/                    # 滑出的历史,供人工查阅但不自动注入
│       └── 000-initial-analysis.md
└── payment-gateway/
    └── current/
        ├── 001-orchestrator-review.md
        └── 002-fee-adapter-refactor.md

每条历史记录的格式应该是高度结构化的摘要,而不是对话全文:

<!-- .claude/skills/history/refund-module/current/002-idempotency-check.md -->
<!-- 历史记录 | 对话日期:2024-03-15 | 任务:增加退款幂等检查 -->

## 决策摘要

**任务**:在RefundService.requestRefund()增加幂等检查

**关键决策**
- 幂等键使用`refund_id`(由客户端生成),不使用`transaction_id`
  - 原因:同一Transaction可能有多次合法的退款申请(部分退款)
- 幂等检查在`RefundValidationService.checkDuplicate()`中实现,不在`requestRefund`直接实现
  - 原因:复用已有的校验框架,避免绕过审计日志

**已修改的文件**
-`RefundService.java`:第89行,增加了idempotency check调用
-`RefundValidationService.java`:新增`checkDuplicate(RefundRequest)`方法

**遗留问题/下次需要关注**
- FAILED状态的退款是否允许使用相同的refund_id重试(当前实现是允许的,需要PM确认)

4.6 完整的目录结构

.claude/
├── CLAUDE.md                       # 主入口,仅引用宪法层,不包含具体内容
├── skills/
│   ├── constitution.yaml           # Layer 0:宪法层(始终加载)
│   ├── module-index.md             # Layer 1:模块索引层
│   ├── impl/                       # Layer 2:各模块实现详情
│   │   ├── refund-service.md       #   - 退款服务
│   │   ├── payment-orchestrator.md #   - 支付编排器
│   │   ├── transaction-state.md    #   - 状态管理器
│   │   ├── fee-calculator-adapter.md  # - 费率适配器
│   │   └── risk-service.md         #   - 风控服务
│   ├── history/                    # Layer 3:历史上下文(滑动窗口)
│   │   ├── refund-module/
│   │   │   ├── current/            #   - 窗口内历史(最多5条)
│   │   │   └── archive/            #   - 归档历史
│   │   └── payment-gateway/
│   │       └── current/
│   └── triggers/                   # 触发器规则配置
│       ├── task-type-mapping.yaml  #   - 任务类型→加载层级映射
│       └── module-keywords.yaml    #   - 关键词→模块映射
└── settings.json                   # Claude Code配置

4.7 触发器设计:任务类型与加载层级的映射

触发器是连接”任务描述”和”加载策略”的桥梁。它把自然语言的任务描述转换成具体的文档加载指令。

# .claude/skills/triggers/task-type-mapping.yaml
# 任务类型到加载层级的映射规则

task_types:

# 快速修复类任务:L0 + L2(具体模块)
quick_fix:
description:"修复bug、调整参数、小范围修改"
keywords: ["修复""fix""bug""调整""修改""改一下""update"]
load_layers: [02]
load_history:false
notes:"快速修复不需要模块索引,直接加载具体模块实现即可"

# 新增功能类任务:L0 + L1 + L2(目标模块)
new_feature:
description:"新增功能点,可能影响多个模块"
keywords: ["新增""增加""add""实现""开发""feature""功能"]
load_layers: [012]
load_history:false
notes:"需要索引层了解模块边界,再加载目标模块的实现细节"

# 重构类任务:L0 + L1 + L2 + L3
refactoring:
description:"重构、优化、技术债还清"
keywords: ["重构""refactor""优化""清理""解耦""技术债"]
load_layers: [0123]
load_history:true
history_window:5
notes:"重构需要历史上下文,了解之前的决策以避免返工"

# 跨模块改造:L0 + L1(不加L2,避免溢出)
cross_module:
description:"影响3个以上模块的大型改造"
keywords: ["架构调整""接口变更""跨模块""全局修改"]
load_layers: [01]
load_history:false
notes:"跨模块改造中L2不自动加载,需要手动指定具体模块,避免同时加载过多实现层"

# 模块关键词映射(确定任务涉及哪个模块)
module_detection:
refund-service: ["退款""refund""RefundService""退款服务"]
payment-orchestrator: ["支付""payment""PaymentGateway""Orchestrator"]
transaction-state: ["状态""状态机""TransactionState""state machine"]
fee-calculator: ["费率""手续费""fee""FeeCalculator"]
risk-service: ["风控""risk""RiskService""风险"]

4.8 跨层冲突处理

一个非常实际的工程问题:当Layer 2的具体实现细节与Layer 0的架构原则出现矛盾时,AI应该如何处理?

这种情况在屎山项目中非常常见。典型场景:Layer 0规定”禁止跨层依赖”,但Layer 2里的某个模块实现里有一段15年前的代码直接从API层访问了数据库。

处理原则应该是明确的,并且要写进宪法层:

# 在constitution.yaml中添加冲突处理规则

conflict_resolution:
principle:"Layer 0是不可违反的约束,但不是强制立即修复的标准"

rules:
-when:"Layer 2中存在违反Layer 0原则的现有代码"
action:"不要在此次任务中修复历史违规,保留现状"
reason:"历史违规的修复需要专门的技术债还清任务,夹带修复风险大"
mark:"在代码注释中标注TODO: ARCH-VIOLATION"

-when:"新增代码需要遵循Layer 0原则"
action:"严格遵循Layer 0,不要以'现有代码也这么做'为由违反"

-when:"Layer 0与Layer 2有明显矛盾(不是历史违规,而是文档更新滞后)"
action:"暂停任务,告知开发者:'Layer 0第N条与Layer 2的描述存在矛盾,需要先确认以哪个为准'"

这个设计体现了一个重要的工程判断:在屎山项目中,”发现违规并立即修复”往往比”保留现状并标记”风险更高。屎山的危险在于隐性依赖,修复一个看起来明显的违规,可能破坏三个你不知道的隐性约定。

4.9 四层架构的大小预算计算

最后,用一个具体的数字验证这套架构的可行性:

层级
单次对话Token预算
说明
Layer 0:宪法层
~400 tokens
始终加载,精炼约束清单
Layer 1:模块索引层
~1500 tokens
8个模块的接口摘要
Layer 2:实现详情层
~4000 tokens
通常只加载1-2个模块
Layer 3:历史层
~2500 tokens
5条历史摘要 × 500 tokens/条
总计(最大值) ~8400 tokens
四层全开时的峰值
实际代码/问题讨论
~15000 tokens
给实际对话内容留出空间
对话历史(20轮)
~30000 tokens
每轮平均1500 tokens
单次对话总消耗 ~53400 tokens
远低于20万窗口上限

这意味着即使进行20轮深度对话,上下文消耗也只有总窗口的27%,留有足够的余量。相比之下,全量加载的4.2万字KNOWLEDGE.md(约5.5万tokens),加上20轮对话历史,会在第8轮左右开始进入危险区。


全局审视:四层结构的逻辑串联

核心洞察:上下文管理不是AI问题,是工程问题。全量注入是把架构决策推给了模型,渐进式Skill是把架构决策收回到工程师手里。

与后续文章的衔接:第4篇将讨论在渐进式Skill体系建立之后,如何安全地进行屎山模块的实际代码改造——这时候上下文管理已经不是瓶颈,真正的挑战变成了”如何在不破坏隐性依赖的前提下进行改动”。第5篇则会讨论改造后的验证策略:测试覆盖、回归验证、以及如何用AI辅助编写针对屎山代码的测试用例。


延伸思考

个人思考与判断

这套渐进式Skill架构有两个核心假设,值得审视:

假设一:文档可以被有效分层。这个假设在大多数项目里成立,但在某些高度耦合的屎山里,可能很难把”必须在任何情况下了解的”和”只在特定场景下需要的”清晰分开。我处理过一个项目,它的核心业务规则分散在代码注释、数据库枚举值、和一个Excel表格里,完全无法形成清晰的层次结构。在这种情况下,建立Skill层级的前置工作是先做文档整合(第2篇的内容),而不是直接跳到Skill设计。

假设二:任务类型可以被预先枚举。触发器设计里我列了4种任务类型,但实际工作中会遇到无法归类的混合任务。这时候的策略应该是”保守注入”:默认只加载L0+L1,需要更多信息时由开发者手动触发L2的加载。宁可AI因为缺少信息而主动问”需要我查看X模块的实现细节吗”,也不要自动加载过多内容。

最可能出问题的环节:历史层的维护。历史摘要需要开发者每次对话结束后手动更新——这是一个高摩擦的步骤。实践中这个步骤往往被跳过,导致历史层要么空着、要么过时。解决方法是在CLAUDE.md里明确指示AI在每次对话结束时生成一份标准格式的历史摘要,开发者只需要确认并保存,而不是从头写。

替代方案的审视

替代方案
核心思路
为什么未采用
RAG(检索增强生成)
把所有文档向量化,按查询语义检索相关片段注入
需要额外基础设施(向量数据库),且对”精确约束”的检索可靠性不足——宪法层的规则必须100%被注入,不能因为语义距离远就被过滤掉
动态压缩(LLM摘要)
每次对话前用LLM把KNOWLEDGE.md压缩成更小的摘要
压缩本身也消耗token,且会引入摘要的不确定性——被LLM压缩过的约束,其准确性无法保证
多Agent分工
不同Agent负责不同模块,避免单Agent窗口溢出
增加了协调成本,且Agent间的上下文同步比解决的问题更复杂,目前Claude Code的多Agent支持也尚未成熟
代码注释驱动
把所有约束直接写进代码注释,依赖AI读代码时自然获取
注释和文档分离,维护成本高,且注释级别的约束粒度太细,缺乏全局视角

适用边界与局限性

明确适用的场景

  • 项目代码量5万行以上,文档超过1万字
  • 团队2人以上协作使用AI Coding工具
  • 项目有明确的模块边界(即使是屎山,也能粗略划分出模块)
  • 改造周期超过1个月(前期投入的Skill设计需要足够长的使用周期来回收成本)

不适用或效果有限的场景

  • 一次性小任务(设计成本超过使用收益)
  • 文档本身极度缺乏(缺少可供分层的素材,需要先完成第1篇和第2篇的工作)
  • 项目模块间高度耦合、无法划定清晰边界(只能做到粗粒度的分层,效果有限)
  • 对话任务本身的上下文就很小(如果你每次都只做孤立的5行修改,上下文管理不是瓶颈)

已知局限:这套架构假设开发者能准确判断任务类型,能在对话结束后更新历史层,能在文档变更时同步更新Skill文件。这些人工步骤是整个体系的弱点,也是未来可以通过工具化来减少摩擦的方向。


屎山项目 AI Coding 实战系列

  • 第1篇:屎山诊断——在改之前先读懂它
  • 第2篇:知识抽取——把散落的文档变成AI可消费的结构
  • 第3篇:渐进式文档加载——防止上下文溢出的Skill设计(本篇)
  • 第4篇:安全改造策略——在不破坏隐性依赖的前提下修改屎山代码(待发布)
  • 第5篇:改造验证——AI辅助测试覆盖与回归验证(待发布)