31 万行 TypeScript 单仓接入 AI 编码实战
一句话结论:历史包袱不是 AI 的天敌,没有类型信息才是。 我们把同一份 8 年历史的老代码在「清理类型前后」分别喂给同一个模型, 一次通过率从 23% 跳到 61%。模型一行没换。
M 公司是一家做仓储物流 SaaS 的中厂,平台团队 14 人,守着一个 wms-platform 单仓:
| 指标 | 落地前(2026-08) |
|---|---|
| 代码量 | 312,400 行 TypeScript |
| package 数 | 14(含 3 个已停维) |
| 仓库历史 | 8 年,历经 4 代 Owner |
| 类型覆盖 | strict: false + 大量 implicit any |
| 单测覆盖率 | 41% |
| PR 提交→合并中位 | 4.2 天 |
| 敢动核心库存模块的人 | 2 个 |
2026 年 8 月第一次试 AI 的结果很难看。 给 12 位工程师开了 Cursor Pro,两周后做匿名问卷:
| 反馈选项 | 占比 |
|---|---|
| 「它写的东西我基本要重写」 | 41% |
| 「能用,但要大改」 | 33% |
| 「基本可用,小改」 | 19% |
| 「几乎不用改」 | 7% |
74% 的人认为不值。按这个数据继续推,第三周就会有人开始质疑这笔预算。
六周之后我们重测同一份问卷,「几乎不用改 + 小改」合计 86%。下面记录中间到底做了什么。
一、背景:为什么同样的工具在别的团队有效,在我们这儿无效
1.1 第一次复盘找到的三个错
第一周定性访谈了 9 个人,把所有负面反馈归拢,发现没有一条是在骂模型笨:
| 抱怨 | 占比 | 真实根因 |
|---|---|---|
| 「它不知道我们的约定」 | 38% | 团队规约从未写成机器可读的形式 |
| 「改完编译不过」 | 31% | 仓库没有任何类型信息,模型只能猜 API 形状 |
| 「它改了一处漏了三处」 | 22% | 跨文件改动缺治理流程 |
| 「输出质量不行」 | 9% | — |
91% 的抱怨落在「信息缺失」和「流程缺失」上,只有 9% 落在模型能力上。
这个分布决定了整个项目的方向:不要去换模型,去补信息。
1.2 一个决定性实验
在第二周量化会上做了个对照实验:同一批 20 个真实任务(都来自过去半年的 PR), 分别对两个版本的同一片代码跑:
- A 组:原样代码(
implicit any遍地,JSDoc 缺失) - B 组:同份代码,仅人工补了导出函数的返回类型注解 + 关键 interface 的字段注释,逻辑一行没改
同一个模型(当时用的是 Sonnet 4.5),同一个 prompt 模板:
| 组别 | 一次通过(tsc 无错 + 测试通过) | 需小改 | 需重写 | 平均 prompt 轮次 |
|---|---|---|---|---|
| A 组(无类型) | 23% | 34% | 43% | 4.7 |
| B 组(补类型) | 61% | 28% | 11% | 2.1 |
一次通过率翻了 2.65 倍,对话轮次减少 55%。 而这个提升里,模型贡献为 0。
1.3 由此定出的四步路线
第 1-2 周:类型基线 把 implicit any 清出来,让模型有东西可读
第 3 周:约束固化 14 条 .cursor/rules,把团队约定变成机器可读
第 4 周:稳定性 自建 OpenAI 兼容代理,解决 429 中断与 Key 管控
第 5-6 周:内部知识 MCP 接服务元数据与埋点查询,让 Agent 能查不能猜
每一步都不碰模型。
二、第 1-2 周:类型基线清理
2.1 不是全面转 strict,是「按包渐进」
strict: true 一开就是 8,400 个报错,任何人都会放弃。用的是按包渐进 + 只严「就该严的部分」:
// packages/core/tsconfig.json —— 只改这一个包先试点
{
"compilerOptions": {
"noImplicitAny": true, // ← 第一个开,收益最大
"strictNullChecks": false, // 先不开,这个最痛
"strictFunctionTypes": true,
"noImplicitThis": true,
"skipLibCheck": true
}
}
打开 noImplicitAny 之后 packages/core 报 1,240 个错。分类后只留该留的:
| 错误类型 | 数量 | 处理 |
|---|---|---|
| 函数参数缺类型 | 610 | AI 批量补,人工抽检 15% |
useState() / ref() 缺泛型 | 240 | AI 批量补 |
| 第三方库回调参数 | 190 | 补类型或用 (e: unknown) + 收窄 |
| 真的该用 any 的地方(动态解构) | 200 | 改 unknown + 收窄 |
| 历史债务(类型本身就是错的) | 0 | — |
2.2 AI 批量补类型的 prompt
这是整个项目 ROI 最高的一段 prompt,两天补了 850 处:
@packages/core/src/services/order.ts
这个文件的函数参数缺类型标注。请按下面的规则补:
1. 只补参数类型和返回值类型,不改任何函数体逻辑、不改命名、不改格式
2. 参数类型从以下来源推断,按优先级:
a. 同一文件里对该参数的实际用法
b. packages/core/src/types/ 下已有的 interface / type
c. 调用方传入的真实值(用 grep 找调用点)
3. 三种及以上来源都推不出具体形状时,用 unknown 而不是 any,
并在该参数上方加一行 // TODO-TYPE: <你缺什么信息>,不要编造一个看起来合理的类型
4. 如果某个参数的用法本身就自相矛盾(有时传 string 有时传 object),
不要强行 union,标 // TODO-TYPE: usage conflict 交回人工
输出:完整 diff,不要解释。
第 3、4 条是这条 prompt 的关键。禁止 any、禁止编造类型 把 AI 的不确定性显式暴露出来,
而不是让它用一个假类型把问题藏到更深的地方。850 处里产生了 61 条 TODO-TYPE,
人工处理花了一个下午 —— 相比省下的时间,这是极划算的。
2.3 两周产出
| 指标 | Week 0 | Week 2 |
|---|---|---|
| 启用 noImplicitAny 的包 | 0 / 14 | 11 / 14 |
| 剩余 any(含 justified) | 3,900+ | 640 |
其中 unknown 替换 | 0 | 1,270 |
| 导出函数有显式返回类型 | 31% | 88% |
| 投入 | — | 9 人日(AI 完成约 70%) |
剩下 3 个包是已停维的,暂时不动。
三、第 3 周:把团队约定写成 Rules
3.1 约束的来源不是拍脑袋,是 PR review 记录
拉了过去 6 个月 1,842 条 review comment,人工聚类,取出现频次 > 20 的类型:
| review comment 类型 | 出现次数 | 占比 |
|---|---|---|
| 命名不符合规范 | 318 | 17% |
| 错误处理不完整(裸 catch / 未抛异常) | 292 | 16% |
| 缺边界条件判断 | 241 | 13% |
| 重复造轮子(已有 util 没用) | 218 | 12% |
| 日志缺失或不规范 | 197 | 11% |
| 事务边界不对 | 143 | 8% |
| 其他 | 433 | 23% |
前六项占 77%。这六类就是 Rules 要覆盖的全部内容 —— 不追求大全,只治理高频。
3.2 14 条 Rules 的组织
.cursor/rules/
├── 00-minimum.mdc alwaysApply: true 最小约束集(8 条)
├── 01-naming.mdc globs: src/**/*.ts 命名
├── 02-error-handling.mdc globs: src/**/*.ts 错误处理
├── 03-boundary.mdc globs: src/**/*.ts 边界条件
├── 04-reuse.mdc globs: src/**/*.ts 已有 util 清单 + 禁用写法
├── 05-logging.mdc globs: src/**/*.ts 日志规范
├── 06-transaction.mdc globs: src/db/** 事务边界
├── 10-vitest.mdc globs: **/*.test.ts 测试写法
├── 11-react.mdc globs: packages/ui/**/*.tsx
├── 20-api-contract.mdc globs: src/api/** 接口契约
├── 21-inventory.mdc globs: src/inventory/** 库存域业务规则
├── 30-no-touch.mdc alwaysApply: true 🔴 禁止触碰清单
├── 31-package-json.mdc globs: package.json 禁止改版本号
└── 40-code-review.mdc alwaysApply: false 自查清单(AI 提交前自检用)
3.3 三条最有用的 Rules
04-reuse.mdc —— 直接列出 util 清单
这是收益最高的一条。AI 不会用你已有的 util,纯粹是因为它不知道那东西存在。
---
description: 已有工具函数清单,禁止重复造轮子
globs: src/**/*.ts
alwaysApply: false
---
# 已有 util(改动前必须确认没在这里)
## 日期时间 —— src/utils/date.ts
- `formatRange(start, end)` 格式化时间区间
- `toISO8601(d)` 转 ISO 字符串
- `addBusinessDays(d, n)` 加工作日(跳过周末与公司假期表)
## 金额 —— src/utils/money.ts
- `fenToYuan(fen)` 分转元,**所有对外展示必须过这里**
- `safeAdd(a, b)` 浮点安全加法,禁止用 a + b 算金额
## 分页 —— src/utils/paginate.ts
- `paginate(list, {page, size})`
## 禁止的写法
- ❌ 手写 `new Date(x).toLocaleDateString()` → 用 formatRange
- ❌ `(total / 100).toFixed(2)` 直接算金额 → 用 fenToYuan
- ❌ 手写币种换算 → 走 src/utils/currency.ts
30-no-touch.mdc —— 禁止触碰清单
---
description: 禁止 AI 修改的文件清单
globs: **/*
alwaysApply: true
---
# 禁止触碰
未经明确书面确认,禁止修改以下任何文件:
- `src/inventory/core/**` — 库存核心算法,改错了影响财务对账
- `src/db/migrations/**` — 已上线迁移,禁止回填修改
- `**/*.lock` / `package-lock.json`
- `docker-compose.prod.yml`
- 任何含 `// DO-NOT-MODIFY` 注释的文件及其被依赖关系
如果你的任务逻辑上必须改动上述文件之一:
停下来输出 OPEN_QUESTION,说明为什么必须改、改了会影响什么、以及你建议的替代方案。
不要绕过这个约束去改它的「等价位置」。
最后一句很重要。不写的话 AI 会找出「等价方式」绕过:src/inventory/core/x.ts 不让改,
它就新建一个 src/inventory/core-v2/x.ts。这个 Constraint 我们吃过亏。
40-code-review.mdc —— 提交前自查
---
description: AI 完成改动后的自查清单
globs: **/*
alwaysApply: false
---
# 自查清单(改动完成后必须逐条回答,不许跳过)
改完之后,在给出最终回答前先跑一遍:
1. 我改的文件里,有没有任何一个在禁止清单里?
2. 我引入的新类型、新函数,有没有走 src/utils 里已有的实现?
3. 我读过的文件,是不是改之前的版本?(有没有可能存在我没读到的更新)
4. 我改动的每一个函数,它的所有调用点都被我核对过吗?列出你核对过的调用点数量
5. 我有没有顺手改了与任务无关的东西?(格式化/重命名/调整顺序)
6. 有没有任何地方我是不确定的?有就列出来
**第 4 条的调用点数量不许写「已全部核对」这种模糊表达,必须给数字。**
这条 Rule 独立于执行过程,在 AI 「准备收尾」时触发,价值是把跨文件漏改从 weekly 层级压到 0。
3.4 第三周产出
| 指标 | Week 2 | Week 3 |
|---|---|---|
| AI 生成代码中违反团队规范的比率 | 34% | 6% |
| review 往返均值 | 3.1 次 | 1.4 次 |
| 投入 | — | 4 人日(写 Rules + 翻 review 记录) |
四、第 4 周:自建 OpenAI 兼容代理
4.1 逼着我们做这件事的两个现象
第三周推到日均 1,200 次 AI 请求之后暴露两个问题:
问题一:Agent 任务频繁中断。 DeepSeek 晚高峰(20:00-23:00)429 率高, 而 Cursor 对自定义模型不做重试,一个 60 文件的重构任务跑到第 12 个文件就断了。 断了还没法续 —— 重开时它不记得改到哪了。
问题二:Key 管控。 12 个人每人本地存一份 sk-,离职/轮换时要挨个通知,
而且 Key 明文在 ~/.cursor/ 下,任何本地进程都能读。
4.2 部署形态
公司内网一台 4C8G 的机器上跑 one-api,所有开发者连它:
# /opt/mcp-proxy/docker-compose.yml
version: "3.8"
services:
gateway:
image: ghcr.io/one-api/one-api:latest
restart: unless-stopped
ports:
- "127.0.0.1:13001:3000"
environment:
TZ: Asia/Shanghai
# 渠道失败自动重试,这是解决 429 的核心
RETRY_TIMES: "3"
volumes:
- ./data:/data
每个开发者拿独立的 token(便于配额管理与成本核算),但不持有真 Key。
4.3 三个额外收益(原计划外的)
| 收益 | 说明 |
|---|---|
| 成本归因到个人 | Langfuse 打 userId,能看到每个人每月花了多少,比平均数有用得多 |
| 多 Key 自动轮换 | 两个 DeepSeek 账户配两个渠道,一个限额 top up 就自动切另一个 |
| 灰度开关 | 想临时禁用某模型,改网关配置,不用通知 12 个人 |
4.4 第四周产出
| 指标 | Week 3 | Week 4 |
|---|---|---|
| Agent 任务因 429 中断率 | 18.4% | 0.9% |
| 60 文件重构任务成功率 | 61% | 94% |
| Key 轮换成本 | 挨个通知(约 40 分钟) | 改一处(2 分钟) |
| 投入 | — | 2 人日 |
五、第 5-6 周:MCP 把内部知识接进来
5.1 真正卡住 Agent 的是什么
到第四周,AI 已经能写出符合规范、能编译通过、测试能过的代码。剩下的 Bug 集中在一类: 它不知道某个服务/某张表/某个字段的业务含义。
典型现场:让它给「库位推荐」加一个字段,它不知道:
location_score这个字段在三个服务里有三种不同的计算口径wms.picking.tasks表上个月刚做过分片,直接 count 会全表扫- 「可用库存」在我们的业务里是
qty - locked - damaged,不是qty
这些不在代码里。它们在 Confluence(过期两年)、在老员工脑子里、在当初的需求文档里。
5.2 自建的 MCP Server:两个 Resource、三个 Tool
没有 MCP 之前这些信息在哪都查不到。做了个 Server 把它们结构化:
| 类型 | 名称 | 作用 |
|---|---|---|
| Resource | service://catalog | 14 个 package 的职责边界、Owner、依赖关系、稳定性等级 |
| Resource | field://dictionary | 387 个业务字段的口径定义、所属服务、变更历史 |
| Tool | lookup_field | 按字段名/中文名查口径定义,返回定义 + 使用方 + 最近变更 |
| Tool | explain_table | 查表结构说明、分片状态、慢查询告警史 |
| Tool | find_owner | 按文件路径查当前 Owner 与最近 3 位修改者 |
lookup_field 的实现很朴素 —— 数据源是一份工程师协作维护的 YAML:
# .data/field-dictionary.yaml(每个 package 的 Owner 负责维护自己那份)
location_score:
cn: 库位推荐分
definition: 推荐该库位的综合得分,0-100,越高越优先推荐
services:
picking: 取值范围 0-100,每 15 分钟重算,公式见 src/picking/score.ts
putaway: 取值范围 0-1,**与 picking 口径不一致**,这是历史遗留
report: 只读,等于 picking 口径
pitfall: |
putaway 服务的 location_score 是 0-1,三者不可直接比较。
跨服务传递时必须显式换算,历史上出过 3 次由此导致的上架位置错误。
owner: "@wang.lei"
updated: 2026-05-14
5.3 效果:告诉模型「口径不一致」这件事,比什么都值钱
同样是「给库位推荐加个 score 阈值过滤」,有没有 MCP 的差别:
| 无 MCP | 有 MCP | |
|---|---|---|
| 是否发现三个服务口径不同 | ❌ 不知道有这回事 | ✅ 主动提示 |
| 生成的代码 | 直接拿 picking 的 0-100 阈值去比 putaway 的字段 | 显式换算并加注释 |
| 上线后 | 上架位置错误,回滚 | 无事故 |
| 对话轮次 | 2 轮 | 2 轮 |
最后一行的轮次一样 —— MCP 不省时间,它省的是事故。
5.4 第五、六周产出
| 指标 | Week 4 | Week 6 |
|---|---|---|
| AI 生成代码里「业务口径错误」类缺陷 | 每 100 PR 有 7.3 个 | 每 100 PR 有 1.1 个 |
| 新人独立完成陌生模块改动的比例 | 23% | 64% |
| 投入 | — | 8 人日(Server 3 天 + 数据整理 5 天) |
数据整理(5 人日)比 Server 开发(3 人日)贵。这部分没有捷径,让各 Owner 自己填。
六、六周总账
6.1 核心指标
| 指标 | Week 0 | Week 6 | 变化 |
|---|---|---|---|
| PR 提交→合并中位 | 4.2 天 | 1.6 天 | -61.9% |
| PR review 往返均值 | 3.1 次 | 1.3 次 | -58.1% |
| 单测覆盖率 | 41% | 67% | +26pp |
| AI 代码「几乎不用改」 | 7% | 31% | +24pp |
| AI 代码「几乎不用改 + 小改」 | 26% | 86% | +60pp |
| 跨文件漏改引发的返工 | 每周 4.1 次 | 每周 0.3 次 | -92.7% |
| 敢动库存核心模块的人数 | 2 | 7 | +5 |
| 月模型支出 | ¥0(未启用) | ¥3,180 | 人均 ¥106 |
6.2 投入
| 阶段 | 人日 | 占 |
|---|---|---|
| 类型基线清理 | 9 | 39% |
| Rules 固化 | 4 | 17% |
| 自建代理 | 2 | 9% |
| MCP + 数据整理 | 8 | 35% |
| 合计 | 23 |
23 人日,14 人团队,摊到每人 1.6 天。 换来的效能提升按月算是持续的。
6.3 最反直觉的一条数据
补瞧这两个数字:
- 类型基线(Week 1-2):投入 9 人日,一次通过率 23% → 61%
- MCP 知识层(Week 5-6):投入 8 人日,业务口径缺陷 7.3 → 1.1 /100PR
两者投入差不多。但如果不先做类型,MCP 的价值发挥不出来 —— 模型连 fn(a, b) 的 a、b 是什么都不知道,
拿到 location_score 的口径定义也不知道该往哪用。
顺序比力度重要。 先解决「能不能读懂」,再解决「懂不懂业务」。
七、12 条踩坑清单
| # | 坑 | 现场 | 解法 |
|---|---|---|---|
| 1 | 一次性开 strict | 8,400 个报错,团队当场放弃 | 按包渐进,noImplicitAny 单独先开 |
| 2 | 允许 AI 用 any 兜底 | 表面解决了报错,实际把不确定藏更深 | 强制 unknown + TODO-TYPE 显式暴露 |
| 3 | AI 编造看似合理的类型 | 编了个 `OrderStatus = "1" | "2"`,实际是 number |
| 4 | Rules 全设 alwaysApply | system prompt 4,100 token,约束命中率反而降到 63% | 只有 3 条 Always,其余用 globs |
| 5 | Rules 写在根目录 .cursorrules | Cursor 0.48 已不推荐 | 迁到 .cursor/rules/*.mdc |
| 6 | 只写禁止规则,不管绕过 | AI 新建 core-v2/ 绕过禁止清单 | 明写「不许绕过去改等价位置」 |
| 7 | 让 AI 自查时允许写「已全部核对」 | 得到虚假的安全感 | 强制给数字(「核对了 17 个调用点」) |
| 8 | 走 Cursor 直连自定义模型 | 晚高峰 429 中断率 18.4% | 自建代理做重试,降到 0.9% |
| 9 | 代理没关 gzip 缓冲 | SSE 流式输出卡住 | Nginx proxy_buffering off |
| 10 | 代理只重试 429 不重试 5xx | 上游 502 时同样会断 | 429 与 5xx 都重试,401/402/400 不重试 |
| 11 | MCP 数据层由一个人统一维护 | 两周就过期了 | 拆分所有权,每个 package Owner 管自己的 YAML |
| 12 | 以为 MCP 会省时间 | 轮次完全没少 | 正确预期:MCP 省的是事故与返工,不是时间 |
八、三条经验
8.1 「AI 效果不好」绝大多数是信息问题,不是模型问题
六周里我们一次都没换模型 —— 一直是 deepseek-chat 那一档。
一次通过率从 23% 到 58%,全部来自补信息。下次再有人说「换个更强的模型试试」,
先把这三个数字摆出来:91% 的抱怨落在信息/流程,9% 落在模型。
8.2 顺序:类型 → 约定 → 稳定 → 知识
这四个前置条件的依赖关系是严格的:
类型可读 → 约定可遵循 → 调用不中断 → 知识可用
↑ ↑ ↑ ↑
没有它 没有它 没有它 没有它
模型在猜 改了被 跑到一半 不知道业务
reviewer 打回 就断了 口径,上线就炸
跳过第一步直接做 MCP,等于给一个看不懂代码的人送字典。
8.3 上限靠模型,下限靠流程
这次最有说服力的数据是跨模型对照:同一套流程,在不同模型上的一次通过率
| 模型 | 一次通过率 | 月成本(同样用量) |
|---|---|---|
| DeepSeek Reasoner(推理档) | 61% | ¥9,400 |
| DeepSeek V4.1 Flash | 58% | ¥3,180 |
| Qwen3.8 Flash | 52% | ¥2,700 |
差 3 个百分点,成本差 3 倍。 流程把下限托住之后, 选模型就是在「要不要为 3pp 付费」之间做决策 —— 这才有讨论的意义。
九、可以做但不能做的事清单
值得抄过去的:
- 补完 850 处类型标注用了 2 天 —— 这是整个项目 ROI 最高的动作
30-no-touch.mdc禁止清单 —— 4 行配置省下的 review 时间超乎想象- 「不许编造类型,标 TODO 交回人工」这条 prompt 约束
- 代理层做 429 重试 —— 所有用自定义模型 + Agent 的场景都要做
我们这边成立、换环境不一定成立的:
- 14 人团队、严格 Eslint 规约、Google 式 review 文化 —— Rules 的收益依赖「本来就有规范」
- 字段字典 YAML 从零建了 387 条 —— 如果你们原本有数据字典/元数据平台,接过来比重建快得多
- 只用 deepseek 一家 —— 缺第二渠道兜底,这次是我们自己承担的风险
十、小结
遗留大仓接 AI 编码,最容易犯的错就是「先把工具铺开再说」。 铺工具是最省事的一步(刷卡就行),也是最没用的一步。
实际有效的顺序是反过来 —— 先把代码改到 AI 能读的状态:
Week 1-2 清理类型:一次通过率 23% → 61%(模型没换)
Week 3 固化约定:规范违反率 34% → 6%
Week 4 自建代理:任务中断率 18.4% → 0.9%
Week 5-6 接入知识:业务口径缺陷 7.3 → 1.1 /100PR
────────────────────────────────────────
总计 23 人日,PR 周期 4.2 天 → 1.6 天,月支出 ¥3,180
最后一行的 PR 周期,是全项目唯一没有被我们直接优化过的指标 —— 它是前三件事做完的副产品。
附录:四份可直接复制的配置文件
下面四份是本项目真正落到磁盘上的配置,脱敏后原样给出。按 §6.3 的顺序使用。
附录 A:渐进严格 tsconfig(第 1-2 周)
不要一次性开 strict。按这个顺序逐个包推进,
每个包单独一份 tsconfig,extends 根配置:
// packages/core/tsconfig.json —— 第 1 周只改这一个包试点
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
// 第一批开的四项:收益最大、连锁报错最少
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
// 第二批(第 2 周)再开:
// "strictPropertyInitialization": true,
// 全程不开(等整体迁完再说,这两个一开就是几千个错)
"strictNullChecks": false,
"useUnknownInCatchVariables": false,
// 必须开:第三方库的类型错误不参与本包门禁,否则会卡死
"skipLibCheck": true
}
}
配套一条 CI 门禁(对每个已签署「我不使用 implicit any」的包单独检查, 不通过就 block merge,防止回退):
# .gitlab-ci.yml(节选)
tsc-gate:
stage: test
script:
- for pkg in ${STRICT_PACKAGES}; do
echo "== checking $pkg ==";
npx tsc -p "$pkg/tsconfig.json" --noEmit || exit 1;
done
variables:
# 已经完成清理的包清单,每清完一个就往里加一个
STRICT_PACKAGES: "core user order picking putaway ui shared-utils api-gateway report admin auth billing inventory-common"
STRICT_PACKAGES 这个变量是整个渐进过程的进度条。每次有人清完一个包就 append,
从来没有「一次性大改」的压力。
附录 B:14 条 Rules 的索引与两份全文(第 3 周)
索引见 §3.2。这里给出两份收益最高的全文。
.cursor/rules/30-no-touch.mdc(alwaysApply)
---
description: 禁止 AI 修改的文件清单,改动前必读
globs: **/*
alwaysApply: true
---
# 禁止触碰
未经明确书面确认,禁止修改以下任何文件:
- `src/inventory/core/**` 库存核心算法,改错影响财务对账
- `src/db/migrations/**` 已上线迁移,禁止回填修改
- `**/*.lock` / `package-lock.json`
- `docker-compose.prod.yml`
- 任何含 `// DO-NOT-MODIFY` 注释的文件,及其被依赖关系
如果你的任务逻辑上必须改动上述文件之一:
输出 OPEN_QUESTION,说明 (1) 为什么必须改 (2) 改了会影响什么 (3) 你建议的替代方案。
**不要绕过这个约束去改它的「等价位置」**——
不新建 `core-v2/`、不复制一份到别处改、不改 import 路径绕过。
.cursor/rules/04-reuse.mdc(globs 限定)
---
description: 已有工具函数清单,禁止重复造轮子
globs: src/**/*.ts
alwaysApply: false
---
# 已有 util
## 日期 —— src/utils/date.ts
- `formatRange(start, end)` 时间区间展示
- `toISO8601(d)` 转 ISO
- `addBusinessDays(d, n)` 加工作日(跳过假期表)
## 金额 —— src/utils/money.ts
- `fenToYuan(fen)` 分转元,**所有对外展示必须过这里**
- `safeAdd(a, b)` 浮点安全加法,禁止 a + b 算金额
## 分页 —— src/utils/paginate.ts
- `paginate(list, { page, size })`
## 禁止写法
- ❌ `(total / 100).toFixed(2)` → fenToYuan
- ❌ `new Date(x).toLocaleDateString()` → formatRange
- ❌ 手写币种换算 → src/utils/currency.ts
附录 C:自建网关的完整 compose + Nginx 反代(第 4 周)
关键点有两个:RETRY_TIMES 必须 ≥ 3,Nginx 必须 proxy_buffering off(否则 SSE 卡死)。
# /opt/mcp-gateway/docker-compose.yml
version: "3.8"
services:
oneapi:
image: ghcr.io/one-api/one-api:latest
restart: unless-stopped
volumes:
- ./data:/data
environment:
TZ: Asia/Shanghai
RETRY_TIMES: "3" # ← 解决 Agent 中断的核心
CHANNEL_RETRY_INTERVAL: "1500"
# 首登即改密码 / 关闭自注册
networks: [gw]
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "13001:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks: [gw]
depends_on: [oneapi]
networks: { gw: {} }
# nginx.conf —— SSE 必须关掉 buffering
server {
listen 80;
location / {
proxy_pass http://oneapi:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off; # ← 不关流式输出会卡住
proxy_cache off;
proxy_read_timeout 300s; # Agent 长任务要放宽
chunked_transfer_encoding on;
gzip off; # ← gzip 也会导致 SSE 攒包
}
}
开发者侧配置(写在项目 README,所有人同一份):
// .vscode/mcp.json + ~/.cursor/mcp.json 通用写法
// Settings → Models
// OpenAI API Key: <个人 token,找网关管理员拿>
// Override OpenAI Base URL: https://gw.internal:13001/v1
// Model: deepseek-chat
附录 D:字段字典 YAML 与 MCP Server 骨架(第 5-6 周)
数据格式见 §5.2。这里给 Server 的最小骨架 —— 只有 3 个 Tool, 用 stdio 跑,Cursor / VS Code / Claude Code 都能挂:
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema, ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { readFileSync } from 'node:fs';
import { load } from 'js-yaml';
const dict = load(readFileSync(process.env.DICT_PATH!, 'utf8')) as Record<string, any>;
const server = new Server(
{ name: 'wms-metadata', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'lookup_field',
description: '按字段名或中文名查业务口径定义。返回定义、各服务差异、陷阱、Owner',
inputSchema: {
type: 'object',
properties: { keyword: { type: 'string', description: '字段名或中文名' } },
required: ['keyword'],
},
},
{
name: 'explain_table',
description: '查表结构说明、分片状态、慢查询告警史',
inputSchema: {
type: 'object',
properties: { table: { type: 'string' } },
required: ['table'],
},
},
{
name: 'find_owner',
description: '按文件路径查当前 Owner 与最近 3 位修改者',
inputSchema: {
type: 'object',
properties: { path: { type: 'string' } },
required: ['path'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
if (name === 'lookup_field') {
const kw = String((args as any).keyword);
const hits = Object.entries(dict).filter(([k, v]: any) =>
k.includes(kw) || String(v?.cn ?? '').includes(kw),
);
if (hits.length === 0) {
return {
content: [{ type: 'text',
text: `字段字典里没有「${kw}」。这可能是新字段、也可能是字典过期了。\n` +
`请向 @platform 提 ISSUE 补录 —— 不要凭猜推断字段口径。` }],
isError: true,
};
}
return { content: [{ type: 'text', text: hits.map(([k, v]: any) =>
`## ${k}(${v.cn})\n${v.definition}\n\n各服务:\n${v.services}\n\n` +
`⚠️ 陷阱:${v.pitfall ?? '无'}\nOwner: ${v.owner} 更新: ${v.updated}`
).join('\n\n---\n\n') }] };
}
// explain_table / find_owner 同理
throw new Error(`unknown tool: ${name}`);
});
await server.connect(new StdioServerTransport());
最后那个 isError: true 分支很重要 —— 字典查不到时必须明确报错,
而不是返回一个「可能是 XX」的猜测。我们相信猜测的成本远高于承认不知道。
相关阅读
- DeepSeek V4.1 Flash 多模型路由降本实战 —— 用量起来之后的下一步
- 多文件重构锚点提示法 —— 本文里提到的漏改治理
- MCP Roots 与 Sampling 实战 —— 本文 MCP Server 的下一步演进
- DeepSeek V4.1 Flash 接入 Cursor 完整指南 —— 工具层的完整配置