最近AI vibe coding多了发现一些问题,其中包括AI生成代码的review问题。目前我对于AI代码的态度偏谨慎,大部分AI代码会review一遍,但毕竟人工review费时费力,代码量大了之后review就更痛苦了。
于是想到了之前博文提到的让AI去review的方式,尝试了在AI coding ide上通过skill来review,发现效果不太好,最近看到阿里发布了open-code-review的开源智能体,试了下好奇这个agent的逻辑,便写篇文章分析下。
项目地址: open-code-review ,分支main,当前版本commit 1c8f930,go语言编写,以下简称ocr,官方介绍:
Open Code Review 是一款 AI 驱动的代码审查 CLI 工具。它的前身是阿里集团内部官方 AI 代码审查助手,过去两年在内部服务了数万开发者,识别了数百万个代码缺陷。经过大规模充分验证后,我们将其孵化为开源项目,对社区开放。只需配置一个模型端点即可使用。
它读取 Git diff,通过具备工具调用能力的 Agent 将变更文件发送至可配置的 LLM,生成具有行级精度的结构化审查意见。Agent 可以读取完整文件内容、搜索代码库、检查其他变更文件以获取上下文,从而进行深度审查——而非仅停留在表面的 diff 反馈。除了 diff 审查,ocr scan 可以审查整个文件,适用于审计不熟悉的代码库或没有有意义 diff 的目录。
源码解析
大致走一遍代码:
main
cmd/opencodereview/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { llm.AppVersion = Version llm.InitEmbeddedLoader() ctx := context.Background() if telemetry.Init(ctx) { defer telemetry.ShutdownWithTimeout(ctx, 5 *time.Second) } if err := dispatch(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n" , err) os.Exit(1 ) } }
InitEmbeddedLoader初始化内置分词器,分词器用于估算token量。
telemetry.Init(ctx)初始化opentelemetry,用于后续上报trace数据。
dispatch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 switch args[0 ] {case "--version" , "-V" : printVersion() return nil case "version" : printVersion() return nil case "review" , "r" : return runReview(args[1 :]) case "scan" , "s" : return runScan(args[1 :]) case "config" : return runConfig(args[1 :]) case "llm" : return runLLM(args[1 :]) case "rules" : return runRules(args[1 :]) case "viewer" : return runViewer(args[1 :]) case "delegate" , "d" : return runDelegate(args[1 :]) case "session" , "sessions" : return runSession(args[1 :]) case "-h" , "--help" : printTopLevelUsage() return nil default : return fmt.Errorf("unknown command: %s\nRun 'ocr' for usage" , args[0 ]) }
根据命令行参数选择执行命令,这里我们分析runReview
runReview
cmd/opencodereview/review_cmd.go
1 2 3 4 func runReview (args []string ) error { cc, err := loadCommonContext(opts.repoDir, opts.rulePath, opts.maxTools, opts.maxGitProcs, true )
loadCommonContext准备通用上下文,包含提示词模板、代码目录准备、审查规则、git命令执行器
loadCommonContext
cmd/opencodereview/shared.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func loadCommonContext (repoDirInput, rulePath string , maxTools, maxGitProcs int , requireGit bool ) (*commonContext, error ) { tpl, err := template.LoadDefault() repoDir, isGit, err := resolveWorkingDir(repoDirInput, requireGit) resolver, fileFilter, err := rules.NewResolver(repoDir, rulePath) return &commonContext{ Template: tpl, RepoDir: repoDir, Resolver: resolver, FileFilter: fileFilter, GitRunner: gitcmd.New(maxGitProcs), IsGitRepo: isGit, }, nil }
template.LoadDefault
internal/config/template/template.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func LoadDefault () (*Template, error ) { data, err := templateFS.ReadFile("task_template.json" ) var m templateManifest if err := json.Unmarshal(data, &m); err != nil { return nil , fmt.Errorf("unmarshal task_template manifest: %w" , err) } var tpl Template tpl.MaxTokens = m.MaxTokens tpl.MaxToolRequestTimes = m.MaxToolRequestTimes tpl.PlanModeLineThreshold = m.PlanModeLineThreshold if tpl.MainTask, err = resolveConversation(m.MainTask); err != nil { return nil , fmt.Errorf("MAIN_TASK: %w" , err) } if tpl.PlanTask, err = resolveOptionalConversation(m.PlanTask, "PLAN_TASK" ); err != nil { return nil , err } if tpl.MemoryCompressionTask, err = resolveConversation(m.MemoryCompressionTask); err != nil { return nil , fmt.Errorf("MEMORY_COMPRESSION_TASK: %w" , err) } if tpl.ReLocationTask, err = resolveOptionalConversation(m.ReLocationTask, "RE_LOCATION_TASK" ); err != nil { return nil , err } if tpl.ReviewFilterTask, err = resolveOptionalConversation(m.ReviewFilterTask, "REVIEW_FILTER_TASK" ); err != nil { return nil , err } return &tpl, nil }
加载任务模板,task_template.json在源码目录internal/config/template/task_template.json,内容为json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 { "MAIN_TASK" : { "timeout" : 120 , "messages" : [ { "role" : "system" , "prompt_file" : "main_task_system.md" } , { "role" : "user" , "prompt_file" : "main_task_user.md" } ] } , "PLAN_TASK" : { "timeout" : 180 , "messages" : [ { "role" : "system" , "prompt_file" : "plan_task_system.md" } , { "role" : "user" , "prompt_file" : "plan_task_user.md" } ] } , "MEMORY_COMPRESSION_TASK" : { "timeout" : 120 , "messages" : [ { "role" : "system" , "prompt_file" : "memory_compression_task_system.md" } , { "role" : "user" , "prompt_file" : "memory_compression_task_user.md" } ] } , "REVIEW_FILTER_TASK" : { "timeout" : 180 , "messages" : [ { "role" : "system" , "prompt_file" : "review_filter_task_system.md" } , { "role" : "user" , "prompt_file" : "review_filter_task_user.md" } ] } , "RE_LOCATION_TASK" : { "timeout" : 180 , "messages" : [ { "role" : "system" , "prompt_file" : "re_location_task_system.md" } , { "role" : "user" , "prompt_file" : "re_location_task_user.md" } ] } , "MAX_TOOL_REQUEST_TIMES" : 30 , "PLAN_MODE_LINE_THRESHOLD" : 50 , "MAX_TOKENS" : 58888 }
template.LoadDefault即是解析上述配置,加载各任务的提示词模板,plan_task是规划用任务,main_task是主review任务,memory_compression_task是上下文压缩任务,review_filter_task是最终review llm输出的任务,prompt文件位于源码目录internal/config/template/prompts下,解析后的go数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type LlmConversation struct { Timeout int `json:"timeout"` Messages []ChatMessage `json:"messages"` } type Template struct { MainTask LlmConversation `json:"MAIN_TASK"` PlanTask *LlmConversation `json:"PLAN_TASK,omitempty"` MemoryCompressionTask LlmConversation `json:"MEMORY_COMPRESSION_TASK"` MaxTokens int `json:"MAX_TOKENS"` MaxToolRequestTimes int `json:"MAX_TOOL_REQUEST_TIMES"` PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"` ReLocationTask *LlmConversation `json:"RE_LOCATION_TASK,omitempty"` ReviewFilterTask *LlmConversation `json:"REVIEW_FILTER_TASK,omitempty"` }
loadCommonContext其他内容不再赘述。
applyCLIExcludes
1 2 3 func runReview (args []string ) error { applyCLIExcludes(cc, splitPaths(opts.excludes))
将命令行参数exclude追加到commonContext的fileFilter中
validateReviewRefs
1 2 3 func runReview (args []string ) error { if err := validateReviewRefs(cc.RepoDir, opts); err != nil {
ocr可以指定review的git commit,这里校验其有效性
getCommitMessage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func runReview (args []string ) error { if opts.commit != "" && opts.background == "" { if msg, err := getCommitMessage(cc.RepoDir, opts.commit); err == nil && msg != "" { opts.background = msg } } if opts.backgroundFile != "" { bgPath := resolveBackgroundFilePath(cc.RepoDir, opts.backgroundFile) fileBackground, err := loadBackgroundFile(bgPath) if err != nil { return err } opts.background = mergeBackground(opts.background, fileBackground) }
background最终会被替换到提示词中,这里将commit的注释和用户指定bgfile读取
runPreview
1 2 3 4 5 func runReview (args []string ) error { if opts.preview { return runPreview(cc, opts) }
preview模式时,runPreview仅打印要review的文件,不做实际llm交互
loadReviewResumeState
1 2 3 func runReview (args []string ) error { resumeState, err := loadReviewResumeState(cc.RepoDir, opts)
断点续审,读取上次的审查状态
loadLLMRuntime
1 2 3 func runReview (args []string ) error { rt, err := loadLLMRuntime(cc.Template, opts.toolConfigPath, opts.model)
cmd/opencodereview/shared.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func loadLLMRuntime (tpl *template.Template, toolConfigPath, modelOverride string ) (*llmRuntime, error ) { toolEntries, err := toolsconfig.Load(toolConfigPath) if err != nil { return nil , fmt.Errorf("load tools: %w" , err) } planToolDefs := agent.BuildToolDefs(toolEntries, true ) mainToolDefs := agent.BuildToolDefs(toolEntries, false ) appCfg, err := LoadAppConfig(cfgPath) var lang string if appCfg != nil { lang = appCfg.Language } tpl.ApplyLanguage(lang) ep, err := llm.ResolveEndpointWithModelOverride(cfgPath, modelOverride) return &llmRuntime{ Client: llm.NewLLMClient(ep), Model: ep.Model, PlanToolDefs: planToolDefs, MainToolDefs: mainToolDefs, Collector: tool.NewCommentCollector(), AppCfg: appCfg, }, nil }
BuildToolDefs读取工具的定义,这里的ToolDefs只有工具的描述信息;LoadAppConfig加载配置;ApplyLanguage会根据语言配置给模板提示词追加一句"\n\nAlways respond in ";ResolveEndpointWithModelOverride解析实际的llm api endpoint。
1 2 3 4 5 6 7 8 9 10 11 func runReview (args []string ) error { mode := tool.ParseReviewMode(opts.from, opts.to, opts.commit) ref, _ := mode.RefValue(opts.to, opts.commit) fileReader := &tool.FileReader{ RepoDir: cc.RepoDir, Mode: mode, Ref: ref, Runner: cc.GitRunner, } tools := buildToolRegistry(rt.Collector, fileReader)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const ( ModeWorkspace ReviewMode = iota ModeRange ModeCommit ) func ParseReviewMode (from, to, commit string ) ReviewMode { if commit != "" { return ModeCommit } if from != "" && to != "" { return ModeRange } return ModeWorkspace }
目前ocr支持三种review模式,指定单次commit,指定范围commit,或者针对当前项目。
1 2 3 4 5 6 7 8 9 10 func (m ReviewMode) RefValue(toRef, commit string ) (string , bool ) { switch m { case ModeRange: return toRef, true case ModeCommit: return commit, true default : return "" , false } }
RefValue没啥说的,最后整合成为FileReader结构传入buildToolRegistry
1 2 3 4 5 6 7 8 9 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 }
内置5个工具,代码位于internal/tool/:
FileRead — 通过git或者文件系统读取文件内容
FileFind — 通过git ls查找文件
FileReadDiff — 读取指定文件的diff
CodeSearch — grep代码内容搜索
CodeComment — 提交审查评论
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (r *Registry) Register(p Provider) { if r.frozen { panic ("tool: Register called on frozen registry" ) } r.providers[p.Tool().name] = p } type Tool struct { name string } type Provider interface { Tool() Tool Execute(ctx context.Context, args map [string ]any) (string , error ) }
Execute即是工具的入口函数,其执行结果会放入字符串返回给llm。
initMCPClients
1 2 3 4 5 6 7 8 9 10 func runReview (args []string ) error { mcpClients := initMCPClients(context.Background(), rt.AppCfg, tools, cc.RepoDir, Version) defer func () { for _, mc := range mcpClients { if err := mc.Close(); err != nil { fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to close MCP server %q: %v\n" , mc.Name(), err) } } }()
初始化MCP客户端,如果配置有命令行的mcp server也会启动
1 2 3 4 5 func runReview (args []string ) error { mcpToolDefs := mcp.CollectToolDefs(mcpClients, tools) rt.PlanToolDefs = append (rt.PlanToolDefs, mcpToolDefs...) rt.MainToolDefs = append (rt.MainToolDefs, mcpToolDefs...)
这里将mcp工具转化为了tool的定义并合并入了llmRuntime的PlanToolDefs和MainToolDefs中。
agent.New
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func runReview (args []string ) error { ag := agent.New(agent.Args{ RepoDir: cc.RepoDir, From: opts.from, To: opts.to, Commit: opts.commit, ReviewMode: reviewModeFromOptions(opts), Template: *cc.Template, SystemRule: cc.Resolver, FileFilter: cc.FileFilter, LLMClient: rt.Client, Tools: tools, PlanToolDefs: rt.PlanToolDefs, MainToolDefs: rt.MainToolDefs, CommentCollector: rt.Collector, CommentWorkerPool: agent.NewCommentWorkerPool(opts.concurrency), MaxConcurrency: opts.concurrency, ConcurrentTaskTimeout: opts.perFileTimeout, Model: rt.Model, Background: opts.background, GitRunner: cc.GitRunner, Resume: resumeState, })
构造Agent结构
ag.Run
1 2 3 4 5 6 7 8 9 10 11 func runReview (args []string ) error { comments, err := ag.Run(ctx) if err != nil { span.SetStatus(codes.Error, err.Error()) span.RecordError(err) if id := ag.SessionID(); id != "" { fmt.Fprintf(os.Stderr, "[ocr] Session: %s (retry with: --resume %s)\n" , id, id) } return fmt.Errorf("review failed: %w" , err) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func (a *Agent) Run(ctx context.Context) ([]model.LlmComment, error ) { if err := a.loadDiffs(ctx); err != nil { diffSpan.End() return nil , fmt.Errorf("load diffs: %w" , err) } a.injectDiffMap() a.args.Tools.Freeze() totalChanged := len (a.diffs) reviewCount := a.countReviewable(a.diffs) fmt.Fprintf(stdout.Writer(), "[ocr] %d file(s) changed, reviewing %d in %s\n" , totalChanged, reviewCount, a.args.RepoDir) a.diffs = a.filterDiffs(a.diffs) comments, err := a.dispatchSubtasks(ctx) if len (comments) > 0 { telemetry.RecordCommentsGenerated(ctx, int64 (len (comments))) } a.session.Finalize() return comments, err }
loadDiffs
1 2 3 4 5 6 7 8 9 10 11 12 13 func (a *Agent) loadDiffs(ctx context.Context) error { var provider *diff.Provider switch { case a.args.Commit != "" : provider = diff.NewCommitProvider(a.args.RepoDir, a.args.Commit, a.args.GitRunner) case a.args.From != "" && a.args.To != "" : provider = diff.NewProvider(a.args.RepoDir, a.args.From, a.args.To, a.args.GitRunner) default : provider = diff.NewWorkspaceProvider(a.args.RepoDir, a.args.GitRunner) } parsed, err := provider.GetDiff(ctx)
根据当前场景执行git命令(GitRunner来自agent亦即commonContext)获取文件diff
injectDiffMap
将此次review的diff注入回FileReadDiff工具里,供下次FileReadDiff工具调用时使用;注入后,a.args.Tools.Freeze()工具被Freeze
countReviewable && filterDiffs
根据用户配置的规则来决定是否review文件,countReviewable只计算数量,filterDiffs剔除不需要review的文件
dispatchSubtasks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 func (a *Agent) dispatchSubtasks(ctx context.Context) ([]model.LlmComment, error ) { a.diffs = a.filterLargeDiffs(a.diffs) toDispatch := a.applyResume(a.diffs) var wg sync.WaitGroup concurrency := a.args.MaxConcurrency if concurrency <= 0 { concurrency = 8 } sem := make (chan struct {}, concurrency) timeout := time.Duration(a.args.ConcurrentTaskTimeout) * time.Minute var dispatched int64 for i := range toDispatch { if toDispatch[i].IsDeleted { continue } dispatched++ wg.Add(1 ) sem <- struct {}{} go func (d model.Diff) { fingerprint := reviewItemFingerprint(a.reviewMode(), d) defer wg.Done() defer func () { <-sem }() defer func () { if r := recover (); r != nil { atomic.AddInt64(&a.subtaskFailed, 1 ) a.session.RecordReviewItemFailed(d.NewPath, d.OldPath, d.NewPath, fingerprint, fmt.Sprintf("panic: %v" , r)) fmt.Fprintf(stdout.Writer(), "[ocr] Subtask panic for %s: %v\n%s\n" , d.NewPath, r, debug.Stack()) a.recordWarning("subtask_error" , d.NewPath, fmt.Sprintf("panic: %v" , r)) } }() var fileCtx context.Context var cancel context.CancelFunc if timeout > 0 { fileCtx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } else { fileCtx = ctx } completed, skipReason, err := a.executeSubtask(fileCtx, d) if err != nil { } if !completed { if skipReason != "" { a.session.RecordReviewItemFailed(d.NewPath, d.OldPath, d.NewPath, fingerprint, skipReason) } return } comments := a.args.CommentCollector.CommentsForPath(d.NewPath) a.session.RecordReviewItemDone(d.NewPath, d.OldPath, d.NewPath, fingerprint, comments) }(toDispatch[i]) } wg.Wait() if dispatched == 0 { return a.args.CommentCollector.Comments(), nil } if a.args.CommentWorkerPool != nil { a.args.CommentWorkerPool.Await() } failed := atomic.LoadInt64(&a.subtaskFailed) if failed > 0 && failed == dispatched { return nil , fmt.Errorf("all %d file review(s) failed — check your LLM configuration and API key" , dispatched) } return a.args.CommentCollector.Comments(), nil }
dispatchSubtasks内给每个待review文件建立gorotine
executeSubtask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 func (a *Agent) executeSubtask(ctx context.Context, d model.Diff) (bool , string , error ) { newPath := d.NewPath changeFilesExcludingCurrent := a.buildChangeFilesExcept(newPath) rule := a.resolveSystemRule(strings.ToLower(newPath)) threshold := a.args.Template.PlanModeLineThreshold changeLines := d.Insertions + d.Deletions var planResult string if a.args.Template.PlanTask != nil && len (a.args.Template.PlanTask.Messages) > 0 && threshold > 0 && changeLines < int64 (threshold) { fmt.Fprintf(stdout.Writer(), "[ocr] Skipping plan phase for %s (%d lines < threshold %d)\n" , newPath, changeLines, threshold) } else if a.args.Template.PlanTask != nil && len (a.args.Template.PlanTask.Messages) > 0 { var err error planResult, err = a.executePlanPhase(ctx, newPath, d.Diff, changeFilesExcludingCurrent, rule) if err != nil { fmt.Fprintf(stdout.Writer(), "[ocr] Plan phase failed for %s: %v (continuing without plan)\n" , newPath, err) planResult = "" } } if len (a.args.Template.MainTask.Messages) == 0 { return false , "" , fmt.Errorf("main_task.messages is empty in template" ) } rawMsgs := a.args.Template.MainTask.Messages messages := make ([]llm.Message, 0 , len (rawMsgs)) for _, m := range rawMsgs { content := m.Content content = strings.ReplaceAll(content, "{{current_system_date_time}}" , a.currentDate) content = strings.ReplaceAll(content, "{{current_file_path}}" , newPath) content = strings.ReplaceAll(content, "{{system_rule}}" , rule) content = strings.ReplaceAll(content, "{{change_files}}" , changeFilesExcludingCurrent) content = strings.ReplaceAll(content, "{{diff}}" , d.Diff) content = strings.ReplaceAll(content, "{{requirement_background}}" , a.args.Background) if planResult == "" { content = stripEmptyPlanBlock(content) } content = strings.ReplaceAll(content, "{{plan_guidance}}" , planResult) messages = append (messages, llm.NewTextMessage(m.Role, content)) } tokenCount := llmloop.CountMessagesTokens(messages) maxAllowed := a.args.Template.MaxTokens tokenLimit := llmloop.PromptTokenLimit(maxAllowed) if tokenCount > tokenLimit { msg := fmt.Sprintf("prompt tokens (%d) exceed %d%% of max_tokens(%d)" , tokenCount, 80 , maxAllowed) fmt.Fprintf(stdout.Writer(), "[ocr] WARNING: %s for %s\n" , msg, newPath) a.recordWarning("token_threshold_exceeded" , newPath, msg) return false , msg, nil } mainCompleted, err := func () (bool , error ) { completed, err := a.runner.RunPerFile(ctx, messages, newPath) if err != nil { mainSpan.SetStatus(codes.Error, err.Error()) mainSpan.RecordError(err) return false , err } return completed, nil }() if err == nil { if a.args.CommentWorkerPool != nil { a.args.CommentWorkerPool.AwaitKey(newPath) } a.executeReviewFilter(ctx, d, newPath) } if err != nil { return false , "" , err } if !mainCompleted { return false , "main_task did not complete before stopping" , nil } return true , "" , nil }
executePlanPhase
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func (a *Agent) executePlanPhase(ctx context.Context, newPath, rawDiff, changeFiles, rule string ) (string , error ) { pt := a.args.Template.PlanTask messages := make ([]llm.Message, 0 , len (pt.Messages)) for _, m := range pt.Messages { content := m.Content content = strings.ReplaceAll(content, "{{current_system_date_time}}" , a.currentDate) content = strings.ReplaceAll(content, "{{current_file_path}}" , newPath) content = strings.ReplaceAll(content, "{{system_rule}}" , rule) content = strings.ReplaceAll(content, "{{change_files}}" , changeFiles) content = strings.ReplaceAll(content, "{{diff}}" , rawDiff) content = strings.ReplaceAll(content, "{{requirement_background}}" , a.args.Background) content = strings.ReplaceAll(content, "{{plan_tools}}" , formatToolDefs(a.args.PlanToolDefs)) messages = append (messages, llm.NewTextMessage(m.Role, content)) } fs := a.session.GetOrCreateFileSession(newPath) rec := fs.AppendTaskRecord(session.PlanTask, messages) startTime := time.Now() resp, err := a.args.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ Model: a.args.Model, Messages: messages, MaxTokens: a.args.Template.MaxTokens, }) duration := time.Since(startTime) if err != nil { rec.SetError(err, duration) span.SetStatus(codes.Error, err.Error()) span.RecordError(err) return "" , fmt.Errorf("plan request: %w" , err) } var totalTokens int64 if resp.Usage != nil { totalTokens = resp.Usage.TotalTokens } rec.SetResponse(resp, duration) a.runner.RecordUsage(resp.Usage) fmt.Fprintf(stdout.Writer(), "[ocr] Plan completed for %s\n" , newPath) return resp.Content(), nil }
与大模型交互执行提示词模板中的PlanTask部分,plantask的主要功能是review的规划。
RunPerFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 func (r *Runner) RunPerFile(ctx context.Context, messages []llm.Message, newPath string ) (bool , error ) { toolReqCount := r.deps.Template.MaxToolRequestTimes const maxConsecutiveEmptyRounds = 3 consecutiveEmptyRounds := 0 sessionID := uuid.NewString() st := &compressionState{} defer r.cancelPendingCompression(st) for toolReqCount > 0 { select { case <-ctx.Done(): return false , ctx.Err() default : } toolReqCount-- fs := r.deps.Session.GetOrCreateFileSession(ewPath) rec := fs.AppendTaskRecord(session.MainTask, append ([]llm.Message(nil ), messages...)) startTime := time.Now() resp, err := r.deps.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ Model: r.deps.Model, Messages: messages, Tools: r.deps.MainToolDefs, MaxTokens: r.deps.Template.MaxTokens, SessionID: sessionID, }) duration := time.Since(startTime) if err != nil { rec.SetError(err, duration) return false , fmt.Errorf("LLM completion error: %w" , err) } rec.SetResponse(resp, duration) totalTokens := int64 (0 ) if resp.Usage != nil { totalTokens = resp.Usage.TotalTokens atomic.AddInt64(&r.totalInputTokens, resp.Usage.PromptTokens) atomic.AddInt64(&r.totalOutputTokens, resp.Usage.CompletionTokens) atomic.AddInt64(&r.totalCacheReadTokens, resp.Usage.CacheReadTokens) atomic.AddInt64(&r.totalCacheWriteTokens, resp.Usage.CacheWriteTokens) } content := resp.Content() calls := resp.ToolCalls() if len (calls) == 0 { fmt.Fprintf(stdout.Writer(), "[ocr] No tool calls parsed for %s, retrying...\n" , newPath) messages = append (messages, llm.NewTextMessage("user" , "You did not successfully call any tools. Please try again or use task_done if finished." )) if content != "" { messages = append (messages[:len (messages)-1 ], llm.NewTextMessage("assistant" , content), messages[len (messages)-1 ]) } continue } var results []tool.ToolCallResult taskCompleted := false hasValidResult := false for _, call := range calls { cp := r.executeToolCall(ctx, newPath, call, rec) if cp.Completed { results = append (results, tool.ToolCallResult{ ToolCallID: call.ID, Name: call.Function.Name, Result: "Task completed successfully." , }) taskCompleted = true } else if cp.Data != "" { results = append (results, tool.ToolCallResult{ ToolCallID: call.ID, Name: call.Function.Name, Result: cp.Data, }) hasValidResult = true } else { results = append (results, tool.ToolCallResult{ ToolCallID: call.ID, Name: call.Function.Name, Result: "Error: Tool execution returned no result." , }) } } if taskCompleted { return true , nil } if !hasValidResult { consecutiveEmptyRounds++ if consecutiveEmptyRounds >= maxConsecutiveEmptyRounds { fmt.Fprintf(stdout.Writer(), "[ocr] Too many empty retries for %s, stopping.\n" , newPath) break } fmt.Fprintf(stdout.Writer(), "[ocr] No valid tool results for %s, retrying...\n" , newPath) } else { consecutiveEmptyRounds = 0 } succeed := r.addNextMessage(ctx, content, calls, results, &messages, newPath, st) if !succeed { fmt.Fprintf(stdout.Writer(), "[ocr] Context compression exceeded threshold for %s, stopping.\n" , newPath) break } } if toolReqCount <= 0 { fmt.Fprintf(stdout.Writer(), "[ocr] Max tool requests reached for %s.\n" , newPath) } return false , nil }
review的LLM主循环,MainTask的提示词在executeSubtask中被replace之后被放入messages,每次循环会往该messages中添加,addNextMessage会触发上下文压缩
addNextMessage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func (r *Runner) addNextMessage(ctx context.Context, assistantContent string , toolCalls []llm.ToolCall, results []tool.ToolCallResult, messages *[]llm.Message, filePath string , st *compressionState) bool { maxAllowed := r.deps.Template.MaxTokens softLimit := int (float64 (maxAllowed) * tokenSoftThreshold) warnLimit := PromptTokenLimit(maxAllowed) r.tryApplyPendingCompression(st, messages) if CountMessagesTokens(*messages) > warnLimit { r.cancelPendingCompression(st) var err error if *messages, err = r.runCompression(ctx, *messages, filePath); err != nil { fmt.Fprintf(stdout.Writer(), "[ocr] Memory compression failed: %v\n" , err) } } if len (toolCalls) > 0 { *messages = append (*messages, llm.NewToolCallMessage(assistantContent, toolCalls)) } else if assistantContent != "" { *messages = append (*messages, llm.NewTextMessage("assistant" , assistantContent)) } for _, rs := range results { *messages = append (*messages, llm.NewToolResultMessage(rs.ToolCallID, rs.Result)) } finalCount := CountMessagesTokens(*messages) if finalCount > warnLimit { r.cancelPendingCompression(st) var err error if *messages, err = r.runCompression(ctx, *messages, filePath); err != nil { fmt.Fprintf(stdout.Writer(), "[ocr] Memory compression failed: %v\n" , err) } finalCount = CountMessagesTokens(*messages) } if finalCount > softLimit && finalCount < warnLimit { r.triggerAsyncCompression(ctx, st, *messages, filePath) } return finalCount < warnLimit }
runCompression
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func (r *Runner) runCompression(ctx context.Context, msgs []llm.Message, filePath string ) ([]llm.Message, error ) { if len (r.deps.Template.MemoryCompressionTask.Messages) == 0 || len (msgs) <= 2 { return msgs[:min(len (msgs), 2 )], nil } part := partitionMessages(msgs, r.deps.Template.MaxTokens, 0 ) if part.compressEnd <= part.frozenEnd { return msgs, nil } contextXML := buildMessageXML(msgs[part.frozenEnd:part.compressEnd]) compressionMsgs := make ([]llm.Message, 0 , len (r.deps.Template.MemoryCompressionTask.Messages)) for _, m := range r.deps.Template.MemoryCompressionTask.Messages { content := strings.ReplaceAll(m.Content, "{{context}}" , contextXML) compressionMsgs = append (compressionMsgs, llm.NewTextMessage(m.Role, content)) } startTime := time.Now() resp, err := r.deps.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ Model: r.deps.Model, Messages: compressionMsgs, MaxTokens: r.deps.Template.MaxTokens, }) duration := time.Since(startTime) fs := r.deps.Session.GetOrCreateFileSession(filePath) rec := fs.AppendTaskRecord(session.MemoryCompressionTask, compressionMsgs) if err != nil { rec.SetError(err, duration) return msgs, fmt.Errorf("memory compression: %w" , err) } rec.SetResponse(resp, duration) if resp.Usage != nil { atomic.AddInt64(&r.totalInputTokens, resp.Usage.PromptTokens) atomic.AddInt64(&r.totalOutputTokens, resp.Usage.CompletionTokens) atomic.AddInt64(&r.totalCacheReadTokens, resp.Usage.CacheReadTokens) atomic.AddInt64(&r.totalCacheWriteTokens, resp.Usage.CacheWriteTokens) } rawSummary := stripMarkdownFences(resp.Content()) if rawSummary == "" { return msgs, nil } rebuilt := make ([]llm.Message, 2 ) copy (rebuilt, msgs[:2 ]) userMsg := rebuilt[1 ] currentText := userMsg.ExtractText() rebuilt[1 ] = llm.NewTextMessage(userMsg.Role, currentText+"\n\n<previous_review_summary>\n" +rawSummary+"\n</previous_review_summary>" ) for i := part.compressEnd; i < len (msgs); i++ { rebuilt = append (rebuilt, msgs[i]) } return rebuilt, nil }
executeReviewFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 func (a *Agent) executeReviewFilter(ctx context.Context, d model.Diff, newPath string ) { ft := a.args.Template.ReviewFilterTask if ft == nil || len (ft.Messages) == 0 { return } comments := a.args.CommentCollector.CommentsForPath(newPath) if len (comments) == 0 { return } commentsJSON := buildFilterCommentsJSON(comments) messages := make ([]llm.Message, 0 , len (ft.Messages)) for _, m := range ft.Messages { content := m.Content content = strings.ReplaceAll(content, "{{path}}" , newPath) content = strings.ReplaceAll(content, "{{diff}}" , d.Diff) content = strings.ReplaceAll(content, "{{comments}}" , commentsJSON) messages = append (messages, llm.NewTextMessage(m.Role, content)) } if ft.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, time.Duration(ft.Timeout)*time.Second) defer cancel() } fs := a.session.GetOrCreateFileSession(newPath) rec := fs.AppendTaskRecord(session.ReviewFilterTask, messages) startTime := time.Now() _, llmSpan := telemetry.StartLLMSpan(ctx, a.args.Model) resp, err := a.args.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ Model: a.args.Model, Messages: messages, MaxTokens: a.args.Template.MaxTokens, }) duration := time.Since(startTime) var totalTokens int64 if resp.Usage != nil { totalTokens = resp.Usage.TotalTokens } telemetry.RecordLLMResult(llmSpan, duration, totalTokens, nil ) llmSpan.End() rec.SetResponse(resp, duration) a.runner.RecordUsage(resp.Usage) indices := parseFilterResponse(resp.Content(), len (comments)) telemetry.SetAttr(span, "comments.filtered" , len (indices)) if len (indices) == 0 { return } a.args.CommentCollector.RemoveByPathAndIndices(newPath, indices) fmt.Fprintf(stdout.Writer(), "[ocr] Review filter removed %d comment(s) for %s\n" , len (indices), newPath) } 通过再一次的llm对话(但只有一次)来过滤掉有问题的comment
emitRunResult
1 2 3 4 func runReview (args []string ) error { return emitRunResult(ctx, ag, comments, startTime, opts.outputFormat, opts.audience, q) }
对review结果(comment)进行格式化并输出
总结
可以看出,相比我之前使用的skill reivew方式,ocr有着更好的一套工程化方法(plan->并发review->review再过滤)。
后续使用上如果想到啥会再更新这篇,那么ocr的源码解析先到此了。