ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 LangChainGo 构建 AI 代码审查器:从 CLI 工具到 Git Hook 与 CI/CD 的完整实战

用 LangChainGo 构建 AI 代码审查器:从 CLI 工具到 Git Hook 与 CI/CD 的完整实战 用 LangChainGo 构建 AI 代码审查器从 CLI 工具到 Git Hook 与 CI/CD 的完整实战【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo导读本教程基于 LangChainGolangchaingo官方文档 code-reviewer.md 展开手把手教你用 Go 打造一个智能代码审查助手它能分析 Go 源码中的缺陷、风格问题与性能隐患给出可执行的修复建议并原生接入 Git 工作流与 CI/CD 流水线。读完本文你将掌握 LangChainGo 的llms.Model调用、prompts.PromptTemplate模板渲染、JSON 结构化输出WithJSONMode等核心 API并独立完成一个可直接用于日常开发的 AI 审查工具。构建目标与前置条件我们将实现一个名为code-reviewer的命令行工具它具备四个核心能力分析单个 Go 源文件中的潜在问题Bug、性能、风格、安全递归审查目录下的所有.go文件通过git diff只审查当前工作区变更过的文件为每一条建议提供 Why为什么是问题、How如何修复与严重级别Critical / Warning / Suggestion的说明。前置条件Go 1.21注意当前仓库 go.mod 声明的是go 1.24.4建议直接使用与该版本匹配或更新的 Go 工具链并开启 module 模式OpenAI 或 Anthropic 的 API Key已安装 Git。mkdir ai-code-reviewer cd ai-code-reviewer go mod init code-reviewer go get github.com/tmc/langchaingogo get会拉取 langchaingo 主模块。得益于 Go module graph pruning见 go.mod 顶部注释你只 importllms/openai等具体包时构建过程不会引入全部依赖最终二进制保持精简。核心审查器结构理解 LangChainGo 的三个关键抽象创建main.go其中CodeReviewer结构体持有一个llms.Model和一个*prompts.PromptTemplate这是 LangChainGo 最典型的模板 模型组合package main import ( context flag fmt go/ast go/parser go/token log os os/exec path/filepath strings github.com/tmc/langchaingo/llms github.com/tmc/langchaingo/llms/openai github.com/tmc/langchaingo/prompts ) type CodeReviewer struct { llm llms.Model template *prompts.PromptTemplate } func NewCodeReviewer() (*CodeReviewer, error) { llm, err : openai.New() if err ! nil { return nil, err } template : prompts.NewPromptTemplate( You are an expert Go code reviewer. Analyze this Go code for: 1. **Bugs and Logic Issues**: Potential runtime errors, nil pointer dereferences, race conditions 2. **Performance**: Inefficient algorithms, unnecessary allocations, string concatenation issues 3. **Style**: Go idioms, naming conventions, error handling patterns 4. **Security**: Input validation, sensitive data handling Code to review: go {{.code}} File: {{.filename}} Provide specific, actionable feedback. For each issue: - Explain WHY its a problem - Show HOW to fix it with code examples - Rate severity: Critical, Warning, Suggestion Focus on the most important issues first., []string{code, filename}) return CodeReviewer{ llm: llm, template: template, }, nil } func (cr *CodeReviewer) ReviewFile(filename string) error { content, err : os.ReadFile(filename) if err ! nil { return fmt.Errorf(reading file: %w, err) } // Parse Go code to ensure its valid fset : token.NewFileSet() _, err parser.ParseFile(fset, filename, content, parser.ParseComments) if err ! nil { return fmt.Errorf(parsing Go file: %w, err) } prompt, err : cr.template.Format(map[string]any{ code: string(content), filename: filename, }) if err ! nil { return fmt.Errorf(formatting prompt: %w, err) } ctx : context.Background() response, err : cr.llm.GenerateContent(ctx, []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeHuman, prompt), }) if err ! nil { return fmt.Errorf(generating review: %w, err) } fmt.Printf(\n Review for %s \n, filename) fmt.Println(strings.Repeat(, 80)) fmt.Println(response.Choices[0].Content) fmt.Println(strings.Repeat(, 80)) return nil }下面对三个关键 API 做源码级拆解帮助你真正理解背后发生了什么。1.llms.Model与GenerateContentCodeReviewer.llm的类型是llms.Model接口其核心方法是 llms/llms.go 中的GenerateContent(ctx context.Context, messages []MessageContent, options ...CallOption) (*ContentResponse, error)openai.New()返回的*LLM通过 llms/openai/openaillm.go 的构造器创建并显式实现了该接口该文件中有var _ llms.Model (*LLM)(nil)的编译期断言。传入的消息类型MessageContent定义在 llms/generatecontent.go它由Role角色与Parts内容片段序列组成llms.TextParts(role, parts...)则是把若干字符串打包成一条带角色的文本消息的辅助函数见 llms/generatecontent.go。这里我们只发送一条ChatMessageTypeHuman人类角色消息——从 llms/chat_messages.go 可以看到LangChainGo 预定义了Human、AI、System、Tool等角色常量需要系统提示词时也可以改用ChatMessageTypeSystem组合多条消息。GenerateContent返回的ContentResponse带有Choices列表每个ContentChoice的Content字段就是模型生成的文本llms/generatecontent.go因此response.Choices[0].Content是取第一份候选回复。2.prompts.PromptTemplate的渲染原理NewPromptTemplate创建的是基于 Gotext/template语法的模板prompts/prompt_template.go模板字符串中的{{.code}}、{{.filename}}是占位变量。调用Format(map[string]any{...})时模板会把传入的值与PartialVariables若设置合并后渲染prompts/prompt_template.go。需要注意一个细节NewPromptTemplate返回的是PromptTemplate值类型而非指针所以教程代码里用template : prompts.NewPromptTemplate(...)再template取址赋给结构体的*PromptTemplate字段这是正确且常见的用法。模板的InputVariables即第二个参数[]string{code, filename}在渲染前会被校验缺少变量会直接报错这能尽早暴露提示词与传参不一致的问题。3. 客户端初始化与环境变量openai.New()不传任何参数也能工作因为它会按优先级读取环境变量token 读取OPENAI_API_KEY模型读取OPENAI_MODEL服务地址读取OPENAI_BASE_URLllms/openai/openaillm_option.go。如果需要显式控制LangChainGo 提供了一组函数式选项openai.WithToken(token)——覆盖 API Keyopenai.WithModel(model)——指定模型名如gpt-4oopenai.WithBaseURL(url)——对接兼容 OpenAI 协议的第三方网关或本地服务openai.WithAPIType(openai.APITypeAzure)与WithAPIVersion——用于 Azure OpenAI。若OPENAI_API_KEY缺失openai.New()会返回ErrMissingToken错误提示你在环境变量中设置 Key见 llms/openai/llm.go。如果你改用 Anthropic只需把llms/openai换成llms/anthropic其构造器同样会从ANTHROPIC_API_KEY读取密钥见 llms/anthropic/anthropicllm.go因为两者都实现了llms.Model接口其余代码几乎不用改动——这正是面向接口编程带来的可替换性。4. CLI 入口与三种审查模式main函数用标准库flag定义三个互斥的启动参数并分派到三种审查路径func main() { var ( file flag.String(file, , Go file to review) dir flag.String(dir, , Directory to review (all .go files)) git flag.Bool(git, false, Review files changed in git working directory) ) flag.Parse() reviewer, err : NewCodeReviewer() if err ! nil { log.Fatal(err) } switch { case *file ! : if err : reviewer.ReviewFile(*file); err ! nil { log.Fatal(err) } case *dir ! : if err : reviewDirectory(reviewer, *dir); err ! nil { log.Fatal(err) } case *git: if err : reviewGitChanges(reviewer); err ! nil { log.Fatal(err) } default: fmt.Println(Usage:) fmt.Println( code-reviewer -filemain.go) fmt.Println( code-reviewer -dir./pkg) fmt.Println( code-reviewer -git) os.Exit(1) } } func reviewDirectory(reviewer *CodeReviewer, dir string) error { return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err ! nil { return err } if strings.HasSuffix(path, .go) !strings.Contains(path, vendor/) { return reviewer.ReviewFile(path) } return nil }) } func reviewGitChanges(reviewer *CodeReviewer) error { // This is a simplified version - youd want to use a proper git library cmd : exec.Command(git, diff, --name-only, HEAD) output, err : cmd.Output() if err ! nil { return fmt.Errorf(getting git changes: %w, err) } files : strings.Split(strings.TrimSpace(string(output)), \n) for _, file : range files { if strings.HasSuffix(file, .go) file ! { if err : reviewer.ReviewFile(file); err ! nil { log.Printf(Error reviewing %s: %v, file, err) } } } return nil }三个值得学习的工程细节审查前的 AST 校验ReviewFile在请求大模型之前先用go/parser.ParseFile做一次本地解析。这样做一举两得一方面确保待审代码语法合法另一方面为后面的结构化输出与自定义规则引擎提供了*ast.File与token.FileSet基础错误包装全程使用fmt.Errorf(...: %w, err)保留错误链便于errors.Is/errors.As判断根因这是 Go 1.13 的错误处理惯例Git 集成reviewGitChanges通过git diff --name-only HEAD拿到已修改文件清单并过滤出.go文件。教程注释也提醒生产环境建议换用专业的 Git 库如 go-git以处理暂存区、rename 等边界情况。用带缺陷的样例代码验证创建sample.go作为审查对象它故意埋入三类典型问题package main import fmt func badCode() { // This has several issues var users []string for i : 0; i len(users); i { fmt.Println(users[i]) // potential index out of bounds } // String concatenation in loop var result string for i : 0; i 1000; i { result fmt.Sprintf(item-%d,, i) } // Ignoring errors file, _ : os.Open(nonexistent.txt) file.Read(make([]byte, 100)) }这正好覆盖提示词中要求的四类检查点空切片users上循环越界访问——Bugs运行时风险循环内用拼接字符串——Performance每次迭代都产生新的不可变字符串与分配strings.Builder才是正解忽略os.Open错误并直接调用file.Read——Style / Bugs错误处理模式错误且file可能为 nil 导致 panic读取不存在的文件——Security / Robustness未做输入与资源校验。小提示原教程的sample.go使用了os包但 import 块里只有fmt实际编译会报undefined: os。运行前请补上os导入——这本身也是很好的审查素材。运行代码审查器export OPENAI_API_KEYyour-openai-api-key-here go run main.go -filesample.go程序会打印审查分隔线与模型输出 Review for sample.go [Critical] Potential index out of bounds ...模型给出的具体分析 [Warning] String concatenation in loop; prefer strings.Builder ... 你可以组合三种用法code-reviewer -filemain.go # 审查单个文件 code-reviewer -dir./pkg # 递归审查目录 code-reviewer -git # 仅审查 Git 工作区变更进阶结构化 JSON 输出自由文本的审查结果难以被程序消费。LangChainGo 通过llms.WithJSONMode()选项llms/options.go让模型以 JSON 格式返回其实现原理是把CallOptions.JSONMode置为true后端据此设置 response format如 OpenAI 的json_object。对支持该能力但未开启 JSON 模式的模型返回的ResponseFormatJSON也可通过WithResponseFormat显式下发见 llms/openai/openaillm_option.go。创建reviewer.go定义与模型约定一致的 JSON 结构package main import ( encoding/json fmt go/ast go/parser go/token ) type Issue struct { Severity string json:severity Type string json:type Line int json:line Description string json:description Suggestion string json:suggestion } type ReviewResult struct { Filename string json:filename Issues []Issue json:issues Score int json:score // 0-100 } func (cr *CodeReviewer) ReviewFileStructured(filename string) (*ReviewResult, error) { content, err : os.ReadFile(filename) if err ! nil { return nil, fmt.Errorf(reading file: %w, err) } // Parse for line numbers fset : token.NewFileSet() node, err : parser.ParseFile(fset, filename, content, parser.ParseComments) if err ! nil { return nil, fmt.Errorf(parsing Go file: %w, err) } template : prompts.NewPromptTemplate( Analyze this Go code and return a JSON response with this exact structure: { filename: {{.filename}}, issues: [ { severity: critical|warning|suggestion, type: bug|performance|style|security, line: 42, description: Detailed issue description, suggestion: How to fix this issue } ], score: 85 } Code to analyze: go {{.code}} Focus on real issues. Score: 100 perfect, 0 many serious issues., []string{code, filename}) prompt, err : template.Format(map[string]any{ code: string(content), filename: filename, }) if err ! nil { return nil, fmt.Errorf(formatting prompt: %w, err) } ctx : context.Background() response, err : cr.llm.GenerateContent(ctx, []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeHuman, prompt), }, llms.WithJSONMode()) if err ! nil { return nil, fmt.Errorf(generating review: %w, err) } var result ReviewResult if err : json.Unmarshal([]byte(response.Choices[0].Content), result); err ! nil { return nil, fmt.Errorf(parsing JSON response: %w, err) } return result, nil }结构化输出落地后下游消费就变得极其简单CI 脚本可以按issues数组逐条渲染 markdown 评论也可以依据severity critical的数量决定是否阻断合并score字段甚至可以作为代码健康度的量化指标。让审查器融入 Git 工作流pre-commit Hook在.git/hooks/pre-commit中挂接审查器让每次提交前自动跑一遍代码审查#!/bin/bash echo Running AI code review... ./code-reviewer -git if [ $? -ne 0 ]; then echo Code review found issues. Fix them or use --no-verify to skip. exit 1 fi echo Code review passed!然后赋予执行权限chmod x .git/hooks/pre-commit这里有个值得注意的设计取舍教程中的 pre-commit 脚本把审查失败当作硬阻断exit 1。由于code-reviewer -git本身只是审查并打印模型返回的缺陷并不会让进程退出非零实际项目中你可以自行扩展——例如用上一节的结构化结果在检测到 Critical 级别问题时返回非零退出码从而真正卡住提交或者反过来只打印报告不阻断让开发者自行判断。结合--no-verify逃生通道这套机制可以在强制质量与开发效率之间取得平衡。高级特性基于 AST 的自定义规则引擎LLM 审查存在成本与随机性而 Go 标准库的go/ast提供了确定性的静态检查能力。教程给出的思路是把确定性规则当作固定拦截网把 LLM 当作兜底语义分析器两者互补。type RuleEngine struct { rules []Rule } type Rule interface { Check(node ast.Node, fset *token.FileSet) []Issue } type NoGlobalVarsRule struct{} func (r NoGlobalVarsRule) Check(node ast.Node, fset *token.FileSet) []Issue { var issues []Issue ast.Inspect(node, func(n ast.Node) bool { if genDecl, ok : n.(*ast.GenDecl); ok genDecl.Tok token.VAR { for _, spec : range genDecl.Specs { if valueSpec, ok : spec.(*ast.ValueSpec); ok { pos : fset.Position(valueSpec.Pos()) issues append(issues, Issue{ Severity: warning, Type: style, Line: pos.Line, Description: Global variable found, Suggestion: Consider using dependency injection or configuration structs, }) } } } return true }) return issues }NoGlobalVarsRule演示了规则引擎的核心模式ast.Inspect深度遍历语法树遇到token.VAR的*ast.GenDecl即命中全局变量模式再用fset.Position()把 AST 位置换算成源码行号填充到与结构化审查一致的Issue结构里。你可以按同样套路扩展更多规则例如NoPanicRuleast.CallExpr中Fun为panic的调用NoFmtPrintlnRule生产代码中fmt.Println的日志滥用ErrorCheckRule_ 赋值或if err :后未处理的错误。这些规则 0 成本、无延迟适合放在固定检查层再把模板提示词聚焦到语义级问题并发安全、逻辑缺陷、性能瓶颈让每次 LLM 调用花在刀刃上。集成 CI/CDDocker 容器化为了让审查器在任何 CI 环境不依赖本地 Go 工具链中稳定运行教程给出了两阶段构建的DockerfileFROM golang:1.21-alpine AS builder WORKDIR /app COPY . . RUN go build -o code-reviewer FROM alpine:latest RUN apk --no-cache add git WORKDIR /root/ COPY --frombuilder /app/code-reviewer . ENTRYPOINT [./code-reviewer]第一阶段用golang镜像编译出静态二进制第二阶段只保留极简的alpine运行时并安装git因为-git模式依赖git diff命令。接入 GitHub Actions实现 Pull Request 自动审查name: AI Code Review on: [pull_request] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkoutv2 - name: Run AI Code Review env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | docker build -t code-reviewer . docker run --rm -v $PWD:/code code-reviewer -dir/code关键点解读API Key 通过 GitHub Secretssecrets.OPENAI_API_KEY注入环境变量绝不写死在镜像里-v $PWD:/code把 CI 工作区挂载进容器配合-dir/code审查整个代码库由于 Actions 的checkout会带出 PR 的合并结果你也可以把命令换成code-reviewer -git来只审变更文件进一步节省 token 成本。延伸方向与小结教程最后给出了清晰的演进路线你也可以在此基础上继续扩展支持多语言为 Python、JavaScript 等语言编写对应的解析器与提示词从反馈中学习收集开发者对审查结果的采纳/忽略标记反向优化提示词与规则权重Web 界面把ReviewResult渲染成团队可共享的审查看板IDE 集成借助 LSP 协议把 Issue 映射为编辑器内联诊断。本教程完整展示了 LangChainGo 的能力边界远不止聊天机器人llms.Model抽象让 OpenAI / Anthropic 等后端可以即插即用prompts.PromptTemplate让提示词工程化、可复用WithJSONMode让模型输出可直接被程序消费而 Go 标准库的go/ast则提供了确定性的本地检查作为补充。一个本地静态规则 LLM 语义审查的复合工具足以成为开发流程中既便宜又可靠的质量防线。【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表