Files
PR-Helper/docs/07-llm-integration.md
wonder 275e5cc886 docs: 添加 10 份技术文档,README 改为中文
- 01-architecture.md: 架构概览
- 02-backend-services.md: 后端服务层
- 03-frontend-interaction.md: 前端交互设计
- 04-database-design.md: 数据库设计
- 05-api-reference.md: API 接口文档
- 06-sse-streaming.md: SSE 流式传输
- 07-llm-integration.md: LLM 集成
- 08-deployment.md: 部署运维
- 09-development-guide.md: 开发指南
- 10-troubleshooting.md: 故障排查
2026-06-23 22:38:43 +08:00

10 KiB

LLM 集成

概述

PR-Helper 集成了 OpenAI 兼容的 LLM API,用于自动生成 PR 描述和执行 AI 代码审查。支持流式响应,提供实时反馈。

架构

前端 → SSE 客户端 → Handler → LLM 服务 → OpenAI API
                ↑                              ↓
                └──────── 流式响应 ←────────────┘

配置

全局默认配置

var DefaultSettings = map[string]string{
    "llm.endpoint": "https://api.deepseek.com",
    "llm.api_key":  "",
    "llm.model":    "deepseek-v4-pro",
}

用户配置

每个用户可以独立配置 LLM 参数,存储在 user_settings 表中。

键名 说明 示例
llm.endpoint API 端点 https://api.deepseek.com
llm.api_key API 密钥 sk-...
llm.model 模型名称 deepseek-v4-pro

支持的 LLM 提供商

提供商 端点 模型
DeepSeek https://api.deepseek.com deepseek-v4-pro
OpenAI https://api.openai.com gpt-4, gpt-3.5-turbo
Azure OpenAI 自定义 自定义
本地部署 http://localhost:8000 自定义

LLM 服务 (services/llm.go)

配置读取

func GetLLMConfig(db *sql.DB, userID int64) (LLMConfig, error) {
    config := LLMConfig{}

    rows, err := db.Query("SELECT `key`, value FROM user_settings WHERE user_id = ? AND `key` IN ('llm.endpoint', 'llm.api_key', 'llm.model')", userID)
    // ...

    if config.APIKey == "" {
        return config, fmt.Errorf("LLM API key not configured — please set it in the settings page")
    }

    if config.Model == "" {
        config.Model = "deepseek-v4-pro"
    }

    return config, nil
}

流式调用

func ChatStream(config LLMConfig, messages []goopenai.ChatCompletionMessage, callback StreamCallback) (string, error) {
    clientConfig := goopenai.DefaultConfig(config.APIKey)
    if config.Endpoint != "" {
        clientConfig.BaseURL = config.Endpoint
    }
    client := goopenai.NewClientWithConfig(clientConfig)

    ctx := context.Background()
    stream, err := client.CreateChatCompletionStream(ctx, goopenai.ChatCompletionRequest{
        Model:    config.Model,
        Messages: messages,
        Stream:   true,
    })
    // ...

    var fullResponse strings.Builder
    for {
        response, err := stream.Recv()
        if err == io.EOF {
            break
        }
        // ...
        if len(response.Choices) > 0 {
            content := response.Choices[0].Delta.Content
            if content != "" {
                fullResponse.WriteString(content)
                if callback != nil {
                    callback("content", map[string]interface{}{"content": content})
                }
            }
        }
    }

    return fullResponse.String(), nil
}

JSON 提取

func extractJSON(s string) string {
    // 尝试从 Markdown 代码块提取
    if idx := strings.Index(s, "```json"); idx >= 0 {
        start := idx + 7
        if end := strings.Index(s[start:], "```"); end >= 0 {
            return strings.TrimSpace(s[start : start+end])
        }
    }

    // 查找第一个 { 或 [
    startObj := strings.Index(s, "{")
    startArr := strings.Index(s, "[")

    // 找到匹配的闭合括号
    // ...

    return s[start:]
}

PR 描述生成

提示模板

你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。

## Commit 记录
- abc1234 feat: add new feature
- def5678 fix: bug fix

## 代码变更 (Diff)
diff --git a/main.go b/main.go
...

请直接输出 Markdown 格式的 PR 描述,包含以下部分:

# 标题
**类型**: feat|fix|refactor|docs|chore|style|test|perf

## 概述
一段话概述变更内容

## 详细说明
按模块分组的详细变更说明

## 影响范围
影响范围说明

格式要求:
- 引用文件名、函数名、变量名等代码标识时,必须用反引号包裹
- 直接写出实际的代码名称,不要用任何占位符替代
- 不要输出 JSON,直接输出 Markdown

生成流程

  1. 读取 LLM 配置: 从用户设置获取
  2. 打开仓库: 使用 go-git
  3. 获取提交: 过滤 base 和 head 之间的提交
  4. 获取差异: 生成统一差异
  5. 构建提示: 包含提交记录和差异
  6. 调用 LLM: 流式生成
  7. 返回结果: Markdown 格式的 PR 描述

差异截断

// 截断 diff 如果太大(约 60k 字符以保持在 token 限制内)
if len(diff) > 60000 {
    diff = diff[:60000] + "\n\n... [diff truncated due to size]"
}

AI 代码审查

单文件审查提示

你是一个资深代码审查专家。请审查以下代码变更,给出专业的 Review 意见。

## 文件: main.go
## 变更行数: +42 / -10

## Diff
diff --git a/main.go b/main.go
...

请按以下 JSON 格式输出审查意见(直接输出 JSON 数组,不要包含 markdown 代码块标记):
[
  {
    "severity": "critical 或 warning 或 info",
    "description": "问题描述",
    "suggestion": "建议的修改方案",
    "code_example": "建议的代码(如有)"
  }
]

