fix: 修复返回给前端的 totalTokens 为0的问题并提供示例 .env

This commit is contained in:
hhs
2026-06-13 21:09:56 +08:00
parent 3c665cde5d
commit b0a7ce885e
4 changed files with 43 additions and 12 deletions

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ backend/bin/
backend/server backend/server
# ---- 环境变量 ---- # ---- 环境变量 ----
.env
.env.local .env.local
.env.*.local .env.*.local

17
backend/.env Normal file
View File

@@ -0,0 +1,17 @@
# ---- AI 服务 API Key ----
CAMTALK_AI_LLM_API_KEY=sk-ws-H.REHELLY.C4s3.MEUCIQCRee37XWEKp2szaxVLFDtR1rxNNsf372zMvCR0Xl6UvQIgZgvhRTvaa1FmhbCQJgaHu4Jny29AQkn01-3hX9CWBOg
CAMTALK_AI_STT_API_KEY=tp-c9e7scwfx94qvqyhpnahnw8uaiya01za2qzvg4xe24rp3xiv
CAMTALK_AI_TTS_API_KEY=tp-c9e7scwfx94qvqyhpnahnw8uaiya01za2qzvg4xe24rp3xiv
# ---- 可选覆盖(默认值见 config.yaml----
# CAMTALK_AI_LLM_MODEL=qwen3-vl-plus
# CAMTALK_AI_LLM_ENDPOINT=https://api.openai.com/v1
# CAMTALK_AI_LLM_TIMEOUT=10
# CAMTALK_AI_STT_ENDPOINT=wss://api.deepgram.com/v1/listen
# CAMTALK_AI_TTS_ENDPOINT=https://api.openai.com/v1
# CAMTALK_AI_TTS_VOICE=alloy
# CAMTALK_AI_TTS_SPEED=1.0
# CAMTALK_AI_TTS_TIMEOUT=5
# ---- 应用 ----
# APP_ENV=dev

1
backend/.gitignore vendored
View File

@@ -3,7 +3,6 @@
bin/ bin/
# 环境配置 # 环境配置
.env
config.dev.yaml config.dev.yaml
config.prod.yaml config.prod.yaml

View File

@@ -166,11 +166,12 @@ func (p *Pipeline) ProcessQuery(
var ttsErr error var ttsErr error
// goroutine 1: 消费 LLM token + 句子切分 // goroutine 1: 消费 LLM token + 句子切分
var tokenUsage *llm.TokenUsage
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
defer close(sentenceCh) defer close(sentenceCh)
fullText = p.consumeLLMStream(ctx, llmStream, req.RequestID, sender, splitter) fullText, tokenUsage = p.consumeLLMStream(ctx, llmStream, req.RequestID, sender, splitter)
}() }()
// goroutine 2: TTS 合成(如果启用) // goroutine 2: TTS 合成(如果启用)
@@ -205,13 +206,25 @@ func (p *Pipeline) ProcessQuery(
// 发送 llm_done // 发送 llm_done
latency := time.Since(startTime).Milliseconds() latency := time.Since(startTime).Milliseconds()
if err := sender.SendLLMDone(models.WsLLMDone{ done := models.WsLLMDone{
Type: "llm_done", Type: "llm_done",
RequestID: req.RequestID, RequestID: req.RequestID,
FullText: fullText, FullText: fullText,
Model: p.model, Model: p.model,
LatencyMs: latency, LatencyMs: latency,
}); err != nil { }
if tokenUsage != nil {
done.TokensUsed = struct {
Prompt int `json:"prompt"`
Completion int `json:"completion"`
Total int `json:"total"`
}{
Prompt: tokenUsage.Prompt,
Completion: tokenUsage.Completion,
Total: tokenUsage.Total,
}
}
if err := sender.SendLLMDone(done); err != nil {
log.Errorw("发送 llm_done 失败", "error", err) log.Errorw("发送 llm_done 失败", "error", err)
} }
@@ -225,33 +238,36 @@ func (p *Pipeline) ProcessQuery(
} }
// consumeLLMStream 消费 LLM 流式输出,发送 llm_chunk 并进行句子切分。 // consumeLLMStream 消费 LLM 流式输出,发送 llm_chunk 并进行句子切分。
// 返回完整文本和 token 用量。
func (p *Pipeline) consumeLLMStream( func (p *Pipeline) consumeLLMStream(
ctx context.Context, ctx context.Context,
stream <-chan llm.Chunk, stream <-chan llm.Chunk,
requestID string, requestID string,
sender Sender, sender Sender,
splitter *Splitter, splitter *Splitter,
) string { ) (string, *llm.TokenUsage) {
log := logger.Log log := logger.Log
var fullText strings.Builder var fullText strings.Builder
var tokenUsage *llm.TokenUsage
for chunk := range stream { for chunk := range stream {
// 检查上下文是否已取消 // 检查上下文是否已取消
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Infow("LLM 流被中断", "request_id", requestID) log.Infow("LLM 流被中断", "request_id", requestID)
return fullText.String() return fullText.String(), tokenUsage
default: default:
} }
if chunk.Done { if chunk.Done {
// 流结束 // 流结束,记录 token 用量
if chunk.TokensUsed != nil { if chunk.TokensUsed != nil {
tokenUsage = chunk.TokensUsed
log.Infow("LLM 用量统计", log.Infow("LLM 用量统计",
"request_id", requestID, "request_id", requestID,
"prompt_tokens", chunk.TokensUsed.Prompt, "prompt_tokens", tokenUsage.Prompt,
"completion_tokens", chunk.TokensUsed.Completion, "completion_tokens", tokenUsage.Completion,
"total_tokens", chunk.TokensUsed.Total, "total_tokens", tokenUsage.Total,
) )
} }
break break
@@ -277,7 +293,7 @@ func (p *Pipeline) consumeLLMStream(
// 刷新切分器中的剩余文本 // 刷新切分器中的剩余文本
splitter.Flush() splitter.Flush()
return fullText.String() return fullText.String(), tokenUsage
} }
// synthesizeTTS 从句子 channel 读取文本,进行 TTS 合成并发送音频。 // synthesizeTTS 从句子 channel 读取文本,进行 TTS 合成并发送音频。