Skip to content

Commit d6574ad

Browse files
authored
Merge pull request #70 from yokowu/feat-optimize-proxy
feat(proxy): 优化流式代理, 隔离代理与记录逻辑
2 parents 2ca1ac5 + fd86e3b commit d6574ad

8 files changed

Lines changed: 757 additions & 110 deletions

File tree

backend/config/config.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ type Config struct {
4949
} `mapstructure:"redis"`
5050

5151
LLMProxy struct {
52-
Timeout string `mapstructure:"timeout"`
53-
KeepAlive string `mapstructure:"keep_alive"`
54-
ClientPoolSize int `mapstructure:"client_pool_size"`
55-
RequestLogPath string `mapstructure:"request_log_path"`
52+
Timeout string `mapstructure:"timeout"`
53+
KeepAlive string `mapstructure:"keep_alive"`
54+
ClientPoolSize int `mapstructure:"client_pool_size"`
55+
StreamClientPoolSize int `mapstructure:"stream_client_pool_size"`
56+
RequestLogPath string `mapstructure:"request_log_path"`
5657
} `mapstructure:"llm_proxy"`
5758

5859
InitModel struct {
@@ -92,6 +93,7 @@ func Init() (*Config, error) {
9293
v.SetDefault("llm_proxy.timeout", "30s")
9394
v.SetDefault("llm_proxy.keep_alive", "60s")
9495
v.SetDefault("llm_proxy.client_pool_size", 100)
96+
v.SetDefault("llm_proxy.stream_client_pool_size", 5000)
9597
v.SetDefault("llm_proxy.request_log_path", "/app/request/logs")
9698
v.SetDefault("init_model.name", "qwen2.5-coder-3b-instruct")
9799
v.SetDefault("init_model.key", "")

backend/internal/middleware/logger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func RequestID() echo.MiddlewareFunc {
1515
return func(c echo.Context) error {
1616
ctx := c.Request().Context()
1717
requestID := uuid.New().String()
18-
ctx = context.WithValue(ctx, logger.RequestIDKey, requestID)
18+
ctx = context.WithValue(ctx, logger.RequestIDKey{}, requestID)
1919
c.SetRequest(c.Request().WithContext(ctx))
2020
return next(c)
2121
}

backend/internal/middleware/proxy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (p *ProxyMiddleware) Auth() echo.MiddlewareFunc {
4444
}
4545

4646
ctx := c.Request().Context()
47-
ctx = context.WithValue(ctx, logger.UserIDKey, key.UserID)
47+
ctx = context.WithValue(ctx, logger.UserIDKey{}, key.UserID)
4848
c.SetRequest(c.Request().WithContext(ctx))
4949
c.Set(ApiContextKey, key)
5050
return next(c)

backend/internal/proxy/proxy.go

Lines changed: 43 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type LLMProxy struct {
5959
usecase domain.ProxyUsecase
6060
cfg *config.Config
6161
client *http.Client
62+
streamClient *http.Client
6263
logger *slog.Logger
6364
requestLogPath string // 请求日志保存路径
6465
}
@@ -83,7 +84,6 @@ func NewLLMProxy(
8384
logger.Warn("解析保持连接时间失败, 使用默认值 60s", "error", err)
8485
}
8586

86-
// 创建HTTP客户端
8787
client := &http.Client{
8888
Timeout: timeout,
8989
Transport: &http.Transport{
@@ -98,6 +98,18 @@ func NewLLMProxy(
9898
},
9999
}
100100

101+
streamClient := &http.Client{
102+
Timeout: 60 * time.Second,
103+
Transport: &http.Transport{
104+
MaxIdleConns: cfg.LLMProxy.StreamClientPoolSize,
105+
MaxConnsPerHost: cfg.LLMProxy.StreamClientPoolSize,
106+
MaxIdleConnsPerHost: cfg.LLMProxy.StreamClientPoolSize,
107+
IdleConnTimeout: 24 * time.Hour,
108+
TLSHandshakeTimeout: 10 * time.Second,
109+
ExpectContinueTimeout: 1 * time.Second,
110+
},
111+
}
112+
101113
// 获取日志配置
102114
requestLogPath := ""
103115
if cfg.LLMProxy.RequestLogPath != "" {
@@ -111,6 +123,7 @@ func NewLLMProxy(
111123
return &LLMProxy{
112124
usecase: usecase,
113125
client: client,
126+
streamClient: streamClient,
114127
cfg: cfg,
115128
requestLogPath: requestLogPath,
116129
logger: logger,
@@ -174,12 +187,12 @@ type Ctx struct {
174187
func (p *LLMProxy) handle(ctx context.Context, fn func(ctx *Ctx, log *RequestResponseLog) error) {
175188
// 获取用户ID
176189
userID := "unknown"
177-
if id, ok := ctx.Value(logger.UserIDKey).(string); ok {
190+
if id, ok := ctx.Value(logger.UserIDKey{}).(string); ok {
178191
userID = id
179192
}
180193

181194
requestID := "unknown"
182-
if id, ok := ctx.Value(logger.RequestIDKey).(string); ok {
195+
if id, ok := ctx.Value(logger.RequestIDKey{}).(string); ok {
183196
requestID = id
184197
}
185198

@@ -203,11 +216,11 @@ func (p *LLMProxy) handle(ctx context.Context, fn func(ctx *Ctx, log *RequestRes
203216
}
204217

205218
if err := fn(c, l); err != nil {
206-
p.logger.With("userID", userID, "requestID", requestID, "sourceip", sourceip).ErrorContext(ctx, "处理请求失败", "error", err)
219+
p.logger.With("source_ip", sourceip).ErrorContext(ctx, "处理请求失败", "error", err)
207220
l.Error = err.Error()
208221
}
209222

210-
p.saveRequestResponseLog(l)
223+
go p.saveRequestResponseLog(l)
211224
}
212225

213226
func (p *LLMProxy) HandleCompletion(ctx context.Context, w http.ResponseWriter, req domain.CompletionRequest) {
@@ -585,10 +598,6 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
585598
return err
586599
}
587600

588-
prompt := p.getPrompt(ctx, req)
589-
mode := req.Metadata["mode"]
590-
taskID := req.Metadata["task_id"]
591-
592601
upstream := m.APIBase + endpoint
593602
log.UpstreamURL = upstream
594603

@@ -606,9 +615,7 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
606615

607616
newReq.Header.Set("Content-Type", "application/json")
608617
newReq.Header.Set("Accept", "text/event-stream")
609-
if m.APIKey != "" && m.APIKey != "none" {
610-
newReq.Header.Set("Authorization", "Bearer "+m.APIKey)
611-
}
618+
newReq.Header.Set("Authorization", "Bearer "+m.APIKey)
612619

613620
// 保存请求头(去除敏感信息)
614621
requestHeaders := make(map[string][]string)
@@ -622,22 +629,26 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
622629
}
623630
log.RequestHeader = requestHeaders
624631

625-
p.logger.With(
632+
logger := p.logger.With(
633+
"request_id", c.RequestID,
634+
"source_ip", c.SourceIP,
626635
"upstreamURL", upstream,
627636
"modelName", m.ModelName,
628637
"modelType", consts.ModelTypeLLM,
629638
"apiBase", m.APIBase,
630-
"work_mode", mode,
639+
)
640+
641+
logger.With(
642+
"upstreamURL", upstream,
631643
"requestHeader", newReq.Header,
632644
"requestBody", req,
633-
"taskID", taskID,
634645
"messages", cvt.Filter(req.Messages, func(i int, v openai.ChatCompletionMessage) (openai.ChatCompletionMessage, bool) {
635646
return v, v.Role != "system"
636647
}),
637648
).DebugContext(ctx, "转发流式请求到上游API")
638649

639650
// 发送请求
640-
resp, err := p.client.Do(newReq)
651+
resp, err := p.streamClient.Do(newReq)
641652
if err != nil {
642653
p.logger.With("upstreamURL", upstream).WarnContext(ctx, "发送上游流式请求失败", "error", err)
643654
return fmt.Errorf("发送上游请求失败: %w", err)
@@ -655,17 +666,16 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
655666
log.Latency = time.Since(startTime).Milliseconds()
656667

657668
// 在debug级别记录错误的流式响应内容
658-
p.logger.With(
669+
logger.With(
659670
"statusCode", resp.StatusCode,
660671
"responseHeader", resp.Header,
661672
"responseBody", string(responseBody),
662673
).DebugContext(ctx, "上游流式响应错误原始内容")
663674

664675
var errorResp ErrResp
665676
if err := json.Unmarshal(responseBody, &errorResp); err == nil {
666-
p.logger.With(
677+
logger.With(
667678
"endpoint", endpoint,
668-
"upstreamURL", upstream,
669679
"requestBody", newReq,
670680
"statusCode", resp.StatusCode,
671681
"errorType", errorResp.Error.Type,
@@ -677,9 +687,8 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
677687
return fmt.Errorf("上游API返回错误: %s", errorResp.Error.Message)
678688
}
679689

680-
p.logger.With(
690+
logger.With(
681691
"endpoint", endpoint,
682-
"upstreamURL", upstream,
683692
"requestBody", newReq,
684693
"statusCode", resp.StatusCode,
685694
"responseBody", string(responseBody),
@@ -688,12 +697,10 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
688697
return fmt.Errorf("上游API返回非200状态码: %d, 响应: %s", resp.StatusCode, string(responseBody))
689698
}
690699

691-
// 更新日志信息
692700
log.StatusCode = resp.StatusCode
693701
log.ResponseHeader = resp.Header
694702

695-
// 在debug级别记录流式响应头信息
696-
p.logger.With(
703+
logger.With(
697704
"statusCode", resp.StatusCode,
698705
"responseHeader", resp.Header,
699706
).DebugContext(ctx, "上游流式响应头信息")
@@ -705,78 +712,18 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
705712
w.Header().Set("Transfer-Encoding", "chunked")
706713
w.Header().Set("X-Accel-Buffering", "no")
707714

708-
rc := &domain.RecordParam{
709-
RequestID: c.RequestID,
710-
TaskID: taskID,
711-
UserID: c.UserID,
712-
ModelID: m.ID,
713-
ModelType: consts.ModelTypeLLM,
714-
WorkMode: mode,
715-
Prompt: prompt,
716-
Role: consts.ChatRoleAssistant,
717-
}
718-
719-
ch := make(chan []byte, 1024)
720-
defer close(ch)
721-
722-
go func(rc *domain.RecordParam) {
723-
if rc.Prompt != "" {
724-
urc := rc.Clone()
725-
urc.Role = consts.ChatRoleUser
726-
urc.Completion = urc.Prompt
727-
if err := p.usecase.Record(context.Background(), urc); err != nil {
728-
p.logger.With("modelID", m.ID, "modelName", m.ModelName, "modelType", consts.ModelTypeLLM).
729-
WarnContext(ctx, "插入流式记录失败", "error", err)
730-
}
731-
}
732-
733-
for line := range ch {
734-
if bytes.HasPrefix(line, []byte("data:")) {
735-
line = bytes.TrimPrefix(line, []byte("data: "))
736-
line = bytes.TrimSpace(line)
737-
if len(line) == 0 {
738-
continue
739-
}
740-
741-
if bytes.Equal(line, []byte("[DONE]")) {
742-
break
743-
}
744-
745-
var t openai.ChatCompletionStreamResponse
746-
if err := json.Unmarshal(line, &t); err != nil {
747-
p.logger.With("line", string(line)).WarnContext(ctx, "解析流式数据失败", "error", err)
748-
continue
749-
}
750-
751-
p.logger.With("response", t).DebugContext(ctx, "流式响应数据")
752-
if len(t.Choices) > 0 {
753-
rc.Completion += t.Choices[0].Delta.Content
754-
}
755-
if t.Usage != nil {
756-
rc.InputTokens = int64(t.Usage.PromptTokens)
757-
rc.OutputTokens = int64(t.Usage.CompletionTokens)
758-
}
759-
}
760-
}
761-
762-
p.logger.With("record", rc).DebugContext(ctx, "流式记录")
763-
if err := p.usecase.Record(context.Background(), rc); err != nil {
764-
p.logger.With("modelID", m.ID, "modelName", m.ModelName, "modelType", consts.ModelTypeLLM).
765-
WarnContext(ctx, "插入流式记录失败", "error", err)
766-
}
767-
}(rc)
768-
769-
err = streamRead(ctx, resp.Body, func(line []byte) error {
770-
ch <- line
771-
if _, err := w.Write(line); err != nil {
772-
return fmt.Errorf("写入响应失败: %w", err)
773-
}
774-
if f, ok := w.(http.Flusher); ok {
775-
f.Flush()
776-
}
777-
return nil
778-
})
779-
return err
715+
recorder := NewChatRecorder(
716+
ctx,
717+
c,
718+
p.usecase,
719+
m,
720+
req,
721+
resp.Body,
722+
w,
723+
p.logger.With("module", "ChatRecorder"),
724+
)
725+
defer recorder.Close()
726+
return recorder.Stream()
780727
})
781728
}
782729

0 commit comments

Comments
 (0)