第 11 章

第 11 章:语义代码检查——Semantic code linting

第 11 章:语义代码检查——Semantic code linting

传统 linter 管格式、管语法、管已知的坏模式,但它管不了"这段命名跟它做的事不是一回事""这条日志把用户个人信息写进了明文""这里按注释的说法调用 API,其实是误用"。这些判断没有固定写法,只有语义。本章讲怎么把这类检查做成一层可运行的代码检查,以及为什么它更适合当顾问而不是法官。

正则和 AST 管不了的那部分

先分清两类代码检查。第一类是机械检查:缩进、命名规范、未使用变量、循环复杂度、已知危险函数调用。它们有确定的形式,用正则或 AST 就能判,跑得快、零误报、零成本。ESLint、golangci-lint、ruff 覆盖的就是这一层,别拿 Jev 去替代它们。

第二类是语义检查,它问的是"这段代码是不是表达了一个坏意思",而这种"坏"没有固定写法:

  • 命名与实现不符:一个叫 validate() 的函数其实在做数据清洗;一个叫 is_ready() 的方法有副作用。
  • 注释与实现不符:注释说"返回缓存的用户",代码却每次都打数据库。
  • API 误用:用了某个库的接口,但不符合它的调用约定(比如忘了关连接、参数顺序反了、把异步当同步用)。
  • 安全模式:这条日志把邮箱和手机号写进了明文;这个拼接留下了注入面。

这些判断用正则写不出来——只要换一种措辞,模式就失效了。而它们又恰好是代码评审里最花人力的部分。语义检查的价值,就是把这些"只能靠读"的判断,变成机器能批量问出来的问题。

语义检查适合当顾问,而不是法官

在 VS Code 里插一条检查,最怕的不是漏报,是误报。一个阻断性的检查一旦误报,开发者就会立刻把它关掉,往后它再准也没用了。所以在 CI 里,语义检查更合适的定位是 advisory(顾问)层:

  • 高置信的问题给明确指向(文件 + 行号 + 理由),让人一眼能判断;
  • 拿不准的标成 uncertain,而不是"没问题";
  • 只有超过阈值、且规则明确的才允许阻断构建,其余只做提示。

这不是保守,是对误报成本的清醒认识。代码检查的实际收益 = 抓到的问题 − 误报带来的信任损耗。当误报开始让团队反感,收益就转负了。

只查变更行,把成本摁住

一个仓库动辄几十万行,全量跑语义检查既慢又贵,也毫无必要——上一版已经通过检查的代码,没有理由重查一遍。真正的增量是这次 diff 改了哪些行。

于是管道变成:取 diff → 只对**变更的行(或变更的函数)**提问。这样成本随改动量增长,而不是随仓库大小增长,也顺带压低了误报的地毯式噪声。

粒度也要选:按行问定位最准,但同一段逻辑被拆成多行时会切碎上下文;按函数(或 hunk)问能带上更多上下文、减少请求数,代价是报告的位置不如行级精确。多数项目的折中是——先定位到变更的函数,再要求模型指出具体行号,让上下文和定位各取所需。

# 只取出本次改动新增/修改的行,连同文件与行号
git diff --unified=0 --no-color HEAD~1 -- '*.py' \
  | grep -E '^\+[^+]' | sed 's/^+//' > changed_lines.txt
def lint_hunk(file: str, hunk: str, rule: str, threshold: float = 0.7) -> list[dict]:
    d = client.systemone(
        state=hunk,
        questions={"violates": {"type": "noul", "question": rule}},
        instructions="只依据给出的代码片段判断;不确定时倾向判否,避免误报。",
    )
    p = d["violates"].probability
    if p >= threshold:
        return [{"file": file, "confidence": p, "rule": rule}]
    return []

注意 instructions 里那句"不确定时倾向判否"——这是人为把检查器的误报优先调成谨慎优先。顾问层的阈值可以更严,因为它的目标是"指出得对",不是"一个都别跑掉"。

实例一:semcheck——规则就是一句英文

arturobermejo/semcheck 是一个 Go 写的 linter,它的规则直接写成大白话问题,例如:

"does this log call write personal data?"(这条日志调用写了个人数据吗?)

