Files
smartmate/backend/agent/graph.go
LoveLosita 4906f814fd Version: 0.4.6.dev.260307
feat: 🎯 新增 Token 估算与裁剪工具

* 在 `backend/pkg/token_budget.go` 中新增 Token 估算与裁剪工具

  * 最大上下文 Token 数量设置为 224000,预留冗余 28000
  * 从最旧消息开始裁剪,直到历史 Token 数量低于预算
  * 根据裁剪后的历史消息数量动态计算 Redis 动态窗口大小

refactor: ♻️ 接入 Token 裁剪至 Service 主流程

* 在 `backend/service/agent.go` 中接入 Token 裁剪逻辑

  * 先从历史数据获取(缓存未命中则查询数据库)
  * 按 Token 预算裁剪历史消息,裁剪后再喂模型
  * 根据裁剪结果动态调整 Redis 会话窗口

refactor: ♻️ 改造 Redis 历史队列为会话级动态窗口

* 在 `backend/dao/agent-cache.go` 中新增 `SetSessionWindowSize` 与 `EnforceHistoryWindow`
* `PushMessage` 和 `BackfillHistory` 方法使用会话动态窗口,而非固定 20 条历史消息
* 默认窗口大小提升至 128,但会被会话动态窗口值覆盖
2026-03-07 16:37:07 +08:00

153 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"context"
"encoding/json"
"io"
"strings"
"time"
"github.com/cloudwego/eino-ext/components/model/ark"
"github.com/cloudwego/eino/schema"
"github.com/google/uuid"
arkModel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
)
// StreamResponse 为 OpenAI/DeepSeek 兼容的流式 chunk 结构。
type StreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []StreamChoice `json:"choices"`
}
type StreamChoice struct {
Index int `json:"index"`
Delta StreamDelta `json:"delta"`
FinishReason *string `json:"finish_reason"`
}
type StreamDelta struct {
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}
// ToOpenAIStream 将单个 Eino chunk 转为 OpenAI 兼容 JSON。
func ToOpenAIStream(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool) (string, error) {
delta := StreamDelta{}
if includeRole {
delta.Role = "assistant"
}
if chunk != nil {
delta.Content = chunk.Content
delta.ReasoningContent = chunk.ReasoningContent
}
if delta.Role == "" && delta.Content == "" && delta.ReasoningContent == "" {
return "", nil
}
dto := StreamResponse{
ID: requestID,
Object: "chat.completion.chunk",
Created: created,
Model: modelName,
Choices: []StreamChoice{{
Index: 0,
Delta: delta,
FinishReason: nil,
}},
}
jsonBytes, err := json.Marshal(dto)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
// ToOpenAIFinishStream 生成结束 chunkfinish_reason=stop
func ToOpenAIFinishStream(requestID, modelName string, created int64) (string, error) {
stop := "stop"
dto := StreamResponse{
ID: requestID,
Object: "chat.completion.chunk",
Created: created,
Model: modelName,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{},
FinishReason: &stop,
}},
}
jsonBytes, err := json.Marshal(dto)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
func StreamChat(ctx context.Context, llm *ark.ChatModel, modelName string, userInput string, ifThinking bool, chatHistory []*schema.Message, outChan chan<- string) (string, error) {
// 1) 组装提示消息
messages := make([]*schema.Message, 0)
messages = append(messages, schema.SystemMessage(SystemPrompt))
if len(chatHistory) > 0 {
messages = append(messages, chatHistory...)
}
messages = append(messages, schema.UserMessage(userInput))
// 2) 发起流式请求
var thinking *ark.Thinking
if ifThinking {
thinking = &arkModel.Thinking{Type: arkModel.ThinkingTypeEnabled}
} else {
thinking = &arkModel.Thinking{Type: arkModel.ThinkingTypeDisabled}
}
reader, err := llm.Stream(ctx, messages, ark.WithThinking(thinking))
if err != nil {
return "", err
}
defer reader.Close()
if strings.TrimSpace(modelName) == "" {
modelName = "smartflow-worker"
}
requestID := "chatcmpl-" + uuid.NewString()
created := time.Now().Unix()
firstChunk := true
// 3) 持续转发 chunk
var fullText strings.Builder
for {
chunk, err := reader.Recv()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
fullText.WriteString(chunk.Content)
payload, err := ToOpenAIStream(chunk, requestID, modelName, created, firstChunk)
if err != nil {
return "", err
}
if payload != "" {
outChan <- payload
firstChunk = false
}
}
// 4) 发送结束 chunk 和 [DONE]
finishChunk, err := ToOpenAIFinishStream(requestID, modelName, created)
if err != nil {
return "", err
}
outChan <- finishChunk
outChan <- "[DONE]"
return fullText.String(), nil
}