借鉴安全审计,补齐大模型评测盲区
作者把智能合约审计中验证禁忌行为的思路迁移到 LLM 评测,主张优先检查系统可能造成的严重失败。文章指出,正常问题集的平均分可能掩盖缺少针对性测试的危险行为。
AI 深度解读
LLM 功能演示得再顺,也可能在一次越权查询或工具误调用中出大问题。作者参加过二十多场智能合约审计竞赛,如今把同一种思路带到生产 AI 系统:先确定哪些事绝不能发生,再想办法让系统违反这些规则。
这套方法值得程序员借鉴,因为它改变了评估的优先级。回答是否流畅、平均分是否上涨仍然要看,但数据隔离、操作权限和数字来源,应该先成为能被代码检查的发布条件。
平均通过率为什么挡不住严重事故
作者参加过 Sherlock、Code4rena 和 Immunefi 等平台的审计竞赛。这类比赛只奖励能够证明的问题,训练出来的习惯是:团队已经相信代码能运行,审计者要追问的是,它会不会做出绝对不该发生的事。
LLM 演示通常沿着顺利的路径展开:用户提问清晰,检索命中文档,模型给出漂亮答案。评估集即使扩充成 50 道规范问题,再算一个平均分,也可能只是把演示重复了 50 次。
真正危险的结果往往很具体:编造数据源中不存在的数字,没有读取政策就回答政策问题,用户只是询问却触发了写操作,或者把一个客户的数据展示给另一个客户。
这些错误不能被其他回答的高分抵消。
作者用审计中的严重性分级解释这一点:Code4rena 会按严重程度区分发现的问题。修复了 40 个风格问题,却还留着一个能够转走资金的漏洞,不能叫作「97% 安全」。同样,LLM 的 97% 通过率也无法说明剩余 3% 是否包含编造付款金额这样的事故。
因此,他建议把评估分成两组:不可违反的规则作为发布门槛,出现一次失败就阻止变更;有用性、语气等质量指标则观察趋势,小幅下降先调查原因。
把「不能出错」写成代码能检查的规则
智能合约测试里有一个概念叫不变量(invariant),也就是无论经历什么操作序列,都必须成立的性质。例如,代币总供应量始终等于所有账户余额之和。Foundry 的不变量测试会生成随机调用序列,并在每一步检查规则。
迁移到 LLM 系统,关键是把愿望写成可执行的判断:
- 回答里的每个数字,都能在工具返回的数据中找到。
- 被归类为只读的请求,不能触发写工具。
- 登录用户收到的回答,只能包含属于自己的数据行。
- 关于政策的回答,必须引用对应文档。
作者给出的数字检查器,会找出回答中没有工具数据支撑的数值:
import re
NUMBER = re.compile(r"\d[\d,]*(?:\.\d+)?")
def numbers_in(text: str) -> set[float]:
return {float(n.replace(",", "")) for n in NUMBER.findall(text)}
def ungrounded(answer: str, tool_outputs: list[str]) -> set[float]:
"""Numbers in the answer that no tool call returned."""
grounded = set().union(*(numbers_in(out) for out in tool_outputs))
return numbers_in(answer) - grounded
tool_outputs = ['{"region": "north", "revenue": 84200.00}',
'{"region": "south", "revenue": 21700.00}']
answer = "North led with 84,200, ahead of south at 21,700. Total: 105,900."
print(ungrounded(answer, tool_outputs)) # {105900.0}
要看最后一行:105,900 确实是正确总和,检查器仍然把它标了出来,因为工具只返回了 84,200 和 21,700。
这是有意设置的严格限制。作者认为,重要的合计应由工具计算,再作为数据交给模型。模型算对一次,不能保证下一次也算对,而错误数字照样可能写得很有把握。
不过,这只是最小示例。作者明确提到,真实版本需要为日期、「top 5」等情况设置允许名单。规则是否适用,也取决于产品是否允许模型自行推导数字。
对抗输入和线上错误,都应该进入回归集
审计者默认调用者可能是攻击者。LLM 系统要把这个边界再向外推一层:用户是输入来源,检索到的文档、工具抓取的网页、其他服务的响应,同样是输入来源,也都可能夹带看起来像指令的文字。
原文援引 OWASP 将提示注入列为 LLM 应用首要风险,强调的正是这些间接入口。
评估集因此不能只有友好提问,还应包括查询他人账户、文档中隐藏指令、含糊的日期,以及表面无害却需要写工具才能完成的请求。否则,测出来的只是系统接待配合型用户的能力。
另一条纪律是:每个确认的线上错误,都要变成长期保留的评估用例。
模型版本更新、提示词调整、检索分块大小变化,都可能让上周通过的案例重新失败,相关代码差异却未必显眼。因此,作者主张每次变更都运行整套评估,包括那些「只改了提示词」的变更。
这里还存在确定性差异。合约相对确定,模型却可能前 9 次通过、第 10 次失败。重要案例需要重复运行并记录通过率;发布规则要求零容忍,也不代表一次通过就足以证明安全。
我的判断
我赞同把严重错误单独设门槛。对于能读业务数据、能调用工具的 AI 功能,一个综合分数确实容易掩盖权限和数据问题。相比继续堆普通问答样例,先写出不可接受的结果,更容易决定测试资源该花在哪里。
但原文给出的是作者的工程经验和一个最小代码示例,没有团队采用前后的事故率,也没有与其他评估方案的对照数据。它能说明方法为什么合理,不能证明收益有多大。
能被代码检查的规则,也不一定覆盖了真正的风险。
例如,数字出现在工具结果中,不代表模型把它对应到了正确地区;引用了一份政策文档,也不代表回答准确理解了条款。只读请求的分类若先出错,后面的检查同样需要考虑。原文没有展开这些问题。
作者也承认,解释是否清楚、语气是否合适,仍需要人工或模型评分。我的取舍是保留这类评估,用它们观察质量变化,同时用明确规则约束严重失败。两者检查的对象不同,不能互相替代。
能拿来做什么
- 给正在开发的 AI 功能先写下 5 件绝不能发生的事,例如泄露其他用户的数据、未经授权修改记录,再为每件事确定可检查的证据。
- 如果 Node 服务会调用读写工具,在评估中记录实际调用轨迹,检查只读请求是否触发了写操作,而不只检查最终回答是否礼貌、准确。
- 对需要展示金额或合计的回答,让工具返回计算结果,再检查回答中的数字来源;采用原文检查器前,先明确日期、序号和允许推导的范围。
- 把线上错误连同触发输入及必要上下文加入回归集,在修改模型、提示词或检索分块大小时重跑,并对高风险案例重复测试。