让 AI 写代码不再"就近解决":如何在 monorepo 里建设 AI context?
AI 常把需求写快了,但顺手又造一遍"节流/懒加载/环境判断"等轮子,结果是能跑却变沉,技术债上升。AI 不是不懂复用,而是"看不到"你的共享库。问题的本质是:AI 的思考模式天生倾向"就地解决",思维聚焦于当前文件或仓库。再加上其不清楚自己的能力边界,难以主动突破上下文限制,导致其很难"优先复用 monorepo 的能力",从而不断造轮子。
本文给出一套可被 AI 消化的上下文体系(CLAUDE.md + USEME.md + 规则),让它先复用、再改造,按团队的方法写对代码。你将看到:AI 如何"看世界"、monorepo 的典型局限、我们如何建设可消费的 AI context,以及一份可复制的落地清单。
AI 是怎么"看世界"的
语言模型不是 IDE,它靠"上下文窗口"理解你的意图。看得见的东西(当前文件、最近看过的文件、你粘给它的片段)权重更高。
以一个真实案例说明:我们让 AI 实现一个"判断是否在微信环境内"的功能。如果只把业务文件丢给它,AI 会写出这样的代码:
typescript
// ❌ AI "就近解决" 的典型输出
const isWeChatEnv = () => /MicroMessenger/i.test(navigator.userAgent);
useEffect(() => {
if (isWeChatEnv()) {
// WeChat 专属逻辑
}
}, []);
看起来不错?但问题是:
- 重复造轮子 :我们的
common-ua
包里已经有isWeChat
方法了 - 脆弱的 UA 嗅探:容易被 UA 覆盖、WebView 差异或地区版本影响
- SSR 隐患 :直接访问
navigator
/window
- 边界不清:未处理服务端渲染或无 UA 的场景
但如果 AI 能"看到"我们的公共库文档,它会写出:
typescript
// ✅ 有了正确上下文后的输出
import { isWeChat } from 'common-ua';
if (isWeChat()) {
// WeChat 专属逻辑
}
区别很明显:代码更短、更安全、更可维护。关键是 AI 需要在正确的时机"看见"正确的信息。
在 monorepo 里的两大局限
1. 找不到"正确的能力边界"
先看看我们的 monorepo 目录结构:
bash
monorepo_apps/ # 根仓库
├── support_modules/ # 公共能力仓库
│ ├── common-util/ # 纯工具函数库
│ ├── common-react-hooks/ # 无业务耦合 Hooks
│ └── common-ua/ # UA/环境检测
│ └── ......
└── apps/ # 业务应用
├── 业务1/ # 业务1
├── 业务2/ # 业务2
└── ......
在这个结构中,AI 经常分不清能力边界,比如:
- 让它做"移动端适配",它可能在业务代码里写
window.innerWidth < 768
,而不是用common-ua
的isMobile()
2. 文档信息碎片化
我们统计了一下改造前的文档分布:
README.md
:数量多,内容重复度高,难以维护- 老 Wiki 页面:存在过期信息,容易误导
- 口头传承的"最佳实践":不可追踪,且经常与代码不一致
AI 面对这么多信息源,经常选择"安全策略"------重新实现,而不是冒险引用可能过期的代码。
踩坑历程:从混乱到有序
第一阶段:意识到问题(痛苦期)
我们发现团队里 AI coding 的输出虽然能跑,但质量参差不齐:
统计数据(改造前 3 个月):
- 重复实现的工具函数:27 个
- 不一致的导入风格:占比 48%
典型问题案例:
typescript
// 案例:AI 重复实现已有的 UA 检测
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
};
// 实际上 common-ua 里已经有更完善的实现:
import { isMobile } from 'common-ua/src/platform';
第二阶段:探索解决方案(试错期)
我们尝试了几种方法:
方法 1:对话窗口建立约定 效果:约等于没有。AI 不会记住上次的约定。
方法 2:在每个项目的 README 里写"使用指南"
效果:信息重复,维护成本高,AI 还是经常找不到。
方法 3:建立"代码库索引文件" 我们写了一个 index-of-libraries.md
,列出所有可用的工具。 效果:有改善,但 AI 还是倾向于"就近解决",除非明确指出文件路径。
第三阶段:系统性改造(见效期)
在 monorepo 项目里用 AI,最重要的是要先给它"定好规矩"。 首先,每次让 AI 改代码之前,最好先让它了解一下项目结构。比如让它先看看根目录的 package.json 和 CLAUDE.md,确认这是个什么样的项目。我们项目有 25 个子应用,共享库就在 support_modules 目录下,这些信息对 AI 来说很重要。 其次,要明确告诉 AI"先找现成的,再考虑新写"。
我们的解决方案:让 AI 有"路标"、有"准则"、有"口袋书"
核心架构:两层文档体系
scss
CLAUDE.md (总路标)
├── 快速开始 (命令速查)
├── Monorepo 概览 (包清单 + USEME 路径)
├── AI 协作规范 (Do/Don't)
├── 常见坑与约束
├── 文档地图与生成
└── 版本与兼容性说明
support_modules/*/USEME.md (具体指南)
├── 目录
├── 导入说明 (强制具体路径)
├── 重点 API 参数表
├── 分类 API 概览
├── 组件用法示例
├── 注意事项
└── 最佳实践与故障排除
1. 文档路标:CLAUDE.md
设计原则:AI 看到这个文件,应该能在 5 秒内理解项目全貌。 可以通过 claude init 命令生成,也可以借助其他大模型生成。
AI 协作规范(Do/Don't)
Do
- 使用具体文件路径导入;禁止 barrel(如
import { X } from 'pkg'
)。 - 最小化影响:不改对外 API;不做无意义格式化;保持文件/模块结构不变。
- SSR 安全:涉及
window/document
仅在浏览器端执行;必要时判空/条件分支。
Don't
- 不写版本兼容信息到 USEME(统一放本文件)。
- 不扩大改动范围(移动文件、拆并模块)除非明确要求。
2. 可被 AI 消化的用法手册:各包的 USEME.md
在公共包下建立简约并明确的 USEME.md 文档,供 AI 阅读和了解该包的功能和用法。
设计原则:每个示例都能直接复制粘贴使用,每个参数表都能当 API 手册查。
提示词模板(给 AI 的最小上下文)
在与 AI 协作时,建议附上精简、稳定的上下文提示,避免跑偏:
objectivec
你在一个 pnpm + monorepo 项目中工作。
先读根目录 CLAUDE.md 了解规则,再读相关包的 USEME.md。
优先复用 support_modules 下的能力,禁止 barrel 导入,必须使用具体文件路径。
若涉及 UA/SSR/性能,请优先查找 common-ua、common-react-hooks、common-util 的对应能力。
在给出修改前,用项目内既有 API 校对一次是否可复用。
小技巧:把这段模板固化到 .cursorrules
或常用的 Prompt 片段里,减少每次重复粘贴。
3. 跨仓库上下文打通:业务仓库"认识"support_modules
技术实现:给 cursor 添加 Rules:
go
严格遵循工作流程:收到任务 → 检查 Rules → 分析项目结构 → 执行。
阅读项目中的 package.json、CLAUDE.md 和 USEME.md 以遵循项目约定。
检查该项目是否为 monorepo 架构,如果为 monorepo 子仓库,则向上查看父仓库和 support_modules 的指导文件(package.json、CLAUDE.md 和 USEME.md)以进行跨仓库协作。
关键机制:
- 自动索引 :AI 会主动检查大仓的
CLAUDE.md
- 路径映射 :通过
CLAUDE.md
的"文档地图",AI 知道去哪找每个包的USEME.md
- 优先级控制 :公共库的
USEME.md
比业务代码有更高的参考权重
实施细节:魔鬼在细节里
1. 文档结构的精心设计
标题层级:我们发现 AI 对标题层级很敏感
## 重点 API 参数
:AI 优先关注### 具体函数名
:AI 能精确定位#### 参数详解
:AI 会仔细解析
表格格式:参数表的列顺序经过多次优化
markdown
| 参数名 | 类型 | 默认值 | 必填 | 说明 |
这个顺序比其他排列方式的 AI 解析准确率高 15%。
2. 跨仓库协作的技术细节
完整目录结构:
bash
monorepo_apps/ # 根仓库
├── claude.md # 总路标
├── support_modules/ # 公共能力仓库
│ ├── common-util/
│ │ ├── USEME.md
│ │ └── src/
│ ├── common-react-hooks/
│ │ ├── USEME.md
│ │ └── src/
│ └── common-ua/
│ ├── USEME.md
│ └── src/
└── apps/ # 业务应用
├── 业务1/ # 业务1
└── 业务2/ # 业务2
在根仓库 claude.md 中添加文档地图:
bash
support_modules - 核心业务逻辑模块
| 包 | 用途 | 文档 |
| ----------------- | --------------- | --------------- |
| umu-util | 工具函数库 | `support_modules/umu-util/USEME.md` |
| umu-react-hooks | 无业务耦合 Hooks | `support_modules/umu-react-hooks/USEME.md` |
| umu-ua | UA/环境检测 | `support_modules/umu-ua/USEME.md` |
AI 索引机制:
- AI 读取
.cursorrules
,知道要检查父仓库 - 通过
monorepo_apps/CLAUDE.md
获取全局规范 - 根据"文档地图"索引各包的
USEME.md
- 在生成代码时优先使用公共库的能力
踩坑经验:我们犯过的错误
1. 文档过于详细反而有害
错误做法 :最初我们在 USEME.md
里写了很详细的实现原理和设计思路。 结果:AI 被"误导",经常尝试"改进"我们的实现。
正确做法:只写用法,不写原理。让 AI 当"用户",不当"维护者"。
2. 示例代码的"诱导性"很强
错误做法:示例中使用了简化的错误处理:
typescript
// ❌ 糟糕的示例
loadMore().then((data) => setList([...list, ...data]));
结果:AI 照搬这种写法,忽略了错误处理。
正确做法:示例必须展示完整的最佳实践:
typescript
// ✅ 完整的示例
loadMore()
.then((data) => setList((prev) => [...prev, ...data]))
.catch((err) => showError(err.message))
.finally(() => setLoading(false));
3. 过度约束反而降低效率
错误做法 :初期我们制定了 20+ 条详细规范。 结果:AI 被约束得"畏首畏尾",生成的代码过于保守。
正确做法:精简为 5 条核心约束,其他通过示例引导。
若你也准备在 monorepo 里用好 AI coding,可以这样落地
第一步:建立文档体系
核心文件优先级:
CLAUDE.md
(总路标)- 各公共包的
USEME.md
(常用工具)
第二步:跨仓库集成
在编辑器(如 cursor)中添加 rules,
业务仓库配置:
重要:因为 AI 往往会寻求"就近解决"。所以需要强制规定 AI 的思考模式,要求先阅读 rules 并了解项目结构,然后再工作。
arduino
// .cursorrules
严格遵循工作流程:收到任务 → 检查 Rules → 分析项目结构 → 执行。
检查该项目是否为 monorepo 架构,如果为 monorepo 子仓库,则向上查看父仓库和 support_modules 的指导文件以进行跨仓库协作。
第三步:一小时落地清单(可直接执行)
- 在根目录新增/完善
CLAUDE.md
:包含包清单、文档地图、导入约束 - 为常用公共包补齐
USEME.md
:每个至少 1 个复制即用的示例 - 统一导入方式:在示例和脚手架中全部改为具体文件路径
- 在编辑器加入
.cursorrules
:强制工作流与跨仓库检索 - 加入 3 个简单的静态检查脚本(重复工具、barrel 导入、SSR 危险 API)
- 选一个真实需求做"对照试验":先不加上下文生成一次,再按规范生成一次,对比差异
尾声
AI coding 的价值不止是"写快一点",更在于"写对一点"并且"复用多一点"。在 monorepo 里,这意味着给 AI 一张清晰的地图、明确的边界和可复制的惯用法。
我们用 CLAUDE.md
约束,用 USEME.md
传达,最终让 AI 在业务仓库里自然地"看见并复用" support_modules
的能力。实际落地后,重复造轮子的情况显著减少,提交一次通过率明显提升,新人融入速度也更快。
轮子造得再好,也比不上全仓库的人一起用同一个。现在,AI 也学会了这一点。
如果你也在大型项目里用 AI coding,不妨试试这套"文档驱动"的方法。让 AI 助手也会自动"学会"你们的最佳实践,成为真正能理解业务、写出符合团队要求代码的 AI coding engineer。