基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-03 20:14

重复代码检测:语法指纹与向量为何意见相左

dev.to · AI#代码质量#向量模型#静态分析
Editor brief · 编辑速览

作者对比 TypeScript AST 结构指纹与本地嵌入模型的重复代码检测结果,用 14 个示例函数展示两种方法的判断分歧。配套独立脚本已公开,运行需要 Bun,向量部分使用 Ollama 和 nomic-embed-text。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

同一段业务逻辑,改个变量名,静态扫描很容易抓到;把循环改成 reduce,它可能就认不出来。Embedding 模型能补上这个缺口,却也会把「都在创建客户端」误判成重复代码,甚至忽略一个会改变调用结果的分支。

作者用 14 个 TypeScript 示例函数重建了两套检测脚本。结论是:两者适合一起找候选,合并之前还得验证行为。对准备清理工具函数、治理 AI 编程产生的重复实现的团队,这套分工比寻找一个万能相似度阈值更有参考价值。

静态指纹抓的是代码形状,丢掉的信息也会制造误报

作者团队早就有静态检查,但几个月没人运行。到 10 月底全仓扫描时,发现了 118 组跨文件或模块的重复代码,其中 HTML 转义函数曾有六份。常见原因很朴素:开发者没找到已有实现,又写了一遍。

脚本通过 TypeScript 编译器找出函数、方法和箭头函数,只处理函数体至少 120 个字符的情况。作者认为,更短的函数很容易撞形状,通常不值得提取;这是他们的取舍,并非通用标准。

接下来,脚本抹掉名称和值,再把规范化函数体与参数数量组成指纹。这段代码展示了原始实现:

