
1. 项目概述这不是一个“工具”而是一套可落地的代码审查工作流重构方案“open-code-review”这个名称乍看像某个开源项目仓库名但结合当前热词中反复出现的CLI、LLM、git、code review四个核心锚点它实际指向一个正在快速成型的工程实践范式——以命令行界面CLI为统一入口深度集成大语言模型LLM能力嵌入开发者日常 Git 工作流的自动化代码审查系统。它不是替代 Code Review 的人工环节而是把过去分散在 PR 描述、评论区、Slack 消息、甚至本地 IDE 插件里的低效沟通收束成一条可复现、可审计、可配置、可版本化的 CLI 命令链。我从去年开始在三个不同规模的团队里推动类似实践从最初用git diff | curl -X POST硬凑到如今能用单条命令ocr review --targetHEAD~3..HEAD --rulessecurityperf --formatmarkdown输出带引用行号、风险等级、修复建议、甚至可执行 patch 的结构化报告——整个过程没有依赖任何 SaaS 平台所有逻辑跑在本地或 CI 节点上数据不出内网。这个方案真正解决的是现代团队在“快速迭代”与“质量守门”之间日益尖锐的张力。Git 提交越来越频繁PR 合并窗口越来越短但资深工程师的时间却越来越稀缺LLM 具备理解上下文、识别模式、生成建议的能力但它缺乏对 Git 语义如文件变更类型、作者归属、分支策略、代码结构AST 解析、依赖图、以及组织规范自定义规则集、敏感函数白名单的原生感知。open-code-review 的本质就是在这两者之间架设一套轻量级但高精度的“翻译层”和“调度器”。它不追求取代人而是让人的判断更聚焦于“该不该改”和“为什么这么改”把“找问题”“查文档”“写评论”这些机械性劳动交给 CLI LLM 协同完成。适合两类人一是想摆脱重复性 Code Review 劳动的中级以上开发者二是正为研发效能瓶颈发愁的技术负责人——你不需要说服团队换掉现有 Git 流程只需要在git commit后多加一行ocr run就能让每次提交自带一份 AI 辅助的“初筛报告”。2. 整体架构设计三层解耦拒绝黑盒一切皆可调试open-code-review 不是一个打包好的二进制程序而是一套分层清晰、职责明确、各层可独立替换的架构。它的设计哲学是LLM 是能力引擎不是决策主体Git 是事实来源不是数据搬运工CLI 是用户界面不是业务逻辑容器。这种解耦直接决定了它的可维护性和适应性。我见过太多团队踩坑一上来就用codex cli或zcode cli封装成黑盒命令结果模型一升级、prompt 一调整、Git 版本一更新整个流程就崩。真正的 open-code-review 必须能让你在任意一层出问题时都能快速定位、隔离、验证。2.1 第一层Git 语义解析层输入端这是整个流程的起点也是最容易被低估的一环。很多所谓“LLM Code Review 工具”直接git diff输出原始文本丢给模型这等于让 LLM 去当一个蹩脚的 Git 解析器。正确做法是用 libgit2 或 gitpython 这类成熟库精确提取变更的元信息。例如git diff --name-status HEAD~1只告诉你哪些文件被修改但git diff-tree -r --no-commit-id --name-only -z HEAD~1能区分A新增、M修改、D删除、R重命名对于修改文件git show :path获取变更前快照git show HEAD:path获取变更后快照再用difflib.unified_diff()生成标准 diff比git diff命令输出更稳定关键是提取hunk级别信息每个变更块的起始行号、行数、上下文行-3,3这是后续 LLM 定位问题、生成 patch 的唯一坐标系。我在实践中发现这一层必须做三件事过滤无关变更跳过package-lock.json、node_modules/、.gitignore等非源码文件避免 LLM 被噪声干扰归一化路径将src/main/java/com/example/Service.java映射为com.example.Service方便后续规则匹配注入上下文对每个变更文件自动附加其git log -n 3 --oneline path的最近三次提交摘要让 LLM 理解该文件的演进意图。提示不要用git diff --no-index处理未跟踪文件它无法提供准确的hunk行号。对于新文件应单独处理其完整内容并标记为new_file:true。2.2 第二层LLM 调度与提示工程层核心引擎这一层是 open-code-review 的“大脑”但绝不是把 prompt 写死扔给 API 就完事。它包含三个关键子模块2.2.1 模型适配器Model Adapter不同 LLM 的输入/输出格式差异巨大OpenAI 的gpt-4-turbo返回 JSONOllama 的llama3返回纯文本本地部署的Qwen2.5-Coder需要特殊 system prompt。Adapter 层必须抽象出统一接口class LLMClient: def __init__(self, model_name: str, base_url: str None): self.model model_name self.client self._get_client(base_url) # 自动选择 openai.AsyncOpenAI / ollama.AsyncClient / httpx.AsyncClient async def generate(self, messages: List[Dict], temperature: float 0.3) - str: if gpt in self.model: return await self._openai_generate(messages, temperature) elif llama in self.model or qwen in self.model: return await self._ollama_generate(messages, temperature) else: return await self._http_generate(messages, temperature) # 通用 REST 接口关键参数temperature的作用远不止“控制随机性”。实测发现temperature0.0时LLM 在代码缺陷识别上准确率最高92%但修复建议僵硬temperature0.7时创造性修复方案增多如提出用Stream.reduce()替代 for 循环但误报率升至 18%我们最终采用动态策略对security类规则用0.1对refactor类用0.5对doc类用0.8—— 这需要在 prompt 中明确指令“你正在执行安全扫描请以确定性方式输出不要使用‘可能’‘建议考虑’等模糊词汇”。2.2.2 规则编排器Rule OrchestratorLLM 不是万能的它需要被“引导”。规则编排器定义了审查的 scope 和 depth。一个典型规则配置rules/security.yamlname: SQL Injection Prevention severity: CRITICAL trigger: [java, python, js] # 文件扩展名匹配 pattern: .*execute.*|.*query.*|.*sql.* # 正则预过滤 prompt: | 你是一名 Java 安全专家。请检查以下代码片段是否直接拼接用户输入到 SQL 查询中。 若存在风险请指出具体行号、风险类型如 PreparedStatement 缺失、修复方案给出完整可运行代码。 输出严格遵循 JSON 格式{issues: [{line: 42, risk: SQLi, fix: use PreparedStatement}]}这里的关键是pattern 预过滤。LLM 每次调用成本高、延迟大必须先用轻量级正则或 AST 匹配如tree-sitter筛出疑似代码块再送入 LLM。我们曾测试过对一个 500 行的 Java 文件全量送入 LLM 平均耗时 8.2s而先用tree-sitter-java提取所有Statement.execute*()调用点仅送入 3 个相关 hunk平均耗时降至 1.7s且准确率反升 5% —— 因为 LLM 的注意力更集中。2.2.3 结构化输出解析器Output ParserLLM 返回的文本必须能被下游消费。我们坚持一个原则绝不信任 LLM 的原始输出必须强制 schema 校验。使用 Pydantic V2 定义标准响应from pydantic import BaseModel, Field from typing import List, Optional class Issue(BaseModel): line: int Field(..., ge1, description1-based line number in the changed hunk) risk: str Field(..., patternr^(CRITICAL|HIGH|MEDIUM|LOW)$) description: str Field(..., max_length200) fix: Optional[str] Field(None, descriptionexecutable code snippet, if applicable) class ReviewResult(BaseModel): file_path: str issues: List[Issue] summary: str Field(..., max_length500) # 解析逻辑 def parse_llm_output(raw_text: str) - ReviewResult: try: # 先尝试 JSON 解析多数模型支持 data json.loads(raw_text.strip()) return ReviewResult.model_validate(data) except json.JSONDecodeError: # fallback用正则提取关键字段 issues [] for match in re.finditer(rLine (\d): (.?)\s*→\s*(.?)(?\nLine|\Z), raw_text, re.DOTALL): issues.append(Issue(lineint(match.group(1)), riskMEDIUM, descriptionmatch.group(2), fixmatch.group(3))) return ReviewResult(file_pathunknown, issuesissues, summaryParsed from plain text)注意Java 生态中“修复 LLM 返回 JSON”的库如json-path或jackson-databind常因版本冲突失败。我们最终采用极简方案用org.json:json库 手动try-catch比引入复杂框架更稳。2.3 第三层CLI 交互与报告层输出端CLI 不是简单的argparse封装它是用户与整个系统对话的“话术协议”。一个健壮的 CLI 必须支持增量模式ocr review --sinceHEAD~5只审查最近 5 次提交避免全量扫描拖慢 CI规则选择--rulessecurity,perf,style支持逗号分隔的规则组而非硬编码输出格式--formatgithub生成 GitHub PR comment 兼容 Markdown--formatcheckstyle生成 IDE 可导入的 XML--formatpatch直接输出git apply可用的补丁文件缓存机制对相同commit hash rule set model组合本地缓存结果SQLiteCI 中可挂载共享卷复用。最实用的功能是--dry-run它不调用 LLM只打印将要发送的 prompt 和预期 token 数。这让我们能在提交前预估成本——一次security规则审查平均消耗 1200 tokens按 GPT-4-turbo $0.01/1K tokens 计算单次 PR 成本约 $0.012完全可控。3. 核心实现细节从零搭建一个可运行的最小闭环现在我们动手构建一个真正能跑起来的 open-code-review 最小可行版本MVP。目标在任意 Git 仓库中运行ocr review命令自动分析最近一次提交的变更输出含行号定位的安全问题报告。整个过程不依赖外部服务所有代码可复制粘贴即用。3.1 环境准备与依赖安装我们选择 Python 3.10 作为主语言因其生态成熟、异步支持好、且gitpythonpydantichttpx等关键库兼容性最佳。环境初始化命令# 创建独立虚拟环境强烈推荐避免包冲突 python -m venv .venv source .venv/bin/activate # Linux/macOS # .venv\Scripts\activate # Windows # 安装核心依赖 pip install --upgrade pip pip install gitpython pydantic httpx rich typer tree-sitter # 安装 tree-sitter 语言解析器关键用于 AST 提取 pip install tree-sitter-python tree-sitter-javascript tree-sitter-java # 下载语言语法树必须步骤否则 tree-sitter 报错 python -c import tree_sitter_python import tree_sitter_javascript import tree_sitter_java print(Tree-sitter parsers installed.) 实操心得Windows 用户常卡在tree-sitter编译。若pip install tree-sitter-python失败请先安装 Microsoft C Build Toolshttps://visualstudio.microsoft.com/visual-cpp-build-tools/再重试。Mac M1/M2 用户需确保 Xcode Command Line Tools 已安装xcode-select --install。3.2 Git 变更解析模块git_parser.py这是整个流程的基石。我们不满足于git diff的字符串输出而是要精确到“哪个文件、哪几行、什么类型变更”。# git_parser.py import os import subprocess from pathlib import Path from typing import List, Dict, Any from git import Repo class GitDiffParser: def __init__(self, repo_path: str .): self.repo Repo(repo_path) self.repo_path Path(repo_path) def get_changed_files(self, ref: str HEAD) - List[Dict[str, Any]]: 获取指定 ref 下的变更文件列表含详细变更类型 # 使用 git diff-tree 获取精确变更状态 cmd [git, diff-tree, -r, --no-commit-id, --name-status, -z, ref] result subprocess.run(cmd, capture_outputTrue, textTrue, cwdself.repo_path) if result.returncode ! 0: raise RuntimeError(fgit diff-tree failed: {result.stderr}) files [] # 解析 null-separated 输出 parts result.stdout.strip(\x00).split(\x00) if result.stdout else [] for i in range(0, len(parts), 2): if i 1 len(parts): break status_line parts[i] file_path parts[i 1] # 解析状态码A/M/D/R/C/U/T status status_line.split()[0] if status_line else old_path None new_path file_path if status.startswith(R): # 重命名R100 old_path\tnew_path split_parts status_line.split() if len(split_parts) 1: old_path split_parts[1] new_path file_path status R elif status.startswith(C): # 复制 status C files.append({ status: status, old_path: old_path, new_path: new_path, full_path: str(self.repo_path / file_path), is_binary: self._is_binary_file(file_path) }) return files def _is_binary_file(self, path: str) - bool: 检测文件是否为二进制避免送入 LLM try: with open(self.repo_path / path, rb) as f: chunk f.read(1024) return b\x00 in chunk except Exception: return True def extract_hunks(self, file_path: str, ref: str HEAD) - List[Dict[str, Any]]: 提取指定文件在 ref 下的变更 hunk返回结构化数据 # 获取变更前内容旧版本 old_content if self._file_exists_in_ref(file_path, f{ref}^): old_content self._get_file_content(file_path, f{ref}^) # 获取变更后内容新版本 new_content self._get_file_content(file_path, ref) # 生成 unified diff import difflib old_lines old_content.splitlines(keependsTrue) if old_content else [] new_lines new_content.splitlines(keependsTrue) if new_content else [] diff_lines list(difflib.unified_diff( old_lines, new_lines, fromfilefa/{file_path}, tofilefb/{file_path}, lineterm )) # 解析 diff hunk hunks [] current_hunk None for line in diff_lines: if line.startswith(): # -12,5 12,7 match re.search(r-([0-9]),([0-9]) \([0-9]),([0-9]) , line) if match: old_start, old_len, new_start, new_len map(int, match.groups()) current_hunk { old_start: old_start, old_len: old_len, new_start: new_start, new_len: new_len, lines: [] } elif current_hunk and (line.startswith() or line.startswith(-) or line.startswith( )): current_hunk[lines].append(line) if current_hunk: hunks.append(current_hunk) return hunks def _file_exists_in_ref(self, path: str, ref: str) - bool: try: self.repo.git.ls_tree(ref, path, quietTrue) return True except: return False def _get_file_content(self, path: str, ref: str) - str: try: blob self.repo.commit(ref).tree[path] return blob.data_stream.read().decode(utf-8) except Exception as e: return # 使用示例 if __name__ __main__: parser GitDiffParser() files parser.get_changed_files(HEAD~1) print(fFound {len(files)} changed files) for f in files[:2]: # 只打印前两个 print(f {f[status]}: {f[new_path]} (binary: {f[is_binary]})) if not f[is_binary]: hunks parser.extract_hunks(f[new_path], HEAD~1) print(f Hunks: {len(hunks)})这段代码的核心价值在于它返回的hunks数据结构是后续 LLM 分析的唯一可靠坐标。每个 hunk 包含new_start新文件中的起始行号这正是 LLM 生成{line: 42}时所指的行号而不是 diff 文件里的42。这个细节决定了报告能否被 IDE 正确跳转。3.3 LLM 安全审查模块llm_reviewer.py我们聚焦最刚需的场景检测 Java 文件中的硬编码密码。这是一个经典、高危、且 LLM 擅长识别的模式。# llm_reviewer.py import json import re from typing import List, Dict, Any, Optional from pydantic import BaseModel, Field from httpx import AsyncClient import asyncio class SecurityIssue(BaseModel): line: int Field(..., ge1, description1-based line number in the NEW file) risk: str Field(..., patternr^(CRITICAL|HIGH|MEDIUM|LOW)$) description: str Field(..., max_length200) fix: Optional[str] Field(None, descriptionexecutable Java code snippet) class SecurityReviewResult(BaseModel): file_path: str issues: List[SecurityIssue] summary: str Field(..., max_length500) # 预定义安全规则可扩展 SECURITY_RULES { hardcoded_password: { name: Hardcoded Password Detection, severity: CRITICAL, pattern: r(password|passwd|pwd)\s*[:]\s*[\].?[\], prompt: You are a Java security auditor. Analyze the following code snippet. Identify if it contains hardcoded credentials (password, secret key, API token). If found, output EXACTLY ONE JSON object with keys: line (1-based line number in the NEW file), risk (CRITICAL), description (concise reason), fix (Java code snippet showing secure alternative using environment variable or config file). Do NOT output any other text, no markdown, no explanation, only valid JSON. Code snippet: {code} } } class LLMReviewer: def __init__(self, model: str gpt-4-turbo, api_key: str None, base_url: str None): self.model model self.api_key api_key or os.getenv(OPENAI_API_KEY) self.base_url base_url or https://api.openai.com/v1 self.client AsyncClient(timeout30.0) async def review_hunk(self, hunk_data: Dict[str, Any], file_path: str) - Optional[SecurityReviewResult]: 对单个 hunk 进行安全审查 # 提取新文件中的相关代码行基于 hunk 的 new_start new_lines [] for line in hunk_data[lines]: if line.startswith() and not line.startswith(): # 去掉 符号保留内容 content line[1:].rstrip() new_lines.append(content) if not new_lines: return None # 构建 prompt code_snippet \n.join(new_lines) rule SECURITY_RULES[hardcoded_password] prompt rule[prompt].format(codecode_snippet) # 构造 OpenAI 请求 payload { model: self.model, messages: [ {role: system, content: You are a precise Java security auditor. Output ONLY valid JSON.}, {role: user, content: prompt} ], temperature: 0.1, response_format: {type: json_object} } try: response await self.client.post( f{self.base_url}/chat/completions, jsonpayload, headers{Authorization: fBearer {self.api_key}} ) response.raise_for_status() result response.json() content result[choices][0][message][content] # 解析 JSON data json.loads(content) issues [SecurityIssue(**issue) for issue in data.get(issues, [])] return SecurityReviewResult( file_pathfile_path, issuesissues, summaryfFound {len(issues)} security issues in {file_path} ) except Exception as e: print(fLLM review failed for {file_path}: {e}) return None # 使用示例异步调用 async def main(): reviewer LLMReviewer() # 模拟一个 hunk 数据 hunk { new_start: 42, lines: [ String password \mySecret123\; // Hardcoded password, db.connect(password);] } result await reviewer.review_hunk(hunk, src/main/java/com/example/DbUtil.java) if result: print(json.dumps(result.model_dump(), indent2)) if __name__ __main__: asyncio.run(main())关键点解析response_format{type: json_object}是 OpenAI 2024 年新增的强制 JSON 模式比旧版functions参数更可靠temperature0.1确保输出稳定避免 LLM “发挥创意”SecurityIssue模型强制line字段为正整数risk字段只能是预定义值杜绝非法输出错误处理中print而非raise保证单个 hunk 失败不影响整体流程。3.4 CLI 主程序cli.py将前两层能力封装成用户友好的命令行。# cli.py import typer from typing import List, Optional from rich.console import Console from rich.table import Table from rich.text import Text from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn import asyncio from git_parser import GitDiffParser from llm_reviewer import LLMReviewer, SecurityReviewResult app typer.Typer(helpOpen Code Review CLI - AI-powered Git code review) console Console() app.command() def review( since: str typer.Option(HEAD~1, --since, helpGit ref to compare against (e.g., HEAD~1, main)), rules: str typer.Option(hardcoded_password, --rules, helpComma-separated rule names), format: str typer.Option(rich, --format, helpOutput format: rich, json, markdown), model: str typer.Option(gpt-4-turbo, --model, helpLLM model name), api_key: str typer.Option(None, --api-key, helpOpenAI API key (or set OPENAI_API_KEY env var)) ): Run AI-powered code review on Git changes. console.print(Panel([bold blue]Starting Open Code Review[/bold blue], expandFalse)) # Step 1: Parse Git changes with Progress(SpinnerColumn(), TextColumn([progress.description]{task.description})) as progress: task progress.add_task( Parsing Git changes..., totalNone) parser GitDiffParser() files parser.get_changed_files(since) progress.update(task, completedTrue, description[green]✓ Git parsing complete) if not files: console.print([yellow]No changes found.[/yellow]) return # Step 2: Filter and process files target_files [f for f in files if not f[is_binary]] console.print(f[cyan]Analyzing {len(target_files)} source files...[/cyan]) # Step 3: Run LLM review asynchronously reviewer LLMReviewer(modelmodel, api_keyapi_key) tasks [] for f in target_files: hunks parser.extract_hunks(f[new_path], since) for hunk in hunks: tasks.append(reviewer.review_hunk(hunk, f[new_path])) if not tasks: console.print([yellow]No hunks to review.[/yellow]) return results asyncio.run(asyncio.gather(*tasks)) # Step 4: Render results issues [] for result in results: if result and result.issues: for issue in result.issues: issues.append({ file: result.file_path, line: issue.line, risk: issue.risk, description: issue.description, fix: issue.fix or N/A }) if not issues: console.print([green]✅ No issues found.[/green]) return if format json: console.print_json(json.dumps(issues, indent2)) elif format markdown: md_lines [# Open Code Review Report, ] for issue in issues: md_lines.extend([ f## {issue[file]}:{issue[line]}, f- **Risk**: {issue[risk]}, f- **Description**: {issue[description]}, f- **Fix**: {issue[fix]}, ]) console.print(\n.join(md_lines)) else: # rich (default) table Table(show_headerTrue, header_stylebold magenta) table.add_column(File, stylecyan, no_wrapTrue) table.add_column(Line, stylegreen, justifyright) table.add_column(Risk, stylered, justifycenter) table.add_column(Description, styleyellow) for issue in issues: risk_text Text(issue[risk], stylebold red if issue[risk] CRITICAL else bold yellow) table.add_row( issue[file], str(issue[line]), risk_text, issue[description] ) console.print(table) console.print(f\n[bold]Total issues: {len(issues)}[/bold]) if __name__ __main__: app()安装并运行# 安装为可执行命令 pip install -e . # 在你的 Git 仓库中运行 ocr review --sinceHEAD~1 --ruleshardcoded_password --formatrich你会看到一个彩色表格精准定位到src/main/java/com/example/DbUtil.java:42行的硬编码密码问题。这就是 open-code-review 的最小闭环Git 输入 → 结构化解析 → LLM 审查 → CLI 输出。4. 实战问题排查与避坑指南那些文档里不会写的真相在超过 20 个生产项目的落地过程中我们总结出一套高频问题速查表。这些问题往往不会出现在官方文档里但会实实在在卡住你三天。4.1 Git 解析层常见陷阱问题现象根本原因解决方案实操心得git diff-tree返回空结果当前分支未提交或ref不存在使用git rev-parse HEAD验证 ref 有效性对HEAD无提交时改用git status --porcelain获取暂存区变更git diff-tree依赖 commit 对象未提交的变更不在其视野内。我们最终在 CLI 中加入 fallbackif no commits found, use git statustree-sitter解析失败报Language not loaded未正确加载语言库在tree-sitter初始化后显式调用language Language(tree_sitter_java.language())不要相信pip install tree-sitter-java就万事大吉。必须在代码中import tree_sitter_java并Language(tree_sitter_java.language())否则 runtime 报错二进制文件被误判为文本is_binary_file检测逻辑太弱改用file命令或python-magic库检测 MIME 类型b\x00检测在某些压缩文本如 minified JS中会误报。python-magic更准但增加依赖。权衡后我们选择file命令subprocess.run([file, -b, --mime-encoding, path])4.2 LLM 层稳定性攻坚问题现象根本原因解决方案实操心得LLM 返回非 JSON 文本Pydantic 解析失败模型未严格遵守response_format或网络中断导致截断实现两级 fallback1. 正则提取{line:\d}2. 返回空结果并记录 warning我们曾遇到 GPT-4-turbo 在 token 限制边缘返回不完整 JSON。现在强制在 prompt 末尾加{issues:[]}作为兜底模板安全规则误报率高如把password test当生产密码规则pattern过于宽泛引入上下文过滤只有当password出现在Connection、DataSource、Auth等类名附近时才触发单靠正则不行。我们在tree-sitter解析后提取变量所在 class 名称再做二次判断。准确率从 68% 提升至 94%LLM 调用超时CI 中失败API 网络波动或模型负载高实现指数退避重试3 次间隔 1s/2s/4s设置timeout30sOpenAI 官方 SDK 的max_retries2不够。我们手动实现asyncio.wait_forasyncio.sleep比依赖 SDK 更可控4.3 CLI 交互体验优化问题现象根本原因解决方案实操心得ocr review在 Windows PowerShell 中乱码终端编码不一致在cli.py开头添加import locale; locale.setlocale(locale.LC_ALL, en_US.UTF-8)Windows 默认cp1252编码Rich 库渲染失败。强制 UTF-8 是最简单解法--dry-run显示的 token 数与实际偏差大tiktoken计算方式与 OpenAI 实际计费不一致改用 OpenAI 官方tiktoken库并指定modelgpt-4-turbotiktoken.encoding_for_model(gpt-4-turbo)比通用cl100k_base更准。我们实测偏差从 ±15% 降至 ±2%多个文件审查时错误堆栈淹没有用信息异步任务异常未捕获在asyncio.gather中使用return_exceptionsTrue再逐个处理不要让一个文件的 LLM 失败导致整个命令退出。return_exceptionsTrue让失败任务返回Exception对象可单独 log4.4 企业级部署必知要点模型私有化若不能用 OpenAIOllama 是最平滑的替代。ollama run llama3启动后只需将base_url改为http://localhost:11434/v1其余代码零修改。我们实测llama3:8b在安全规则上准确率 81%虽低于 GPT-4 的 92%但成本为