1. 什么是 open-code-review:一个被严重低估的工程实践新范式
“open-code-review”这个词最近在技术社区里频繁出现,但它不是某个具体工具的名字,也不是某家公司的私有产品,而是一套正在快速成型的、以开放性、可解释性、可审计性为底层原则的代码评审新方法论。我从去年开始在三个不同规模的团队里落地这套实践,从最初用 GitHub Actions + 自定义脚本做基础扫描,到后来接入 LLM Agent 构建多语言规则引擎,再到如今把 line-level comments 变成可追溯、可复盘、可训练的知识资产——整个过程踩过太多坑,也验证了一件事:真正的 open-code-review,核心不在“用不用 AI”,而在“评审过程是否对所有人可见、可质疑、可迭代”。
它解决的痛点非常具体:传统 code review 依赖人工经验,新人看不懂资深工程师留下的模糊 comment(比如“这里耦合太重”),老手又没时间逐行解释;静态分析工具只能报错,但报错理由冷冰冰,缺乏上下文;而市面上不少 AI 代码助手生成的建议,要么泛泛而谈(“建议优化性能”),要么脱离业务语境(在金融系统里推荐用 async/await 处理强一致性事务)。open-code-review 的本质,是把每一次评审动作——从哪一行、为什么改、依据哪条规则、参考了哪些历史案例——全部结构化、语义化、版本化地沉淀下来。
适合谁来关注?不是只有架构师或 Tech Lead,而是所有写代码的人:前端工程师能用它快速理解后端接口变更的影响边界;测试同学能直接看到某次提交中所有被标记为“潜在并发风险”的代码段及推理链;甚至产品经理也能订阅关键模块的 review 摘要,知道“支付回调逻辑这次重构规避了哪三类幂等异常”。它不替代人,而是把人的判断力、经验、上下文,变成可复用、可验证、可进化的工程资产。关键词里反复出现的multi-language ruleset,说的就是这个能力——不是写死的正则匹配,而是用统一语义模型描述“Java 的 synchronized 块嵌套超过2层需告警”“Python 的 asyncio.gather() 在无 timeout 控制时应提示”“Rust 的 Arc<Mutex > 在高频写场景下建议替换为 dashmap”——这些规则本身也是代码,可 PR、可 Review、可 A/B 测试。
2. 整体设计思路:为什么必须放弃“AI 替代 Reviewer”的幻想
很多人一看到 “LLM Agent” 和 “open-code-review” 绑定,第一反应就是:“是不是找个大模型 API,喂点代码,让它自动写 comment?” 我实测过七种主流方案,结论很明确:纯 LLM 驱动的自动化 review 是危险的,且不可审计。去年我们团队曾上线一个基于 GPT-4 的 PR 自动评论 bot,两周内就因三条高危误判导致线上故障——它把一段用于兼容旧版协议的 if-else 分支识别为“冗余逻辑”,建议删除;把加密库中故意加入的随机延时判定为“无意义性能损耗”;更致命的是,它对某段涉及资金校验的 Go 代码给出“建议用 map 替代 struct 提升灵活性”的建议,完全无视了该 struct 被严格约束为内存布局固定(unsafe.Sizeof 必须恒定)的合规要求。
所以 open-code-review 的顶层设计,根本不是“让 AI 替人干活”,而是“让人和 AI 协同构建可信赖的评审契约”。它的三层架构是:
2.1 规则层(Ruleset):所有判断的宪法性文件
这不是配置文件,而是用 DSL(领域特定语言)编写的可执行规则集。我们选型时对比过 Rego(OPA)、Starlark(Bazel)、自研 YAML+Jinja 模板,最终采用Starlark + 自定义 AST 解析器。为什么?因为 Starlark 兼具 Python 的可读性和沙箱安全性,且能天然支持 rule 的 dependency graph(比如“HTTP 超时检查”规则依赖“网络客户端初始化”规则的输出)。每条规则必须声明:适用语言、触发条件(AST 节点类型+属性约束)、检查逻辑(Python 函数)、修复建议模板(带变量插槽)、置信度阈值(0.0–1.0)、关联 CWE 编号。例如一条针对 Java 的规则:
def http_timeout_check(node): if node.type == "MethodInvocation" and node.name == "setConnectTimeout": if node.args[0].value < 3000: return { "level": "error", "message": "连接超时设置过短({{value}}ms),低于最低安全阈值3000ms", "cwe": "CWE-489" }提示:规则必须可单测。我们强制要求每条规则附带至少3个正例、3个反例的 AST JSON 快照,CI 中运行
starlark test rules/作为准入门槛。没有测试的规则,连 git commit 都被 pre-commit hook 拦截。
2.2 执行层(Agent Orchestrator):规则与 LLM 的调度中枢
这里的关键认知是:LLM 不是裁判,而是陪审团成员之一。我们的 Agent 不直接输出“是否通过”,而是并行调用三类引擎:
- 静态分析引擎(如 Semgrep、SonarQube 的规则插件):提供确定性结果(true/false)
- AST 语义引擎(自研 Python/Rust 解析器):提取控制流、数据流、跨文件引用关系
- LLM 推理引擎(本地部署的 DeepSeek-Coder 33B):仅处理需要上下文推理的问题,如“这段 retry 逻辑是否覆盖了服务熔断后的降级路径?”
Agent 的核心职责是融合这三路输出:当静态分析报错且 AST 引擎确认调用链存在,LLM 仅需生成 human-readable 的 line-level comment;当三路结果冲突(如静态分析未报错但 LLM 指出潜在 N+1),则标记为 “needs-human-review” 并附上三方证据摘要。我们用 LangChain 的 RouterChain 实现路由,但重写了 OutputParser——它输出的不是自由文本,而是严格 schema 的 JSON:
{ "line": 142, "severity": "warning", "rule_id": "java-http-timeout", "evidence": [ {"source": "static", "result": "true"}, {"source": "ast", "data": {"callers": ["PaymentService.init()"]}}, {"source": "llm", "reasoning": "超时值300ms在高并发支付场景下易触发连接池耗尽"} ], "suggestion": "将 connectTimeout 从300ms提升至5000ms,并添加熔断器配置" }2.3 呈现层(Open Review UI):让评审过程成为团队知识库
GitHub/GitLab 的原生 comment 区域太窄,无法承载结构化证据。我们开发了一个轻量级 sidebar 插件(Chrome Extension + VS Code 插件),当开发者打开 PR 时,自动拉取 open-code-review 服务的 review report。界面分三栏:
- 左栏:代码 diff,高亮问题行,悬停显示 severity 图标(🔴 error / 🟡 warning / 🔵 info)
- 中栏:该行的所有 review 证据折叠卡片,点击展开看到 static/ast/llm 三方结论及原始数据(如 AST 节点 JSON、LLM prompt 模板、Semgrep 规则 ID)
- 右栏:关联知识图谱——自动链接到内部 Wiki 中“HTTP 超时配置规范”文档、历史上同类问题的 5 个已关闭 issue、以及该规则最近一次被修改的 commit 记录
最关键的设计是:所有评论默认公开,且带数字签名。每条评论末尾有小字:“Signed by open-code-review@v2.3.1 (SHA256: a1b2c3...)”,任何人可用公钥验证这条评论确实由当前规则集生成,而非人为伪造。这就实现了真正的“open”——不是开源代码,而是开源判断过程。
3. 核心细节解析:multi-language ruleset 如何真正落地
multi-language ruleset 常被误解为“写一堆不同语言的规则文件”,但实际难点在于语义对齐。Java 的 try-with-resources 和 Rust 的 RAII 看似功能相似,但资源生命周期管理的约束条件完全不同;Python 的装饰器和 TypeScript 的 Decorator 语法相同,但元编程能力差异巨大。我们花了四个月打磨规则抽象层,核心是建立三层映射:
3.1 语言无关的 AST 元模型(Universal AST Schema)
不直接操作各语言 parser 的原始 AST(如 Java 的 Eclipse JDT AST 或 Rust 的 syn::Stmt),而是定义一套最小完备的中间表示:
{ "node_type": "FunctionCall", "callee": "database.query", "arguments": [ {"type": "StringLiteral", "value": "SELECT * FROM users WHERE id = ?"}, {"type": "VariableRef", "name": "userId"} ], "control_flow_context": { "in_loop": false, "in_transaction": true, "has_error_handling": true } }所有语言的 parser 都需实现一个 adapter,将原生 AST 转换为此 schema。例如 Python 的 ast.parse() 输出经 adapter 后,cursor.execute("SELECT ...", [id])和 Java 的stmt.executeQuery("SELECT ...")会映射到同一个FunctionCall节点,只是callee字段值不同。这样,一条规则“禁止在事务中执行未参数化的 SQL 查询”就能跨语言生效——adapter 保证了输入标准化,规则只关心语义,不关心语法。
3.2 规则编写范式:从“if-else”到“模式匹配+约束求解”
早期我们用 Starlark 写规则,很快遇到瓶颈:if node.callee == "query" and len(node.arguments) == 1这样的硬编码无法应对db.session.execute(text("..."))这类 ORM 封装。解决方案是引入Tree Pattern Matching(树模式匹配)。我们借鉴了 Rascal 语言的模式语法,定义规则时用类似正则的表达式描述 AST 结构:
pattern sql_query_call = FunctionCall( callee: "query" | "execute" | "session.execute", arguments: [StringLiteral(_), ...] where not has_parameterized_args() )配套的has_parameterized_args()是一个可插拔的约束函数,Python adapter 实现为检查arguments[1]是否为 list/tuple,Java adapter 则检查arguments.get(1).getType().isAssignableFrom(List.class)。这种设计让规则开发者专注业务逻辑(“什么算危险 SQL”),而语言适配者专注技术细节(“如何检测参数化”)。
3.3 规则生命周期管理:PR 驱动的规则演进
规则本身也是代码,必须走完整工程流程。我们规定:
- 所有规则存放在
rules/目录,按语言分包(rules/java/,rules/python/,rules/rust/) - 新增规则必须包含:规则文件(
.star)、测试用例(test_cases/下的源码文件+期望输出 JSON)、文档(docs/下的 Markdown,说明适用场景、误报率、历史变更) - CI 流程强制:
make test-rules运行所有规则测试;make lint-rules检查 Starlark 语法和最佳实践(如禁止使用eval);make generate-docs自动更新规则索引页 - 关键规则(如安全、资损相关)的修改,需至少 2 名 Senior Engineer + 1 名 Security Team 成员 approve,且必须附带 A/B 测试报告:在 1000 个历史 PR 上回放新旧规则,统计 precision/recall 变化
实操心得:我们曾因一条 Rust 规则误报率从 0.8% 升至 3.2% 被 rollback。教训是——规则的 precision 比 recall 更重要。宁可漏掉 10 个真问题,也不能让工程师养成“忽略所有 AI 评论”的习惯。现在所有新规则上线前,必须在 sandbox 环境跑 72 小时,监控真实 PR 中的误报数,达标(<0.5%)才灰度。
4. 实操过程:从零搭建一个可运行的 open-code-review demo
下面带你一步步搭一个最小可行 demo,支持 Python 和 Java 的基础规则(空指针检查、SQL 注入预警),全程用开源工具,无需 GPU。重点不是教命令,而是讲清每个选择背后的工程权衡。
4.1 环境准备:为什么选这些工具链
- AST 解析器:Python 用
ast标准库(轻量、无依赖);Java 用javaparser(成熟、支持 Java 17+);拒绝 ANTLR 手写 grammar——维护成本太高 - 规则引擎:Starlark(Google 开源,比 YAML 灵活,比 Python 安全)
- LLM 推理:DeepSeek-Coder 33B Qwen2-7B-Instruct(本地量化版,4-bit GGUF,16GB 显存可跑)
- 服务框架:FastAPI(异步、类型提示友好、OpenAPI 自动生成)
- 存储:SQLite(demo 阶段够用,production 换 PostgreSQL)
注意:不要用 Ollama 或 LM Studio 做生产推理。它们缺乏 request-level tracing 和 rate limiting,当 50 个 PR 同时触发 review 时,OOM 是必然的。我们用 vLLM 部署 DeepSeek,配置
--max-num-seqs 256 --gpu-memory-utilization 0.8,实测吞吐达 12 req/s。
4.2 第一步:构建 Universal AST Adapter
创建adapters/python_adapter.py:
import ast from typing import Dict, Any class PythonAdapter: def parse(self, source_code: str) -> Dict[str, Any]: tree = ast.parse(source_code) return self._convert_node(tree) def _convert_node(self, node) -> Dict[str, Any]: # 核心转换逻辑:将 ast.AST 节点映射到 Universal AST Schema if isinstance(node, ast.Call): return { "node_type": "FunctionCall", "callee": self._get_callee_name(node), "arguments": [self._convert_node(arg) for arg in node.args], "control_flow_context": self._get_context(node) } elif isinstance(node, ast.Constant): return {"node_type": "StringLiteral", "value": node.value} # ... 其他节点类型 return {"node_type": "Unknown", "raw": ast.dump(node)} def _get_callee_name(self, call_node) -> str: if isinstance(call_node.func, ast.Name): return call_node.func.id elif isinstance(call_node.func, ast.Attribute): return f"{self._get_attr_path(call_node.func)}" return "unknown" def _get_attr_path(self, attr_node) -> str: if isinstance(attr_node.value, ast.Name): return f"{attr_node.value.id}.{attr_node.attr}" elif isinstance(attr_node.value, ast.Attribute): return f"{self._get_attr_path(attr_node.value)}.{attr_node.attr}" return attr_node.attrJava adapter 类似,用 javaparser 的parse()方法,再遍历 CompilationUnit 节点。关键是保持输出 schema 一致。
4.3 第二步:编写第一条跨语言规则
在rules/common/null_dereference.star中:
# 检测可能的空指针解引用:obj.method() 且 obj 来源未做 null check def null_dereference_check(node): if node.node_type != "FunctionCall": return None # 获取调用者对象(obj in obj.method()) caller_obj = get_caller_object(node) if not caller_obj: return None # 检查 caller_obj 是否在作用域内有 null check if has_null_check(caller_obj, node): return None # 检查 caller_obj 是否来自可能为 null 的来源(如 method return) if is_potentially_null_source(caller_obj): return { "level": "error", "message": "潜在空指针解引用:{{caller}}.{{method}}(),未检查 {{caller}} 是否为 null", "cwe": "CWE-476" } # 此函数需各语言 adapter 实现 def get_caller_object(node): # Python adapter 返回 ast.Attribute 或 ast.Name 节点 # Java adapter 返回 SimpleName 或 QualifiedName 节点 pass注意:get_caller_object是 adapter-specific 函数,在 Python adapter 中实现为提取call_node.func.value,在 Java adapter 中实现为提取methodCall.getExpression()。规则本身不关心实现,只调用接口。
4.4 第三步:Agent Orchestrator 核心逻辑
agent/orchestrator.py:
from typing import List, Dict, Any from fastapi import BackgroundTasks import asyncio class ReviewOrchestrator: def __init__(self, static_engine, ast_engine, llm_engine): self.static_engine = static_engine # Semgrep wrapper self.ast_engine = ast_engine # Universal AST adapter self.llm_engine = llm_engine # vLLM client async def run_review(self, pr_data: Dict[str, Any]) -> List[Dict[str, Any]]: # Step 1: 并行执行三路分析 static_tasks = [self.static_engine.run(rule) for rule in pr_data["rules"]] ast_task = self.ast_engine.parse(pr_data["diff"]) llm_task = self.llm_engine.generate_prompt(pr_data["diff"], pr_data["context"]) static_results, ast_result, llm_result = await asyncio.gather( asyncio.gather(*static_tasks), ast_task, llm_task ) # Step 2: 融合结果(关键!) merged_comments = [] for line_num, static_issues in enumerate(static_results): for issue in static_issues: # 构建 evidence 数组 evidence = [ {"source": "static", "result": issue}, {"source": "ast", "data": self._extract_ast_evidence(ast_result, line_num)}, {"source": "llm", "reasoning": self._extract_llm_reasoning(llm_result, line_num)} ] # 应用融合策略:static 为 true 且 ast 确认调用链存在 → 直接采纳;否则标记 human-review if issue["level"] == "error" and self._ast_confirms_issue(ast_result, issue): merged_comments.append(self._build_comment(issue, evidence)) else: merged_comments.append(self._build_human_review_comment(issue, evidence)) return merged_comments def _build_comment(self, issue, evidence) -> Dict[str, Any]: # 严格按 schema 构造 return { "line": issue["line"], "severity": issue["level"], "rule_id": issue["rule_id"], "evidence": evidence, "suggestion": issue.get("suggestion", "请检查代码逻辑") }实操心得:融合策略是最大难点。我们试过加权平均、投票机制,最终发现最稳的是“Static 为 anchor,AST 为 validator,LLM 为解释器”。即:static 报错是必要条件,AST 必须证明该错误在当前上下文中真实存在(比如变量确实在此行被解引用),LLM 只负责把 technical reason 转成 natural language。这样既保证准确性,又保留可解释性。
4.5 第四步:集成到 GitHub Webhook
main.pyFastAPI 服务:
from fastapi import FastAPI, Request, BackgroundTasks from agent.orchestrator import ReviewOrchestrator app = FastAPI() orchestrator = ReviewOrchestrator(...) @app.post("/webhook") async def github_webhook(request: Request, background_tasks: BackgroundTasks): payload = await request.json() # 验证 GitHub signature(安全必需!) if not verify_github_signature(payload, request.headers.get("X-Hub-Signature-256")): return {"error": "Invalid signature"} # 只处理 pull_request opened/synchronize 事件 if payload.get("action") not in ["opened", "synchronize"]: return {"status": "ignored"} pr_data = { "repo": payload["repository"]["full_name"], "pr_number": payload["number"], "diff_url": payload["pull_request"]["diff_url"], "files": payload["pull_request"]["changed_files"] } # 异步触发 review,避免 webhook 超时 background_tasks.add_task(run_full_review, pr_data) return {"status": "accepted"} async def run_full_review(pr_data: Dict[str, Any]): # 1. 下载 diff diff_content = download_diff(pr_data["diff_url"]) # 2. 解析 AST ast_result = orchestrator.ast_engine.parse(diff_content) # 3. 运行规则 comments = await orchestrator.run_review({ "diff": diff_content, "rules": load_rules_for_repo(pr_data["repo"]), "context": get_pr_context(pr_data) }) # 4. 发送 GitHub comment post_github_comment(pr_data["repo"], pr_data["pr_number"], comments)关键点:Webhook 必须异步处理,且要有重试机制(我们用 Celery + Redis)。GitHub 要求 webhook 在 10 秒内响应,而完整 review 可能需 30 秒,所以必须立即返回 200,再后台执行。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
在落地 open-code-review 过程中,我们整理了 27 个高频问题,按发生阶段分类。以下是最典型的 5 个,附真实日志和解决路径。
5.1 问题:Java adapter 解析失败,报错 “Unsupported Java version: 21”
现象:CI 中mvn compile成功,但 open-code-review 服务解析.java文件时报错,堆栈指向javaparser版本不兼容。
排查:
- 查看
pom.xml中javaparser-core版本:我们用的是 3.25.3,但 Java 21 的新语法(如 record patterns)需 3.26.0+ - 检查 CI runner 的 JDK 版本:
java -version显示 17,但javaparser默认用Runtime.version(),实际运行时加载了 21 的 class
解决:升级javaparser到 3.26.1,并在 adapter 初始化时显式指定ParserConfiguration:
ParserConfiguration config = new ParserConfiguration() .setLanguageLevel(LanguageLevel.JAVA_21); JavaParser parser = new JavaParser(config);注意:不要盲目升级。3.26.x 对 Java 17 的兼容性有 regression,我们最终采用双 parser 策略:Java 17 用 3.25.3,Java 21+ 用 3.26.1,由 adapter 根据文件头
// JAVA_VERSION: 21自动路由。
5.2 问题:LLM 生成的 comment 中文乱码,且包含大量无关符号
现象:DeepSeek-Coder 输出的 suggestion 字段出现 `` 符号,或混入 Markdown 代码块符号(```)未闭合。
根因:vLLM 的 tokenizer 与 DeepSeek-Coder 的原生 tokenizer 不完全一致,且 prompt 模板中未指定stop_token_ids。
解决:
- 在 vLLM 启动参数中添加
--tokenizer_mode auto --trust-remote-code - 修改 prompt 模板,强制结尾加
<|EOT|>(DeepSeek 的 EOS token):
你是一个资深代码评审专家,请基于以下代码和上下文,用中文生成一条简洁、专业的 line-level comment。 代码: {{code_snippet}} 上下文: {{context}} 请严格按 JSON 格式输出,只包含 message 和 suggestion 字段,不要任何其他内容: {"message": "...", "suggestion": "..."}<|EOT|>- 在 Python client 中,
generate()调用时设置stop=["<|EOT|>"]
实测后乱码消失,JSON 解析成功率从 62% 提升至 99.8%。
5.3 问题:multi-language ruleset 在 Python 文件中误报 Java 规则
现象:一个 Python PR 里,rules/java/sql_injection.star规则被意外触发,报错 “Found java.sql.PreparedStatement usage”。
排查:
- 检查规则加载逻辑:
load_rules_for_repo()函数未按语言过滤,而是加载了全部 rules/ 下的.star文件 - 查看规则元数据:
sql_injection.star文件头部缺少# language: java注释
解决: - 在 Starlark loader 中增加语言过滤:
def load_rules_for_repo(repo_name): lang = detect_language_from_repo(repo_name) # 基于 .gitattributes 或文件扩展名 rules = [] for rule_file in glob.glob("rules/**/*.star"): if get_rule_language(rule_file) == lang: rules.append(load_starlark_rule(rule_file)) return rules- 强制所有规则文件首行必须是
# language: python或# language: java,pre-commit hook 检查。
经验:规则语言标识必须是声明式的,不能靠文件路径(
rules/python/)推断。因为有些规则是跨语言的(如common/logging_format.star),路径无法表达这种语义。
5.4 问题:GitHub comment 重复发送,同一行出现 5 条 identical comment
现象:一个 PR 提交后,某行代码下方连续出现 5 条完全相同的 warning comment。
根因:Webhook 事件重复触发(GitHub 有时发多次 synchronize 事件),且服务未做幂等性控制。
解决:
- 在数据库中建唯一索引:
(pr_number, file_path, line_number, rule_id) - 每次 post comment 前,先查询该 (pr, file, line, rule) 是否已有记录
- 若存在,跳过发送;若不存在,插入记录后再发 comment
- 同时在 GitHub webhook 设置中开启 “Synchronize events only when files changed” 选项,减少无效触发
实测后重复率从 12% 降至 0.3%。
5.5 问题:LLM 推理耗时波动极大,从 2s 到 45s 不等
现象:review 请求 P95 延迟 38s,导致 GitHub webhook 超时,部分 comment 丢失。
排查:
vLLMmetrics 显示num_requests_running峰值达 120,远超--max-num-seqs 256- 日志发现大量
CUDA out of memory报错,但nvidia-smi显示显存占用仅 60%
根因:vLLM 的 PagedAttention 机制在 batch size 动态变化时,显存碎片化严重,导致新请求无法分配连续块。
解决: - 固定
--max-num-seqs为 128(原 256),降低碎片概率 - 添加
--block-size 32(默认 16),增大 block 粒度 - 在 client 端实现 request batching:收集 5 个 PR 的 review 请求,合并为一个 batch 发送
调整后 P95 延迟稳定在 8.2s,且显存利用率保持在 75%±3%。
6. 关于 LLM、Agent、Embedding 的本质区别:别再被营销话术带偏
最近刷到很多文章在问“agent 和 llm 和 ai模型 有什么区别”,甚至拿 DeepSeek 当例子说“它是 LLM 还是 Agent”。这种问题暴露了一个根本误区:把技术栈层级和产品形态混为一谈。我用厨房做类比,帮你彻底理清:
LLM(大语言模型)= 厨房里的“顶级厨师”
它掌握海量菜谱(训练数据),能理解复杂指令(prompt),但不会主动做事。你让它“做一道宫保鸡丁”,它能详细说出步骤、火候、调料比例,但不会自己去洗菜、切肉、开火。DeepSeek-Coder、Qwen、Llama 都是这一层——它们是基础模型,能力强大,但需要被调用、被约束、被集成。Embedding 模型= 厨房的“食材分类员”
它不负责烹饪,只负责把食材(文本)映射到数学空间。比如把“宫保鸡丁”、“辣子鸡丁”、“鱼香肉丝”这三个菜名,转成三维向量,让相似的菜离得近,不相似的离得远。它的价值在于检索和聚类:当你搜索“下饭的川菜”,embedding 能快速找到最相关的几道菜。它和 LLM 是互补关系,不是替代关系。Agent(智能体)= 厨房的“主厨助理”
它本身不是厨师,也不懂做菜,但它协调资源、执行流程、处理异常。你告诉它“今晚招待客人,做三菜一汤”,它会:- 查菜谱(调用 LLM)
- 看冰箱库存(查数据库)
- 发现缺花生,下单补货(调用电商 API)
- 火候不对时关火(监控指标,触发 fallback)
Agent 的核心是Orchestration(编排),它把 LLM、Embedding、工具 API、人类反馈串成一条工作流。open-code-review 的 Agent 就是典型——它不写代码,不生成规则,但它决定何时调用静态分析、何时求助 LLM、何时标记 human review。
所以 DeepSeek 是什么?它是一个LLM。当你用curl调它的 API,你是在直接调用厨师;当你用它构建一个自动写单元测试的工具,那个工具才是 Agent;当你用它的 embedding 模型做代码相似度搜索,那部分就是 Embedding 应用。三者不在同一维度,就像不能问“锅铲是厨师还是食材”。
最后分享一个血泪教训:我们最早想用 LLM 直接生成整套 ruleset,结果产出的规则全是“建议使用设计模式”“避免过度工程化”这类废话。后来才明白——LLM 擅长理解,不擅长定义;擅长解释,不擅长立法。真正的规则必须由工程师用代码写出来,LLM 只负责把规则的“为什么”翻译成人类能懂的语言。open-code-review 的力量,从来不在 AI 多聪明,而在人把经验变成可执行、可验证、可传承的代码那一刻。