对规则匹配到的每一段代码,它问 Jev 一个 Noul,超过该规则阈值就报告。它的两条内置规则在三个开源项目里的抽样结果里,12 条发现全部正确(12/12)。

这个项目示范了语义检查最该有的手感:规则是可读的句子,不是一段配置。写规则的人不需要会写正则,只需要能说清"我要退出的是哪种坏味道"。这也让规则容易评审——把规则摊开给团队看,哪条太宽、哪条太窄,一眼就能议。与之呼应的是 ckorhonen/jev-lint,它用 Jev Noul 加本地阈值,在 Claude Code 和 Codex 边改边标团队规则违规,并支持按仓库落不同的规则包。

实例二:JevGate 与 Perch——把 Noul / Choice / Score 组合成一条检查

单一 Noul 只能回答"有没有问题"。要给出可行动的检查结果,往往要把三种输出组合起来。

Tech-Byte-Frontier/jevgate 是一个 CI 与编码代理闸门:它先在本地解析代码,然后针对一个函数、一份文件大纲、一对候选复制的代码、或一个测试,问 Jev 的 Noul、Choice 和 Score。凡是置信度达到 0.80 的回答,就落成带文件和行号的 review 或 consider 发现;review 让构建失败,而未定的文件保持 uncertain 而不是被清成干净。这个"未定不等于干净"的处理很关键:检查器不该用一个含糊的默认值,把没把握的地方伪装成通过。

lakeday-org/perch 也是一个语义代码检查器,但它的上下文更宽:对每个方法,连同它的调用者和被调用者一起看,问一个 Noul(有没有 bug)、一个 Choice(是哪一类、在哪一行)、一个 Score(严重程度),再加语言相关的 CWE Noul 检查,以及用句子写在仓库 / 文件 / 方法三级上的自定义规则,任何超过下限的回答都让 CI 失败。

两个项目合起来说明了三种输出的分工:Noul 判"是不是有问题",Choice 判"是哪一类、在哪一行",Score 判"多严重"。检查结果因此不是一句"这里可疑",而是一条可排序、可定位、可解释的发现。

实例三:local-decider——规则先行,Jev 只兜底

pusucip25/local-decider 是一个浏览器代理的动作前闸门,它的设计几乎就是本章主张的范本:先跑一条确定性的规则链,只有在规则定不了的地方才问 Jev——换句话说,凡是规则能判的,永远不送到权重上去。它给出 allow / confirm / block 三种结果,并且只用一个环境变量就能在托管的 jev-latest 和本地 Jev 兼容端口之间切换。

它的实测数据也印证了"规则先行"的价值:在 112 个已标注用例上,规则本身就解决掉了 61 个,其中 58 个判断正确(95.1%),并且在不可逆工具上没有任何一次误放行(0 false allows)。也就是说,超过一半的判断根本不需要模型,而模型只负责那条规则覆盖不到的长尾。它还特意从 DOM 读取最终状态,而不是听信代理自己的汇报——验证要看事实,不看声明。

这个模式在语义代码检查里同样成立:命名规范、依赖白名单、文件路径这些能用规则判的先判;把"这段日志有没有写个人信息""这个函数名和它的行为符不符"这些留给 Jev。成本与误报都被规则先砍掉了一大截。

顺带一提:把检查扩展到内容产物

语义检查的对象不限于代码,一类同构的实践出现在内容产物上。klauswg 的 jev-suite 里,jev-proof 用每个事实一次 Noul+Choice 调用核验视频字幕里的赞助广告是否满足验收规则,置信度闸门由代码掌握,90 样本校准下门槛内准确率 0.922,0/15 注入翻转;jev-fidelity 对每个事实单元问 Jev 编辑是否保留了原意(保留了 / 等价 / 漂移 / 丢失),闸门设在 0.70,55 样本校准下 91/92 判断正确、0/20 注入翻转;jev-rental 把租房帖里的每条说法分进"到现场再验 / 需补证据 / 高风险话术"三桶,50 样本校准 0.910 门槛内准确率、0/10 注入翻转。三者都反复强调同一个数字——注入翻转为零,意思是:往待检查的文本里塞入诱导性指令,判定结果不会被带跑。 对安全检查来说,这个不变量比准确率更关键。jyatesdotdev/jev-logtriage 则把折叠后的 Loki 日志一次调用问出 Noul、Score、Choice,再把答案在代码里映射成"抑制 / 观察 / 复核 / 通知 / 呼叫",低置信一律走复核、且什么都不自动执行。

