
合约审计 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 扫描出一段代码使用了delegatecallLLM 需要判断这个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_connections2, max_keepalive_connections1) self._client httpx.Client( base_urlbase_url, timeouthttpx.Timeout(connect10.0, read120.0, write30.0, pool10.0), limitslimits, ) self._model model retry( stopstop_after_attempt(3), waitwait_exponential(multiplier2, min2, max30), retryretry_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# 待审计代码\nsolidity\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_factorylist) 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_outputTrue, textTrue, timeout120) if result.returncode ! 0: raise RuntimeError(fSlither 分析失败: {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( severityseverity, categorycheck, functionfunc_name, descriptiondescription, sourceslither, confidence0.85, )) return findings except (json.JSONDecodeError, KeyError) as e: raise RuntimeError(fSlither 输出解析异常: {e}) from e def audit_contract(contract_path: str, ollama: OllamaClient) - AuditReport: 主审计流程静态分析 → LLM 增强 → 聚合报告 report AuditReport(contract_pathcontract_path) # 阶段一Slither 静态扫描 slither_findings run_slither(contract_path) report.findings.extend(slither_findings) # 阶段二提取函数级代码送入 LLM 做语义分析 contract_code Path(contract_path).read_text(encodingutf-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( severityINFO, categorypipeline_error, functionfunc_name, descriptionfLLM 推理异常: {e}, sourcellm, confidence0.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 rfunction\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( severityHIGH, categoryllm_detected, functionfunc_name, descriptionresponse[:500], sourcellm, confidence0.6, )) if 中危 in response: findings.append(AuditFinding( severityMEDIUM, categoryllm_detected, functionfunc_name, descriptionresponse[:500], sourcellm, confidence0.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/checkoutv4 - 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 流水线中的自动化安全卡点不适用场景大规模资金协议的形式化验证、依赖运行时状态的动态漏洞检测