Version: 0.9.47.dev.260427
后端: 1. execute 节点继续拆职责——超大 execute.go 下沉为 node/execute 子包,按决策流、动作路由、上下文锚点、工具执行、状态快照、工具展示与参数解析拆分;顶层 execute.go 收敛为桥接导出,降低单文件编排/业务/模型/工具逻辑混写 2. 节点公共能力继续沉到 shared——抽出 LLM 纠错回灌、完整上下文调试日志、thinking 开关、统一上下文压缩、可见 assistant 文本持久化等 node_* 公共件,减少 execute 独占实现并为其他节点复用铺路 3. speak 文本整理能力独立收口——新增 speak_text 辅助文件,补齐正文归一化的独立承载,继续收缩 execute 主文件体积 前端: 4. NewAgent 时间线接入 business_card 业务卡片协议——schedule_agent.ts 新增 task_query / task_record 卡片载荷类型与 business_card kind;AssistantPanel 增加业务卡片事件存储、时间线恢复、块渲染分支与 BusinessCardRenderer 接入,同时保留 interrupt / status / tool / reasoning 多块并存 5. 新增任务查询卡片与任务记录卡片组件,并补充 DesignDemo 设计预览页与路由,前端可先行验证 business_card 的视觉与交互落点 文档: 6. 新增 newagent business card 前后端对接说明,明确 timeline kind、payload 结构、卡片分类、前后端发射/渲染约束
This commit is contained in:
117
backend/newAgent/shared/node_correction.go
Normal file
117
backend/newAgent/shared/node_correction.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package newagentshared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
correctionHistoryKindKey = "newagent_history_kind"
|
||||
correctionHistoryKindCorrectionUser = "llm_correction_prompt"
|
||||
)
|
||||
|
||||
// AppendLLMCorrection 追加 LLM 修正提示到对话历史。
|
||||
//
|
||||
// 设计目的:
|
||||
// 1. 当 LLM 输出不符合预期(如不支持的 action、格式错误等),不应直接报错终止;
|
||||
// 2. 应该给 LLM 一个自我修正的机会,把错误反馈写回历史,让它重新生成;
|
||||
// 3. 该函数封装了“追加 assistant 消息 + 追加纠正提示”的通用流程。
|
||||
//
|
||||
// 参数说明:
|
||||
// - conversationContext: 对话上下文,用于追加历史消息;
|
||||
// - llmOutput: LLM 的原始输出内容,会作为 assistant 消息追加;
|
||||
// - validOptionsDesc: 合法选项的描述,用于构造纠正提示。
|
||||
func AppendLLMCorrection(
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
llmOutput string,
|
||||
validOptionsDesc string,
|
||||
) {
|
||||
if conversationContext == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assistantContent := strings.TrimSpace(llmOutput)
|
||||
appendCorrectionAssistantIfNeeded(conversationContext, assistantContent)
|
||||
|
||||
correctionContent := fmt.Sprintf(
|
||||
"你的输出不符合预期。%s 请重新分析当前状态,输出正确的内容。",
|
||||
validOptionsDesc,
|
||||
)
|
||||
conversationContext.AppendHistory(&schema.Message{
|
||||
Role: schema.User,
|
||||
Content: correctionContent,
|
||||
Extra: map[string]any{
|
||||
correctionHistoryKindKey: correctionHistoryKindCorrectionUser,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AppendLLMCorrectionWithHint 追加 LLM 修正提示(带自定义错误描述)。
|
||||
//
|
||||
// 相比 AppendLLMCorrection,该函数允许调用方提供更详细的错误描述,
|
||||
// 适用于需要明确告知 LLM 具体哪里出错的场景。
|
||||
func AppendLLMCorrectionWithHint(
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
llmOutput string,
|
||||
errorDesc string,
|
||||
validOptionsDesc string,
|
||||
) {
|
||||
if conversationContext == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assistantContent := strings.TrimSpace(llmOutput)
|
||||
appendCorrectionAssistantIfNeeded(conversationContext, assistantContent)
|
||||
|
||||
correctionContent := fmt.Sprintf(
|
||||
"%s %s 请重新分析当前状态,输出正确的内容。",
|
||||
errorDesc,
|
||||
validOptionsDesc,
|
||||
)
|
||||
conversationContext.AppendHistory(&schema.Message{
|
||||
Role: schema.User,
|
||||
Content: correctionContent,
|
||||
Extra: map[string]any{
|
||||
correctionHistoryKindKey: correctionHistoryKindCorrectionUser,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// appendCorrectionAssistantIfNeeded 在纠错回灌前做最小降噪。
|
||||
//
|
||||
// 1. 空文本直接跳过,避免写入“占位噪音”;
|
||||
// 2. 若与“最近一条 assistant 文本”完全一致则跳过,避免同句反复回灌;
|
||||
// 3. 仅负责“是否回灌”判定,不负责生成纠错 user 提示。
|
||||
func appendCorrectionAssistantIfNeeded(
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
assistantContent string,
|
||||
) {
|
||||
if conversationContext == nil {
|
||||
return
|
||||
}
|
||||
assistantContent = strings.TrimSpace(assistantContent)
|
||||
if assistantContent == "" {
|
||||
return
|
||||
}
|
||||
|
||||
history := conversationContext.HistorySnapshot()
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
msg := history[i]
|
||||
if msg == nil || msg.Role != schema.Assistant {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(msg.Content) == assistantContent {
|
||||
return
|
||||
}
|
||||
// 只看最近一条 assistant,避免误去重很久以前的正常重复表达。
|
||||
break
|
||||
}
|
||||
|
||||
conversationContext.AppendHistory(&schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: assistantContent,
|
||||
})
|
||||
}
|
||||
121
backend/newAgent/shared/node_llm_debug.go
Normal file
121
backend/newAgent/shared/node_llm_debug.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package newagentshared
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// LogNodeLLMContext 将某个节点即将送入 LLM 的完整消息上下文按统一格式打印到日志。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 统一输出 stage / phase / chat / round,方便按一次请求内的多次 LLM 调用串联排查;
|
||||
// 2. 完整展开 messages,不做截断,保证问题复现时能直接对照 prompt 组装结果;
|
||||
// 3. 该函数只负责调试日志,不参与任何业务判断,也不修改上下文内容。
|
||||
func LogNodeLLMContext(
|
||||
stage string,
|
||||
phase string,
|
||||
flowState *newagentmodel.CommonState,
|
||||
messages []*schema.Message,
|
||||
) {
|
||||
chatID := ""
|
||||
roundUsed := 0
|
||||
if flowState != nil {
|
||||
chatID = flowState.ConversationID
|
||||
roundUsed = flowState.RoundUsed
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[DEBUG] %s LLM context begin phase=%s chat=%s round=%d message_count=%d\n%s\n[DEBUG] %s LLM context end phase=%s chat=%s round=%d",
|
||||
stage,
|
||||
strings.TrimSpace(phase),
|
||||
chatID,
|
||||
roundUsed,
|
||||
len(messages),
|
||||
formatLLMMessagesForDebug(messages),
|
||||
stage,
|
||||
strings.TrimSpace(phase),
|
||||
chatID,
|
||||
roundUsed,
|
||||
)
|
||||
}
|
||||
|
||||
// formatLLMMessagesForDebug 将本轮送入 LLM 的完整消息上下文展开成可读多行日志。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 按消息索引逐条输出,便于和上游上下文构造步骤逐项对齐;
|
||||
// 2. 完整输出 content / reasoning_content / tool_calls / extra,不做截断;
|
||||
// 3. 仅用于调试打点,不参与业务决策。
|
||||
func formatLLMMessagesForDebug(messages []*schema.Message) string {
|
||||
if len(messages) == 0 {
|
||||
return "(empty messages)"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, msg := range messages {
|
||||
sb.WriteString(fmt.Sprintf("----- message[%d] -----\n", i))
|
||||
if msg == nil {
|
||||
sb.WriteString("role: <nil>\n\n")
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("role: %s\n", msg.Role))
|
||||
|
||||
if strings.TrimSpace(msg.ToolCallID) != "" {
|
||||
sb.WriteString(fmt.Sprintf("tool_call_id: %s\n", msg.ToolCallID))
|
||||
}
|
||||
if strings.TrimSpace(msg.ToolName) != "" {
|
||||
sb.WriteString(fmt.Sprintf("tool_name: %s\n", msg.ToolName))
|
||||
}
|
||||
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
sb.WriteString("tool_calls:\n")
|
||||
for j, call := range msg.ToolCalls {
|
||||
sb.WriteString(fmt.Sprintf(" - [%d] id=%s type=%s function=%s\n", j, call.ID, call.Type, call.Function.Name))
|
||||
sb.WriteString(" arguments:\n")
|
||||
sb.WriteString(indentMultilineForDebug(call.Function.Arguments, " "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(msg.ReasoningContent) != "" {
|
||||
sb.WriteString("reasoning_content:\n")
|
||||
sb.WriteString(indentMultilineForDebug(msg.ReasoningContent, " "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("content:\n")
|
||||
sb.WriteString(indentMultilineForDebug(msg.Content, " "))
|
||||
sb.WriteString("\n")
|
||||
|
||||
if len(msg.Extra) > 0 {
|
||||
sb.WriteString("extra:\n")
|
||||
raw, err := json.MarshalIndent(msg.Extra, "", " ")
|
||||
if err != nil {
|
||||
sb.WriteString(indentMultilineForDebug("<marshal_error>", " "))
|
||||
} else {
|
||||
sb.WriteString(indentMultilineForDebug(string(raw), " "))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// indentMultilineForDebug 为多行文本统一添加前缀缩进,避免日志折行后难以阅读。
|
||||
func indentMultilineForDebug(text, prefix string) string {
|
||||
if text == "" {
|
||||
return prefix + "<empty>"
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = prefix + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
10
backend/newAgent/shared/node_thinking.go
Normal file
10
backend/newAgent/shared/node_thinking.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package newagentshared
|
||||
|
||||
import infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||||
|
||||
func ResolveThinkingMode(enabled bool) infrallm.ThinkingMode {
|
||||
if enabled {
|
||||
return infrallm.ThinkingModeEnabled
|
||||
}
|
||||
return infrallm.ThinkingModeDisabled
|
||||
}
|
||||
290
backend/newAgent/shared/node_unified_compact.go
Normal file
290
backend/newAgent/shared/node_unified_compact.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package newagentshared
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// UnifiedCompactInput 是统一压缩入口的参数。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. 从各节点输入中提取压缩所需的公共字段,消除对具体节点实现的直接依赖;
|
||||
// 2. 各节点(Plan/Chat/Deliver/Execute)构造此参数时,只需填充自己已有的运行时能力;
|
||||
// 3. StageName 和 StatusBlockID 用于区分日志来源与 SSE 状态推送目标。
|
||||
type UnifiedCompactInput struct {
|
||||
// Client 用于调用 LLM 压缩 msg1/msg2。
|
||||
Client *infrallm.Client
|
||||
// CompactionStore 用于持久化压缩摘要和 token 统计,为 nil 时跳过持久化。
|
||||
CompactionStore newagentmodel.CompactionStore
|
||||
// FlowState 提供 userID / conversationID / roundUsed 等定位信息。
|
||||
FlowState *newagentmodel.CommonState
|
||||
// Emitter 用于推送压缩进度 SSE 事件。
|
||||
Emitter *newagentstream.ChunkEmitter
|
||||
// StageName 标识当前阶段,如 execute / plan / chat / deliver。
|
||||
StageName string
|
||||
// StatusBlockID 是 SSE 状态推送的 block ID,各节点使用自己的 block ID。
|
||||
StatusBlockID string
|
||||
}
|
||||
|
||||
// CompactUnifiedMessagesIfNeeded 检查统一消息结构的 token 预算,
|
||||
// 超限时对 msg1(历史对话)和 msg2(阶段工作区)执行 LLM 压缩。
|
||||
//
|
||||
// 消息布局约定(由统一消息构造器返回):
|
||||
// [0] system - msg0: 系统规则 + 工具简表
|
||||
// [1] assistant - msg1: 历史对话上下文
|
||||
// [2] assistant - msg2: 阶段工作区(Execute=ReAct Loop,其余通常为“暂无”)
|
||||
// [3] system - msg3: 阶段状态 + 记忆 + 指令
|
||||
//
|
||||
// 压缩策略:
|
||||
// 1. msg1 超过可用预算一半时触发 LLM 压缩(合并已有摘要 + 新内容);
|
||||
// 2. msg1 压缩后仍超限,则对 msg2 也做 LLM 压缩;
|
||||
// 3. 压缩结果持久化到 CompactionStore,下一轮可复用摘要避免重复计算。
|
||||
func CompactUnifiedMessagesIfNeeded(
|
||||
ctx context.Context,
|
||||
messages []*schema.Message,
|
||||
input UnifiedCompactInput,
|
||||
) []*schema.Message {
|
||||
if input.FlowState == nil {
|
||||
log.Printf("[COMPACT:%s] FlowState is nil, skip token stats refresh", input.StageName)
|
||||
return messages
|
||||
}
|
||||
|
||||
// 1. 非严格 4 段式时,退化成按角色汇总的统计,确保 context_token_stats 仍能刷新。
|
||||
if len(messages) != 4 {
|
||||
breakdown := estimateFallbackStageTokenBreakdown(messages)
|
||||
log.Printf(
|
||||
"[COMPACT:%s] fallback token stats refresh: total=%d budget=%d count=%d (msg0=%d msg1=%d msg2=%d msg3=%d)",
|
||||
input.StageName, breakdown.Total, breakdown.Budget, len(messages),
|
||||
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
|
||||
)
|
||||
saveUnifiedTokenStats(ctx, input, breakdown)
|
||||
return messages
|
||||
}
|
||||
|
||||
// 2. 提取四条消息的文本内容,供预算检查与后续压缩使用。
|
||||
msg0 := messages[0].Content
|
||||
msg1 := messages[1].Content
|
||||
msg2 := messages[2].Content
|
||||
msg3 := messages[3].Content
|
||||
|
||||
// 3. 执行 token 预算检查,判断是否需要压缩历史对话或阶段工作区。
|
||||
breakdown, overBudget, needCompactMsg1, needCompactMsg2 := pkg.CheckStageTokenBudget(msg0, msg1, msg2, msg3)
|
||||
|
||||
log.Printf(
|
||||
"[COMPACT:%s] token budget check: total=%d budget=%d over=%v compactMsg1=%v compactMsg2=%v (msg0=%d msg1=%d msg2=%d msg3=%d)",
|
||||
input.StageName, breakdown.Total, breakdown.Budget, overBudget, needCompactMsg1, needCompactMsg2,
|
||||
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
|
||||
)
|
||||
|
||||
if !overBudget {
|
||||
// 4. 未超限时仅记录 token 分布,不做压缩。
|
||||
saveUnifiedTokenStats(ctx, input, breakdown)
|
||||
return messages
|
||||
}
|
||||
|
||||
// 5. 先压缩 msg1(历史对话),它通常是最主要的 token 消耗来源。
|
||||
if needCompactMsg1 {
|
||||
msg1 = compactUnifiedMsg1(ctx, input, msg1)
|
||||
messages[1].Content = msg1
|
||||
breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3)
|
||||
}
|
||||
|
||||
// 6. 若 msg1 压缩后仍超限,再压缩 msg2(阶段工作区 / ReAct 记录)。
|
||||
if needCompactMsg2 || breakdown.Total > pkg.StageTokenBudget {
|
||||
msg2 = compactUnifiedMsg2(ctx, input, msg2)
|
||||
messages[2].Content = msg2
|
||||
breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3)
|
||||
}
|
||||
|
||||
// 7. 记录最终 token 分布,供后续调试与监控使用。
|
||||
saveUnifiedTokenStats(ctx, input, breakdown)
|
||||
|
||||
log.Printf(
|
||||
"[COMPACT:%s] after compaction: total=%d budget=%d (msg0=%d msg1=%d msg2=%d msg3=%d)",
|
||||
input.StageName, breakdown.Total, breakdown.Budget,
|
||||
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
|
||||
)
|
||||
return messages
|
||||
}
|
||||
|
||||
// estimateFallbackStageTokenBreakdown 在非统一 4 段式场景下按消息角色做近似统计。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先按消息类型汇总 token,保证总量准确;
|
||||
// 2. 再把最后一个 user 消息尽量视作 msg3,保留阶段指令语义;
|
||||
// 3. 其他历史内容归入 msg1 / msg2,确保上下文统计不会因为结构不标准而断更。
|
||||
func estimateFallbackStageTokenBreakdown(messages []*schema.Message) pkg.StageTokenBreakdown {
|
||||
breakdown := pkg.StageTokenBreakdown{Budget: pkg.StageTokenBudget}
|
||||
if len(messages) == 0 {
|
||||
return breakdown
|
||||
}
|
||||
|
||||
lastUserIndex := -1
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
msg := messages[i]
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
if msg.Role == schema.User {
|
||||
lastUserIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i, msg := range messages {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
tokens := pkg.EstimateMessageTokens(msg)
|
||||
breakdown.Total += tokens
|
||||
|
||||
switch msg.Role {
|
||||
case schema.System:
|
||||
breakdown.Msg0 += tokens
|
||||
case schema.User:
|
||||
if i == lastUserIndex {
|
||||
breakdown.Msg3 += tokens
|
||||
} else {
|
||||
breakdown.Msg1 += tokens
|
||||
}
|
||||
case schema.Tool:
|
||||
breakdown.Msg2 += tokens
|
||||
case schema.Assistant:
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
breakdown.Msg2 += tokens
|
||||
} else {
|
||||
breakdown.Msg1 += tokens
|
||||
}
|
||||
default:
|
||||
breakdown.Msg1 += tokens
|
||||
}
|
||||
}
|
||||
|
||||
return breakdown
|
||||
}
|
||||
|
||||
// compactUnifiedMsg1 对 msg1(历史对话)执行 LLM 压缩。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. CompactionStore 为 nil 时跳过(测试环境 / 骨架期);
|
||||
// 2. 先加载该阶段已有的压缩摘要,与当前 msg1 合并后调 LLM 压缩;
|
||||
// 3. 压缩失败时降级为原始文本,不中断主流程;
|
||||
// 4. 压缩成功后持久化新摘要,供下一轮复用。
|
||||
func compactUnifiedMsg1(
|
||||
ctx context.Context,
|
||||
input UnifiedCompactInput,
|
||||
msg1 string,
|
||||
) string {
|
||||
if input.CompactionStore == nil {
|
||||
log.Printf("[COMPACT:%s] CompactionStore is nil, skip msg1 compaction", input.StageName)
|
||||
return msg1
|
||||
}
|
||||
|
||||
existingSummary, _, err := input.CompactionStore.LoadStageCompaction(ctx, input.FlowState.UserID, input.FlowState.ConversationID, input.StageName)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT:%s] load existing compaction failed: %v, proceed without cache", input.StageName, err)
|
||||
}
|
||||
|
||||
tokenBefore := pkg.EstimateTextTokens(msg1)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_start",
|
||||
fmt.Sprintf("正在压缩对话历史(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
newSummary, err := newagentprompt.CompactMsg1(ctx, input.Client, msg1, existingSummary)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT:%s] compact msg1 failed: %v", input.StageName, err)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_done",
|
||||
"对话历史压缩失败,使用原始文本",
|
||||
false,
|
||||
)
|
||||
return msg1
|
||||
}
|
||||
|
||||
tokenAfter := pkg.EstimateTextTokens(newSummary)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_done",
|
||||
fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter),
|
||||
false,
|
||||
)
|
||||
|
||||
if err := input.CompactionStore.SaveStageCompaction(ctx, input.FlowState.UserID, input.FlowState.ConversationID, input.StageName, newSummary, input.FlowState.RoundUsed); err != nil {
|
||||
log.Printf("[COMPACT:%s] save compaction failed: %v", input.StageName, err)
|
||||
}
|
||||
|
||||
return newSummary
|
||||
}
|
||||
|
||||
// compactUnifiedMsg2 对 msg2(阶段工作区)执行 LLM 压缩。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 非 Execute 阶段的 msg2 通常内容较少,压缩即使收益有限也不应出错;
|
||||
// 2. Execute 阶段的 msg2 包含 ReAct loop 记录,压缩可显著节省 token;
|
||||
// 3. 压缩失败时降级为原始文本,不中断主流程。
|
||||
func compactUnifiedMsg2(
|
||||
ctx context.Context,
|
||||
input UnifiedCompactInput,
|
||||
msg2 string,
|
||||
) string {
|
||||
tokenBefore := pkg.EstimateTextTokens(msg2)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_start",
|
||||
fmt.Sprintf("正在压缩执行记录(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
compressed, err := newagentprompt.CompactMsg2(ctx, input.Client, msg2)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT:%s] compact msg2 failed: %v", input.StageName, err)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_done",
|
||||
"执行记录压缩失败,使用原始文本",
|
||||
false,
|
||||
)
|
||||
return msg2
|
||||
}
|
||||
|
||||
tokenAfter := pkg.EstimateTextTokens(compressed)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_done",
|
||||
fmt.Sprintf("执行记录已压缩:%d → %d tokens", tokenBefore, tokenAfter),
|
||||
false,
|
||||
)
|
||||
|
||||
return compressed
|
||||
}
|
||||
|
||||
// saveUnifiedTokenStats 持久化当前 token 分布到存储层。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. CompactionStore 为 nil 时跳过(测试环境 / 骨架期);
|
||||
// 2. 序列化失败只记日志,不中断主流程;
|
||||
// 3. 写入失败只记日志,不中断主流程。
|
||||
func saveUnifiedTokenStats(
|
||||
ctx context.Context,
|
||||
input UnifiedCompactInput,
|
||||
breakdown pkg.StageTokenBreakdown,
|
||||
) {
|
||||
if input.CompactionStore == nil || input.FlowState == nil {
|
||||
return
|
||||
}
|
||||
statsJSON, err := json.Marshal(breakdown)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT:%s] marshal token stats failed: %v", input.StageName, err)
|
||||
return
|
||||
}
|
||||
if err := input.CompactionStore.SaveContextTokenStats(ctx, input.FlowState.UserID, input.FlowState.ConversationID, string(statsJSON)); err != nil {
|
||||
log.Printf("[COMPACT:%s] save token stats failed: %v", input.StageName, err)
|
||||
}
|
||||
}
|
||||
37
backend/newAgent/shared/node_visible_message.go
Normal file
37
backend/newAgent/shared/node_visible_message.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package newagentshared
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// PersistVisibleAssistantMessage 负责把“真正要展示给用户”的 assistant 文本交给 service 层持久化。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只处理可见的 assistant 消息,不处理内部纠错提示、工具调用结果和纯状态文案;
|
||||
// 2. 持久化失败只记日志,不反向中断节点主流程,避免“已经对外输出但后端补写失败”时把用户请求打断;
|
||||
// 3. 具体的 Redis / MySQL / 乐观缓存写入由 service 回调统一完成。
|
||||
func PersistVisibleAssistantMessage(
|
||||
ctx context.Context,
|
||||
persist newagentmodel.PersistVisibleMessageFunc,
|
||||
state *newagentmodel.CommonState,
|
||||
msg *schema.Message,
|
||||
) {
|
||||
if persist == nil || state == nil || msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
role := strings.TrimSpace(string(msg.Role))
|
||||
content := strings.TrimSpace(msg.Content)
|
||||
if role != string(schema.Assistant) || content == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := persist(ctx, state, msg); err != nil {
|
||||
log.Printf("[WARN] persist visible assistant message failed chat=%s phase=%s err=%v", state.ConversationID, state.Phase, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user