前言

前面几篇文章中,从不同角度学习了 OpenCodeReview 的核心流程。

第一篇从使用者角度跑通了最小 Demo,观察到了 ocr review 在审查过程中会调用一些工具,例如:

file_read
file_find
code_search
code_comment

当时更多是从现象上理解:

file_read 用来读取文件;
code_search 用来搜索代码;
code_comment 用来生成评论。

第二篇分析了 ocr review 命令入口,知道了命令会从 main.go 分发到:

runReview(args[1:])

第三篇分析了 Git Diff,理解了:

Git diff 决定本次审查哪些文件。

第四篇分析了 .opencodereview/rule.json,理解了:

rule.json 决定这些文件按照什么规则审查。

这一篇继续往下看,不过不再只是停留在“工具调用日志长什么样”,而是从源码角度分析:

这些工具是在哪里注册的?
工具名称在哪里定义?
每个工具对应哪个实现文件?
Agent 是怎么执行工具调用的?
工具调用日志为什么会出现在终端?
JSON 中 tool_calls 是怎么统计出来的?

也就是说,第一篇是观察工具调用现象,而第五篇要开始理解工具调用背后的源码结构。


一、本篇学习目标

本文主要解决下面几个问题:

1. runReview 中哪里注册了 Agent 工具?
2. buildToolRegistry 做了什么?
3. file_read、file_find、file_read_diff、code_search、code_comment 的名称在哪里定义?
4. Tool、Provider、Registry 分别是什么?
5. 每个工具对应哪个源码文件?
6. LLM 返回 tool_calls 后,OpenCodeReview 如何执行工具?
7. 工具调用日志是从哪里打印出来的?
8. JSON 中 tool_calls 为什么能统计每个工具的调用次数?
9. Review filter removed comment 是什么意思?

二、先回顾一次运行日志

在练习项目中,执行:

cd D:\agent\open-code-review-main\ocr-practice-demo

git status --short

ocr review --preview

ocr review

当前 Git 状态是:

 M s01/src/user.js

说明 s01/src/user.js 文件发生了修改。

执行:

ocr review --preview

输出:

Preview: 1 file(s) changed  |  +4  -1

Will review (1):
  [M]  s01/src/user.js      +4    -1

说明本次 Review 会审查 1 个文件,新增 4 行,删除 1 行。

然后执行正式审查:

ocr review

终端中出现了下面这些工具调用日志:

[ocr] 1 file(s) changed, reviewing 1 in D:\agent\open-code-review-main\ocr-practice-demo
[ocr] Skipping plan phase for s01/src/user.js (5 lines < threshold 50)
[ocr]   ▶ file_read file_path=s01/src/user.js
[ocr]   ✔ file_read (2ms)
[ocr]   ▶ file_read end_line=60 file_path=s01/src/user.js start_line=1
[ocr]   ✔ file_read (1ms)
[ocr]   ▶ code_comment "s01/src/user.js"
[ocr]   ✔ code_comment (0s)
[ocr]   ▶ code_comment "s01/src/user.js"
[ocr]   ✔ code_comment (0s)
[ocr] Review filter removed 1 comment(s) for s01/src/user.js
[ocr] Summary: 1 file(s) reviewed, 2 comment(s), ~21544 token(s) used (input: ~19356, output: ~2188), 51s elapsed

从日志上看,本次终端执行中主要调用了:

file_read
code_comment

同时,在 JSON 输出中,还能看到更多工具调用统计:

{
  "tool_calls": {
    "total": 14,
    "by_tool": {
      "code_comment": 2,
      "code_search": 3,
      "file_find": 1,
      "file_read": 6,
      "file_read_diff": 2
    }
  }
}

需要注意的是:

ocr review

和:

ocr review --format json

是两次独立执行,所以终端日志和 JSON 中的工具调用次数不一定完全一致。

但它们共同说明了一点:

OpenCodeReview 的审查过程不是一次简单的大模型问答,而是包含工具调用的 Agent 流程。

接下来就从源码里追这些工具是怎么来的。


三、从 runReview 找到工具注册入口

第二篇中已经分析过,ocr review 的核心入口是:

runReview(args []string)

为了找到工具注册位置,在源码目录执行:

Select-String -Path .\cmd\opencodereview\review_cmd.go -Pattern "buildToolRegistry"

输出结果:

