275e5cc886
- 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: 故障排查
10 KiB
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
生成流程
- 读取 LLM 配置: 从用户设置获取
- 打开仓库: 使用 go-git
- 获取提交: 过滤 base 和 head 之间的提交
- 获取差异: 生成统一差异
- 构建提示: 包含提交记录和差异
- 调用 LLM: 流式生成
- 返回结果: 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 密钥
- 不要在代码中硬编码
- 使用环境变量或配置文件
- 定期轮换密钥
数据隐私
- 敏感代码考虑本地部署
- 不要发送不必要的上下文
- 遵守数据合规要求
故障排查
常见问题
-
API 密钥错误
- 检查密钥是否正确
- 确认密钥是否有效
-
连接超时
- 检查网络连接
- 确认端点 URL 正确
-
Token 超限
- 减少输入大小
- 使用更大的上下文窗口
-
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}'