去年在推进一个内部代码安全项目时,我们打算用 LLM 自动识别仓库中的敏感信息和泄露密钥。原以为“调用大模型 + 正则规则”就能直接上线,结果在预演阶段就遇到大量误报和漏报:有的把普通字符串当成了 GitHub Token,有的把真实密钥当成注释忽略掉。后面静下心梳理了一套“生产前评估方案”,才发现真正难的不是写 Prompt,而是如何系统评估 LLM 在秘密扫描场景下的能力边界。
这篇文章就围绕“GitHub 如何在生产前评估 LLM”这一主题,结合秘密扫描的实战经验,整理一套可落地的评估方案。你会了解为什么需要评估、评估哪些指标、如何构建测试集、怎样用 Python 脚本完成一次完整评测,以及生产部署前需要规避的常见坑。无论你是安全工程师、后端开发,还是刚接触 LLM 应用的算法同学,都能按步骤复现这套流程。
1. 背景与核心概念
1.1 什么是秘密扫描,为什么需要 LLM
秘密扫描(Secret Scanning)通常指对源码、配置文件、日志、云平台对象存储中的敏感凭据进行自动检测。常见的敏感信息包括:
- GitHub Personal Access Token
- AWS Access Key ID / Secret Access Key
- 阿里云、腾讯云等云厂商密钥
- 数据库连接串、密码、私钥
- 通用 API Key、Bearer Token
传统方案以正则表达式和熵值检测(Shannon Entropy)为主。例如检测 AWS Key 的格式AKIA[0-9A-Z]{16},或者检测 Base64 字符串的熵值是否超过阈值。这种做法实现简单、可解释性强,但缺陷也很明显:
- 模式匹配只能识别“已知格式”,遇到自定义 Token 或混淆形式容易漏报。
- 正则规则会命中大量示例代码、测试数据,误报率高。
- 没有上下文理解能力,无法区分“示例占位符”和“真实密钥”。
- 维护成本高,需要持续更新规则。
LLM 的出现让秘密扫描从“规则匹配”走向“语义理解”。它可以结合上下文判断一个字符串是否像真实密钥、是否处于可执行代码路径中、是否疑似已暴露的公网仓库等。但 LLM 并不是天然适配生产环境,它存在幻觉、置信度不稳定、上下文长度限制、成本较高等问题,所以上线前必须评估。
1.2 生产前评估的核心任务
“生产前评估”是指在 LLM 真正介入扫描流程之前,用一批带标签的样本数据,量化它在准确率、召回率、误报率、时延、成本等方面的表现,并基于评估结果决定是否上线、用什么阈值、怎么兜底。
这里要区分两个概念:
- 功能测试:验证 LLM 能不能“大致识别”密钥。
- 生产前评估:验证 LLM 是否能在真实场景下稳定工作,并给出可量化的质量指标。
生产前评估还包含与已有规则引擎的对比测试、多轮阈值调优、失败样本分析等。只有通过这些环节,你才有信心把 LLM 从“实验脚本”升级为“生产组件”。
1.3 常见应用场景
具体到 GitHub 生态,LLM 秘密扫描评估常见于以下场景:
| 场景 | 说明 |
|---|---|
| 仓库默认分支扫描 | 对 GitHub 仓库的新提交、历史提交进行秘密扫描,发现泄露的凭据 |
| Pull Request 扫描 | 在代码审查阶段拦截新增密钥,避免敏感信息合入主分支 |
| 组织级合规检查 | 对组织内部所有仓库做周期扫描,满足安全合规要求 |
| 私有密钥轮换 | 扫描已发生泄露的密钥在其他仓库中的复用情况 |
| 近实时监控 | 监听 webhook 或事件流,对新推送代码进行快速检测 |
在上述场景中,LLM 可以作为“第二层检测器”运行在规则引擎之后,先由正则初步过滤,再由 LLM 做二次确认,从而降低误报率,并识别规则漏掉的复杂密钥。
2. 环境准备与版本说明
本文中的示例以 Python 为主,需要准备一个可调用 LLM 的环境。你可以使用 GitHub Copilot、OpenAI API、Azure OpenAI、国内大模型平台的 API,或者本地部署的模型服务。不同 API 的调用方式略有差异,但核心评估流程一致。
2.1 运行环境建议
- 操作系统:Windows 10/11、Ubuntu 20.04+、macOS 均可
- Python:3.9 或以上版本
- 依赖库:
requests、python-dotenv、pandas(可选) - LLM 接口:OpenAI 兼容接口或任何可返回 JSON 的 HTTP 接口
- 代码管理:Git 命令行或 GitHub CLI
以下命令安装基础依赖:
pip install requests python-dotenv pandas如果你的环境还没有准备好 Git 仓库样本,可以先用一个本地文件夹模拟,或者通过 GitHub 官方 API 拉取公开仓库的快照。注意,拉取仓库时不要直接下载包含真实密钥的仓库,建议使用你自己创建的测试仓库或公开的模拟样例。
2.2 评估数据集准备
评估数据集是生产前评估的地基。建议构建三类样本:
- 真实泄露样本:从已公开的泄露报告、安全公告中提取去敏后的密钥片段。
- 模拟泄露样本:自己生成的假密钥,但格式与真实密钥高度相似。
- 正常代码样本:包含 URL、Token 占位符、测试密钥、随机字符串的普通代码。
数据集的格式统一为 JSON 行或 CSV,至少包含:
| 字段 | 类型 | 说明 |
|---|---|---|
id | str | 样本唯一 ID |
content | str | 代码片段或文本内容 |
label | int | 1 表示包含真实密钥,0 表示不包含 |
secret_type | str | 密钥类型,如github_token、aws_key |
source | str | 样本来源 |
示例数据:
{"id": "1", "content": "token = 'ghp_123456789012345678901234567890123456'", "label": 1, "secret_type": "github_token", "source": "synthetic"} {"id": "2", "content": "print('hello world')", "label": 0, "secret_type": "none", "source": "normal"}注意,不要把真实密钥直接用于实验。生产前评估所用样本必须经过脱敏或由程序生成,避免扩大泄露范围。
2.3 LLM 调用配置
为了安全,密钥通过环境变量注入,不写入代码。在项目根目录创建.env文件:
LLM_API_KEY=your_api_key_here LLM_API_BASE=https://api.example.com/v1 LLM_MODEL_NAME=your-model-name然后在 Python 中加载:
import os from dotenv import load_dotenv load_dotenv() api_key = os.getenv("LLM_API_KEY") api_base = os.getenv("LLM_API_BASE") model_name = os.getenv("LLM_MODEL_NAME")如果你的模型接口不是 OpenAI 兼容格式,需要按官方文档修改请求方法。本文的代码示例以 OpenAI 兼容接口为例,你只需要调整chat函数部分的 URL 和请求体。
3. 秘密扫描评估的核心框架
3.1 评估指标
生产前评估不能只看“准确率”。在秘密扫描场景里,不同指标的代表意义不同:
| 指标 | 公式 | 含义 | 关注点 |
|---|---|---|---|
| 精确率(Precision) | TP / (TP + FP) | 被判定为密钥的样本中,真实密钥的比例 | 越高,误报越少 |
| 召回率(Recall) | TP / (TP + FN) | 真实密钥样本中,被正确识别的比例 | 越高,漏报越少 |
| F1 分数 | 2 * P * R / (P + R) | 精确率与召回率的调和平均 | 综合平衡指标 |
| 准确率(Accuracy) | (TP + TN) / Total | 所有样本中分类正确的比例 | 样本均衡时才有参考价值 |
| 误报率(FPR) | FP / (FP + TN) | 正常样本中被误判为密钥的比例 | 越低越好 |
| 漏报率(FNR) | FN / (TP + FN) | 真实密钥样本中被漏掉的比例 | 越低越好 |
在秘密扫描产品中,通常更看重召回率,因为漏掉一个真实令牌可能导致严重的数据泄露。但召回率也不能无限制提高,否则误报会占满工单系统,最终被人工忽略。
3.2 评估流程
一次完整的评估分为五个阶段:
- 样本准备:划分训练集(可选,用于 Prompt 调优)和测试集。
- 基线测试:先跑正则规则或现有扫描引擎,得到基线指标。
- LLM 推理:让 LLM 对测试集每个样本输出是否包含密钥,并给出置信度。
- 结果对比:汇总 LLM 预测结果与真实标签,计算各项指标。
- 失败分析:抽取误报和漏报样本,分析 Prompt 缺陷、模型知识盲区或样本质量问题。
下面是评估流程的简图:
准备样本 -> 运行基线规则 -> 调用 LLM 判断 -> 汇总指标 -> 失败分析 -> 调整 Prompt / 阈值 -> 再评估3.3 Prompt 设计的评估要点
LLM 在秘密扫描任务中的表现高度依赖 Prompt。生产前评估时,建议同时评估几类 Prompt 的差异:
- 零样本(Zero-shot):直接要求模型判断文本中是否有密钥。
- 少样本(Few-shot):给出一两个正负示例后再让模型判断。
- 结构化输出:要求模型返回 JSON
{"is_secret": true/false, "type": "...", "reason": "..."}。 - 角色限定:告诉模型“你是代码安全专家,只关注敏感信息”。
评估时要固定 Prompt 模板,不要边测试边频繁修改,否则结果不可比。
4. 实战:构建 LLM 秘密扫描评估脚本
下面我们完成一个最小可运行评估脚本。脚本会读取测试数据集,调用 LLM 判断每条样本是否包含密钥,最后输出精确率、召回率、F1 等指标。
4.1 创建项目结构
先建一个干净的目录:
llm-secret-scanner-eval/ ├── .env ├── data/ │ └── test_samples.jsonl ├── eval_secret_scanner.py └── requirements.txtrequirements.txt内容:
requests==2.31.0 python-dotenv==1.0.0 pandas==2.1.4安装依赖:
pip install -r requirements.txt4.2 准备测试数据集
这里给出一个简单的生成脚本,用来构造 10 条样本(实际生产评估至少需要几百条):
# gen_samples.py import json import random samples = [] # 模拟 GitHub Token samples.append({"id": "1", "content": "const token = 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij';", "label": 1, "secret_type": "github_token"}) samples.append({"id": "2", "content": "token = os.environ.get('GITHUB_TOKEN')", "label": 0, "secret_type": "none"}) # 模拟 AWS Key samples.append({"id": "3", "content": "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'", "label": 1, "secret_type": "aws_key"}) samples.append({"id": "4", "content": "key = 'AKIAIOSFODNN7EXAMPLE' # 示例,请勿使用", "label": 0, "secret_type": "none"}) # 模拟数据库密码 samples.append({"id": "5", "content": "password: 'P@ssw0rd!2024'", "label": 1, "secret_type": "database_password"}) samples.append({"id": "6", "content": "password_input = input('Enter password:')", "label": 0, "secret_type": "none"}) # 模拟通用 API Key samples.append({"id": "7", "content": "api_key = 'sk-proj-9f8s7d6f5s4d3f2s1d0f'", "label": 1, "secret_type": "api_key"}) samples.append({"id": "8", "content": "api_key = get_api_key_from_vault()", "label": 0, "secret_type": "none"}) # 模拟私钥 samples.append({"id": "9", "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----", "label": 1, "secret_type": "private_key"}) samples.append({"id": "10", "content": "print('begin processing')", "label": 0, "secret_type": "none"}) with open("data/test_samples.jsonl", "w", encoding="utf-8") as f: for s in samples: f.write(json.dumps(s, ensure_ascii=False) + "\n") print("生成完成,共", len(samples), "条样本")运行:
python gen_samples.py4.3 编写核心评估脚本
下面实现eval_secret_scanner.py。该脚本定义了一个scan_with_llm函数,负责向 LLM 发送请求;同时定义evaluate函数,计算指标并输出结果。
# eval_secret_scanner.py import json import os import time import requests from dotenv import load_dotenv load_dotenv() # LLM 配置 API_KEY = os.getenv("LLM_API_KEY") API_BASE = os.getenv("LLM_API_BASE") MODEL_NAME = os.getenv("LLM_MODEL_NAME") # Prompt 模板 SYSTEM_PROMPT = """你是一个代码安全检测专家。你的任务是从给定的代码片段中识别是否包含真实的敏感信息(如 API 密钥、令牌、密码、私钥等)。仅检测代码中硬编码的敏感凭据,不检测环境变量引用、函数调用或明显的示例占位符。 请以 JSON 格式返回结果: { "is_secret": true 或 false, "secret_type": "密钥类型,如果 is_secret 为 false 则为 null", "confidence": 0 到 1 之间的数字, "reason": "简要说明判断依据" } """ def call_llm(content: str) -> dict: """ 调用 LLM 接口,返回解析后的 JSON 结果。 这里使用 OpenAI 兼容接口,请根据你的模型调整 URL 和请求体。 """ url = f"{API_BASE}/chat/completions" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "model": MODEL_NAME, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": content} ], "temperature": 0.1, "max_tokens": 200 } try: resp = requests.post(url, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() text = data["choices"][0]["message"]["content"].strip() # 提取 JSON 部分 if text.startswith("```"): text = text.strip("`") if text.startswith("json"): text = text[4:] return json.loads(text) except Exception as e: print(f"LLM 调用失败: {e}") # 失败时保守返回,认为没有密钥,避免误报 return {"is_secret": False, "secret_type": None, "confidence": 0.0, "reason": f"调用异常: {e}"} def scan_with_llm(content: str, threshold: float = 0.5) -> int: """ 返回 1 表示判定为密钥,0 表示未判定为密钥。 threshold 是置信度阈值,只有 is_secret 为 true 且 confidence 不低于阈值才算命中。 """ result = call_llm(content) if result.get("is_secret") and result.get("confidence", 0) >= threshold: return 1 return 0 def evaluate(test_file: str, threshold: float = 0.5): """ 读取测试集,执行评估并输出指标。 """ y_true = [] y_pred = [] details = [] with open(test_file, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue sample = json.loads(line) label = sample["label"] content = sample["content"] pred = scan_with_llm(content, threshold) y_true.append(label) y_pred.append(pred) details.append({ "id": sample["id"], "label": label, "pred": pred, "content": content[:50] }) time.sleep(0.5) # 避免触发速率限制,生产评估可去掉 # 计算指标 tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1) fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1) fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0) tn = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 0) precision = tp / (tp + fp) if (tp + fp) > 0 else 0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 accuracy = (tp + tn) / len(y_true) if y_true else 0 fpr = fp / (fp + tn) if (fp + tn) > 0 else 0 print(f"测试样本数: {len(y_true)}") print(f"TP={tp}, FP={fp}, FN={fn}, TN={tn}") print(f"精确率 Precision: {precision:.4f}") print(f"召回率 Recall: {recall:.4f}") print(f"F1 分数: {f1:.4f}") print(f"准确率 Accuracy: {accuracy:.4f}") print(f"误报率 FPR: {fpr:.4f}") # 输出失败样本 print("\n失败样本分析:") for d in details: if d["label"] != d["pred"]: print(f" ID: {d['id']}, 真实label: {d['label']}, 预测: {d['pred']}, 内容: {d['content']}") return { "tp": tp, "fp": fp, "fn": fn, "tn": tn, "precision": precision, "recall": recall, "f1": f1, "accuracy": accuracy, "fpr": fpr } if __name__ == "__main__": evaluate("data/test_samples.jsonl", threshold=0.5)4.4 运行与验证
确保.env文件已正确配置,然后运行:
python eval_secret_scanner.py预期输出示例:
测试样本数: 10 TP=4, FP=1, FN=1, TN=4 精确率 Precision: 0.8000 召回率 Recall: 0.8000 F1 分数: 0.8000 准确率 Accuracy: 0.8000 误报率 FPR: 0.2000 失败样本分析: ID: 4, 真实label: 0, 预测: 1, 内容: key = 'AKIAIOSFODNN7EXAMPLE' # 示例,请勿使用 ID: 7, 真实label: 1, 预测: 0, 内容: api_key = 'sk-proj-9f8s7d6f5s4d3f2s1d0f'实际结果取决于你的模型和 Prompt。上述输出仅作为格式参考。
4.5 结果分析与阈值调优
从失败样本中可以看到两个典型问题:
- 示例 AWS Key 被误报为真实密钥。模型缺少对“注释说明这是示例”的上下文理解,或者 Prompt 对“示例占位符”的界定不够清晰。
- 一种 OpenAI 风格的 Key 被漏报。模型可能没认出新格式的
sk-proj-前缀,也可能是置信度不够。
这时候可以尝试调整两个方向:
- 修改 Prompt:在系统提示中明确写明“如果字符串被注释为示例、测试、demo,则不是真实密钥”。
- 调整置信度阈值:把阈值从 0.5 降到 0.3,可能会召回更全,但误报也会增加。
为了找到最优阈值,可以在测试集上遍历不同阈值,画出 Precision-Recall 曲线。
# threshold_tuning.py import json from eval_secret_scanner import scan_with_llm # 加载样本 samples = [] with open("data/test_samples.jsonl", "r", encoding="utf-8") as f: for line in f: if line.strip(): samples.append(json.loads(line)) # 先获取每个样本的置信度(简化处理,真实环境建议缓存结果) results = [] for s in samples: # 为了演示,直接调用一次并拿到置信度 from eval_secret_scanner import call_llm r = call_llm(s["content"]) results.append({"label": s["label"], "is_secret": r.get("is_secret"), "confidence": r.get("confidence", 0)}) # 遍历阈值 for threshold in [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]: tp = fp = fn = tn = 0 for r in results: pred = 1 if r["is_secret"] and r["confidence"] >= threshold else 0 if r["label"] == 1 and pred == 1: tp += 1 elif r["label"] == 0 and pred == 1: fp += 1 elif r["label"] == 1 and pred == 0: fn += 1 else: tn += 1 precision = tp / (tp + fp) if tp + fp else 0 recall = tp / (tp + fn) if tp + fn else 0 f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0 print(f"threshold={threshold:.1f} precision={precision:.2f} recall={recall:.2f} f1={f1:.2f}")这段脚本演示了如何通过遍历阈值找到最佳平衡点。在实际项目中,应该把 LLM 推理结果缓存下来,避免重复调用浪费时间和成本。
5. 常见问题与排查思路
在实际评估过程中,大家经常会遇到以下几类问题。
5.1 LLM 接口报错或返回空值
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
requests.exceptions.ConnectionError | 网络不通或 API Base 配置错误 | 检查.env中的 API_BASE 是否可访问,网络策略是否允许 |
| 返回内容不是 JSON | 模型输出格式不稳定 | 在 Prompt 中强制要求 JSON,并使用response_format参数(如果模型支持) |
rate limit exceeded | 调用频率过高 | 增加time.sleep,或使用官方 SDK 的重试机制 |
context length exceeded | 单条样本太长 | 截断代码片段,或按行拆分处理 |
5.2 评估指标不合理
如果精确率和召回率都很低,不要急着调 Prompt,先检查数据集质量。
- 标签是否准确?比如把注释中的示例当成真实密钥,会导致模型无论如何都会误报。
- 样本是否覆盖多种密钥类型?如果测试集只有一种密钥,结论不具备代表性。
- 真实样本与模拟样本比例是否均衡?建议真实泄露样本占 20%-30%,其余为合成和正常代码。
5.3 误报集中在某个特定模式
比如模型会对长字符串变量名误判。可以在失败分析后,在 Prompt 中增加负样本示例,或对疑似字符串做熵值预过滤。
5.4 LLM 调用成本太高
如果测试集很大,每次评估都调 API 会花不少钱。可以这样做:
- 先用规则引擎粗筛,只让 LLM 处理规则命中的候选样本。
- 在本地用小参数量模型先跑一遍,收集失败样本再调用更强模型复核。
- 开启结果缓存,避免对同一份样本反复调用。
6. 最佳实践与工程建议
6.1 评估数据管理
不要把评估数据直接放在公开仓库里。建议:
- 使用内部版本管理或 OSS 存储,权限最小化。
- 对真实泄露样本做脱敏,替换掉能还原真实凭据的字符串。
- 记录每个样本的来源和生成方式,方便追溯。
6.2 Prompt 版本化管理
Prompt 也会“更新迭代”,建议像管理代码一样管理 Prompt 版本。保存每个版本的评估指标,方便对比哪个 Prompt 更适合生产。
# prompt_version.yaml version: "1.2" date: "2025-06-01" changes: - "增加对 sk-proj 前缀的识别" - "强化示例密钥判断逻辑" eval_f1: 0.87 eval_recall: 0.926.3 双阶段扫描架构
生产环境不要只依赖 LLM。建议采用“规则引擎 + LLM 复核”的架构:
- 第一层:正则、熵值、GitHub 官方 Secret Scanning 规则,高召回、低精度。
- 第二层:LLM 对第一层命中的候选结果做二次判断,降低误报。
- 第三层:人工或定时任务抽样复核,持续优化。
这种架构可以在保证召回的同时控制误报率,同时减少 LLM 调用量,降低成本。
6.4 日志与告警
LLM 判断结果应该记录以下字段:
- 样本 ID、仓库地址、文件路径、提交 SHA
- 模型名称、Prompt 版本
- 输出置信度、secret_type、reason
- 最终处置状态(确认、忽略、待人工)
不要只记录一个布尔值,否则后续排查和复盘会非常困难。
6.5 安全与合规
调用云端 LLM 时,禁止把未经脱敏的真实验证数据直接发送给外部 API。必须确认:
- 数据是否允许出域。
- 是否有数据保留策略。
- 是否使用私有化部署模型。
否则,一旦发生二次泄露,就是严重事故。
7. 总结与学习路线
本文从秘密扫描业务背景出发,解释了为什么要在生产前评估 LLM,给出了评估指标、评估流程、Prompt 设计要点,并提供了一份可运行的 Python 评估脚本。你可以把这份脚本应用到自己团队的代码安全流程中,也可以根据本文章节中的阈值调优方法,找到适合你业务场景的置信度阈值。
接下来如果你要继续深入,建议按以下顺序学习:
- 掌握现有规则引擎的能力边界,例如 GitHub Advanced Security 的 secret scanning 规则。
- 学习更多 LLM 结构化输出方法,比如 function calling、JSON mode,减少解析异常。
- 尝试用更多真实场景样本扩充测试集,比如不同编程语言、不同密钥格式。
- 探索语义嵌入 + 分类模型的方式,用一个小模型先过滤简单样本,把 LLM 留给复杂样本。
生产安全无小事。LLM 能提高秘密扫描的上限,但也会带来新的不确定性和成本。请务必在充分评估后再推上线,并始终保留人工审核和灰度回滚的通道。
如果你在评估过程中遇到其他问题,欢迎在评论区交流,也可以收藏这篇文章作为日常参考。