AI 写代码总在重复造轮子我们把 Utils 复用门禁做进了 Agent 运行时已开源让 Cursor / Claude Code 一类 Agent 在前端项目里实现功能你大概率遇到过这种场面任务描述很清晰Agent 也很「勤快」——Read 了几个文件然后开始 Write。等 diff 出来一看业务目录里又多了一套和公共utils几乎一样的 regex / DOM 解析 / 序列化逻辑。更气人的是它往往读过工具目录甚至搜到过某个xxxUtils.ts最后却用「展示文案不一样」「组件要瘦」「这个文件里还有很多别的 export」之类的理由静默 fork了一份实现——你 Review 的时候那种「这玩意儿 utils 里不是有吗」的既视感懂的都懂。前段时间 Cursor 帮我改一个列表页。它先 Read 了整个src/utils我心想这次应该会复用了吧。结果几秒钟后它还是在业务目录里写了一份几乎一样的降序排序函数。更离谱的是它读过sortArray.ts里的sortDesc。那一刻我意识到问题往往不是 Agent找不到而是它从来没有在 chat 里证明「为什么不用已有 util」——Read 了不等于判过了搜到了也不等于可以 reuse。作为长期用 AI 写业务的前端我的体会是模型会不会写代码是一回事它能不能先证明「现有 util 能接」再动手对维护成本影响更大。重复实现分叉起来修 bug 真的修两遍别问我怎么知道的。这套方案已开源agent-utils-reuse。下文讲我为什么这样设计急着试用的可直接跳 §九、快速上手。文中主线例子只用sortAsc/sortDesc/sortArray.ts来自 examples/minimal。其它 util 场景upload、URL 解析等判定逻辑相同不再另开例子。阅读导航按需跳转只想知道解决什么问题→ §一 · §二 Demo · §八 结语想了解设计→ §三 · §四 流程与铁律 · §六 五问与 Verdict准备接入→ §九 快速上手索引 / Hook 实现细节→ 见 GitHub README 与placement-decision.md§五、§七 仅概述一、引言读过 utils还是在 fork上面那个排序函数的故事不是孤例。把「复用 utils」当成一次技术选型我见过的翻车大致有这些发现阶段全库乱 Grep、凭记忆猜路径漏候选或误选同名符号判断阶段只看索引一行摘要就 reject/reuse和源码行为对不上执行阶段展示层用词不同就复制实现两套逻辑慢慢漂移扩展阶段改已有 export 的默认语义去「凑合用」隐性坑旧调用方时序阶段Read 了 util 就认为「门禁过了」无 Confirm 直接 Write多写几句 README、提醒 Agent「要复用」往往拦不住这些——因为缺少可审计的证明链Agent 可以在 chat 里含糊其辞你 Review 时没法核对它到底有没有认真比过源码。所以我做了Discovery → Confirm → Verdict → Write这条管道并用 Rules 可选 Hook 生成索引固化。下面先用一条完整需求把效果摆出来再拆设计。二、完整 Demo一条需求走到底先看最终效果。下面再拆解它为什么会这样工作。先别管名词。假设项目已initgen:utils-book你在 Cursor 里丢一句你列表页有个number[]展示前要降序排好别改原数组。用项目里的公共 utils别在组件里再写一遍排序。没上门禁时Agent 可能 Read 了src/utils然后还是在ListPage.vue里 WritefunctionsortDescLocal(nums:number[]){return[...nums].sort((a,b)b-a)}你sortArray.ts里那个sortDesc是摆设吗上了门禁之后同一条需求Agent先聊天、后 Write——三拍Discovery → Confirm → Write。Discovery— KV 检索是Agent 在会话里做的不是让你去终端敲命令D1 search 数组 降序: sortDesc src/utils/array/sortArray.ts:11 — 数字数组降序排序返回新数组 siblings src/utils/array/sortArray.ts: sortAscConfirm Verdict— Write 之前出现在 chat此前 Agent 已 ReadsortArray.ts源码见 §四 铁律 2| Symbol | Read path | Q4替换 sibling 拒选 | Verdict | | sortDesc | array/sortArray.ts | 降序、返回新数组reject sortAsc升序不符 | reuse(sortDesc) | Verdict最终: reuse(sortDesc)Write— 业务文件里一行 import没有sortDescLocalimport{sortDesc}from/utils/array/sortArrayconstdisplayListcomputed(()sortDesc(props.rawScores))Cursor_Agent你Cursor_Agent你number[] 降序别改原数组D1 KV检索 sortDescRead sortArray.tsConfirm Verdict reuse sortDescWrite import sortDescChat 留痕Review 省口水。其它 VerdictnoUtil / newUtil / featureLocal见 §六。三、问题从哪来为什么 README 拦不住§一 里那些翻车根因可以收成一句话Agent 缺的不是文档而是「证明我已经比过源码」的固定步骤。「我们在 README 里写了要复用 utils」——Agent 会话一长还是会忘就算记得也可以只 Read 不证明你 Review 时无从核对。口头约定 vs 这套门禁差别在这里维度口头约定 / 多写 READMEagent-utils-reuse找候选自由 Grep 全库Agent D1KVsearch/ Grep index零候选 → D2 GreputilsDir是否 reuse摘要 / 直觉五问 Confirm必须 Read util源码 export细小差异Agent 自行脑补问用户展示层未写明时见 §四 铁律 3结论模糊chat 里Verdict最终六种见 §六Write 前证明可有可无必须输出 Confirm Verdict可选 Hook 硬拦所以我没有再写一份更长的「复用规范 README」而是把Discovery → Confirm → Verdict写进 Agent 运行时。下一节说这条管道长什么样。四、我设计的流程Discovery → Confirm → Verdict核心不是「再多一份 Markdown」而是给 Agent 一条可审计的决策管道。默认同一条 assistant 回复里先 Confirm再 Write单轮六步┌────────────────────────────────────────────────────────────────────┐ │ 1 Analyze — Read AGENTS.md、业务代码与现有 import │ └───────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────────▼────────────────────────────────────┐ │ 2 Discovery — Agent 做 KV 检索粗筛不构成 reuse 证明 │ │ D1: search / Grep utils-index.json │ │ D1 零候选 → D2: Grep/SemanticSearch utilsDir │ │ ※ Agent 禁止 Read utils-book/*.md 做 Shortlist │ └───────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────────▼────────────────────────────────────┐ │ 3 Identify — symbol path 拟写/保留的 feature helper │ └───────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────────▼────────────────────────────────────┐ │ 4 Read — Read 将调用的 util export 源码同文件 sibling 须核对 │ └───────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────────▼────────────────────────────────────┐ │ 5 Confirm — 五问 Verdict最终展示层未写明 → AskQuestion │ └───────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────────▼────────────────────────────────────┐ │ 6 Implement — Write / StrReplace │ └────────────────────────────────────────────────────────────────────┘三条铁律全文只在这里展开一次后文若写「见 §四 铁律 N」指的就是下面三条——不再重复论证。铁律 1 — Discovery ≠ reuse索引和 KV 检索只做「找候选」。命中sortDesc一行摘要不能 Direct Write。铁律 2 — Read util ≠ ConfirmRead 源码是必要步骤但不等于reuse。必须在用户可见 chat里输出五问 Verdict最终见 §二 Demo。铁律 3 — 展示层差异 alone 不能 silent forkQ2 不看 UI 文案。逻辑对齐、仅芯片/placeholder 用词不同时reuse 或 AskQuestion禁止静默复制实现。另有一条工程约束No extend— 必须改已有 export 默认语义才能用时走newUtil新符号不污染旧 API。五、索引KV 给 Agentbook 给人v0.3.0 起Agent 的 Discovery 从「读工具书 Markdown」改成了KV 检索——Agent 用utils-index.json做倒排人类仍用utils-book/浏览全貌。Agent禁止Read book 做 Shortlist见 §四。utils-bookJSDoc 写在 export 上pnpm gen:utils-book扫描utilsDir同时生成 KV 与人类 book/** utils-book 数字数组降序排序返回新数组 */exportfunctionsortDesc(nums:number[]):number[]{...}src/utils/**/*.ts → gen → utils-index.jsonAgent utils-book/*.md人类utils-index.json按 symbol 存 path、summary、searchText、siblingsByPath等——完整示例见 examples/minimal/…/utils-index.json。Agent 做 D1 时记住三点即可summary 仅 Discovery铁律 1不能当 reuse 证明siblingsByPath→ Confirm 里须reject sortAsc或说明为何选sortDescambiguous同名多文件→ 必须 Read 具体 pathD1 在 chat 里的形态同 §二0 命中 → Agent 必须 D2 Grepsrc/utils。人类自测关键词、CI 维护索引时才需要自己跑search——日常提需求KV 检索交给 Agent。utils-book/标HUMAN ONLY适合 Code Review 扫sortAsc/sortDesc表能不能 reuse仍回源码 五问。历史 util 缺utils-book导致中文 D1 零命中 → 按 README BACKFILL 补 JSDoc 再gen索引信息不足不是门禁 bug。六、判决五问与 Verdict索引帮你Discovery铁律 1。真正的 reuse 证明是五问 ConfirmVerdict最终铁律 2。#问题要点Q1输入契约一致类型、空值语义Q2输出 / 存储 / API 一致不含UI 文案铁律 3Q3副作用可接受DOM / storage / APIQ4util(x)≡ 拟写的f(x)须写 sibling 拒选如 reject sortAscQ5必须改已有 export 内部是 →newUtil否 → 倾向reuseQ1–Q4 通过且 Q5否 →reuse(sortDesc)即 §二 Demo。Bulk Confirm一次 import 多个 symbol 时chat 里用压缩表≥3 个 symbol单轮 reuse 建议 ≤5 个。§二 的表就是单行 Bulk 的形态。常见误 reject别静默 fork「展示文案不一样」→ Q2 不看展示层reuse 或问用户「类里还有很多 export」→ 只 import 需要的sortDesc「需求只是子集」→ reuse 子集 API「组件要瘦」→ UI 留 feature纯函数仍走五问。Verdict 六种reuse 之外各一句Verdict什么时候用reuse(sym)五问过直接 import — §二sortDescpartialReuse featureLocal(wrapper)util 干核心 IO页面只差薄包装newUtil(name)要跨页共享但现有 export 接不住或 Q5是noUtil(kw)D1D2 确认 utils 里没有可 import 对象featureLocal(reason)强绑本页 UI/DOM不宜抽 utilsfeatureLocal placement debt从别的 feature 抄了纯函数注明日后该抽noUtil ≠ featureLocalnewUtil ≠ featureLocal。必须改旧 export 语义时newUtil别 extend 旧 API。五问之外什么时候 AskQuestion当 Q1–Q4 已过、仅 UI 文案与需求不一致且需求没写明时Write 前 AskQuestion铁律 3。例如 util 芯片显示「图片」、稿子是「参考图」——逻辑可 reuse展示层须用户拍板A) reuse 接受现状B) 页面包装或 newUtil。不要替产品 silent fork。七、怎么落地概述init一键写入AGENTS.md、Cursor Rules、placement-decision.md、可选 Hook、gen/searchCLI。工程上还有不少细节本文不展开感兴趣直接看仓库hookMode默认offRules 软约束 Confirm测 compliance 须合并.utils-bookrc.compliance.json开confirm见 production-rollout §验收模式分层 gate只改 template 不必重出整表uiOnly / delta / newCall / sameSymbol / fulltranscript 回读压测发现 preToolUse payload 常无 assistant 文本v0.3.18验收pnpm test:hooks .agent-utils-reuse 仓库内传入业务项目根、status/verify-index细则 SSOTREADME.zh-CN ·placement-decision.md·utils-reuse-gate.mdc自己从零搭先五问 Write 前 Verdict最小闭环再gen:utils-bookutils-book不必等「完美知识库」才上门禁。八、压测踩坑与结语第一次开hookMode: confirm压测几乎把所有 Write 都拦了——合规 Agent 也中招。查下来发现 preToolUse payload 里常常没有 assistant 文本只有transcript_path。v0.3.18 加了 transcript 回读误拦verdict_not_recorded才少下来。还有一次Agent 因为芯片写「参考图」、util 写「图片」就直接 reject reuse——其实 Q2 不该看 UI铁律 3。后来改成问用户别 silent fork。其它踩坑各记一笔读了 util 仍复制逻辑 → 缺 Verdict铁律 2只改 template 仍被要求整表 Confirm → 分层 gatev0.3.22中文 D1 零命中 → 历史 util 缺utils-bookBACKFILL 后 regen如果你也在和 Cursor / Claude Code 的「重复造轮子」较劲希望这套方案能帮你少 Review 几次重复代码。做下来我的感受是Agent Coding 越来越像工程问题——找得到、证明得了、别静默分叉——靠流程和索引不是再多一篇 README。规范面前复用有据这种工程直觉我觉得挺重要我自己也还在练。如果它真的帮到了你欢迎 点个 Star ⭐ 或 Issue 一起聊更好的实践。谢谢各位观众姥爷 Star 又不花钱对不对。九、快速上手仓库agent-utils-reuse。在业务项目根含package.jsonpnpmadd-Dgithub:qianfan-cmd/agent-utils-reuse#v0.3.23nodenode_modules/agent-utils-reuse/bin/cli.mjs init--forcepnpmgen:utils-book文档README.zh-CN · changelog示例索引/bookexamples/minimal/docs/agent-catalog/BACKFILL / 卸载 / hookMode见 README本文不展开升级门禁pnpm upgrade:utils-reuse在声明了依赖的子包目录执行。