package rag import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) // embedClient 是 OpenAI 兼容的 embeddings 客户端(provider 抽象)。 // 开发期指向第三方在线 API 或本地 mock;生产期换自部署/在线 embedding 模型。 type embedClient struct { baseURL string apiKey string model string hc *http.Client } func newEmbedClient(baseURL, apiKey, model string) *embedClient { return &embedClient{ baseURL: baseURL, apiKey: apiKey, model: model, hc: &http.Client{Timeout: 30 * time.Second}, } } func (e *embedClient) ready() bool { return e != nil && e.baseURL != "" && e.model != "" } // Embed 把若干文本向量化(OpenAI 兼容 /embeddings)。 func (e *embedClient) Embed(ctx context.Context, texts []string) ([][]float32, error) { if !e.ready() { return nil, fmt.Errorf("embedding not configured") } body, _ := json.Marshal(map[string]any{"model": e.model, "input": texts}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.baseURL+"/embeddings", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if e.apiKey != "" { req.Header.Set("Authorization", "Bearer "+e.apiKey) } resp, err := e.hc.Do(req) if err != nil { return nil, fmt.Errorf("embed request: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) return nil, fmt.Errorf("embed http %d: %s", resp.StatusCode, buf.String()) } var out struct { Data []struct { Embedding []float32 `json:"embedding"` Index int `json:"index"` } `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, fmt.Errorf("embed decode: %w", err) } vecs := make([][]float32, len(out.Data)) for _, d := range out.Data { if d.Index >= 0 && d.Index < len(vecs) { vecs[d.Index] = d.Embedding } } return vecs, nil }