Files

94 lines
2.5 KiB
Go

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、表情符号或列表编号。`