合约审计 AI 助手本地部署:从模型下载到私有化扫描管线的完整搭建
合约审计 AI 助手本地部署:从模型下载到私有化扫描管线的完整搭建
一、审计盲区:为什么我们需要一套私有化 AI 审计管线?
智能合约审计是整个 Web3 安全链路里最让团队焦虑的环节。传统审计流程依赖人工逐行审查,两名审计师背靠背交叉验证 —— 听起来严谨,但实际交付周期往往以周为单位,费用动辄数万美元。更致命的是,审计报告是静态的:今天审计通过的代码,明天加一个delegatecall或改一个call目标,攻击面就可能重新打开。
2025 年以来,AI 辅助审计工具陆续出现,但它们多数是以 SaaS 形态提供服务。这对 DeFi 协议类项目来说存在两个根本性问题:第一,你需要把未公开的合约源码上传到第三方服务器,这本身就是安全策略的悖论;第二,SaaS 模型的审计引擎是黑箱 —— 你不知道它背靠什么模型、用了几层 RAG、检索了什么参考数据。你只能相信它返回的"高中低"风险标签,无法追溯推理路径。
一个更务实的方案,是在本地甚至 CI 环境内部署一套可控的审计 AI 管线:用开源指令微调模型配合自定义审计规则,对合约代码执行静态分析 + LLM 语义理解的双通道扫描。这样做的好处是明确的:源码不出本地、扫描策略可审计、每次 CI 提交自动触发检查。
flowchart TD A[合约源码提交] --> B{CI 触发器} B --> C[Slither 静态分析] B --> D[Ollama 推理服务] C --> E[结构化漏洞特征] D --> F[LLM 语义分析] E --> G[规则引擎聚合] F --> G G --> H{风险评级} H -->|高危| I[阻断合并请求] H -->|中低危| J[生成报告并放行] I --> K[通知开发者修复] J --> L[归档审计记录]二、双通道架构原理:规则引擎与 LLM 推理的协作机制
这套系统的核心设计思想是"双通道互补":传统静态分析工具擅长检测已知模式 —— 重入、整数溢出、时间戳依赖、未检查的返回值 —— 这些问题有明确的 AST 模式可供匹配。但静态分析面对逻辑层面的漏洞几乎无能为力,比如权限检查顺序错误、经济模型参数不合理、或者跨合约调用链上的间歇性状态不一致。
LLM 的作用是对静态分析结果做语义增强。当 Slither 扫描出一段代码使用了delegatecall,LLM 需要判断:这个delegatecall的目标地址是否被权限机制保护?是否在升级代理模式中是合理使用?参数编码是否经过充分的输入校验?这种判断需要上下文理解能力,正是 LLM 相对于正则规则的优势所在。
模型选型上,不建议直接使用通用聊天模型做审计。通用模型对 Solidity 语法的 tokenization 效率低,且缺乏安全审计专用训练数据。更好的选择是使用经过安全审计指令精调的模型,比如基于 CodeLlama 或 DeepSeek-Coder 微调的安全专用版。在 Ollama 生态中,这类模型通常以 GGUF 格式发布,量化到 Q4_K_M 精度即可在 16GB 显存的 GPU 上流畅推理。
sequenceDiagram participant Dev as 开发者 participant CI as CI Runner participant Slither as Slither 引擎 participant Ollama as Ollama 推理 participant DB as 审计数据库 Dev->>CI: 推送合并请求 CI->>Slither: 执行静态扫描 Slither-->>CI: 返回 AST 分析结果 CI->>Ollama: 发送函数级代码片段 Note over CI,Ollama: 传入审计 Prompt + 静态分析上下文 Ollama-->>CI: 返回语义分析结果 CI->>CI: 规则引擎聚合评分 CI->>DB: 写入审计记录 alt 高危漏洞 CI->>Dev: 阻断 PR,通知详情 else 通过 CI->>Dev: 生成报告附件 end三、代码实践:搭建完整的合约审计管线
下面构建一条从合约扫描到 LLM 推理的完整管线。核心组件包括:Slither 静态分析、Ollama 推理服务、审计 Prompt 模板、以及结果聚合的规则引擎。
3.1 Ollama 推理客户端
""" ollama_client.py — Ollama 推理客户端 设计考量: - 使用 httpx 的 transport 层配置超时,而非全局 timeout 上下文,避免并发请求间超时串扰 - 重试策略仅在服务端 5xx 和网络错误时生效,客户端 4xx 直接抛出(Prompt 格式错误不应重试) - 连接池限制设为 2,因为本地 Ollama 实例的并行推理能力受限于 GPU 显存 """ import httpx from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception AUDIT_SYSTEM_PROMPT = """你是一名智能合约安全审计专家。按以下结构输出审计结论: ## 漏洞摘要 - 严重程度:[高危/中危/低危/信息] - 漏洞类型:[重入/权限缺陷/逻辑错误/溢出/其他] - 影响范围:[受影响函数/状态变量] ## 攻击路径 描述可能的攻击步骤和前置条件。 ## 修复建议 给出具体的代码修复方案。 仅输出以上内容,不添加无关解释。""" def _should_retry(exception: BaseException) -> bool: """仅对可恢复的错误进行重试:网络故障和服务端临时不可用""" if isinstance(exception, httpx.HTTPStatusError): return exception.response.status_code >= 500 return isinstance(exception, (httpx.ConnectError, httpx.TimeoutException)) class OllamaClient: def __init__(self, base_url: str = "http://localhost:11434", model: str = "solidity-auditor:latest"): # 使用连接池限制并发量,防止显存 OOM limits = httpx.Limits(max_connections=2, max_keepalive_connections=1) self._client = httpx.Client( base_url=base_url, timeout=httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0), limits=limits, ) self._model = model @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=2, min=2, max=30), retry=retry_if_exception(_should_retry), ) def audit_code(self, code: str, context: str = "") -> dict | None: """ 对合约代码执行 LLM 审计推理 :param code: 待审计的函数级代码片段 :param context: Slither 静态分析结果摘要,作为上下文注入 """ user_prompt = f"# 静态分析上下文\n{context}\n\n# 待审计代码\n```solidity\n{code}\n```" response = self._client.post("/api/generate", json={ "model": self._model, "system": AUDIT_SYSTEM_PROMPT, "prompt": user_prompt, "stream": False, "options": { "temperature": 0.1, # 低温度保证审计结论的一致性 "top_p": 0.95, "num_predict": 2048, # 限制最大输出 token,防止无限生成 } }) response.raise_for_status() data = response.json() return {"response": data.get("response", ""), "eval_count": data.get("eval_count", 0)} def check_model_available(self) -> bool: """验证模型是否已正确加载到 Ollama 服务中""" try: resp = self._client.get("/api/tags") resp.raise_for_status() models = [m["name"] for m in resp.json().get("models", [])] return self._model in models or f"{self._model}:latest" in models except httpx.RequestError: return False def close(self): self._client.close()3.2 Slither 分析集成与结果聚合
""" audit_pipeline.py — 审计管线主流程 设计考量: - Slither 输出的 JSON 需要字段容错处理,因为不同版本 Slither 的输出 schema 存在差异 - 函数粒度切片原因:LLM 的 context window 有限,一次传入整个合约会稀释注意力并消耗 token 配额 - 结果聚合使用加权评分而非简单加和,避免 LLM 重复检测已知漏洞导致的假阳性放大 """ import json import subprocess from pathlib import Path from dataclasses import dataclass, field from ollama_client import OllamaClient @dataclass class AuditFinding: severity: str # HIGH / MEDIUM / LOW / INFO category: str # 漏洞分类标签 function: str # 所属函数 description: str source: str # slither / llm / merged confidence: float # 0.0 ~ 1.0 @dataclass class AuditReport: contract_path: str findings: list[AuditFinding] = field(default_factory=list) high_count: int = 0 medium_count: int = 0 def is_blocking(self) -> bool: """高危漏洞应阻断 CI 流水线""" return self.high_count > 0 def run_slither(contract_path: str) -> list[AuditFinding]: """执行 Slither 静态分析,将 JSON 输出转换为统一 AuditFinding 格式""" cmd = [ "slither", contract_path, "--json", "-", "--filter-paths", "node_modules|lib|test", "--exclude-dependencies", ] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode != 0: raise RuntimeError(f"Slither 分析失败: {result.stderr[:500]}") slither_output = json.loads(result.stdout) findings = [] # Slither 输出结构嵌套较深,需要防御性解析 detectors = slither_output.get("results", {}).get("detectors", []) for detector in detectors: check = detector.get("check", "") description = detector.get("description", "") elements = detector.get("elements", []) for elem in elements: func_name = elem.get("source_mapping", {}).get("filename_used", "") # Slither severity → 统一评级映射 severity_map = {"High": "HIGH", "Medium": "MEDIUM", "Low": "LOW", "Informational": "INFO"} severity = severity_map.get(detector.get("impact", ""), "INFO") findings.append(AuditFinding( severity=severity, category=check, function=func_name, description=description, source="slither", confidence=0.85, )) return findings except (json.JSONDecodeError, KeyError) as e: raise RuntimeError(f"Slither 输出解析异常: {e}") from e def audit_contract(contract_path: str, ollama: OllamaClient) -> AuditReport: """主审计流程:静态分析 → LLM 增强 → 聚合报告""" report = AuditReport(contract_path=contract_path) # 阶段一:Slither 静态扫描 slither_findings = run_slither(contract_path) report.findings.extend(slither_findings) # 阶段二:提取函数级代码,送入 LLM 做语义分析 contract_code = Path(contract_path).read_text(encoding="utf-8") functions = _extract_functions(contract_code) for func_name, func_code in functions.items(): # 将 Slither 对该函数的发现作为上下文注入 slither_context = _build_context_for_function(func_name, slither_findings) try: result = ollama.audit_code(func_code, slither_context) llm_findings = _parse_llm_response(result["response"], func_name) report.findings.extend(llm_findings) except Exception as e: # LLM 推理失败不应中断管线,降级为仅静态分析 report.findings.append(AuditFinding( severity="INFO", category="pipeline_error", function=func_name, description=f"LLM 推理异常: {e}", source="llm", confidence=0.0, )) # 阶段三:加权聚合 report.high_count = sum(1 for f in report.findings if f.severity == "HIGH") report.medium_count = sum(1 for f in report.findings if f.severity == "MEDIUM") return report def _extract_functions(code: str) -> dict[str, str]: """基于大括号匹配提取函数级代码块,避免依赖整个 Solidity AST 解析器""" import re functions = {} pattern = r'function\s+(\w+)\s*\([^)]*\)[^{]*\{' for match in re.finditer(pattern, code): func_name = match.group(1) start = match.start() # 简单的大括号深度计数提取函数体 brace_count = 0 started = False end = start for i, ch in enumerate(code[start:], start): if ch == '{': brace_count += 1 started = True elif ch == '}': brace_count -= 1 if started and brace_count == 0: end = i + 1 break functions[func_name] = code[start:end] return functions def _build_context_for_function(func_name: str, findings: list[AuditFinding]) -> str: """将 Slither 对该函数的分析结果拼装为 LLM 上下文""" relevant = [f for f in findings if func_name in f.function] if not relevant: return "静态分析未对该函数发现显式问题。" lines = [f"- [{f.severity}] {f.category}: {f.description}" for f in relevant] return "Slither 静态分析发现:\n" + "\n".join(lines) def _parse_llm_response(response: str, func_name: str) -> list[AuditFinding]: """将 LLM 自然语言输出解析为结构化发现""" # 简化解析:按严重程度关键词切分 findings = [] if "高危" in response: findings.append(AuditFinding( severity="HIGH", category="llm_detected", function=func_name, description=response[:500], source="llm", confidence=0.6, )) if "中危" in response: findings.append(AuditFinding( severity="MEDIUM", category="llm_detected", function=func_name, description=response[:500], source="llm", confidence=0.6, )) return findings if __name__ == "__main__": import sys client = OllamaClient() try: if not client.check_model_available(): print("错误: 审计模型未加载,请先执行 ollama pull solidity-auditor:latest") sys.exit(1) report = audit_contract(sys.argv[1], client) print(f"审计完成: 高危 {report.high_count}, 中危 {report.medium_count}") finally: client.close()3.3 GitHub Actions CI 集成
# .github/workflows/contract-audit.yml name: 合约安全审计 on: pull_request: paths: - 'contracts/**/*.sol' jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 安装 Slither run: pip install slither-analyzer - name: 启动 Ollama # 使用 Docker 运行 Ollama 以避免在 GitHub Runner 上处理 GPU 依赖 run: | docker run -d --name ollama -p 11434:11434 \ -v ollama-data:/root/.ollama ollama/ollama:latest docker exec ollama ollama pull solidity-auditor:latest - name: 执行审计管线 run: python scripts/audit_pipeline.py contracts/ - name: 阻断高危 PR if: failure() run: | echo "::error::检测到高危漏洞,已阻断合并。请查看审计报告。" exit 1四、边界分析:这套方案的局限与禁用场景
模型幻觉是最大风险。LLM 可能对安全的代码报告漏洞(假阳性),更危险的是对真实漏洞保持沉默(假阴性)。这套系统中 LLM 的 confidence 默认设为 0.6(远低于 Slither 的 0.85),就是这个原因 —— 永远不要把 LLM 的判定作为阻断 CI 的唯一依据。高危判定必须同时满足:Slither 命中已知模式 AND LLM 给出高危语义确认。
不适用于形式化验证场景。如果你的合约管理着超过 1000 万美元的 TVL,这套系统只能作为辅助筛查工具,不能替代 Certora Prover 或 Foundry 不变式测试。LLM 不会做 SMT 求解,它不知道数学上的不变量是否成立。
静态分析 vs 动态分析。本系统是纯静态分析管线,不会执行合约字节码,也不会模拟 EVM 状态。对于依赖运行时状态的漏洞(如闪电贷攻击中的价格操纵),需要搭配 Foundry 的 fork 测试或 Echidna 的 fuzzing 来覆盖。
离线 IDE 场景不适配。如果开发环境完全离线(无网络),需要预下载模型文件并设置 Ollama 的本地模型仓库路径。但 Slither 本身需要联网安装,这一点需要在环境初始化脚本中提前处理。
五、总结
| 维度 | 要点 |
|---|---|
| 架构设计 | 静态分析 + LLM 语义增强的双通道管线,Slither 负责已知模式检测,LLM 负责逻辑层面的语义判断 |
| 模型选型 | 使用安全审计专用精调模型替代通用聊天模型,量化到 Q4_K_M 精度平衡性能与精度 |
| 工程要点 | Ollama 本地推理 + 函数级代码切片 + CI 集成,源码不出本地环境 |
| 安全策略 | LLM 判定不作为阻断 CI 的唯一依据;高危漏洞必须双通道交叉确认 |
| 适用场景 | 中小型 DeFi 协议的日常开发安全门禁、CI 流水线中的自动化安全卡点 |
| 不适用场景 | 大规模资金协议的形式化验证、依赖运行时状态的动态漏洞检测 |