fix(ai): 测试连接改为结构化结果,不再倾倒模型原始回复
返回 {model, latencyMs, tokensUsed, replyLength};前端提示「连接正常:模型 · 耗时 · tokens」。
模型原始回复不回传——测试要的是通没通/多快,不是看模型说了什么(ECHO 假模型下尤其明显)。
空回复时明确报错提示检查 baseURL 是否 OpenAI 兼容。
This commit is contained in:
1 parent
f1d9480caf
commit
5d898e3592
6 files changed
+30
-11
No files matched your search
@@ -64,12 +64,12 @@ func (ctrl *AIController) TestConfig(c *gin.Context) {
|
|||||||
cfg.APIKey = req.APIKey
|
cfg.APIKey = req.APIKey
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, err := services.TestAIConnection(cfg)
|
result, err := services.TestAIConnection(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error(c, 400, err.Error())
|
utils.Error(c, 400, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
utils.Success(c, gin.H{"reply": reply, "model": cfg.Model})
|
utils.Success(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsMask(s string) bool {
|
func containsMask(s string) bool {
|
||||||
|
|||||||
@@ -115,17 +115,36 @@ func aiChat(ctx context.Context, cfg models.AIConfig, messages []aiMessage) (str
|
|||||||
return parsed.Choices[0].Message.Content, parsed.Usage.PromptTokens, parsed.Usage.CompletionTokens, nil
|
return parsed.Choices[0].Message.Content, parsed.Usage.PromptTokens, parsed.Usage.CompletionTokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAIConnection 连通性测试:发一句最小请求,验证 baseURL/key/model 三件套。
|
// AIConnectionTest 连通性测试结果:面向展示的结构化信息,不外发原始回复
|
||||||
func TestAIConnection(cfg models.AIConfig) (string, error) {
|
type AIConnectionTest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
LatencyMs int64 `json:"latencyMs"`
|
||||||
|
TokensUsed int `json:"tokensUsed"`
|
||||||
|
ReplyLength int `json:"replyLength"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAIConnection 连通性测试:发一句最小请求,验证 baseURL/key/model 三件套。
|
||||||
|
// 返回结构化结果(模型/耗时/token);模型原始回复不回传 —— 测试要的是"通没通、多快",不是看模型说了什么。
|
||||||
|
func TestAIConnection(cfg models.AIConfig) (*AIConnectionTest, error) {
|
||||||
cfg.MaxTokens = 32
|
cfg.MaxTokens = 32
|
||||||
cfg.TimeoutSec = 30
|
cfg.TimeoutSec = 30
|
||||||
reply, _, _, err := aiChat(context.Background(), cfg, []aiMessage{
|
start := time.Now()
|
||||||
|
reply, promptTokens, completionTokens, err := aiChat(context.Background(), cfg, []aiMessage{
|
||||||
{Role: "user", Content: "只回复两个字:正常"},
|
{Role: "user", Content: "只回复两个字:正常"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(reply), nil
|
trimmed := strings.TrimSpace(reply)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, fmt.Errorf("模型返回了空内容(检查 baseURL 是否为 OpenAI 兼容端点)")
|
||||||
|
}
|
||||||
|
return &AIConnectionTest{
|
||||||
|
Model: cfg.Model,
|
||||||
|
LatencyMs: time.Since(start).Milliseconds(),
|
||||||
|
TokensUsed: promptTokens + completionTokens,
|
||||||
|
ReplyLength: len([]rune(trimmed)),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatMessage 浮窗助手的多轮消息(user/assistant 交替)
|
// ChatMessage 浮窗助手的多轮消息(user/assistant 交替)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const aiApi = {
|
|||||||
getConfig: () => http.get<AIConfigResponse>('/ai/config'),
|
getConfig: () => http.get<AIConfigResponse>('/ai/config'),
|
||||||
saveConfig: (data: Partial<AIConfig>) => http.put<{ config: AIConfig }>('/ai/config', data),
|
saveConfig: (data: Partial<AIConfig>) => http.put<{ config: AIConfig }>('/ai/config', data),
|
||||||
testConfig: (data: { baseUrl?: string; model?: string; apiKey?: string }) =>
|
testConfig: (data: { baseUrl?: string; model?: string; apiKey?: string }) =>
|
||||||
http.post<{ reply: string; model: string }>('/ai/config/test', data, { timeout: 45000 }),
|
http.post<{ model: string; latencyMs: number; tokensUsed: number; replyLength: number }>('/ai/config/test', data, { timeout: 45000 }),
|
||||||
analyze: (data: { sourceId: number; start: number; end: number; question?: string; app?: string }) =>
|
analyze: (data: { sourceId: number; start: number; end: number; question?: string; app?: string }) =>
|
||||||
http.post<AIAnalyzeResponse>('/ai/analyze', data, { timeout: 180000 }),
|
http.post<AIAnalyzeResponse>('/ai/analyze', data, { timeout: 180000 }),
|
||||||
runs: (page: number, pageSize: number) =>
|
runs: (page: number, pageSize: number) =>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default {
|
|||||||
save: 'Save',
|
save: 'Save',
|
||||||
test: 'Test connection',
|
test: 'Test connection',
|
||||||
testing: 'Testing…',
|
testing: 'Testing…',
|
||||||
testOk: 'Connection OK, model replied: {{reply}}',
|
testOk: 'Connected: {{model}} · {{latency}}s · {{tokens}} tokens',
|
||||||
usedToday: '{{used}} tokens used today',
|
usedToday: '{{used}} tokens used today',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default {
|
|||||||
save: '保存配置',
|
save: '保存配置',
|
||||||
test: '测试连接',
|
test: '测试连接',
|
||||||
testing: '测试中…',
|
testing: '测试中…',
|
||||||
testOk: '连接正常,模型回复:{{reply}}',
|
testOk: '连接正常:{{model}} · {{latency}}s · {{tokens}} tokens',
|
||||||
usedToday: '今日已用 {{used}} tokens',
|
usedToday: '今日已用 {{used}} tokens',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export default function AiopsPage() {
|
|||||||
model: config?.model,
|
model: config?.model,
|
||||||
apiKey: apiKeyInput.trim() || undefined,
|
apiKey: apiKeyInput.trim() || undefined,
|
||||||
})
|
})
|
||||||
toast.success(t('config.testOk', { reply: res.reply || 'OK' }))
|
toast.success(t('config.testOk', { model: res.model, latency: (res.latencyMs / 1000).toFixed(1), tokens: res.tokensUsed }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : '连接失败')
|
toast.error(err instanceof Error ? err.message : '连接失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in new issue
Block a user