我平时手头有两类事情,差异特别大。
一类是在一个大型iOS app里维护常规业务模块。另一类,是某些人的脑子里突然冒出个idea,然后需要用AI落地创新app,MVP上线。
这两件事都重度依赖Claude Code,但踩过的坑完全不在一个方向。小项目AI基本上来就能干,大工程里你如果不事先搭好一套指令体系,AI跑不了几轮就开始漂,最后有效产出低得离谱。
为啥想写这个东西?因为最近这类话题一度变得很热,大家都在聊AI coding、聊agent、聊各种奇奇怪怪的自动化。
然鹅,真把它放进一个大型iOS工程里以后,问题往往不是"模型会不会写代码",而是"你有没有把人类平时那套开发迭代,翻译成AI真正能执行的约束和步骤"。这两件事差得还挺远。
什么大工程里最贵的是迭代回路?
CLAUDE.md、Rules、Skills、Subagent 分别该放什么?
MCP和Context在大工程中如何提供优化空间?
1. 背景
1.1 大工程里,AI的难题
小项目中,AI基本是上来就干:扫目录、改代码、跑构建、验证,闭环很快。一开始我觉得这东西太猛了,恨不得所有项目都上。
等把它带进大型项目中,才发现完全不是那么回事。
小项目里,Claude Code这种东西上来先扫一遍目录,改两行代码,跑一下测试,大概就能闭合一个回路。这个过程虽然不一定优雅,但通常还能跑通。
众所周知,很多演示视频也基本都是这个模式,所以容易让人产生一种错觉,仿佛把模型接进仓库,工程效率就会自然涨上去。
但大型工程中...
出现的问题非常偏 --- 工程化:模块多、target多、构建脚本多、项目生成规则多、局部修改和全量验证之间的成本差异极大。
熟悉的老人可以凭借经验本能地绕开这些坑,先改局部,先跑模块级构建,再决定要不要全量build。AI如果没有被明确教过,很容易每做一步就去试一下完整构建,最后把自己和Context一起搞裂开。
1.2 解法
我觉得这里最值得抄作业的地方,并不是某一段具体的指令文案,而是把"指令"当成一个分层系统来设计。
也就是说,哪些知识应该常驻,哪些知识应该按路径加载,哪些操作应该隔离到Subagent,哪些东西干脆不要让AI直接生成,而是交给CLI或者脚本兜底。
这个分层一旦理顺,很多原来看起来很玄学的问题,突然就变得不那么神秘了,因为它开始像正常的软件工程。
2. 关于昂贵的成本:试错回路
2.1 iOS 工程不是微服务
这两年的AI coding演进归纳下,明显的变化:最早大家更关心"它能不能补全",后来开始关心"它能不能改文件",再后来就开始关心"它能不能自己探索、自己执行命令、自己验证结果"。
问题是,能力边界一旦从"生成一段文本"变成"执行一串开发动作",成本模型就完全变了。
举栗,一个前端demo项目,全量构建可能几十秒,甚至几秒。一个大型iOS工程,全量构建可能是十几分钟起步,而且中间还夹着各种代码生成、工程再生成、签名、缓存命中率、Derived Data污染之类的问题。
人类会自然发展出一套"先局部、后整体"的工作流。AI如果还按照最原始的"改一下,完整验证一下"去跑,速度一下子就没了。
"如何把人类开发迭代抽象成一套机器能执行的控制面"
2.2 我们平时到底怎么做迭代
把大型iOS工程的日常开发流程抽成下面这5步:
- 准备构建环境
- 写代码 & 单测
- 必要时再生成Xcode工程
- run 模块级构建 和 模块级测试
- 最后再做全量构建和行为确认
写成图,大概像这样:
这里最关键的不是图本身,而是第4步。精度高的迭代,基本都建立在"局部验证能不能足够快"上。
没有这一步,AI再聪明也只能在大循环里磨洋工。
举个命令层面的例子,如果项目允许模块级构建,那通常优先级应该接近这样:
bash
# 先局部
xcodebuild build -workspace BigSweetPotato.xcworkspace -scheme MyFeature -configuration Debug -destination 'generic/platform=iOS Simulator'
# 最后才全量
xcodebuild build -workspace BigSweetPotato.xcworkspace -scheme BigSweetPotato -configuration Debug -destination 'generic/platform=iOS Simulator'
2.3 AI 为什么总漂移
直接说结论:因为我用 /init 生成的 CLAUDE.md,只告诉模型"这是个什么项目",但没有把"应该怎么迭代"写清楚。
于是就会出现几类非常典型的问题:
- 不知道该把功能落在哪个模块
- 不知道某个
import对应的实现文件藏在哪 - 一改工程配置就不知道要不要重生工程
- 最贵的一个问题,是动不动就跑全量build
各种奇奇怪怪的问题叠在一起,agent就开始一度看起来很努力,实际上有效迭代次数很少。
3. CLAUDE.md里到底该放什么
3.1 共享记忆只放高频、稳定、跨任务的东西
这件事我觉得很重要。
CLAUDE.md不是项目百科全书,也不是团队文化墙,它更像一个启动期的最小公共上下文。里面应该放那些"几乎每次做事都会用到,而且短期内不太会变"的信息。
比如下面这种结构,我觉得就比较合理:
md
# Project basics
- Repository uses modularized architecture. // 1
- If project spec changes, regenerate Xcode project before build. // 2
- Prefer module build and module tests before full app build. // 3
- Full app build requires explicit user approval. // 4
// 1这一条解决的是"AI到底在什么地形里干活"。不知道仓库结构,后面所有路径判断都会漂。// 2这一条解决的是"哪些改动会影响工程生成产物"。这类规则如果缺失,AI会出现一种很奇怪的状态,看起来改对了,实际工程根本没更新。// 3这一条本质上是在规定默认迭代半径。先局部,后整体。// 4这一条是在控制成本上限。没有这个护栏,大工程里token和等待时间都会一起爆掉。
对于大型iOS工程,共享记忆应该被控制在很短的体量上。
常驻上下文越短,越说明里面的东西是被挑过的,而不是想到什么塞什么。
如果你想检查自己是不是写成了百科全书,一个很土但是有效的命令是:
bash
wc -w CLAUDE.md
当然,字数不是唯一标准,但超长共享记忆往往意味着边界糊了。
3.2 功能细节,拆成Rules
路径作用域的Rules,本质上是在说一件很朴素的事:只有在处理某个目录时,才加载这个目录的特定知识。
这个设计非常像编译器里的延迟加载,平时不进Context,真碰到这个功能再说。
举个简化后的样子:
yaml
---
paths: Modules/Features/Timeline
---
This module uses a presenter-first structure. // 1
Never expose experimental project names in strings. // 2
Regenerate project spec if adding new dependency nodes. // 3
// 1是功能内部的结构性知识,适合局部加载,不适合全局常驻。// 2是这个功能特有的约束,和别的模块没关系。// 3是只在这里才成立的工程规则,没必要污染所有任务。
这类东西一旦不分层,CLAUDE.md最后就会变成一个巨大的垃圾场。
表面上信息更全,实际上可用性更差。
如果仓库里真的开始多起来,至少命名和目录要规整,不然AI没先迷路,人先迷路了:
bash
find .claude/rules -type f | sort
3.3 个人偏好不要强行共享,本地化更合理
还有一个我觉得很巧的小点,就是在共享记忆最后去@一个用户本地文件。如果这个文件存在,就继续加载;如果不存在,也没关系。
这个思路的好处在于,它把"团队共识"和"个人习惯"拆开了。
比如有人喜欢先跑测试,有人喜欢先扫目录,有人需要更保守的编辑策略,这些都不该直接写进团队共享配置里。
尤其是多worktree环境下,本地文件放在用户目录往往比仓库内的ignore文件更省事。
一个大概的样子:
text
If @~/.claude/my-team/ios.local.md exists, follow it as well. // 1
// 1不是为了让每个人搞一套完全不同的世界观,而是给局部偏好一个合法出口。没有这个出口,大家最后还是会回到复制粘贴自己的Prompt小纸条。
4. 让AI能跑迭代,但别乱跑
4.1 第一条铁律:尽量别让它先全量build
这条原则看起来很朴素,实际上是整个系统能不能跑起来的根。
小项目里,全量build经常是最省脑子的验证手段。
然鹅,在大型iOS工程里,这几乎等于让agent每做一步都去排一次长队。更麻烦的是,长构建日志会把主上下文塞满,后面连真正重要的推理空间都没了。
所以更合理的策略应该像这样:
这个图看着有点废话,但我觉得它非常重要,因为它把"全量build"从默认动作改成了条件动作。顺序一换,迭代成本就完全不是一个量级。
4.2 把 build 和 test 任务塞进 Subagent,context 更干净
在大型iOS工程中常见的几个Subagent类型:
prebuilt-tasks-runnermodule-builderapp-buildermodule-test-runnerworkspace-cleaner
我觉得这套拆法比较合理。因为这些任务有两个共同特征:
- 它们和主任务之间是弱耦合的
- 它们极度容易产生长日志
只要这两点同时成立,就很适合下沉到Subagent。
一个简化后的定义,大概长这样:
yaml
---
name: module-builder
description: Build specified modules in the iOS workspace.
tools: Bash, Read, Grep, Glob
skills: creating-xcode-build-scheme // 1
---
Build changed modules first. // 2
Do not run full app build without approval. // 3
If a build scheme is missing, create it via the assigned skill. // 4
// 1说明这个Subagent被授予了额外的专业能力。// 2限定默认动作,别上来就干重活。// 3把成本边界写死。// 4说明它不仅能"执行命令",还知道遇到缺失前置条件该怎么办。
这块比较有意思的地方在于,Subagent不是为了显得高级,而是为了把脏活和主推理隔离开。
程序中没有什么是加一个中间层不能解决的。如果有,那就两层。
4.3 Skill和CLI一起上,专治那种格式严苛的脏活
大型iOS工程里经常有一种任务,非常不适合让模型自由发挥:它知道最终目标,但输出格式过于机械,错一个字段整个文件就废了。
Xcode的*.xcscheme就挺像这种东西。
这里的处理方式我觉得很工程:不是让模型直接手搓XML,而是提供一个专门的Skill,再把真正的生成动作交给CLI。
也就是说,Skill负责"什么时候用、怎么用",CLI负责"稳定地产出正确格式"。
举个简化后的Skill定义:
yaml
---
name: creating-xcode-build-scheme
description: Create build scheme for a specified target.
allowed-tools: Read, Grep, Glob, Bash(./create-scheme:*) // 1
---
Use ./create-scheme <Target> --project <path> when scheme is missing. // 2
// 1这里最核心的是权限被收窄了,它不是一把任意执行的瑞士军刀。// 2这里把自然语言决策映射成受控命令。模型只需要判断"该不该调",不用自己拼那堆细节。
这个设计其实特别像编译器前端和后端分工。前端做语义判断,后端做确定性产物生成。
虽然但是,把这个思路搬到agent系统里以后,味儿一下就对了。
命令层面,大概会收敛成这种形式:
bash
./create-scheme MyFeature --project Modules/Feature/MyFeature/project.yml --shared
5. Skill
从 claude.md 中抽离成 Skill
5.1 因为不是所有知识都值得常驻Context
Skill有个很实用的点:description常驻,正文按需加载。这件事在大工程里非常关键。
真正浪费空间的,不是"这个Skill干嘛的",而是那个Skill背后那一整套具体操作说明。
把它们都塞进共享记忆,agent当然知道得更多了,问题是这些知识绝大多数时间根本用不上。长期来看,这种做法只会让Context越来越胖。
所以比较自然的分工应该是:
CLAUDE.md放项目地形和默认迭代规则- Rules放路径局部知识
- Skill放专业操作说明
- Subagent放可隔离执行的任务
- CLI和脚本放确定性强、格式敏感的产物生成
5.2 举两个很典型的Skill例子
一个是"从模块名推目录"。这件事听起来很小,实际很值钱。
大型仓库里,AI看到一行import FooBarUI,如果能迅速把实现目录定位到正确位置,后续阅读、改依赖、找Project Spec都会顺很多。
另一个是"改Project Spec"。如果项目是XcodeGen这类生成式工程配置,真正要改的往往不是*.xcodeproj,而是描述工程的那层YAML。
这里面如果还有模板继承、特殊依赖写法、内部约定字段,那就更适合收敛成Skill。
举个偏土的方法,先把目录定位做实,命令就能少走很多弯路:
bash
rg --files Modules | rg 'BigSweetPotatoFeature|FooBarUI|project.yml'
6. MCP这件事,项目级别的也需要吗?
先谨慎,不急着把MCP server放进项目共享层。尤其是胖一点的项目。
原因看起来也很简单,实际上一点也不难:
- 很多MCP工具即便没被调用,元信息本身也会吃掉不少Context
- 一旦项目级统一提供,还会连带带来运行时工具链、安装方式、版本管理这些额外维护成本
对于一个已经很重的大工程来说,这些东西都不是零成本。
这不等于MCP没用。相反,语义级代码检索、符号级引用分析、重构辅助这类能力是很有价值的。
只是它现阶段更像"按开发者个人习惯选装的增强件",而不是必须焊死在项目基座里的标配。
这一点就不多展开了,有兴趣可以申请研究扩展一下,海量代码下依然依赖grep命令会指数级失控。
7.一些主观
如果把这篇文章压缩成几条原则,我自己的版本大概是下面这些:
7.1 设计迭代和好好写Prompt一样重要
不要一上来就研究措辞多优雅。真正决定效率的,往往是你有没有把"先做什么、后做什么、遇到什么条件转向什么路径"讲清楚。Prompt写得再漂亮,默认动作要是错的,最后还是慢。
7.2 知识按需加载(别常驻
这不是省几个token那么简单,而是在保护主要推理链路。共享记忆一长,所有任务都会背着它跑。能拆成Rules和Skills的,就尽量拆。
如何按需加载,rules和skill这两个就是按需加载,一个通过路径,一个通过场景。
7.3 模型是概率,不用模型就不会出错 --- 能确定性生成的东西,就用模型赌
像XML、工程定义、特殊配置、命令拼接这些活,只要格式错误代价高,就尽量往脚本和CLI那边收。
模型做调度和判断,比模型做格式体力活,稳定得多。
7.4 Subagent 一定要 职责窄
不是agent越多越高级。职责切得不清,最后一样互相污染上下文。
真正好用的Subagent,通常一眼就能看出它负责什么,也一眼就能看出它不负责什么。
8. summary
- 大型iOS工程里最贵的不是生成代码,而是错误的验证回路。
- 共享记忆应该只放高频公共知识,功能局部规则拆到Rules,专业操作说明拆到Skills。
- build、test、clean这类高日志任务适合下沉到Subagent,不要污染主上下文。
- 格式严苛、确定性强的产物,最好交给CLI或者脚本生成,模型只负责决策和调度。
- 我认为MCP很值得关注,但项目级接入仍然要看Context成本和维护成本,没必要着急。
太多比较有意思的点值得深挖,后续有空继续深入研究更新后续wiki。NFY占坑一篇:怎么给大型仓库设计"可失败、可恢复、可观测"的agent迭代流程。
9. References
- Claude Code 官方文档
- Overview:CLI、IDE、桌面端、Web等各入口的总览
- Memory:CLAUDE.md 与
.claude/rules/:共享记忆、路径作用域 Rules、auto memory 都在这一页,本文里关于"什么进共享记忆、什么拆成 Rules"的判断基本对应这里的paths字段和claudeMdExcludes - Skills:
SKILL.md的 frontmatter 字段(description、allowed-tools、context: fork、disable-model-invocation),以及"description常驻、正文按需加载"这件事的来源 - Subagents:独立 context、工具白名单、
skills预加载,本文里module-builder/app-builder那套拆法对应这里 - MCP:
claude mcp add、远程/本地 server、以及 MCP 元信息进 Context 的成本问题 - Settings:
permissions、claudeMdExcludes、disableSkillShellExecution等大工程里会用到的开关 - Hooks:把"每次提交前必须做 X"这种硬约束从 Prompt 里挪到事件钩子上,比写进 CLAUDE.md 可靠得多
- 工程层面相关