cmd\opencodereview\review_cmd.go:62:    tools := buildToolRegistry(rt.Collector, fileReader)
cmd\opencodereview\review_cmd.go:183:func buildToolRegistry(collector *tool.CommentCollector, fr *tool.FileReader) *tool.Registry {

这说明在 review_cmd.go 中,runReview 会调用:

tools := buildToolRegistry(rt.Collector, fileReader)

也就是说,正式创建 Agent 之前,OpenCodeReview 会先构建一个工具注册表。

可以先画出当前调用链:

runReview
  ↓
创建 FileReader
  ↓
buildToolRegistry
  ↓
得到 tools
  ↓
agent.New(...)
  ↓
ag.Run(...)

其中 tools 就是后面 Agent 可以调用的工具集合。


四、分析 buildToolRegistry

继续查看 buildToolRegistry 的源码:

Get-Content .\cmd\opencodereview\review_cmd.go | Select-Object -Skip 180 -First 40

源码如下:

func buildToolRegistry(collector *tool.CommentCollector, fr *tool.FileReader) *tool.Registry {
        reg := tool.NewRegistry()
        reg.Register(tool.NewFileRead(fr))
        reg.Register(tool.NewFileFind(fr))
        reg.Register(tool.NewFileReadDiff(tool.DiffMap{}))
        reg.Register(tool.NewCodeSearch(fr))
        reg.Register(&tool.CodeCommentProvider{Collector: collector})
        return reg
}

这段代码非常关键。

它说明 ocr review 注册了 5 个核心工具:

file_read
file_find
file_read_diff
code_search
code_comment

对应的注册代码是:

注册代码 对应工具
tool.NewFileRead(fr) file_read
tool.NewFileFind(fr) file_find
tool.NewFileReadDiff(tool.DiffMap{}) file_read_diff
tool.NewCodeSearch(fr) code_search
tool.CodeCommentProvider{Collector: collector} code_comment

需要注意:buildToolRegistry 只是注册工具的 Go 实现,让程序知道“这个工具被调用时应该执行哪段代码”。

但 LLM 能不能看到这些工具、每个工具有哪些参数、工具在哪个阶段可用,并不是由 buildToolRegistry 决定的,而是由 tools.json 中的 tool schema 决定。


五、工具注册表 Registry 是什么

buildToolRegistry 可以看到,工具注册表是这样创建的:

reg := tool.NewRegistry()

然后通过:

reg.Register(...)

把一个个工具注册进去。

这说明 OpenCodeReview 内部有一个类似“工具容器”的结构:

Registry
  ↓
保存工具名和工具实现的映射关系

Agent 后面如果要调用:

file_read

就可以从这个 Registry 中找到对应的工具实现。

可以简单理解为:

Registry = Agent 的工具箱

工具箱里放了:

file_read
file_find
file_read_diff
code_search
code_comment

Agent 并不是随便调用系统函数,而是只能调用注册表中提供的工具。

这也是 Agent 工程化中很重要的一点:

LLM 不能无限制操作系统;
它只能调用工程代码明确暴露出来的工具。

六、工具注册和 tools.json 不是一回事

这里有一个容易混淆的点:

Registry 决定工具能不能被执行;
tools.json 决定 LLM 能看到哪些工具,以及工具参数 schema 是什么。

从源码看,工具 schema 定义在:

internal/config/toolsconfig/tools.json

里面会定义每个工具的:

name
plan_task
main_task
description
parameters
required

例如:

file_read 的参数是 file_path / start_line / end_line;
code_search 的参数是 search_text / file_patterns / case_sensitive / use_perl_regexp;
file_read_diff 的参数是 path_array;
code_comment 的参数是 comments。

加载链路可以简化为:

loadLLMRuntime
  ↓
toolsconfig.Load
  ↓
读取 embedded tools.json 或 --tools 指定的 JSON
  ↓
ToolDefsByPhase
  ↓
生成 PlanToolDefs / MainToolDefs
  ↓
LLM 请求中携带 Tools

所以完整理解应该分成两层:

工具实现层:buildToolRegistry -> Registry -> Provider.Execute
工具描述层:tools.json -> ToolDef -> LLM tool schema

只有这两层都存在,Agent 工具调用才能真正工作:

LLM 先根据 tools.json 知道有哪些工具可以调用;
当 LLM 返回 tool_calls 后,llmloop 再通过 Registry 找到 Go 代码实现并执行。

七、工具名称定义在哪里

接下来查看工具名称定义文件:

Get-Content .\internal\tool\definitions.go

可以看到:

var (
        Unknown      = Tool{name: "unknown"}
        TaskDone     = Tool{name: "task_done"}
        CodeComment  = Tool{name: "code_comment"}
        FileRead     = Tool{name: "file_read"}
        FileFind     = Tool{name: "file_find"}
        FileReadDiff = Tool{name: "file_read_diff"}
        CodeSearch   = Tool{name: "code_search"}
)

这里集中定义了 OpenCodeReview 中的工具名称。

也就是说:

file_read
file_find
file_read_diff
code_search
code_comment

这些名称都定义在:

internal/tool/definitions.go

这很重要。

因为这些工具名会贯穿多个地方:

1. 工具注册;
2. LLM tool call;
3. 工具执行;
4. 终端日志;
5. JSON tool_calls 统计;
6. Viewer 展示;
7. 测试用例。

所以工具名是 Agent 工具调用链路中的核心标识。


八、Tool 结构体:工具的名称抽象

definitions.go 中,可以看到:

type Tool struct {
        name string
}

这说明一个工具最基础的信息就是它的名字。

例如:

FileRead = Tool{name: "file_read"}

表示定义了一个名为:

file_read

的工具。

同时还有:

func (t Tool) Name() string { return t.name }

也就是说,外部可以通过:

t.Name()

拿到工具名称。

这也是后面日志打印和统计时使用的名称。


九、Provider 接口:所有工具都要实现的能力

definitions.go 中还有一个接口:

type Provider interface {
        // Tool returns which tool this provider implements.
        Tool() Tool
        // Execute runs the tool with the given arguments and returns the result string.
        Execute(ctx context.Context, args map[string]any) (string, error)
}

这个接口非常关键。

它说明所有具体工具都要实现两个方法:

Tool()
Execute(...)

其中:

Tool()

用于告诉系统:

我实现的是哪个工具?

而:

Execute(...)

用于真正执行工具逻辑。

例如:

file_read 的 Execute 负责读取文件;
code_search 的 Execute 负责搜索代码;
code_comment 的 Execute 负责提交评论。

所以可以把 Provider 理解为:

工具实现的统一接口。

只要一个结构体实现了:

Tool() Tool
Execute(ctx context.Context, args map[string]any) (string, error)

它就可以被注册到 Registry 中,供 Agent 调用。


十、Registry:工具名到工具实现的映射

继续看 definitions.go

type Registry struct {
        providers map[string]Provider
        frozen    bool
}

这个结构体中有一个:

providers map[string]Provider

它表示:

工具名 → 工具实现

例如可以理解成:

"file_read"      → FileReadProvider
"file_find"      → FileFindProvider
"code_search"    → CodeSearchProvider
"code_comment"   → CodeCommentProvider

注册工具时,调用的是:

func (r *Registry) Register(p Provider) {
        if r.frozen {
                panic("tool: Register called on frozen registry")
        }
        r.providers[p.Tool().name] = p
}

这里可以看到:

p.Tool().name

就是工具名。

p

就是具体工具实现。

所以 Register 的作用是:

把工具名和工具实现保存到 map 中。

后面执行工具时,可以通过:

func (r *Registry) Get(name string) (Provider, bool) {
        p, ok := r.providers[name]
        return p, ok
}

根据工具名拿到对应实现。

这就是工具调用的基础。


十一、internal/tool 目录中有哪些工具文件

为了看工具实现分布,我执行:

Get-ChildItem .\internal\tool

输出中可以看到很多文件:

code_comment.go
code_search.go
comment_collector.go
definitions.go
filereader.go
file_find.go
file_read.go
file_read_diff.go
response_message.go
stub.go

从文件名就可以大致看出:

文件 作用
definitions.go 定义 Tool、Provider、Registry
file_read.go 实现 file_read
file_find.go 实现 file_find
file_read_diff.go 实现 file_read_diff
code_search.go 实现 code_search
code_comment.go 实现 code_comment
comment_collector.go 收集代码评论
filereader.go 提供文件读取能力
response_message.go 工具调用结果结构
stub.go 测试或占位相关工具

这说明 OpenCodeReview 把 Agent 工具相关逻辑集中放在了:

internal/tool

目录下。


十二、file_read 对应的实现文件

搜索 NewFileRead

Select-String -Path .\internal\tool\*.go -Pattern "NewFileRead"

输出中可以看到:

internal\tool\file_read.go:16:func NewFileRead(fr *FileReader) *FileReadProvider { return &FileReadProvider{FileReader: fr} }

这说明:

file_read 的实现文件是 internal/tool/file_read.go

buildToolRegistry 中注册的是:

reg.Register(tool.NewFileRead(fr))

所以链路是:

buildToolRegistry
  ↓
tool.NewFileRead(fr)
  ↓
FileReadProvider
  ↓
注册为 file_read

运行日志中的:

[ocr]   ▶ file_read file_path=s01/src/user.js
[ocr]   ✔ file_read (2ms)

就是这个工具在执行。

它的作用是读取文件内容,为 Agent 提供代码上下文。

源码里还有几个值得注意的细节:

1. file_read 支持 start_line / end_line,只读取指定行范围;
2. 单次最多返回 500 行,超过会标记 IS_TRUNCATED;
3. workspace 模式从当前工作区磁盘读取文件;
4. commit / range 模式通过 git show <ref>:<path> 读取指定版本的文件。

这说明 file_read 不是简单的 cat 文件,而是会根据 Review 模式读取正确版本的代码。


十三、file_find 对应的实现文件

搜索 NewFileFind

Select-String -Path .\internal\tool\*.go -Pattern "NewFileFind"

输出:

internal\tool\file_find.go:25:func NewFileFind(fr *FileReader) *FileFindProvider { return &FileFindProvider{FileReader: fr} }

这说明:

file_find 的实现文件是 internal/tool/file_find.go

buildToolRegistry 中注册的是:

reg.Register(tool.NewFileFind(fr))

链路是:

buildToolRegistry
  ↓
tool.NewFileFind(fr)
  ↓
FileFindProvider
  ↓
注册为 file_find

file_find 主要用于按文件名关键词查找相关文件。

它底层会优先通过 Git 列出文件:

workspace 模式:git ls-files --cached --others --exclude-standard
range / commit 模式:git ls-tree -r --name-only <ref>

然后根据 query_name 在文件名中做包含匹配,默认大小写不敏感,最多返回 100 个匹配文件。

在小 Demo 中,它的作用可能不明显,但在真实项目里很有用。

比如修改了:

src/user.js

Agent 可能想找:

userService.js
userController.js
user.test.js
userRepository.js

这类相关文件。


十四、file_read_diff 对应的实现文件

搜索 NewFileReadDiff

Select-String -Path .\internal\tool\*.go -Pattern "NewFileReadDiff"

输出:

internal\tool\file_read_diff.go:34:func NewFileReadDiff(dm DiffMap) *FileReadDiffProvider {

这说明:

file_read_diff 的实现文件是 internal/tool/file_read_diff.go

buildToolRegistry 中注册的是:

reg.Register(tool.NewFileReadDiff(tool.DiffMap{}))

file_read_diff 关注的是 Git diff,而不是完整文件。

可以简单区分:

工具 关注点
file_read 文件内容
file_read_diff 本次变更内容

这也是 Code Review 场景中非常重要的区别。

因为审查工具不仅要知道文件现在长什么样,还要知道:

本次到底改了什么。

这里还有一个源码细节:buildToolRegistry 注册 file_read_diff 时传入的是空的 DiffMap

reg.Register(tool.NewFileReadDiff(tool.DiffMap{}))

真正的 diff 内容不是在这里注入的,而是在 Agent.Run 里完成:

Agent.Run
  ↓
loadDiffs
  ↓
injectDiffMap
  ↓
把解析后的每个文件 diff 放入 DiffMap
  ↓
FileReadDiffProvider.SetDiffMap
  ↓
Tools.Freeze

也就是说,file_read_diff 的执行依赖两个阶段:

注册阶段:先注册一个 provider;
运行阶段:加载 Git diff 后再注入真实 DiffMap。

十五、code_search 对应的实现文件

搜索 NewCodeSearch

Select-String -Path .\internal\tool\*.go -Pattern "NewCodeSearch"

输出:

internal\tool\code_search.go:24:func NewCodeSearch(fr *FileReader) *CodeSearchProvider { return &CodeSearchProvider{FileReader: fr} }

这说明:

code_search 的实现文件是 internal/tool/code_search.go

buildToolRegistry 中注册的是:

reg.Register(tool.NewCodeSearch(fr))

code_search 的作用是搜索代码。

源码中它底层主要通过 git grep 实现,支持:

search_text:搜索文本;
file_patterns:限制搜索范围;
case_sensitive:是否大小写敏感;
use_perl_regexp:是否使用 Perl 正则。

默认情况下它使用字面量搜索,并且最多返回 100 条结果,避免一次搜索把上下文撑爆。

例如本次代码里有:

db.query("UPDATE users SET email = '" + email + "' WHERE id = " + userId);

Agent 可能会搜索:

db.query
UPDATE users
userId

它这样做是为了判断:

项目里有没有类似写法;
有没有参数化查询示例;
有没有统一数据库封装;
有没有错误处理模式。

普通 Prompt Review 只能看用户粘贴的代码,而 OpenCodeReview 可以通过 code_search 继续探索代码库上下文。


十六、code_comment 对应的实现文件

搜索 CodeCommentProvider

Select-String -Path .\internal\tool\*.go -Pattern "CodeCommentProvider"

输出中可以看到:

internal\tool\code_comment.go:11:// CodeCommentProvider submits review comments to the per-Agent CommentCollector.
internal\tool\code_comment.go:12:type CodeCommentProvider struct {
internal\tool\code_comment.go:16:func (p *CodeCommentProvider) Tool() Tool { return CodeComment }
internal\tool\code_comment.go:18:func (p *CodeCommentProvider) Execute(_ context.Context, args map[string]any) (string, error) {

这说明:

code_comment 的实现文件是 internal/tool/code_comment.go

注释中有一句非常关键:

CodeCommentProvider submits review comments to the per-Agent CommentCollector.

也就是说,code_comment 并不是读取上下文的工具,而是提交审查评论的工具。

前面的工具主要是:

获取信息

code_comment 是:

输出结果

它会把 Agent 生成的代码评论提交给当前 Agent 的 CommentCollector

不过 code_commentllmloop.executeToolCall 中还有特殊处理,不完全等同于普通工具的 Provider.Execute 路径。

源码里会做几件关键事情:

1. 强制把 path 覆盖为当前正在审查的文件,避免模型写错路径;
2. 解析 comments 参数;
3. 根据 diff 解析 existing_code 对应的行号;
4. 如果定位失败,可能触发 re-location 任务重新定位;
5. 可以通过 CommentWorkerPool 异步处理;
6. 最后写入 CommentCollector。

所以 code_comment 不只是“保存一段文本”,它还承担了评论结构化、定位和收集的职责。

这也解释了为什么运行日志中会出现:

[ocr]   ▶ code_comment "s01/src/user.js"
[ocr]   ✔ code_comment (0s)

它表示 Agent 确认了一个问题,并通过 code_comment 提交了一条审查评论。


十七、工具可以分成两类

结合源码和功能,可以把这几个工具分成两类。

1. 上下文获取工具

file_read
file_find
file_read_diff
code_search

这些工具的作用是帮助 Agent 获取上下文。

它们分别解决:

读取文件内容;
查找相关文件;
读取本次 diff;
搜索相关代码。

2. 评论输出工具

code_comment

这个工具的作用是提交最终审查评论。

可以总结为:

上下文获取工具
  ↓
帮助 Agent 理解代码

评论输出工具
  ↓
帮助 Agent 生成结构化 Review 结果

这也是 OpenCodeReview 具备 Agent 特征的核心原因之一。


十八、LLM 返回 tool_calls 后如何执行工具

前面分析的是工具如何注册。

接下来继续看工具如何被执行。

我搜索了:

Select-String -Path .\internal\**\*.go -Pattern "ToolCall"

输出中可以看到很多关键位置,其中最重要的是:

internal\llmloop\loop.go

里面出现了:

resp.ToolCalls()
executeToolCall
recordToolCall
PrintToolCallStarted
PrintToolCallFinished
PrintToolCallError

这说明工具调用大致发生在 internal/llmloop/loop.go 中。

从搜索结果可以整理出一条链路:

LLM 返回响应
  ↓
resp.ToolCalls()
  ↓
遍历 tool calls
  ↓
executeToolCall(...)
  ↓
tool.OfName(call.Function.Name)
  ↓
判断是否是 task_done / code_comment / 普通工具
  ↓
从 Registry 中找到对应 Provider
  ↓
执行 Provider.Execute(...) 或 code_comment 特殊处理
  ↓
记录工具调用次数
  ↓
打印工具调用日志
  ↓
把工具结果返回给 LLM loop

这里还要注意 task_done

它也定义在工具名称里,并且会暴露给 LLM 使用,但它不是普通 Provider。llmloop 看到 task_done 后会把当前文件任务标记为完成,用来结束本轮工具调用循环。

这就是 Agent 工具调用的核心流程。

可以简单理解为:

LLM 负责决定要调用哪个工具;
OpenCodeReview 负责执行这个工具;
工具执行结果再回到对话上下文中;
LLM 根据结果继续分析或生成评论。

十九、recordToolCall:为什么 JSON 能统计工具次数

搜索结果中可以看到:

internal\llmloop\loop.go:99:// ToolCalls returns a snapshot of the per-tool call counts.
internal\llmloop\loop.go:100:func (r *Runner) ToolCalls() map[string]int64
internal\llmloop\loop.go:110:func (r *Runner) recordToolCall(name string)
internal\llmloop\loop.go:276:   r.recordToolCall(t.Name())

这里说明 llmloop.Runner 内部会记录每个工具的调用次数。

当工具执行时,会调用类似:

r.recordToolCall(t.Name())

其中:

t.Name()

就是工具名称,例如:

file_read
code_search
code_comment

所以 JSON 中才能输出:

{
  "tool_calls": {
    "total": 14,
    "by_tool": {
      "code_comment": 2,
      "code_search": 3,
      "file_find": 1,
      "file_read": 6,
      "file_read_diff": 2
    }
  }
}

也就是说:

JSON 中的 tool_calls 不是事后猜出来的;
而是在 LLM loop 执行工具时逐次记录的。

这让 OpenCodeReview 的 Agent 执行过程具备了可观测性。


二十、工具调用日志从哪里打印

终端中看到的日志是:

[ocr]   ▶ file_read file_path=s01/src/user.js
[ocr]   ✔ file_read (2ms)

搜索结果中可以看到:

internal\telemetry\events.go:84:// PrintToolCallStarted prints a line when a tool begins execution.
internal\telemetry\events.go:96:// PrintToolCallFinished prints a line when a tool finishes successfully.
internal\telemetry\events.go:102:// PrintToolCallError prints a line when a tool fails.

也就是说,终端日志主要来自:

internal/telemetry/events.go

结合搜索结果,在 llmloop 中可以看到相关调用:

telemetry.PrintToolCallStarted(...)
telemetry.PrintToolCallFinished(...)
telemetry.PrintToolCallError(...)

所以日志符号可以这样理解:

日志符号 含义
工具开始调用
工具调用成功
工具调用失败

比如:

[ocr]   ▶ file_read file_path=s01/src/user.js

表示开始调用 file_read

[ocr]   ✔ file_read (2ms)

表示 file_read 执行成功,用时 2ms。

如果工具执行失败,就会出现:

[ocr]   ✘ file_read failed: ...

我之前在错误目录执行 ocr review 时,就遇到过 file_read 失败的问题。

这说明工具日志不仅是展示信息,也能帮助排查 Agent 执行问题。


二十一、工具调用结果如何回到 LLM 对话

搜索 ToolCallResult 时,可以看到:

internal\tool\response_message.go:3:// ToolCallResult holds a single tool call and its execution result.

同时在 llmloop 中还能看到:

llm.NewToolCallMessage(...)
llm.NewToolResultMessage(...)

这说明工具调用不是执行完就结束,而是会把工具结果重新放回 LLM 对话中。

流程可以理解为:

LLM:我要调用 file_read,读取 s01/src/user.js
  ↓
OpenCodeReview:执行 file_read
  ↓
file_read 返回文件内容
  ↓
OpenCodeReview 把工具结果作为 tool result message 返回给 LLM
  ↓
LLM 基于文件内容继续分析

这就是典型的工具调用型 Agent 流程。

普通 Prompt Review 中,模型只能看一开始输入的内容。

而在这里,模型可以通过工具逐步获取信息。


二十二、Agent 工具调用完整流程图

结合目前看到的源码,可以整理出一张完整流程图:

runReview
  ↓
loadLLMRuntime
  ↓
tools.json -> PlanToolDefs / MainToolDefs
  ↓
创建 FileReader
  ↓
buildToolRegistry
  ↓
注册工具实现:
  - file_read
  - file_find
  - file_read_diff
  - code_search
  - code_comment
  ↓
agent.New(...)
  ↓
ag.Run(...)
  ↓
loadDiffs
  ↓
injectDiffMap 注入 file_read_diff 的真实 diff 数据
  ↓
Tools.Freeze
  ↓
LLM 请求携带 MainToolDefs
  ↓
LLM 生成 tool_calls
  ↓
llmloop.Runner 读取 resp.ToolCalls()
  ↓
executeToolCall(...)
  ↓
处理 task_done / code_comment / 普通工具
  ↓
Registry.Get(toolName)
  ↓
Provider.Execute(...) 或 code_comment 特殊处理
  ↓
recordToolCall(toolName)
  ↓
telemetry 打印工具调用日志
  ↓
工具结果返回给 LLM
  ↓
LLM 继续分析
  ↓
code_comment 提交评论
  ↓
CommentCollector 收集评论
  ↓
Review filter 校验并移除可证明错误的评论
  ↓
输出终端结果或 JSON

这张图是本文最重要的源码理解。

它说明 OpenCodeReview 的 Agent 工具调用并不是抽象概念,而是由几个明确的源码模块协作完成:

review_cmd.go
  负责注册工具实现

internal/config/toolsconfig/tools.json
  负责定义暴露给 LLM 的工具 schema

internal/tool
  负责定义和实现工具 Provider

internal/llmloop
  负责执行工具调用循环

internal/telemetry
  负责打印工具调用日志和记录指标

CommentCollector
  负责收集 code_comment 提交的评论

二十三、Review filter removed comment 是什么

本次终端日志中还有一行:

[ocr] Review filter removed 1 comment(s) for s01/src/user.js

为了定位它,我搜索:

Select-String -Path .\internal\**\*.go -Pattern "Review filter removed"

输出结果:

internal\agent\agent.go:540:    fmt.Fprintf(stdout.Writer(), "[ocr] Review filter removed %d comment(s) for %s\n", len(indices), newPath)

这说明这条日志来自:

internal/agent/agent.go

它表示:

Agent 原本生成了一些评论,
但是 Review filter 过滤掉了其中一部分。

为什么要过滤?

这里需要说得更准确一点:Review filter 不是普通去重器,也不是泛化的质量评分器。

源码注释里的语义是:

移除那些仅根据当前 diff 就能证明是错误的评论。

它会把当前文件的 diff 和已经生成的评论再次交给 LLM,让它返回需要删除的评论编号。

简化流程是:

executeReviewFilter
  ↓
读取当前文件 comments
  ↓
把 diff 和 comments 组装成 ReviewFilterTask prompt
  ↓
LLM 返回要移除的评论 ID 列表
  ↓
parseFilterResponse
  ↓
CommentCollector.RemoveByPathAndIndices

这一步体现了 OpenCodeReview 的工程化设计:

不是 LLM 生成什么就全部输出,
而是增加了一层基于 diff 的结果校验。

二十四、结合本次 Review 评论理解 code_comment

本次终端执行最终输出了 2 条评论。

第一条是 SQL 注入问题:

s01/src/user.js:37-37

指出:

email 和 userId 被直接拼接到 SQL 查询字符串中,存在 SQL 注入风险。

建议改成:

db.query("UPDATE users SET email = ? WHERE id = ?", [email, userId]);

第二条是异步和错误处理问题:

s01/src/user.js:36-39

指出:

db.query 可能是异步操作;
当前函数没有 await;
没有 try/catch;
可能在数据库操作失败时仍然返回 true。

建议改成:

async function updateUserEmail(db, userId, email) {
  try {
    await db.query("UPDATE users SET email = ? WHERE id = ?", [email, userId]);
    return true;
  } catch (error) {
    console.error('Failed to update user email:', error);
    throw new Error('Failed to update user email');
  }
}

从源码角度看,这些最终评论不是普通文本随便输出的,而是通过:

code_comment
  ↓
CommentCollector
  ↓
Review filter
  ↓
emitRunResult

这样的链路进入最终结果。


二十五、普通 Prompt Review 和 OpenCodeReview 的区别

通过这一篇源码分析,可以更清楚地区分普通 Prompt Review 和 OpenCodeReview。

对比项 普通 Prompt Review OpenCodeReview
输入 用户手动复制代码 Git diff 自动识别
规则 临时写在 Prompt 中 .opencodereview/rule.json
上下文 只能看用户粘贴内容 可通过工具读取文件、diff、搜索代码
工具注册 没有明确工具注册表 buildToolRegistry 注册 Provider
工具描述 没有结构化 schema tools.json 定义工具参数和阶段
工具实现 没有工程实现 internal/tool 中有具体 Provider
工具执行 不存在 llmloop 执行 tool calls
日志 不透明 telemetry 输出工具调用日志
统计 很难统计 JSON 中有 tool_calls
评论收集 普通文本 CommentCollector 收集结构化评论
评论过滤 通常没有 Review filter 过滤低质量评论

所以 OpenCodeReview 更像是:

确定性工程 + Agent 工具调用 + LLM 分析 + 评论过滤 + 结构化输出

而不是一个简单的大模型脚本。


二十六、本篇使用的 PowerShell 命令记录

1. 搜索工具注册入口

Select-String -Path .\cmd\opencodereview\review_cmd.go -Pattern "buildToolRegistry"

2. 查看工具注册函数

Get-Content .\cmd\opencodereview\review_cmd.go | Select-Object -Skip 180 -First 40

3. 查看工具名称定义

Get-Content .\internal\tool\definitions.go

4. 查看工具目录

Get-ChildItem .\internal\tool

5. 查看工具 schema 配置

Get-Content .\internal\config\toolsconfig\tools.json
Get-Content .\internal\config\toolsconfig\toolsconfig.go

6. 搜索各工具实现

Select-String -Path .\internal\tool\*.go -Pattern "NewFileRead"
Select-String -Path .\internal\tool\*.go -Pattern "NewFileFind"
Select-String -Path .\internal\tool\*.go -Pattern "NewFileReadDiff"
Select-String -Path .\internal\tool\*.go -Pattern "NewCodeSearch"
Select-String -Path .\internal\tool\*.go -Pattern "CodeCommentProvider"

7. 搜索工具调用链路

Select-String -Path .\internal\**\*.go -Pattern "ToolCall"

8. 搜索 file_read 相关日志和测试

Select-String -Path .\internal\**\*.go -Pattern "file_read"

9. 搜索 Review filter 日志

Select-String -Path .\internal\**\*.go -Pattern "Review filter removed"

二十七、本篇总结

通过这一篇,对 OpenCodeReview 的 Agent 工具调用有了更深入的理解。

第一篇中,从运行日志看到:

file_read
file_find
code_search
code_comment

而这一篇通过源码进一步确认:

这些工具是在 review_cmd.go 的 buildToolRegistry 中注册 Provider 实现的;
工具名称定义在 internal/tool/definitions.go;
工具 schema 定义在 internal/config/toolsconfig/tools.json;
工具实现分布在 internal/tool 目录下;
所有普通工具都实现 Provider 接口;
Registry 负责保存工具名和工具实现的映射;
file_read_diff 会在 Agent.Run 加载 diff 后通过 injectDiffMap 注入真实 diff;
LLM 返回 tool_calls 后,由 llmloop 执行工具;
task_done 和 code_comment 在 llmloop 中有特殊处理;
工具执行时会通过 telemetry 打印日志;
工具调用次数会通过 recordToolCall 统计;
code_comment 会把评论提交给 CommentCollector;
Review filter 会移除那些仅根据 diff 就能证明错误的评论。

当前对 ocr review 的理解可以串成下面这条主线:

Git diff
  ↓
确定审查文件

rule.json
  ↓
确定审查规则

buildToolRegistry
  ↓
注册 Agent 工具实现

tools.json
  ↓
定义 LLM 可见的工具 schema

llmloop
  ↓
执行 LLM 返回的 tool_calls

internal/tool
  ↓
真正读取文件、搜索代码、提交评论

telemetry
  ↓
打印工具调用日志

CommentCollector
  ↓
收集评论

Review filter
  ↓
移除可由 diff 证明错误的评论

emitRunResult
  ↓
输出文本或 JSON

这说明 OpenCodeReview 的审查流程不是:

diff → LLM → 评论

而更接近:

diff → rules → tools → agent loop → comments → filter → output

二十八、下一篇计划:JSON 输出结构与 Markdown 报告设计

下一篇准备继续学习:

从 0 学习 Alibaba OpenCodeReview:JSON 输出结构与 Markdown 报告设计

前面几篇已经知道:

Git diff 决定审查哪些文件;
rule.json 决定按什么规则审查;
Agent 工具调用负责获取上下文和生成评论。

接下来要继续分析:

OpenCodeReview 的审查结果是如何被结构化输出的?

在第一篇和第五篇中,我都使用过:

ocr review --format json > ocr-review-result.json

下一篇会重点分析:

1. JSON 输出中的 status、summary、tool_calls、comments;
2. comments 中 path、start_line、end_line、existing_code 的作用;
3. tool_calls 如何支撑 Agent 可观测性;
4. summary 中 token 和 elapsed 如何反映成本;
5. 如何把 OCR JSON 转换成 Markdown Report;
6. 这一步如何为后续 Code Review Agent MVP 做准备。

如果说第五篇解决的是:

Agent 工具是如何注册和调用的?

那么第六篇要解决的是:

Agent 审查结果如何被结构化保存,并为后续工程化处理做准备?
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