误报成本与阈值,是这类系统的核心参数

回顾整章,语义代码检查的工程重心其实不在模型,而在阈值与定位:

flowchart TD
    C["改动行"] --> R{"能用规则判?"}
    R -- 能 --> X["本地规则
零成本"] R -- 不能 --> J["Jev:Noul / Choice / Score"] J --> P{"置信度 >= 阈值?"} P -- 是且规则硬 --> B["阻断构建(review)"] P -- 是但规则软 --> A["顾问提示(consider)"] P -- 否 --> U["标为 uncertain
不伪装成干净"]

DanRWilloughby/snifftest 之类的检查器给出的数据很能说明误报的分量:它以 0.7 为阈值对每段文字问十个 Boolean 问题,中位数 182 ms,在 54 段干净文本里只误报了 1 段,而作为对照的某个小模型误报了 37 段。mblode/taste-lint 也采取同样策略:可度量的规则留在本地且始终生效,而语义上的"味道"判断用 Jev 概率,只有明确的发现才会让一次运行失败。这些数字共同指向一条经验——语义检查要能上线,靠的不是更高的召回,而是更低的误报和更清晰的定位。

小结

  • 语义检查补的是正则与 AST 管不了的那部分:命名与实现不符、注释与实现不符、API 误用、安全模式。
  • 它更适合当 CI 里的 advisory 层:高置信才阻断,拿不准标 uncertain 而不是"干净",因为误报会直接摧毁开发者对它的信任。
  • 只对 diff 的变更行提问,成本随改动量而非仓库规模增长。
  • semcheck 把规则写成大白话句子,两条内置规则在三个开源项目里 12/12 判断正确;jev-lint 边改边标。
  • JevGate 组合 Noul/Choice/Score,0.80 以上落成带行号的发现、review 让构建失败、未定保持 uncertain;Perch 连调用者/被调用者一起看。
  • local-decider 规则先行、Jev 只兜底:112 例中规则解决 61 例、正确 58 例(95.1%),不可逆工具 0 误放行;jev-suite 三项目在门槛内准确率 0.910–0.922,注入翻转均为 0。

在仓库里守住"代码有没有坏意思"之后,下一章我们把判定带进数据管道:怎么把非结构化文本变成能喂给预测模型的特征,又怎么用语义信号做需求预测。

📑 Jev 实战:用类型化决策重构软件

1 第 1 章:为什么需要决策层——从"生成一切"到"只做判定" 2 第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约 3 第 3 章:十个决策形态——什么时候该把判断交给 Jev 4 第 4 章:Classification 与 Routing——把分诊做成一等公民 5 第 5 章:Detection 与 Scoring——二元判断与有序评分 6 第 6 章:Search 与 Retrieval——在候选集里找相关 7 第 7 章:Ranking 与 Verification——排序与校验 8 第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据 9 第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs 10 第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails 11 第 11 章:语义代码检查——Semantic code linting 12 第 12 章:特征提取与需求预测——Feature extraction / Demand forecasting 13 第 13 章:招聘与线索生成——Recruiting / Lead generation 14 第 14 章:客户支持——Customer support 15 第 15 章:理赔、金融犯罪与风险评估——Insurance claims / Financial crime / Risk assessment 16 第 16 章:法律与合规——Legal and compliance 17 第 17 章:电商市场与广告——E-commerce marketplaces / Advertising 18 第 18 章:内容审核与信任安全——Moderation and trust and safety 19 第 19 章:游戏——Gaming 20 第 20 章:实时应用——150 毫秒预算 21 第 21 章:科学发现——Scientific discovery 22 第 22 章:大数据上的 AI Map Reduce——为什么便宜 100 倍 23 第 23 章:通用验证——Universal Verification 24 第 24 章:Harness Engineering——把决策织进智能体骨架
← 返回本书大纲