十月四号在 dev.to 上刷到一个叫 OriginTrace 的项目,投的是 Sanity Challenge 的 Path One 赛道。作者的起点很朴素:他在 DEV.to 后台的流量来源里看到几个陌生站点,点进去发现自己的文章被整篇搬走了。这篇笔记不讲它的完整架构,只讲我圈出来的那一段——一个页面到底是「转载」还是「搬运」,它是怎么判的,以及为什么恰恰是这一段,它没让 LLM 参与。
先拆开看看整条扫描流程。用户把一篇 DEV.to 文章的链接贴进去,系统先从正文里提一批特征短语,拿这些短语走 Serper 的 Google 搜索能力去找疑似转载页;候选页最多抓 15 个,抓下来之后每一页都跟原文做多信号重叠分析。重叠引擎是三个信号加权:
wordOverlap 20% // 词重叠
5-gram shingle 50% // 5-gram 分片
LCS ratio 30% // 最长公共子序列占比
5-gram 占一半,LCS 占三成,最容易实现的词重叠反而只占两成。这个配比我第一眼看的时候有点意外,因为玩具脚本基本都停在词重叠那一步。但纯词重叠有个很明显的问题:正常引用你两句话,在词面上看起来跟抄袭是一回事。评论区也有人提了这条,说 5-gram 加 LCS 比单纯算词重叠靠得住。作者的说法是这样组合能扛住删减句子、替换同义词这类规避手段——具体哪个信号负责挡哪一种改写,我没自己跑样本验证过,这条先照记。
重叠率高只说明「相似」,说明不了「未署名」。所以作者在这里插了单独的一层:去候选页里找署名证据,页面提没提原作者姓名、有没有链回原文、有没有用转述来源的措辞。这些是布尔字段,跟重叠率是两个维度,不混在一起算分。存出来大概长这样:
{
"overlapPercent": 87.4,
"verdict": "unattributed_repost",
"attribution": {
"hasAuthorName": false,
"hasOriginalLink": false,
"hasAttributionPhrase": false,
"isProperlyAttributed": false,
"signals": [],
"missing": []
}
}
两个数组里具体放什么字符串,我没去翻源码确认,这里先空着。判定结果主要两类:credited_syndication(已注明来源的转载,不进 DMCA 流程)和 unattributed_repost(未署名搬运,才进 DMCA 通知生成)。这里有个我挺喜欢的设定——默认安全。只要页面沾上任何一个署名信号,就归到 credited_syndication 去,宁可漏判也不误判。整套署名判定用的是 Cheerio 解析 DOM 加正则,全程没让 LLM 做模糊分类。
这个选择我觉得是对的,也是这个项目里我最想抄的一段。判定环节的失败成本和生成环节完全不对称:生成一段描述写歪了,改一下重生成就行;把一个合法转载判成未署名搬运,后面跟着的是一封寄给人家主机商的 DMCA,收不回来。作者把「发送」留给自己,只让 agent 起草模板文本,也是同一套考虑。
但我也不能就这么叫好。作者自己在讨论里承认了:这套确定性解析会漏掉「感谢 @某某 分享」这类改写式署名,正则会漏。他打算 v2 用小模型做语义署名抽取,把这块补上。我一开始没想通——都要上模型了,为什么不干脆让模型直接出 verdict?
后来想明白,一个是加法,一个是减法。让模型从页面正文里多捞一条「这里有署名措辞」的信号出来,这是加法:捞到了就多一条证据,漏了顶多维持一个保守的原判,最坏结果是少发一封 DMCA。让模型直接出 verdict 是减法:它得在「发不发律师函」这件事上下结论,错了就是实打实发错一封。两个错的方向不对称,设计上就不该对称。所以我理解 v2 那个方向是让模型只做抽取、不下结论——不过这只是我从它的计划里读出来的,作者本人没这么写,也可能是我过度解读了。
顺便说一个我比较在意的缺口。作者讲得很直白:hackathon 时间限制,没搭已知转载和已知抄袭的留出数据集,也没算精度和召回;v2 列表里排第一位的就是攒一份约 500 条已知转载加 500 条已知抄袭的基准集。这一条我记下来了,因为它比前面任何一条都重要——没有基准集,那套 20/50/30 到底是不是比别的配比好,谁也说不清,包括作者自己。我自己也干过类似的事:几个权重调到最后全靠感觉,跑三五个样本看着像模像样就收手了,那种「我觉得准了」是最容易骗自己的状态。
还有一个跟主线同源的细节值得顺手记一下。一次完整扫描要 15 到 25 秒,瓶颈在 Serper 搜索和最多 15 个页面的抓取。所以作者没让 agent 每次去现场抓,而是抓一次就把结果落成结构化文档写进 Sanity Content Lake,后面所有查询直接走 GROQ。开新会话时,后端还会先跑一遍 fetchSanityContext() 预取文章和检查记录的快照,塞进 agent 的系统提示里,简单问题连一轮 MCP 往返都省掉了。Agent 本体通过 @ai-sdk/mcp 以 HTTP transport 接 Sanity Context MCP,跑在 NVIDIA NIM 的 Nemotron 3.5 Lightning 上:
const model = "nvidia/nemotron-3.5-lightning-30b-a3b";
// HTTP transport → Sanity Context MCP
这跟署名判定用确定性规则其实是同一件事的两面:都把「每次都靠现场推理」换成「先存下来、后面直接查或直接匹配」。省的不只是那十几秒。
划重点。第一,相似度和搬运是两件事,判相似用统计信号,判搬运用署名证据,中间插一层,别一步到位。第二,检测和判定的失败成本不对称,模型可以去找证据,别让它一个人下结论。第三,没有留出基准集的检测规则,等于没验证过。
想自己试的话,顺序别搞反:先去 origintrace.onrender.com 扫一篇你发过的 DEV.to 帖子,让知识库里有可查的结构化数据,再去 /chat 问它哪个副本重叠得最多。
