Version: 0.9.26.dev.260417
后端: 1. Prompt 层从 execute 专属骨架重构为全节点统一四段式 buildUnifiedStageMessages - 新增 unified_context.go:定义 StageMessagesConfig + buildUnifiedStageMessages 统一骨架,所有节点(Chat/Plan/Execute/Deliver/DeepAnswer)共用同一套 msg0~msg3 拼装逻辑 - 新增 conversation_view.go:通用对话历史渲染 buildConversationHistoryMessage,各节点复用,不再各自维护提取逻辑 - 新增 chat_context.go / plan_context.go / deliver_context.go:各节点自行渲染 msg1(对话视图)和 msg2(工作区),统一层只负责"怎么拼",不再替节点决定"放什么" - Chat/Plan/Deliver/Execute 的 BuildXXXMessages 全部从 buildStageMessages 切到 buildUnifiedStageMessages,移除旧路径 - 删除 execute_pinned.go:execute 记忆渲染合并到统一层 renderUnifiedMemoryContext - Plan prompt 不再在 user prompt 中拼装任务类 ID 列表和 renderStateSummary,改为依赖 msg2 规划工作区;Chat 粗排判断从"上下文有任务类 ID"改为"批量调度需求" - Deliver prompt 新增 IsAborted/IsExhaustedTerminal 区分,支持粗排收口和主动终止场景 2. Execute ReAct 上下文简化——移除归档搬运、窗口裁剪和重复工具压缩 - 移除 splitExecuteLoopRecordsByBoundary、findLatestExecuteBoundaryMarker、tailExecuteLoops、compressExecuteLoopObservationsByTool、buildEarlyExecuteReactSummary、trimExecuteMessage1ByBudget 等六个函数 - 移除 executeLoopWindowLimit / executeConversationTurnLimit / executeMessage1MaxRunes 等预算常量 - msg1 不再从历史中归档上一轮 ReAct 结果,只保留真实对话流(user + assistant speak),全量注入 - msg2 不再按 loop_closed / step_advanced 边界切分"归档/活跃",直接全量注入全部 ReAct Loop 记录 - token 预算由统一压缩层兜底,prompt 层不再做提前裁剪 3. 压缩层从 Execute 专属提升为全节点通用 UnifiedCompact - 删除 execute_compact.go(Execute 专属压缩文件) - 新增 unified_compact.go:UnifiedCompactInput 参数化,各节点(Plan/Chat/Deliver/Execute)构造时从自己的 NodeInput 提取公共字段,消除对 Execute 的直接依赖 - CompactionStore 接口扩展 LoadStageCompaction / SaveStageCompaction,各节点按 stageKey 独立维护压缩状态互不覆盖 - 非 4 段式消息时退化成按角色汇总统计,确保 context_token_stats 仍然刷新 4. Retry 重试机制全面下线 - dao/agent.go:saveChatHistoryCore / SaveChatHistory / SaveChatHistoryInTx 移除 retry_group_id / retry_index / retry_from_user_message_id / retry_from_assistant_message_id 四个参数,修复乱码注释 - dao/agent-cache.go:移除 ApplyRetrySeed 和 extractMessageHistoryID 两个方法 - conv/agent.go:ToEinoMessages 不再回灌 retry_* 字段到运行期上下文 - service/agentsvc/agent.go:移除 chatRetryMeta 及 resolveRetryGroupID / buildRetrySeed 等全部重试逻辑 - service/agentsvc/agent_quick_note.go:整个文件删除(retry 快速补写路径已无用) - service/events/chat_history_persist.go:移除 retry 参数传递 5. 节点层瘦身 + 可见消息逐条持久化 - agent_nodes.go 大幅简化:Chat/Plan/Execute/Deliver 节点方法移除 ToolSchema 注入、状态摘要渲染等逻辑,只做参数转发和状态落盘 - 新增 visible_message.go:persistVisibleAssistantMessage 统一处理可见 assistant speak 的实时持久化,失败仅记日志不中断主流程 - 新增 llm_debug.go:logNodeLLMContext 统一打印 LLM 上下文调试日志 - graph_run_state.go 新增 PersistVisibleMessageFunc 类型 + AgentGraphDeps.PersistVisibleMessage 字段 - service/agentsvc/agent_newagent.go 精简主循环,注入 PersistVisibleMessage 回调;agent_history.go 精简历史构建 - token_budget.go 移除 Execute 专属预算检查,统一到通用预算 前端: 1. 移除 retry 相关 UI 和类型 - agent.ts 移除 retry_group_id / retry_index / retry_total 字段及 normalize 逻辑 - AssistantPanel.vue 移除 retry 相关 UI 和交互代码(约 700 行精简) - dashboard.ts 移除 retry 相关类型定义 - AssistantView.vue 微调 2. ContextWindowMeter 压缩次数展示和数值格式优化 - 新增 formatCompactCount 工具函数,千位以上用 k 单位压缩(如 80k) - 新增压缩次数显示 3.修复了新对话发消息时,user和assistant消息被自动调换的bug 仓库:无
This commit is contained in:
@@ -84,12 +84,7 @@ func (s *AgentService) runNewAgentGraph(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建重试元数据。
|
||||
retryMeta, err := s.buildChatRetryMeta(requestCtx, userID, chatID, extra)
|
||||
if err != nil {
|
||||
pushErrNonBlocking(errChan, err)
|
||||
return
|
||||
}
|
||||
// 3. retry 机制已下线,不再构建重试元数据。
|
||||
|
||||
// 4. 从 StateStore 加载或创建 RuntimeState。
|
||||
// 恢复场景(confirm/ask_user)同时拿到快照中保存的 ConversationContext,
|
||||
@@ -137,6 +132,31 @@ func (s *AgentService) runNewAgentGraph(
|
||||
}
|
||||
}
|
||||
|
||||
cs = runtimeState.EnsureCommonState()
|
||||
|
||||
// 5.7 先把本轮用户输入落库,确保后续可见 assistant 消息按真实时间线追加。
|
||||
userMsg := schema.UserMessage(userMessage)
|
||||
if err := s.persistNewAgentConversationMessage(requestCtx, userID, chatID, userMsg, 0); err != nil {
|
||||
pushErrNonBlocking(errChan, err)
|
||||
return
|
||||
}
|
||||
|
||||
persistVisibleMessage := func(persistCtx context.Context, state *newagentmodel.CommonState, msg *schema.Message) error {
|
||||
targetState := state
|
||||
if targetState == nil {
|
||||
targetState = runtimeState.EnsureCommonState()
|
||||
}
|
||||
if targetState != nil {
|
||||
if targetState.UserID <= 0 {
|
||||
targetState.UserID = userID
|
||||
}
|
||||
if strings.TrimSpace(targetState.ConversationID) == "" {
|
||||
targetState.ConversationID = chatID
|
||||
}
|
||||
}
|
||||
return s.persistNewAgentConversationMessage(persistCtx, userID, chatID, msg, 0)
|
||||
}
|
||||
|
||||
// 6. 构造 AgentGraphRequest。
|
||||
var confirmAction string
|
||||
if len(extra) > 0 {
|
||||
@@ -163,22 +183,23 @@ func (s *AgentService) runNewAgentGraph(
|
||||
|
||||
// 9. 构造 AgentGraphDeps(由 cmd/start.go 注入的依赖)。
|
||||
deps := newagentmodel.AgentGraphDeps{
|
||||
ChatClient: chatClient,
|
||||
PlanClient: planClient,
|
||||
ExecuteClient: executeClient,
|
||||
DeliverClient: deliverClient,
|
||||
ChunkEmitter: chunkEmitter,
|
||||
StateStore: s.agentStateStore,
|
||||
ToolRegistry: s.toolRegistry,
|
||||
ScheduleProvider: s.scheduleProvider,
|
||||
SchedulePersistor: s.schedulePersistor,
|
||||
CompactionStore: s.compactionStore,
|
||||
RoughBuildFunc: s.makeRoughBuildFunc(),
|
||||
WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(),
|
||||
MemoryFuture: memoryFuture,
|
||||
ThinkingPlan: viper.GetBool("agent.thinking.plan"),
|
||||
ThinkingExecute: viper.GetBool("agent.thinking.execute"),
|
||||
ThinkingDeliver: viper.GetBool("agent.thinking.deliver"),
|
||||
ChatClient: chatClient,
|
||||
PlanClient: planClient,
|
||||
ExecuteClient: executeClient,
|
||||
DeliverClient: deliverClient,
|
||||
ChunkEmitter: chunkEmitter,
|
||||
StateStore: s.agentStateStore,
|
||||
ToolRegistry: s.toolRegistry,
|
||||
ScheduleProvider: s.scheduleProvider,
|
||||
SchedulePersistor: s.schedulePersistor,
|
||||
CompactionStore: s.compactionStore,
|
||||
RoughBuildFunc: s.makeRoughBuildFunc(),
|
||||
WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(),
|
||||
MemoryFuture: memoryFuture,
|
||||
ThinkingPlan: viper.GetBool("agent.thinking.plan"),
|
||||
ThinkingExecute: viper.GetBool("agent.thinking.execute"),
|
||||
ThinkingDeliver: viper.GetBool("agent.thinking.deliver"),
|
||||
PersistVisibleMessage: persistVisibleMessage,
|
||||
}
|
||||
|
||||
// 10. 构造 AgentGraphRunInput 并运行 graph。
|
||||
@@ -197,12 +218,13 @@ func (s *AgentService) runNewAgentGraph(
|
||||
pushErrNonBlocking(errChan, fmt.Errorf("graph 执行失败: %w", graphErr))
|
||||
|
||||
// Graph 出错时回退普通聊天,保证可用性。回退使用 Pro 模型。
|
||||
s.runNormalChatFlow(requestCtx, s.AIHub.Pro, resolvedModelName, userMessage, "", nil, retryMeta, thinkingModeToBool(thinkingMode), userID, chatID, traceID, requestStart, outChan, errChan)
|
||||
s.runNormalChatFlow(requestCtx, s.AIHub.Pro, resolvedModelName, userMessage, true, "", nil, thinkingModeToBool(thinkingMode), userID, chatID, traceID, requestStart, outChan, errChan)
|
||||
return
|
||||
}
|
||||
|
||||
// 11. 持久化聊天历史(用户消息 + 助手回复)。
|
||||
s.persistChatAfterGraph(requestCtx, userID, chatID, userMessage, finalState, retryMeta, requestStart, outChan, errChan)
|
||||
requestTotalTokens := snapshotRequestTokenMeter(requestCtx).TotalTokens
|
||||
s.adjustNewAgentRequestTokenUsage(requestCtx, userID, chatID, requestTotalTokens)
|
||||
// 11.5. 将最终状态快照异步写入 MySQL(通过 outbox)。
|
||||
// Deliver 节点已将快照保存到 Redis(2h TTL),此处通过 outbox 异步写入 MySQL 做永久存储。
|
||||
if finalState != nil {
|
||||
@@ -369,135 +391,89 @@ func (s *AgentService) loadConversationContext(ctx context.Context, chatID, user
|
||||
return conversationContext
|
||||
}
|
||||
|
||||
// persistChatAfterGraph graph 执行完成后持久化聊天历史。
|
||||
func (s *AgentService) persistChatAfterGraph(
|
||||
// persistNewAgentConversationMessage 负责把 newAgent 链路里"真正对用户可见"的消息统一落到 Redis + MySQL。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只做单条消息的持久化,不做 graph 流程控制;
|
||||
// 2. TokensConsumed 由调用方显式传入,newAgent 逐条可见消息默认写 0;
|
||||
// 3. Redis 失败只记日志,DB 失败返回错误,便于调用方决定是否中止当前链路。
|
||||
func (s *AgentService) persistNewAgentConversationMessage(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
chatID string,
|
||||
userMessage string,
|
||||
finalState *newagentmodel.AgentGraphState,
|
||||
retryMeta *chatRetryMeta,
|
||||
requestStart time.Time,
|
||||
outChan chan<- string,
|
||||
errChan chan error,
|
||||
) {
|
||||
if finalState == nil {
|
||||
return
|
||||
msg *schema.Message,
|
||||
tokensConsumed int,
|
||||
) error {
|
||||
if s == nil || msg == nil {
|
||||
return nil
|
||||
}
|
||||
role := strings.TrimSpace(string(msg.Role))
|
||||
content := strings.TrimSpace(msg.Content)
|
||||
if role == "" || content == "" {
|
||||
return nil
|
||||
}
|
||||
if userID <= 0 || strings.TrimSpace(chatID) == "" {
|
||||
return fmt.Errorf("newAgent visible message persist: invalid conversation identity")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 1. 持久化用户消息:先写 LLM 上下文 Redis,再落 DB,最后更新 UI 历史缓存。
|
||||
userMsg := &schema.Message{Role: schema.User, Content: userMessage}
|
||||
if retryExtra := retryMeta.CacheExtra(); len(retryExtra) > 0 {
|
||||
userMsg.Extra = retryExtra
|
||||
persistMsg := &schema.Message{
|
||||
Role: msg.Role,
|
||||
Content: content,
|
||||
ReasoningContent: strings.TrimSpace(msg.ReasoningContent),
|
||||
}
|
||||
if err := s.agentCache.PushMessage(ctx, chatID, userMsg); err != nil {
|
||||
log.Printf("写入用户消息到 LLM 上下文 Redis 失败 chat=%s: %v", chatID, err)
|
||||
if len(msg.Extra) > 0 {
|
||||
persistMsg.Extra = make(map[string]any, len(msg.Extra))
|
||||
for key, value := range msg.Extra {
|
||||
persistMsg.Extra[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
userPayload := model.ChatHistoryPersistPayload{
|
||||
UserID: userID,
|
||||
ConversationID: chatID,
|
||||
Role: "user",
|
||||
Message: userMessage,
|
||||
ReasoningContent: "",
|
||||
ReasoningDurationSeconds: 0,
|
||||
RetryGroupID: retryMeta.GroupIDPtr(),
|
||||
RetryIndex: retryMeta.IndexPtr(),
|
||||
RetryFromUserMessageID: retryMeta.FromUserMessageIDPtr(),
|
||||
RetryFromAssistantMessageID: retryMeta.FromAssistantMessageIDPtr(),
|
||||
TokensConsumed: 0,
|
||||
if err := s.agentCache.PushMessage(ctx, chatID, persistMsg); err != nil {
|
||||
log.Printf("写入 newAgent 可见消息到 Redis 失败 chat=%s role=%s: %v", chatID, role, err)
|
||||
}
|
||||
if err := s.PersistChatHistory(ctx, userPayload); err != nil {
|
||||
pushErrNonBlocking(errChan, err)
|
||||
|
||||
reasoningDurationSeconds := 0
|
||||
if persistMsg.Extra != nil {
|
||||
switch v := persistMsg.Extra["reasoning_duration_seconds"].(type) {
|
||||
case int:
|
||||
reasoningDurationSeconds = v
|
||||
case int64:
|
||||
reasoningDurationSeconds = int(v)
|
||||
case float64:
|
||||
reasoningDurationSeconds = int(v)
|
||||
}
|
||||
}
|
||||
userCreatedAt := time.Now()
|
||||
|
||||
persistPayload := model.ChatHistoryPersistPayload{
|
||||
UserID: userID,
|
||||
ConversationID: chatID,
|
||||
Role: role,
|
||||
Message: content,
|
||||
ReasoningContent: strings.TrimSpace(persistMsg.ReasoningContent),
|
||||
ReasoningDurationSeconds: reasoningDurationSeconds,
|
||||
TokensConsumed: tokensConsumed,
|
||||
}
|
||||
if err := s.PersistChatHistory(ctx, persistPayload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
s.appendConversationHistoryCacheOptimistically(
|
||||
context.Background(),
|
||||
ctx,
|
||||
userID,
|
||||
chatID,
|
||||
buildOptimisticConversationHistoryItem("user", userMessage, "", 0, retryMeta, userCreatedAt),
|
||||
buildOptimisticConversationHistoryItem(
|
||||
role,
|
||||
content,
|
||||
persistPayload.ReasoningContent,
|
||||
reasoningDurationSeconds,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
// 2. 从 ConversationContext 提取助手回复(最后一条 assistant 消息)。
|
||||
conversationContext := finalState.ConversationContext
|
||||
if conversationContext == nil || len(conversationContext.History) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var lastAssistantMsg *schema.Message
|
||||
for i := len(conversationContext.History) - 1; i >= 0; i-- {
|
||||
msg := conversationContext.History[i]
|
||||
if msg.Role == schema.Assistant {
|
||||
lastAssistantMsg = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastAssistantMsg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assistantReply := lastAssistantMsg.Content
|
||||
reasoningContent := lastAssistantMsg.ReasoningContent
|
||||
var reasoningDurationSeconds int
|
||||
if lastAssistantMsg.Extra != nil {
|
||||
if dur, ok := lastAssistantMsg.Extra["reasoning_duration_seconds"].(float64); ok {
|
||||
reasoningDurationSeconds = int(dur)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 持久化助手消息:先写 LLM 上下文 Redis,再落 DB,最后更新 UI 历史缓存。
|
||||
assistantMsg := &schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: assistantReply,
|
||||
ReasoningContent: reasoningContent,
|
||||
}
|
||||
if reasoningDurationSeconds > 0 {
|
||||
assistantMsg.Extra = map[string]any{"reasoning_duration_seconds": reasoningDurationSeconds}
|
||||
}
|
||||
if retryExtra := retryMeta.CacheExtra(); len(retryExtra) > 0 {
|
||||
if assistantMsg.Extra == nil {
|
||||
assistantMsg.Extra = make(map[string]any)
|
||||
}
|
||||
for k, v := range retryExtra {
|
||||
assistantMsg.Extra[k] = v
|
||||
}
|
||||
}
|
||||
if err := s.agentCache.PushMessage(context.Background(), chatID, assistantMsg); err != nil {
|
||||
log.Printf("写入助手消息到 LLM 上下文 Redis 失败 chat=%s: %v", chatID, err)
|
||||
}
|
||||
|
||||
requestTotalTokens := snapshotRequestTokenMeter(ctx).TotalTokens
|
||||
assistantPayload := model.ChatHistoryPersistPayload{
|
||||
UserID: userID,
|
||||
ConversationID: chatID,
|
||||
Role: "assistant",
|
||||
Message: assistantReply,
|
||||
ReasoningContent: reasoningContent,
|
||||
ReasoningDurationSeconds: reasoningDurationSeconds,
|
||||
RetryGroupID: retryMeta.GroupIDPtr(),
|
||||
RetryIndex: retryMeta.IndexPtr(),
|
||||
RetryFromUserMessageID: retryMeta.FromUserMessageIDPtr(),
|
||||
RetryFromAssistantMessageID: retryMeta.FromAssistantMessageIDPtr(),
|
||||
TokensConsumed: requestTotalTokens,
|
||||
}
|
||||
if err := s.PersistChatHistory(ctx, assistantPayload); err != nil {
|
||||
pushErrNonBlocking(errChan, err)
|
||||
} else {
|
||||
s.appendConversationHistoryCacheOptimistically(
|
||||
context.Background(),
|
||||
userID,
|
||||
chatID,
|
||||
buildOptimisticConversationHistoryItem(
|
||||
"assistant",
|
||||
assistantReply,
|
||||
reasoningContent,
|
||||
reasoningDurationSeconds,
|
||||
retryMeta,
|
||||
time.Now(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeRoughBuildFunc 把 AgentService 上的 HybridScheduleWithPlanMultiFunc 封装成
|
||||
@@ -509,6 +485,38 @@ func (s *AgentService) persistChatAfterGraph(
|
||||
// placement,普通时段放置的任务全部被丢弃。
|
||||
// 正确做法:使用第一个返回值 []HybridScheduleEntry,过滤 Status="suggested" 且 TaskItemID>0 的条目,
|
||||
// 这样嵌入和非嵌入的粗排结果都能正确写入 ScheduleState。
|
||||
// adjustNewAgentRequestTokenUsage 负责把本轮 graph 的请求级 token 一次性回写到账本。
|
||||
//
|
||||
// 说明:
|
||||
// 1. newAgent 逐条可见消息都按 0 token 落库,最终统一在这里补记整轮消耗;
|
||||
// 2. 如果启用了 outbox,就沿用异步 token 调整事件,保持写账口径一致;
|
||||
// 3. 该步骤属于请求收尾,不应反过来打断用户已看到的回复。
|
||||
func (s *AgentService) adjustNewAgentRequestTokenUsage(ctx context.Context, userID int, chatID string, deltaTokens int) {
|
||||
if s == nil || userID <= 0 || strings.TrimSpace(chatID) == "" || deltaTokens <= 0 {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if s.eventPublisher != nil {
|
||||
if err := eventsvc.PublishChatTokenUsageAdjustRequested(ctx, s.eventPublisher, model.ChatTokenUsageAdjustPayload{
|
||||
UserID: userID,
|
||||
ConversationID: chatID,
|
||||
TokensDelta: deltaTokens,
|
||||
Reason: "new_agent_request",
|
||||
TriggeredAt: time.Now(),
|
||||
}); err != nil {
|
||||
log.Printf("写入 newAgent 请求级 token 调整事件失败 chat=%s tokens=%d err=%v", chatID, deltaTokens, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.repo.AdjustTokenUsage(ctx, userID, chatID, deltaTokens); err != nil {
|
||||
log.Printf("同步写入 newAgent 请求级 token 调整失败 chat=%s tokens=%d err=%v", chatID, deltaTokens, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AgentService) makeRoughBuildFunc() newagentmodel.RoughBuildFunc {
|
||||
if s.HybridScheduleWithPlanMultiFunc == nil {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user