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: 故障排查
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
# 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}'
|
||||
```
|
||||
Reference in New Issue
Block a user