s = s.replace(/"[^"]*"/g, "#");
s = s.replace(/'[^']*'/g, "#");
s = s.replace(/`[^`]*`/g, "#");
s = s.replace(/\b\d+(\.\d+)?n?\b/g, "#");
s = s.replace(/\b(true|false|null|undefined)\b/g, "#");
s = s.replace(/\b[a-zA-Z_$][\w$]*\b/g, (m) => (KEYWORDS.has(m) ? m : "_"));

要留意前三行:它们用正则识别字符串,没有真正遵守 TypeScript 的词法边界。

指纹相同就进入精确分组;可选的模糊比较则按 70% 的规范化函数体编辑距离、30% 的标识符重叠度计算。运行 bun ast-duplicates.ts samples --fuzzy 0.8,示例得到 3 组结果。

enableAccount 与 disableAccount 只差布尔值和审计字符串,因此得分 1.00。作者团队确实遇到过这种重复,最后用一个接收状态标志的工厂收拢实现。精确匹配没有阈值需要调,同样输入总会得到同样结果,适合持续检查。

问题也来自信息丢失。isFieldRow 检查表单对象的两个字段,isInstantLike 检查时间对象的两个字段,抹掉字段名和字符串后,它们同样得分 1.00,但没有合理的合并理由。

反过来,orderTotal 用循环计算折后总价,invoiceTotal 用 reduce 完成同样计算,静态检查连模糊阶段都没抓到。

代码形状相同,不保证职责相同;形状不同,也不保证逻辑不同。

作者还发现了检测器自己的缺陷:escapeHtml 与 escapeXml 仅在 ' 和 ' 上不同,原本应归入精确组,却只得到 0.87。原因是正则字面量 /"/g 里的引号,被字符串替换规则错误地当成了字符串边界。

生产实现随后改用已解析语法树的 token:字符串、模板和 JSX 文本替换为 #,保留正则字面量,去掉注释。第一次运行就多找出 12 组跨文件重复,大部分此前被注释差异遮住。示例脚本保留旧版本,是为了展示这个漏洞。

Embedding 能找到另一种写法,却不能证明行为一致

第二套脚本通过本地 Ollama 调用 nomic-embed-text,去掉函数注释、压缩空白,再比较向量的余弦相似度。请求中的关键内容是 body: JSON.stringify({ model: MODEL, input: search_document: ${text} }),其中 search_document: 是模型要求的文档前缀。

运行 bun embedding-duplicates.ts samples --threshold 0.8,会得到 5 对候选:

函数对 相似度
账户启用与停用 0.9881
HTML 与 XML 转义 0.9312
两个相关系数实现 0.9304
订单与发票总价 0.8261
Redis 与数据库连接工厂 0.8084

它补上了循环与 reduce 之间的缺口。误导静态检查的两个类型守卫得分约 0.78,也暂时落在阈值下面。

但这个间隔很窄。使用 --threshold 0 查看完整分布时,毫不相关的 slugify 和 parseDuration 仍有 0.67;真正的总价重复只比类型守卫高约 0.05。采用脚本默认阈值 0.9,又会漏掉总价这一对,只留下得分 0.93 及以上的三对。分数还依赖模型及其版本,原文没有给出具体版本。

连接工厂暴露了另一种误报:两者都读取环境变量、检查 URL、解析数字、创建客户端,但一个返回带监听连接的 Postgres pool,另一个返回带错误处理器的 Redis client,没有适合提取的共同实现。作者真实仓库里的三个数据库连接工厂甚至出现过 0.9961 和 1.0;七个查询也曾因为共享 JSON 列名和表名聚在一起。

更危险的是相关系数。pearsonCorrelation 与 correlation 使用相同公式,相似度高达 0.9304,但遇到零方差序列时,一个返回 0,另一个返回 NaN。作者保留了测试里的版本,因为调用方的容差检查依赖 NaN。另有一对函数也没有合并:其中一个会排序输入,后续步骤依赖这个顺序。

语义接近,离行为等价还有一段距离。

两套结果的交集用来排序,差集用来决定怎么查

bun combine.ts samples 将候选分成三类:两套都报、只有静态检查报、只有 embedding 报。前者优先审查;静态独报时检查职责是否一致;embedding 独报时重点验证行为。

作者用 check-equivalence.ts 给函数喂入相同输入,展示了验证能发现什么:

$ bun check-equivalence.ts
correlation [1,2,3,4] [2,4,6,8]: stats 1, reports 1
correlation [1,2,3,4] [4,3,2,1]: stats -1, reports -1
correlation [5,5,5,5] [1,2,3,4]: stats 0, reports NaN
total [{"price":19.99,"count":3}]: orders 59.97, invoices 59.97
total [{"price":19.99,"count":3,"rebate":15},{"price":4.5,"count":3}]: orders 64.47, invoices 64.47
total []: orders 0, invoices 0

关键是第三行:前两组正常输入完全一致,零方差输入才暴露差异。总价函数通过三个示例,只能说明值得尝试合并,不能证明所有输入都等价。连接工厂连返回类型都不同,因此没有进入这一步。

持续治理则靠基线。两套检查共享有意保留重复的忽略列表,例如实现同一接口的插件;静态检查还支持 // guard:dup-ok。生产流程在合并 PR 的每轮审查后运行静态检查,已知组通过,新增组进入审查队列,基线只降不升。作者报告的序列是 49 → 22 → 3 → 0;原文没有解释它与最初 118 组的统计口径关系。

示例提供 --baseline,发现新组时以退出码 1 结束,可接入 CI。Embedding 因为需要 Ollama,仍在清理前手动运行。

真实效果也没有示例那么整齐:一次清理中,0.90 至 0.95 区间有 77 个簇,只有 9 个是真重复,23 个部分重复,13 个已在清单里,32 个完全不是重复。作者尚未解决约 0.8 至 0.95 区间真假混杂的问题,考虑加入签名和返回类型、让语言模型复审,以及按参数类型自动生成等价性测试输入。这些都是待验证方向。

我的判断

我认同把静态检查放进日常流程,把 embedding 留作周期性补充。前者便于复现和追踪新增问题,后者帮助发现独立写出的相似逻辑。它们解决的盲区不同,分开承担职责比较合理。

但这仍是一份工程经验报告。14 个示例便于解释失败机制,不能代表检测准确率;77 个簇的人工分类更接近真实现场,却没有与其他克隆检测工具、代码专用 embedding 模型做对照,也没有清理耗时数据,无法据此判断投入产出。

相较于把相似度阈值直接设成合并门禁,这套流程保留人工审查更稳妥。不过,两套都命中也不等于可以直接提取:HTML 与 XML 转义的替换值仍有区别,需要保留上下文要求。

检测器负责提供线索,是否共享实现,要由行为契约和调用方需求决定。

能拿来做什么

  • 在 TypeScript 或 Node 仓库先扫描一次重复候选,建立存量基线,再把新增组送入审查,避免第一次接入就要求清空所有历史问题。
  • 编写规范化脚本时使用 TypeScript 的 token 信息,覆盖正则字面量、模板、JSX 文本和注释,别让检测器自己的文本替换制造漏报。
  • 对循环改成 reduce 等疑似重复,补上空输入、零值、异常和输入顺序检查;返回值之外,也要检查是否修改入参及产生副作用。
  • 审查 AI 新生成的工具函数时,先检索现有实现;需要用 embedding 扩大搜索范围时,结合签名、返回类型与调用位置判断,不把高分直接当成合并依据。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
检索鉴权不能漏掉缓存和关键词分支
下一篇
本地编程 Agent 的提示注入防线怎么建

资讯速览

dev.to · AI · 2026-11-03 20:14

AI 评分 8.0 / 10

相关标签

#代码质量#向量模型#静态分析

继续阅读

  • MCP 多语言 SDK 暴露请求体耗尽内存风险
  • 检索鉴权不能漏掉缓存和关键词分支
  • 本地编程 Agent 的提示注入防线怎么建
  • SaaS 模型选型:同时测质量、成本与稳定性
  • EGM 实测:设计审查收益未转化为代码提升