feat: LLM 调用日志 + ModelSelector 优化 + devtools.bat 编码修复

- 新增 call_log.go: 全局环形缓冲区记录每次 LLM 调用(模型/Token/耗时/错误)
- OpenAIProvider.doChat/ChatStreamWithTools 自动记录调用日志
- ai-core 暴露 GET /api/v1/llm-calls 端点, DevTools 代理 + UI 面板
- ModelSelector.envProvider 改为单例缓存, 避免重复创建 HTTP Client
- 新增 PurposeToolCalling 适配器, 后台思考工具调用走专用路由
- envFallback 超时 120s→180s, 显式设置 MaxRetries
- devtools.bat 全英文, 解决 Windows CMD GBK 编码乱码问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:44:53 +08:00
parent 7eb5e984c2
commit 47f9de2409
8 changed files with 266 additions and 22 deletions
+23 -3
View File
@@ -67,7 +67,8 @@ func main() {
APIKey: cfg.LLMAPIKey,
Model: cfg.LLMModel,
FallbackModel: cfg.LLMFallbackModel,
Timeout: 120 * time.Second,
MaxRetries: 3,
Timeout: 180 * time.Second,
}
// 创建 ModelSelector (优先使用 models.json,回退到 .env)
@@ -82,10 +83,12 @@ func main() {
thinkerAdapter := llm.NewAdapter(provider)
provider, _ = modelSelector.Select(context.Background(), llm.PurposeMemoryExtraction)
memoryAdapter := llm.NewAdapter(provider)
provider, _ = modelSelector.Select(context.Background(), llm.PurposeToolCalling)
toolAdapter := llm.NewAdapter(provider)
if configLoader != nil && configLoader.HasConfig() {
log.Printf("LLM适配器已就绪: models.json 驱动 (chat=%s, intent=%s, think=%s, memory=%s)",
chatAdapter.ModelName(), intentAdapter.ModelName(), thinkerAdapter.ModelName(), memoryAdapter.ModelName())
log.Printf("LLM适配器已就绪: models.json 驱动 (chat=%s, intent=%s, think=%s, memory=%s, tool=%s)",
chatAdapter.ModelName(), intentAdapter.ModelName(), thinkerAdapter.ModelName(), memoryAdapter.ModelName(), toolAdapter.ModelName())
} else {
log.Printf("LLM适配器已就绪: .env 驱动 (模型=%s)", chatAdapter.ModelName())
}
@@ -193,6 +196,7 @@ func main() {
personaLoader,
memRetriever,
thinkerAdapter,
toolAdapter,
iotClient,
memStore,
toolRegistry,
@@ -311,6 +315,22 @@ func main() {
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + chatAdapter.ModelName() + `"}`))
})
// LLM 调用日志(调试用)
mux.HandleFunc("/api/v1/llm-calls", func(w http.ResponseWriter, r *http.Request) {
limit := 50
if n, err := fmt.Sscanf(r.URL.Query().Get("limit"), "%d", &limit); n != 1 || err != nil || limit <= 0 {
limit = 50
}
if limit > 500 {
limit = 500
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"calls": llm.GetCalls(limit),
"total": len(llm.GetCalls(0)),
})
})
// 启动HTTP服务
srv := &http.Server{
Addr: ":" + cfg.Port,