Node.js 视觉审核:用结构化契约约束输出
文章以物流招聘图片审核为例,要求视觉模型返回版本化结构,再由应用代码校验、存储并转交复核。重点防范字段缺失、枚举漂移和模糊证据被误判,避免直接用模型文字决定候选人去留。
AI 深度解读
物流招聘里,一张照片能不能证明候选人穿了反光背心、工牌是否清晰,适合交给视觉模型辅助检查;但候选人能否进入下一轮,不该由模型的一段回答直接决定。作者提出的方案是:模型提交结构化观察,应用代码校验,再按明确规则分流。
这篇文章值得读的地方,是把一个看似简单的图片识别需求,拆成了可以测试、追溯和人工接管的工程流程。不过,它提供的是设计建议,没有给出识别准确率、成本或上线效果,也不是一份完整的 Node.js 接入教程。
为什么要先定义结果,再写提示词
作者担心的失败很隐蔽:模型回答读起来合理,却漏掉一个检查项,把 pass 写成 qualified,或者把看不清的工牌当作已经验证。系统如果直接解析这类自然语言,业务规则就会跟着模型措辞漂移。
因此,先明确模型可以回答什么。这个案例只检查防护装备和身份标签的可读性,不要求模型根据外貌推断年龄、健康、族裔或岗位适合程度。
结果需要包含版本、逐项结论、有长度限制的证据,以及复核去向。其中,unknown 必须和 pass、fail 平级:反光、遮挡、低分辨率或相关对象未出现在图中,都可能让证据不足。
下面的结构把输出限制在一个可验证的契约里。原文虽然以 Node.js 为题,示例实际使用 Python 和 Pydantic:
from typing import Literal
from pydantic import BaseModel, Field
Outcome = Literal["pass", "fail", "unknown"]
class CriterionResult(BaseModel):
criterion_id: str = Field(pattern=r"^[a-z0-9_]{1,40}$")
outcome: Outcome
evidence: str = Field(min_length=1, max_length=280)
confidence: float = Field(ge=0.0, le=1.0)
class ScreeningResult(BaseModel):
schema_version: Literal["screening.v1"]
decision: Literal["advance", "reject", "review"]
criteria: list[CriterionResult]
review_reason: str | None = Field(default=None, max_length=280)
重点是 Outcome 里的 unknown,以及固定的 screening.v1。前者让不确定性有正式出口,后者让后续升级能够追溯。confidence 被限制在 0.0 到 1.0,但原文没有证明这个数值经过校准,也没有提供可用于招聘决策的置信度阈值。
模型负责描述证据,应用负责执行规则。
即使模型填写了 decision,最终路线也要重新计算,不能直接照收。
JSON 能解析,为什么还不能进入业务流程
JSON.parse 成功,只能说明文本符合 JSON 语法。空的 criteria、数值为 1.2 的置信度,甚至凭空编造的证据,都可能出现在语法正确的 JSON 里。JSON Schema 和 RFC 8259 也不能替业务判断证据是否充分。
作者把校验分成两段:请求模型前,拦截格式错误或体积超限的输入;模型返回后,检查结构,再执行领域规则。不静默转换非法值,不把未知字段当作证据,也不用字符串操作补齐被截断的 JSON。
下面的函数解决的是必检项如何决定去向:
REQUIRED = {"ppe_vest", "id_label"}
def derive_decision(criteria: list[CriterionResult]) -> tuple[str, str | None]:
by_id = {item.criterion_id: item for item in criteria}
missing = REQUIRED - by_id.keys()
if missing:
return "review", "missing criteria: " + ", ".join(sorted(missing))
if any(by_id[key].outcome == "fail" for key in REQUIRED):
return "reject", None
if any(by_id[key].outcome == "unknown" for key in REQUIRED):
return "review", "visual evidence is inconclusive"
return "advance", None
要看清判断顺序:先查缺项,再查 fail,然后查 unknown,最后才允许 advance。因此,必检项齐全时,若一项失败、另一项未知,这段代码会返回 reject。
把规则放进代码后,测试可以直接检查路线:清晰背心、被遮挡的背心、不可读标签、非图片载荷分别会走到哪里,无须断言模型必须使用某一句话。传输或解析失败可以重试,但第二次回答并不天然更可信。
结构校验仍有边界。它能检查证据文字不超过 280 个字符,却不能证明文字确实来自图片;这部分还要靠样本评估和人工复核。
换模型之前,先把失败路径设计好
作者建议服务只暴露一个内部接口:输入图片引用和招聘规则版本,输出经过验证的 ScreeningResult。不同供应商的适配器负责对接 chat-completions endpoint、托管模型运行时或专用审核 API。
这里比较了三类选择:OpenAI 的视觉模型、Amazon Bedrock、Google Vertex AI。作者没有做效果排名,关注的是请求格式、输出约束、图片限制、区域控制、数据留存和失败行为。
每次部署都应记录实际模型家族、图片表示方式、最大请求大小、拒答结构,以及在 CI 中验证过的 schema 模式。必需能力不受支持时,应阻止启动;切换适配器前,用清晰图片、模糊图片、拒答、畸形输出和超时做契约测试。原文没有提供具体模型名、SDK 版本、端点或请求参数,无法据此直接完成 Node.js 接入。
失败后的分流同样重要。坏图片不是候选人不合格,超时也不是审核证据。队列应区分 invalid_input、model_timeout、schema_error、policy_unknown 和 human_review,让运营人员知道该要求重新提交、排查服务,还是处理业务疑问。
幂等性则要围绕候选人提交和规则版本建立。保存稳定的提交标识,计算规范化图片字节的哈希,结果键包含规则版本;同一键重试不能生成第二次招聘事件,规则改变后要建立新评估。
原图引用、规则版本、schema 版本、模型响应、校验错误和最终路线应进入受控证据库。普通日志只记录标识和有限错误分类,并观察 schema 失败、未知结果、复核耗时、重试量以及自动路线与人工裁定的分歧。
我的判断
我赞同它把不确定性和业务规则显式化。与「提示词写严一点,再解析模型回答」相比,这套方案让漏项、超时和规则变化都有了可检查的位置。原生结构化输出可以减少格式问题,但不能替代业务校验,更不能保证图片判断正确。
不过,文章是工程经验建议,没有给出对照实验,也没有提供实际运行数据,不能据此判断它比人工审核节省多少时间。
示例本身也需要补强。criteria 没有设置最少项目数;by_id 会让重复的 criterion_id 覆盖前值;上面的 Pydantic 声明也没有显式禁止额外字段。正文提出的严格契约,比示例代码实际落实的范围更大。
还有一个边界没有讲透:图片不可读与图片中确实缺少必需装备,应该怎样区分?原文把相关对象不在图中也列入 unknown,但没有给出足够细的判定规则。这需要写进招聘规则和人工裁定说明,不能留给模型自行理解。
函数能够返回 reject,不代表系统已经具备自动拒绝候选人的依据。
作者最后建议先运行影子模式,只生成评估、不改变候选人状态,再用固定样本和书面规则与受训审核员比较。先开放要求补交清晰图片这类低风险路线,拒绝仍经人工复核。这个上线边界,比示例函数本身更关键。
能拿来做什么
- 给 Node.js 图片审核服务先写输出契约,固定版本、枚举、证据长度和必检项,再接模型;把缺项、重复项和非法值分别处理清楚。
- 把模型观察和最终路线分开存储,由独立函数计算去向,并用遮挡、不可读标签、非图片输入等样本验证规则。
- 为异步 worker 建立包含规则版本的幂等键,让重试复用同一次评估,让规则升级保留独立记录。
- 上线前运行影子模式,分别统计缺字段率、未知率、复核负担和逐项分歧,再决定哪些路线可以自动执行。