严重程度说明:
- critical: 严重问题(安全漏洞、数据丢失风险、崩溃风险)
- warning: 建议改进(性能问题、代码规范、可维护性)
- info: 提示信息(最佳实践、可选优化)

如果代码没有问题,输出空数组 []。
请用中文回复。

汇总生成提示

以下是多个文件的代码审查结果,请给出整体评估:

### main.go (42 行变更)
[warning] 变量未使用
[info] 可以使用更简洁的写法

### utils.go (10 行变更)
没有发现问题

请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
{
  "score": 7,
  "overall": "总体评价(2-3 句话)",
  "findings": "按严重程度排序的主要发现汇总",
  "recommendations": "改进建议,用换行分隔多条建议"
}
注意:所有字段必须是字符串类型,不要使用数组。请用中文回复。

Top-N 策略

// 按变更行数排序
sort.Slice(files, func(i, j int) bool {
    return countDiffLines(files[i].Patch) > countDiffLines(files[j].Patch)
})

// 应用 Top-N
if topN > 0 && topN < len(files) {
    files = files[:topN]
}
  • 默认 Top-N: 20
  • 可通过设置或请求参数调整
  • 0 表示分析所有文件

并发审查

sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup

for i, file := range files {
    wg.Add(1)
    go func(idx int, f FileDiff) {
        defer wg.Done()
        sem <- struct{}{}        // 获取槽位
        defer func() { <-sem }() // 释放槽位
        // ... 审查逻辑
    }(i, file)
}
  • 默认并发数: 5
  • 信号量控制最大并发
  • 线程安全的回调函数

流式响应处理

后端回调

type StreamCallback func(event string, data interface{})

// 使用回调发送事件
callback("content", map[string]interface{}{"content": content})
callback("suggestion", map[string]interface{}{
    "file":     filename,
    "severity": severity,
    "content":  content,
})

前端处理

SSE.post(url, body, {
    content: (data) => {
        // 追加 Markdown 内容
        appendToOutput(data.content);
    },
    suggestion: (data) => {
        // 显示审查建议
        showSuggestion(data.file, data.severity, data.content);
    },
    summary: (data) => {
        // 显示汇总
        showSummary(data);
    }
});

错误处理

API 密钥未配置

if config.APIKey == "" {
    return config, fmt.Errorf("LLM API key not configured — please set it in the settings page")
}

API 调用失败

stream, err := client.CreateChatCompletionStream(ctx, request)
if err != nil {
    return "", fmt.Errorf("create stream: %w", err)
}

流式读取错误

response, err := stream.Recv()
if err != nil {
    return fullResponse.String(), fmt.Errorf("stream recv: %w", err)
}

JSON 解析失败

jsonStr := extractJSON(fullResponse)
var suggestions []ReviewSuggestion
if err := json.Unmarshal([]byte(jsonStr), &suggestions); err != nil {
    // 尝试单个对象
    var single ReviewSuggestion
    if err2 := json.Unmarshal([]byte(jsonStr), &single); err2 == nil {
        suggestions = []ReviewSuggestion{single}
    } else {
        // 使用原始响应作为描述
        suggestions = []ReviewSuggestion{{
            Severity:    "info",
            Description: fullResponse,
        }}
    }
}

Token 管理

估算

  • 1 个中文字符 ≈ 2 tokens
  • 1 个英文单词 ≈ 1 token
  • 代码行 ≈ 2-3 tokens

限制

  • 输入: 通常 4k-128k tokens
  • 输出: 通常 4k-8k tokens

优化

  • 截断大型差异
  • 只分析 Top-N 文件
  • 压缩提示文本

模型选择建议

DeepSeek

  • 优势: 中文支持好,代码理解强
  • 适用: 中文项目,代码审查
  • 模型: deepseek-v4-pro

OpenAI GPT-4

  • 优势: 综合能力强
  • 适用: 复杂项目,多语言
  • 模型: gpt-4, gpt-4-turbo

本地部署

  • 优势: 数据隐私,无 API 费用
  • 适用: 敏感代码,离线环境
  • 模型: CodeLlama, Qwen

成本优化

减少 Token 使用

  • 截断大型差异
  • 只分析关键文件
  • 使用更小的模型

缓存

  • 缓存相同输入的输出
  • 避免重复分析

批量处理

  • 合并多个小请求
  • 减少 API 调用次数

监控

关键指标

  • API 调用次数
  • Token 使用量
  • 响应时间
  • 错误率

日志

log.Printf("LLM call: model=%s, tokens=%d, duration=%v", model, tokens, duration)

安全

API 密钥

  • 不要在代码中硬编码
  • 使用环境变量或配置文件
  • 定期轮换密钥

数据隐私

  • 敏感代码考虑本地部署
  • 不要发送不必要的上下文
  • 遵守数据合规要求

故障排查

常见问题

  1. API 密钥错误

    • 检查密钥是否正确
    • 确认密钥是否有效
  2. 连接超时

    • 检查网络连接
    • 确认端点 URL 正确
  3. Token 超限

    • 减少输入大小
    • 使用更大的上下文窗口
  4. JSON 解析失败

    • 检查提示格式
    • 添加更明确的格式要求

调试命令

# 测试 API 连接
curl https://api.deepseek.com/v1/models \
  -H "Authorization: Bearer sk-..."

# 测试流式响应
curl -N https://api.deepseek.com/v1/chat/completions \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v4-pro","messages":[{"role":"user","content":"Hello"}],"stream":true}'