package main import ( "bufio" "bytes" "context" "encoding/json" "fmt" "net/http" "strings" ) // llmChat: 调用百炼 OpenAI 兼容接口,客服机器人人设,流式返回。 func llmChat(ctx context.Context, cfg agentCfg, history []chatMsg) (<-chan string, <-chan error) { textCh := make(chan string, 16) errCh := make(chan error, 1) msgs := make([]chatMsg, 0, len(history)+1) msgs = append(msgs, chatMsg{Role: "system", Content: systemPrompt}) msgs = append(msgs, history...) body, _ := json.Marshal(map[string]any{ "model": cfg.LLMModel, "messages": msgs, "stream": true, // qwen3 默认开 thinking(多耗时~0.5s且无播报价值),关掉 "enable_thinking": false, }) req, err := http.NewRequestWithContext(ctx, "POST", cfg.BailianBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { close(textCh) errCh <- err return textCh, errCh } req.Header.Set("Authorization", "Bearer "+cfg.BailianKey) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { close(textCh) errCh <- err return textCh, errCh } go func() { defer resp.Body.Close() defer close(textCh) if resp.StatusCode != 200 { b := new(bytes.Buffer) b.ReadFrom(resp.Body) errCh <- fmt.Errorf("LLM %s: %s", resp.Status, b.String()) return } sc := bufio.NewScanner(resp.Body) sc.Buffer(make([]byte, 64*1024), 1024*1024) defer func() { // ponytail: buffered errCh,扫完总能写入,避免读端永久阻塞 errCh <- sc.Err() }() for sc.Scan() { line := sc.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { return } var chunk struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if json.Unmarshal([]byte(data), &chunk) != nil { continue } for _, c := range chunk.Choices { if c.Delta.Content != "" { textCh <- c.Delta.Content } } } }() return textCh, errCh } const systemPrompt = `你是"小智",某电商平台的电话客服机器人。规则: - 用口语化、亲切的中文简短回答,每句不超过两句话,适合语音播报。 - 常见问题直接答:订单查询请提供订单号;退换货支持7天无理由;发货一般48小时内;客服热线工作时间9:00-21:00。 - 超出范围的问题,礼貌说明并引导转人工客服。 - 不要使用 markdown、表情符号或列表编号。`