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: 故障排查
482 lines
10 KiB
Markdown
482 lines
10 KiB
Markdown
# LLM 集成
|
|
|
|
## 概述
|
|
|
|
PR-Helper 集成了 OpenAI 兼容的 LLM API,用于自动生成 PR 描述和执行 AI 代码审查。支持流式响应,提供实时反馈。
|
|
|
|
## 架构
|
|
|
|
```
|
|
前端 → SSE 客户端 → Handler → LLM 服务 → OpenAI API
|
|
↑ ↓
|
|
└──────── 流式响应 ←────────────┘
|
|
```
|
|
|
|
## 配置
|
|
|
|
### 全局默认配置
|
|
|
|
```go
|
|
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)
|
|
|
|
### 配置读取
|
|
|
|
```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
|
|
}
|
|
```
|
|
|
|
### 流式调用
|
|
|
|
```go
|
|
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 提取
|
|
|
|
```go
|
|
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 描述生成
|
|
|
|
### 提示模板
|
|
|
|
```markdown
|
|
你是一个专业的技术文档撰写助手。根据以下 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 描述
|
|
|
|
### 差异截断
|
|
|
|
```go
|
|
// 截断 diff 如果太大(约 60k 字符以保持在 token 限制内)
|
|
if len(diff) > 60000 {
|
|
diff = diff[:60000] + "\n\n... [diff truncated due to size]"
|
|
}
|
|
```
|
|
|
|
## AI 代码审查
|
|
|
|
### 单文件审查提示
|
|
|
|
```markdown
|
|
你是一个资深代码审查专家。请审查以下代码变更,给出专业的 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: 提示信息(最佳实践、可选优化)
|
|
|
|
如果代码没有问题,输出空数组 []。
|
|
请用中文回复。
|
|
```
|
|
|
|
### 汇总生成提示
|
|
|
|
```markdown
|
|
以下是多个文件的代码审查结果,请给出整体评估:
|
|
|
|
### main.go (42 行变更)
|
|
[warning] 变量未使用
|
|
[info] 可以使用更简洁的写法
|
|
|
|
### utils.go (10 行变更)
|
|
没有发现问题
|
|
|
|
请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
|
|
{
|
|
"score": 7,
|
|
"overall": "总体评价(2-3 句话)",
|
|
"findings": "按严重程度排序的主要发现汇总",
|
|
"recommendations": "改进建议,用换行分隔多条建议"
|
|
}
|
|
注意:所有字段必须是字符串类型,不要使用数组。请用中文回复。
|
|
```
|
|
|
|
### Top-N 策略
|
|
|
|
```go
|
|
// 按变更行数排序
|
|
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 表示分析所有文件
|
|
|
|
### 并发审查
|
|
|
|
```go
|
|
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
|
|
- 信号量控制最大并发
|
|
- 线程安全的回调函数
|
|
|
|
## 流式响应处理
|
|
|
|
### 后端回调
|
|
|
|
```go
|
|
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,
|
|
})
|
|
```
|
|
|
|
### 前端处理
|
|
|
|
```javascript
|
|
SSE.post(url, body, {
|
|
content: (data) => {
|
|
// 追加 Markdown 内容
|
|
appendToOutput(data.content);
|
|
},
|
|
suggestion: (data) => {
|
|
// 显示审查建议
|
|
showSuggestion(data.file, data.severity, data.content);
|
|
},
|
|
summary: (data) => {
|
|
// 显示汇总
|
|
showSummary(data);
|
|
}
|
|
});
|
|
```
|
|
|
|
## 错误处理
|
|
|
|
### API 密钥未配置
|
|
|
|
```go
|
|
if config.APIKey == "" {
|
|
return config, fmt.Errorf("LLM API key not configured — please set it in the settings page")
|
|
}
|
|
```
|
|
|
|
### API 调用失败
|
|
|
|
```go
|
|
stream, err := client.CreateChatCompletionStream(ctx, request)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create stream: %w", err)
|
|
}
|
|
```
|
|
|
|
### 流式读取错误
|
|
|
|
```go
|
|
response, err := stream.Recv()
|
|
if err != nil {
|
|
return fullResponse.String(), fmt.Errorf("stream recv: %w", err)
|
|
}
|
|
```
|
|
|
|
### JSON 解析失败
|
|
|
|
```go
|
|
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 使用量
|
|
- 响应时间
|
|
- 错误率
|
|
|
|
### 日志
|
|
|
|
```go
|
|
log.Printf("LLM call: model=%s, tokens=%d, duration=%v", model, tokens, duration)
|
|
```
|
|
|
|
## 安全
|
|
|
|
### API 密钥
|
|
|
|
- 不要在代码中硬编码
|
|
- 使用环境变量或配置文件
|
|
- 定期轮换密钥
|
|
|
|
### 数据隐私
|
|
|
|
- 敏感代码考虑本地部署
|
|
- 不要发送不必要的上下文
|
|
- 遵守数据合规要求
|
|
|
|
## 故障排查
|
|
|
|
### 常见问题
|
|
|
|
1. **API 密钥错误**
|
|
- 检查密钥是否正确
|
|
- 确认密钥是否有效
|
|
|
|
2. **连接超时**
|
|
- 检查网络连接
|
|
- 确认端点 URL 正确
|
|
|
|
3. **Token 超限**
|
|
- 减少输入大小
|
|
- 使用更大的上下文窗口
|
|
|
|
4. **JSON 解析失败**
|
|
- 检查提示格式
|
|
- 添加更明确的格式要求
|
|
|
|
### 调试命令
|
|
|
|
```bash
|
|
# 测试 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}'
|
|
```
|