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:
@@ -479,24 +479,17 @@ LLM 的一次性文本输出通过 `SplitPseudoStreamText` 拆分成多个 chunk
|
||||
|
||||
### 9.1 prompt 构造模式
|
||||
|
||||
所有阶段共享 `buildStageMessages()` 函数:
|
||||
所有阶段现在统一共享 `buildUnifiedStageMessages()` 函数:
|
||||
|
||||
```
|
||||
System Prompt(节点专属)
|
||||
│
|
||||
v
|
||||
Pinned Blocks(置顶上下文块,作为独立 system 消息注入)
|
||||
│
|
||||
v
|
||||
Tool Schemas(工具 schema,作为独立 system 消息注入)
|
||||
│
|
||||
v
|
||||
History(对话历史,Tool 消息降级为 User 消息以兼容 API)
|
||||
│
|
||||
v
|
||||
User Prompt(节点专属用户提示)
|
||||
msg0(system) = 全局 system prompt + 阶段 system prompt + 工具简表
|
||||
msg1(assistant) = 对话历史 + 归档摘要
|
||||
msg2(assistant) = 阶段工作区
|
||||
msg3(system) = 阶段状态 + 记忆 + 本轮指令
|
||||
```
|
||||
|
||||
统一构造由 `StageMessagesConfig` 驱动,具体阶段只负责填充各自的 `Msg2Content`、`Msg3StageState` 和 `UserInstruction`。
|
||||
|
||||
### 9.2 各阶段 prompt 要点
|
||||
|
||||
| 阶段 | 核心指令 | 关键约束 |
|
||||
|
||||
@@ -93,7 +93,6 @@ type CommonState struct {
|
||||
// TaskClasses 本次排课涉及的任务类约束元数据(含日期、策略、时段预算等),
|
||||
// 在 Service 层从 DB 加载并注入,供 Plan prompt 直接消费,避免 LLM 因信息不足而追问用户。
|
||||
TaskClasses []schedule.TaskClassMeta `json:"task_classes,omitempty"`
|
||||
|
||||
// NeedsRoughBuild 由 Plan 节点在 plan_done 时写入,标记 Confirm 后是否需要走粗排节点。
|
||||
// 粗排节点执行完毕后会将此字段重置为 false。
|
||||
NeedsRoughBuild bool `json:"needs_rough_build,omitempty"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// AgentGraphRequest 描述一次 agent graph 运行的请求级输入。
|
||||
@@ -52,6 +53,14 @@ type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([
|
||||
// 2. deliver 结束时再做最终覆盖写,保障收口状态一致。
|
||||
type WriteSchedulePreviewFunc func(ctx context.Context, state *schedule.ScheduleState, userID int, conversationID string, taskClassIDs []int) error
|
||||
|
||||
// PersistVisibleMessageFunc 是 newAgent 主循环逐条持久化可见消息的回调签名。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只处理真正对用户可见的 assistant speak,不处理工具结果或内部纠错提示;
|
||||
// 2. 由节点在 AppendHistory 之后主动调用,让上层同步把这条消息写入 Redis + MySQL;
|
||||
// 3. 执行方可以做无损降级(例如 Redis 写失败只记日志),但应返回 error 便于上层记录。
|
||||
type PersistVisibleMessageFunc func(ctx context.Context, state *CommonState, msg *schema.Message) error
|
||||
|
||||
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。
|
||||
//
|
||||
// 设计目的:
|
||||
@@ -81,6 +90,10 @@ type AgentGraphDeps struct {
|
||||
// channel 携带已渲染的文本内容(非原始 ItemDTO),节点直接写入 pinned block。
|
||||
MemoryFuture chan string // buffered(1),携带 renderMemoryPinnedContentByMode 的输出
|
||||
MemoryConsumed bool // 保证 channel 只读一次,后续 Execute ReAct 循环跳过等待
|
||||
|
||||
// PersistVisibleMessage 按 Service 注入,newAgent 每个节点产出的可见 speak
|
||||
// 都会在 AppendHistory 之后立刻调用这个回调,把消息同步落到 Redis + MySQL。
|
||||
PersistVisibleMessage PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// --- 记忆 pinned block 常量(供 agentsvc 和 node 层共享) ---
|
||||
|
||||
@@ -81,9 +81,16 @@ type SchedulePersistor interface {
|
||||
}
|
||||
|
||||
// CompactionStore 定义上下文压缩的持久化接口。
|
||||
// 由 Service 层实现(组合 DAO + Redis Cache),注入到 ExecuteNodeInput。
|
||||
// 由 Service 层实现(组合 DAO + Redis Cache),注入到各阶段 NodeInput。
|
||||
type CompactionStore interface {
|
||||
LoadCompaction(ctx context.Context, userID int, chatID string) (summary string, watermark int, err error)
|
||||
SaveCompaction(ctx context.Context, userID int, chatID string, summary string, watermark int) error
|
||||
SaveContextTokenStats(ctx context.Context, userID int, chatID string, statsJSON string) error
|
||||
|
||||
// LoadStageCompaction 按 stageKey 加载压缩摘要和水位线。
|
||||
// stageKey 区分不同节点(如 "execute"/"plan"/"chat"/"deliver"),
|
||||
// 使各节点可以独立维护各自的压缩状态,互不覆盖。
|
||||
LoadStageCompaction(ctx context.Context, userID int, chatID string, stageKey string) (summary string, watermark int, err error)
|
||||
// SaveStageCompaction 按 stageKey 保存压缩摘要和水位线。
|
||||
SaveStageCompaction(ctx context.Context, userID int, chatID string, stageKey string, summary string, watermark int) error
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// AgentNodes 是 newAgent 通用图的节点容器。
|
||||
// AgentNodes 负责把 graph 层的节点调用统一转成 node 层真正的执行入口。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 node 层真正实现的方法统一暴露给 graph 注册;
|
||||
// 2. 负责收口"graph 只编排、node 真执行"的结构约束;
|
||||
// 3. 负责在每个节点执行成功后统一做状态持久化(Save/Delete)。
|
||||
// 1. 这里只做参数转发、依赖注入和状态落盘,不承载业务决策。
|
||||
// 2. 各节点真正的执行逻辑仍在对应的 RunXXXNode 内。
|
||||
// 3. 节点成功后统一保存快照,方便断线恢复。
|
||||
type AgentNodes struct{}
|
||||
|
||||
// NewAgentNodes 创建通用节点容器。
|
||||
@@ -25,104 +25,71 @@ func NewAgentNodes() *AgentNodes {
|
||||
return &AgentNodes{}
|
||||
}
|
||||
|
||||
// Chat 是聊天入口的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的入口逻辑仍由 RunChatNode 负责;
|
||||
// 3. Chat 的 Save 交给 Service 层处理,这里不做持久化。
|
||||
// Chat 负责把 graph 的 chat 节点请求转给 RunChatNode。
|
||||
func (n *AgentNodes) Chat(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("chat node: state is nil")
|
||||
}
|
||||
|
||||
// 注入工具 schema 到 ConversationContext,让路由决策更智能。
|
||||
if st.Deps.ToolRegistry != nil {
|
||||
schemas := st.Deps.ToolRegistry.Schemas()
|
||||
toolSchemas := make([]newagentmodel.ToolSchemaContext, len(schemas))
|
||||
for i, s := range schemas {
|
||||
toolSchemas[i] = newagentmodel.ToolSchemaContext{
|
||||
Name: s.Name,
|
||||
Desc: s.Desc,
|
||||
SchemaText: s.SchemaText,
|
||||
}
|
||||
}
|
||||
st.EnsureConversationContext().SetToolSchemas(toolSchemas)
|
||||
}
|
||||
// 1. Chat 阶段只负责路由与纯对话,不需要看到工具目录,避免能力细节干扰判断。
|
||||
st.EnsureConversationContext().SetToolSchemas(nil)
|
||||
|
||||
if err := RunChatNode(
|
||||
ctx,
|
||||
ChatNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
ConfirmAction: st.Request.ConfirmAction,
|
||||
Client: st.Deps.ResolveChatClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
if err := RunChatNode(ctx, ChatNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
ConfirmAction: st.Request.ConfirmAction,
|
||||
Client: st.Deps.ResolveChatClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
CompactionStore: st.Deps.CompactionStore,
|
||||
PersistVisibleMessage: st.Deps.PersistVisibleMessage,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Confirm 是确认阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的确认逻辑仍由 RunConfirmNode 负责;
|
||||
// 3. 不需要 LLM Client — 确认内容由已有状态机械格式化。
|
||||
// 4. Confirm 执行成功后保存状态,因为它创建了 PendingInteraction。
|
||||
// Confirm 负责把 graph 的 confirm 节点请求转给 RunConfirmNode。
|
||||
func (n *AgentNodes) Confirm(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("confirm node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunConfirmNode(
|
||||
ctx,
|
||||
ConfirmNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
if err := RunConfirmNode(ctx, ConfirmNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
} else if st.Deps.WriteSchedulePreview != nil && st.ScheduleState == nil {
|
||||
flowState := st.EnsureFlowState()
|
||||
log.Printf("[WARN] deliver: schedule state is nil, skip preview write chat=%s", flowState.ConversationID)
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Plan 是规划阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的单轮规划逻辑仍由 RunPlanNode 负责;
|
||||
// 3. Plan 执行成功后保存状态,支持意外断线恢复。
|
||||
// Plan 负责把 graph 的 plan 节点请求转给 RunPlanNode。
|
||||
func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("plan node: state is nil")
|
||||
}
|
||||
|
||||
// 等待后台记忆检索完成,注入最新记忆后再启动 Plan。
|
||||
// 等待后端记忆检索完成,再把最新结果注入上下文。
|
||||
ensureFreshMemory(st)
|
||||
|
||||
if err := RunPlanNode(
|
||||
ctx,
|
||||
PlanNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
Client: st.Deps.ResolvePlanClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ResumeNode: "plan",
|
||||
AlwaysExecute: st.Request.AlwaysExecute,
|
||||
ThinkingEnabled: st.Deps.ThinkingPlan,
|
||||
},
|
||||
); err != nil {
|
||||
if err := RunPlanNode(ctx, PlanNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
Client: st.Deps.ResolvePlanClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ResumeNode: "plan",
|
||||
AlwaysExecute: st.Request.AlwaysExecute,
|
||||
ThinkingEnabled: st.Deps.ThinkingPlan,
|
||||
CompactionStore: st.Deps.CompactionStore,
|
||||
PersistVisibleMessage: st.Deps.PersistVisibleMessage,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -130,12 +97,7 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// RoughBuild 是粗排阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 调用注入的 RoughBuildFunc 执行粗排算法;
|
||||
// 2. 把粗排结果写入 ScheduleState;
|
||||
// 3. 完成后保存状态,支持意外断线恢复。
|
||||
// RoughBuild 负责把 graph 的 rough_build 节点请求转给 RunRoughBuildNode。
|
||||
func (n *AgentNodes) RoughBuild(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("rough_build node: state is nil")
|
||||
@@ -149,48 +111,31 @@ func (n *AgentNodes) RoughBuild(ctx context.Context, st *newagentmodel.AgentGrap
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Interrupt 是中断阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的中断逻辑仍由 RunInterruptNode 负责;
|
||||
// 3. 不需要 LLM Client — 所有文本已在 PendingInteraction 里。
|
||||
// 4. 不需要 Save — 上游节点(Plan/Execute/Confirm)已经存过了。
|
||||
// Interrupt 负责把 graph 的 interrupt 节点请求转给 RunInterruptNode。
|
||||
func (n *AgentNodes) Interrupt(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("interrupt node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunInterruptNode(
|
||||
ctx,
|
||||
InterruptNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
if err := RunInterruptNode(ctx, InterruptNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
PersistVisibleMessage: st.Deps.PersistVisibleMessage,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Execute 是执行阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的单轮执行逻辑仍由 RunExecuteNode 负责。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. LLM 主导:LLM 自己判断 done_when 是否满足,自己决定何时推进/完成;
|
||||
// 2. 后端兜底:只做资源控制、安全兜底、证据记录;
|
||||
// 3. 不做硬校验:后端不质疑 LLM 的 advance/complete 决策。
|
||||
// 4. Execute 每轮执行成功后保存状态,支持意外断线恢复。
|
||||
// Execute 负责把 graph 的 execute 节点请求转给 RunExecuteNode。
|
||||
func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("execute node: state is nil")
|
||||
}
|
||||
|
||||
// 按需加载 ScheduleState(首次执行时从 DB 加载,后续复用内存中的 state)。
|
||||
// 1. 首次进入时按需加载日程状态,后续轮次复用内存状态。
|
||||
var scheduleState *schedule.ScheduleState
|
||||
if ss, loadErr := st.EnsureScheduleState(ctx); loadErr != nil {
|
||||
return nil, fmt.Errorf("execute node: 加载日程状态失败: %w", loadErr)
|
||||
@@ -198,7 +143,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
scheduleState = ss
|
||||
}
|
||||
|
||||
// 注入工具 schema 到 ConversationContext,让 LLM 能看到可用工具列表。
|
||||
// 2. 把工具 schema 注入上下文,供 LLM 看到真实工具边界。
|
||||
if st.Deps.ToolRegistry != nil {
|
||||
schemas := st.Deps.ToolRegistry.Schemas()
|
||||
toolSchemas := make([]newagentmodel.ToolSchemaContext, len(schemas))
|
||||
@@ -212,28 +157,26 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
st.EnsureConversationContext().SetToolSchemas(toolSchemas)
|
||||
}
|
||||
|
||||
// 等待后台记忆检索完成,注入最新记忆后再启动 Execute。
|
||||
// 3. 等待后端记忆检索结果,再把最新结果注入上下文。
|
||||
ensureFreshMemory(st)
|
||||
|
||||
if err := RunExecuteNode(
|
||||
ctx,
|
||||
ExecuteNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
Client: st.Deps.ResolveExecuteClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ResumeNode: "execute",
|
||||
ToolRegistry: st.Deps.ToolRegistry,
|
||||
ScheduleState: scheduleState,
|
||||
SchedulePersistor: st.Deps.SchedulePersistor,
|
||||
CompactionStore: st.Deps.CompactionStore,
|
||||
WriteSchedulePreview: st.Deps.WriteSchedulePreview,
|
||||
OriginalScheduleState: st.OriginalScheduleState,
|
||||
AlwaysExecute: st.Request.AlwaysExecute,
|
||||
ThinkingEnabled: st.Deps.ThinkingExecute,
|
||||
},
|
||||
); err != nil {
|
||||
if err := RunExecuteNode(ctx, ExecuteNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
UserInput: st.Request.UserInput,
|
||||
Client: st.Deps.ResolveExecuteClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ResumeNode: "execute",
|
||||
ToolRegistry: st.Deps.ToolRegistry,
|
||||
ScheduleState: scheduleState,
|
||||
SchedulePersistor: st.Deps.SchedulePersistor,
|
||||
CompactionStore: st.Deps.CompactionStore,
|
||||
WriteSchedulePreview: st.Deps.WriteSchedulePreview,
|
||||
OriginalScheduleState: st.OriginalScheduleState,
|
||||
AlwaysExecute: st.Request.AlwaysExecute,
|
||||
ThinkingEnabled: st.Deps.ThinkingExecute,
|
||||
PersistVisibleMessage: st.Deps.PersistVisibleMessage,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -241,12 +184,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// OrderGuard 是顺序守卫阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责调用 RunOrderGuardNode 做 suggested 相对顺序校验;
|
||||
// 2. 不负责交付文案生成,校验结果统一交给 Deliver 节点收口;
|
||||
// 3. 节点执行后保存状态,保证异常中断后仍可复盘守卫结果。
|
||||
// OrderGuard 负责把 graph 的 order_guard 节点请求转给 RunOrderGuardNode。
|
||||
func (n *AgentNodes) OrderGuard(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("order_guard node: state is nil")
|
||||
@@ -260,38 +198,32 @@ func (n *AgentNodes) OrderGuard(ctx context.Context, st *newagentmodel.AgentGrap
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Deliver 是交付阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的交付逻辑仍由 RunDeliverNode 负责;
|
||||
// 3. 调 LLM 生成任务总结,失败时降级到机械格式化。
|
||||
// 4. 任务完成后保存最终状态到 Redis(2h TTL),支持断线恢复和 MySQL outbox 异步持久化。
|
||||
// Deliver 负责把 graph 的 deliver 节点请求转给 RunDeliverNode。
|
||||
func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("deliver node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunDeliverNode(
|
||||
ctx,
|
||||
DeliverNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
Client: st.Deps.ResolveDeliverClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ThinkingEnabled: st.Deps.ThinkingDeliver,
|
||||
},
|
||||
); err != nil {
|
||||
// 1. Deliver 只做最终收口总结,不需要工具目录,避免无关能力信息污染总结。
|
||||
st.EnsureConversationContext().SetToolSchemas(nil)
|
||||
|
||||
if err := RunDeliverNode(ctx, DeliverNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
Client: st.Deps.ResolveDeliverClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ThinkingEnabled: st.Deps.ThinkingDeliver,
|
||||
CompactionStore: st.Deps.CompactionStore,
|
||||
PersistVisibleMessage: st.Deps.PersistVisibleMessage,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 任务完成后写排程预览缓存:只有走到 Deliver 才代表排程结果已稳定,
|
||||
// 中断(confirm/ask_user)路径不写,避免把中间态暴露给前端。
|
||||
// 只有真正完成时才写入排程预览,避免中间态污染前端展示。
|
||||
if st.Deps.WriteSchedulePreview != nil && st.ScheduleState != nil {
|
||||
flowState := st.EnsureFlowState()
|
||||
if flowState != nil && flowState.IsCompleted() {
|
||||
if err := st.Deps.WriteSchedulePreview(ctx, st.ScheduleState, flowState.UserID, flowState.ConversationID, flowState.TaskClassIDs); err != nil {
|
||||
// 写缓存失败不阻断主流程,降级为仅 log。
|
||||
log.Printf("[WARN] deliver: 写入排程预览缓存失败 chat=%s: %v", flowState.ConversationID, err)
|
||||
}
|
||||
} else if flowState != nil {
|
||||
@@ -303,19 +235,16 @@ func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// --- 记忆预取消费辅助 ---
|
||||
|
||||
// ensureFreshMemory 等待后台记忆检索完成,将最新结果注入 ConversationContext。
|
||||
// ensureFreshMemory 等待后端记忆检索完成,并把最新结果写入 ConversationContext。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. 只在首次调用时等待 channel(最多 500ms),后续调用直接跳过;
|
||||
// 2. 覆盖 ConversationContext 中已有的缓存记忆(UpsertPinnedBlock 按 key 覆盖);
|
||||
// 3. timeout 后保留缓存记忆不替换,保证 Execute ReAct 循环不会因超时丢失记忆。
|
||||
// 1. 只在首次调用时等待 channel,后续调用直接跳过。
|
||||
// 2. 超时后保留原有上下文,不额外覆盖。
|
||||
// 3. 记忆为空时也不做额外写入,避免污染 prompt。
|
||||
func ensureFreshMemory(st *newagentmodel.AgentGraphState) {
|
||||
if st == nil || st.Deps.MemoryConsumed || st.Deps.MemoryFuture == nil {
|
||||
return
|
||||
}
|
||||
st.Deps.MemoryConsumed = true // 标记已消费,后续调用直接跳过
|
||||
st.Deps.MemoryConsumed = true
|
||||
|
||||
select {
|
||||
case content := <-st.Deps.MemoryFuture:
|
||||
@@ -327,20 +256,11 @@ func ensureFreshMemory(st *newagentmodel.AgentGraphState) {
|
||||
})
|
||||
}
|
||||
case <-time.After(newagentmodel.MemoryFreshTimeout):
|
||||
// timeout:保留 ConversationContext 中已有的缓存记忆,不做额外操作
|
||||
// 超时后保留原有上下文即可。
|
||||
}
|
||||
}
|
||||
|
||||
// --- 持久化辅助 ---
|
||||
|
||||
// saveAgentState 在节点执行成功后,将当前运行态快照保存到 Redis。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. Save 失败只记日志,不中断 Graph 流程;
|
||||
// 2. StateStore 为空时静默跳过(骨架期 / 测试环境);
|
||||
// 3. conversationID 为空时也静默跳过,避免写入无效 key。
|
||||
//
|
||||
// TODO: 接入项目统一的日志框架后,把 _ = err 改成结构化日志。
|
||||
// saveAgentState 在节点成功执行后保存运行快照。
|
||||
func saveAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
if st == nil {
|
||||
return
|
||||
@@ -370,14 +290,7 @@ func saveAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
_ = store.Save(ctx, flowState.ConversationID, snapshot)
|
||||
}
|
||||
|
||||
// deleteAgentState 在任务完成后,删除 Redis 中的运行态快照。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. Delete 失败只记日志,不中断 Graph 流程;
|
||||
// 2. 删除是幂等的,key 不存在也视为成功;
|
||||
// 3. StateStore 为空时静默跳过。
|
||||
//
|
||||
// TODO: 接入项目统一的日志框架后,把 _ = err 改成结构化日志。
|
||||
// deleteAgentState 在任务完成后删除运行快照。
|
||||
func deleteAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
if st == nil {
|
||||
return
|
||||
|
||||
@@ -45,12 +45,14 @@ const (
|
||||
// 3. ConversationContext 提供历史对话;
|
||||
// 4. ConfirmAction 仅在 confirm 恢复场景下由前端传入 "accept" / "reject"。
|
||||
type ChatNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
UserInput string
|
||||
ConfirmAction string
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
UserInput string
|
||||
ConfirmAction string
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
CompactionStore newagentmodel.CompactionStore // 上下文压缩持久化
|
||||
PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// RunChatNode 执行一轮聊天节点逻辑。
|
||||
@@ -94,6 +96,15 @@ func RunChatNode(ctx context.Context, input ChatNodeInput) error {
|
||||
}
|
||||
nonce := uuid.NewString()
|
||||
messages := newagentprompt.BuildChatRoutingMessages(conversationContext, input.UserInput, flowState, nonce)
|
||||
messages = compactUnifiedMessagesIfNeeded(ctx, messages, UnifiedCompactInput{
|
||||
Client: input.Client,
|
||||
CompactionStore: input.CompactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: chatStageName,
|
||||
StatusBlockID: chatStatusBlockID,
|
||||
})
|
||||
logNodeLLMContext(chatStageName, "routing", flowState, messages)
|
||||
|
||||
reader, err := input.Client.Stream(ctx, messages, infrallm.GenerateOptions{
|
||||
Temperature: 0.7,
|
||||
@@ -281,7 +292,7 @@ func handleDirectReplyStream(
|
||||
if effectiveThinking {
|
||||
return handleThinkingReplyStream(ctx, reader, input, emitter, conversationContext, flowState)
|
||||
}
|
||||
return handleDirectReplyContinueStream(ctx, reader, emitter, conversationContext, flowState, firstVisible)
|
||||
return handleDirectReplyContinueStream(ctx, reader, input, emitter, conversationContext, flowState, firstVisible)
|
||||
}
|
||||
|
||||
// handleThinkingReplyStream 处理需要思考的回复:关闭路由流 → 第二次 thinking 流式调用。
|
||||
@@ -295,7 +306,16 @@ func handleThinkingReplyStream(
|
||||
) error {
|
||||
_ = reader.Close()
|
||||
|
||||
deepMessages := newagentprompt.BuildDeepAnswerMessages(conversationContext, input.UserInput)
|
||||
deepMessages := newagentprompt.BuildDeepAnswerMessages(flowState, conversationContext, input.UserInput)
|
||||
deepMessages = compactUnifiedMessagesIfNeeded(ctx, deepMessages, UnifiedCompactInput{
|
||||
Client: input.Client,
|
||||
CompactionStore: input.CompactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: chatStageName,
|
||||
StatusBlockID: chatStatusBlockID,
|
||||
})
|
||||
logNodeLLMContext(chatStageName, "direct_reply_thinking", flowState, deepMessages)
|
||||
deepReader, err := input.Client.Stream(ctx, deepMessages, infrallm.GenerateOptions{
|
||||
Temperature: 0.5,
|
||||
MaxTokens: 2000,
|
||||
@@ -322,6 +342,7 @@ func handleThinkingReplyStream(
|
||||
deepText = strings.TrimSpace(deepText)
|
||||
if deepText != "" {
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(deepText, nil))
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, schema.AssistantMessage(deepText, nil))
|
||||
}
|
||||
|
||||
flowState.Phase = newagentmodel.PhaseChatting
|
||||
@@ -332,6 +353,7 @@ func handleThinkingReplyStream(
|
||||
func handleDirectReplyContinueStream(
|
||||
ctx context.Context,
|
||||
reader infrallm.StreamReader,
|
||||
input ChatNodeInput,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
flowState *newagentmodel.CommonState,
|
||||
@@ -370,7 +392,9 @@ func handleDirectReplyContinueStream(
|
||||
|
||||
text := fullText.String()
|
||||
if strings.TrimSpace(text) != "" {
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(text, nil))
|
||||
msg := schema.AssistantMessage(text, nil)
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
|
||||
flowState.Phase = newagentmodel.PhaseChatting
|
||||
@@ -568,7 +592,16 @@ func handleDeepAnswerStream(
|
||||
if effectiveThinking {
|
||||
thinkingOpt = infrallm.ThinkingModeEnabled
|
||||
}
|
||||
deepMessages := newagentprompt.BuildDeepAnswerMessages(conversationContext, input.UserInput)
|
||||
deepMessages := newagentprompt.BuildDeepAnswerMessages(flowState, conversationContext, input.UserInput)
|
||||
deepMessages = compactUnifiedMessagesIfNeeded(ctx, deepMessages, UnifiedCompactInput{
|
||||
Client: input.Client,
|
||||
CompactionStore: input.CompactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: chatStageName,
|
||||
StatusBlockID: chatStatusBlockID,
|
||||
})
|
||||
logNodeLLMContext(chatStageName, "deep_answer", flowState, deepMessages)
|
||||
deepReader, err := input.Client.Stream(ctx, deepMessages, infrallm.GenerateOptions{
|
||||
Temperature: 0.5,
|
||||
MaxTokens: 2000,
|
||||
@@ -601,7 +634,9 @@ func handleDeepAnswerStream(
|
||||
}
|
||||
|
||||
// 4. 完整回复写入 history。
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(deepText, nil))
|
||||
msg := schema.AssistantMessage(deepText, nil)
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
|
||||
flowState.Phase = newagentmodel.PhaseChatting
|
||||
return nil
|
||||
|
||||
@@ -28,11 +28,13 @@ const (
|
||||
// 3. ConversationContext 提供执行阶段的对话历史;
|
||||
// 4. 交付完成后标记流程结束。
|
||||
type DeliverNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ThinkingEnabled bool // 是否开启 thinking,由 config.yaml 的 agent.thinking.deliver 注入
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ThinkingEnabled bool // 是否开启 thinking,由 config.yaml 的 agent.thinking.deliver 注入
|
||||
CompactionStore newagentmodel.CompactionStore // 上下文压缩持久化
|
||||
PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// RunDeliverNode 执行一轮交付节点逻辑。
|
||||
@@ -65,10 +67,11 @@ func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
|
||||
}
|
||||
|
||||
// 2. 调 LLM 生成交付总结。
|
||||
summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext, input.ThinkingEnabled)
|
||||
summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext, input.ThinkingEnabled, input.CompactionStore, emitter)
|
||||
|
||||
// 3. 伪流式推送总结。
|
||||
if strings.TrimSpace(summary) != "" {
|
||||
msg := schema.AssistantMessage(summary, nil)
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
deliverSpeakBlockID,
|
||||
@@ -78,7 +81,8 @@ func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
|
||||
); err != nil {
|
||||
return fmt.Errorf("交付总结推送失败: %w", err)
|
||||
}
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(summary, nil))
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
|
||||
// 4. 推送最终完成状态。
|
||||
@@ -100,6 +104,8 @@ func generateDeliverSummary(
|
||||
flowState *newagentmodel.CommonState,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
thinkingEnabled bool,
|
||||
compactionStore newagentmodel.CompactionStore,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) string {
|
||||
if flowState != nil {
|
||||
switch {
|
||||
@@ -115,6 +121,15 @@ func generateDeliverSummary(
|
||||
}
|
||||
|
||||
messages := newagentprompt.BuildDeliverMessages(flowState, conversationContext)
|
||||
messages = compactUnifiedMessagesIfNeeded(ctx, messages, UnifiedCompactInput{
|
||||
Client: client,
|
||||
CompactionStore: compactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: deliverStageName,
|
||||
StatusBlockID: deliverStatusBlockID,
|
||||
})
|
||||
logNodeLLMContext(deliverStageName, "summarizing", flowState, messages)
|
||||
result, err := client.GenerateText(
|
||||
ctx,
|
||||
messages,
|
||||
|
||||
@@ -60,6 +60,7 @@ type ExecuteNodeInput struct {
|
||||
OriginalScheduleState *schedule.ScheduleState
|
||||
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行
|
||||
ThinkingEnabled bool // 是否开启 thinking,由 config.yaml 的 agent.thinking.execute 注入
|
||||
PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// ExecuteRoundObservation 记录执行阶段每轮的关键观察。
|
||||
@@ -184,19 +185,16 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
messages := newagentprompt.BuildExecuteMessages(flowState, conversationContext)
|
||||
|
||||
// 5.1 Token 预算检查 & 上下文压缩。
|
||||
messages = compactExecuteMessagesIfNeeded(
|
||||
ctx, messages, input, flowState, emitter,
|
||||
)
|
||||
messages = compactUnifiedMessagesIfNeeded(ctx, messages, UnifiedCompactInput{
|
||||
Client: input.Client,
|
||||
CompactionStore: input.CompactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: executeStageName,
|
||||
StatusBlockID: executeStatusBlockID,
|
||||
})
|
||||
|
||||
log.Printf(
|
||||
"[DEBUG] execute LLM context begin chat=%s round=%d message_count=%d\n%s\n[DEBUG] execute LLM context end chat=%s round=%d",
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
len(messages),
|
||||
formatExecuteLLMMessagesForDebug(messages),
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
)
|
||||
logNodeLLMContext(executeStageName, "decision", flowState, messages)
|
||||
decision, rawResult, err := infrallm.GenerateJSON[newagentmodel.ExecuteDecision](
|
||||
ctx,
|
||||
input.Client,
|
||||
@@ -337,6 +335,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
|
||||
if !isConfirmWithCard && !isAskUser && !isAbort {
|
||||
// 推流给前端
|
||||
msg := schema.AssistantMessage(speakText, nil)
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
executeSpeakBlockID,
|
||||
@@ -346,6 +345,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
); err != nil {
|
||||
return fmt.Errorf("执行文案推送失败: %w", err)
|
||||
}
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
// 1. confirm / ask_user 的 speak 仍要写入历史,避免下一轮 LLM 丢失自己的执行上下文。
|
||||
// 2. abort 不在这里写历史,避免先输出中间 speak,再在 deliver 收到第二份终止文案。
|
||||
@@ -1674,79 +1674,3 @@ func flattenForLog(text string) string {
|
||||
text = strings.ReplaceAll(text, "\r", " ")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// formatExecuteLLMMessagesForDebug 将本轮送入 LLM 的完整消息上下文展开成可读多行日志。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 按消息索引逐条输出,便于和上游上下文构造步骤逐项对齐;
|
||||
// 2. 完整输出 content / reasoning_content / tool_calls / extra,不做截断;
|
||||
// 3. 仅用于调试打点,不参与业务决策。
|
||||
func formatExecuteLLMMessagesForDebug(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")
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package newagentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// compactExecuteMessagesIfNeeded 检查 Execute prompt 的 token 预算,
|
||||
// 超限时对 msg1(历史对话)和 msg2(ReAct Loop)执行 LLM 压缩。
|
||||
//
|
||||
// 消息布局约定(由 BuildExecuteMessages 返回):
|
||||
//
|
||||
// [0] system — msg0: 系统规则
|
||||
// [1] assistant — msg1: 历史对话上下文
|
||||
// [2] assistant — msg2: 当轮 ReAct Loop 记录
|
||||
// [3] system — msg3: 当前状态 + 用户提示
|
||||
func compactExecuteMessagesIfNeeded(
|
||||
ctx context.Context,
|
||||
messages []*schema.Message,
|
||||
input ExecuteNodeInput,
|
||||
flowState *newagentmodel.CommonState,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) []*schema.Message {
|
||||
if len(messages) != 4 {
|
||||
return messages
|
||||
}
|
||||
|
||||
// 提取四条消息的文本内容
|
||||
msg0 := messages[0].Content
|
||||
msg1 := messages[1].Content
|
||||
msg2 := messages[2].Content
|
||||
msg3 := messages[3].Content
|
||||
|
||||
// Token 预算检查
|
||||
breakdown, overBudget, needCompactMsg1, needCompactMsg2 := pkg.CheckExecuteTokenBudget(msg0, msg1, msg2, msg3)
|
||||
|
||||
log.Printf(
|
||||
"[COMPACT] token budget check: total=%d budget=%d over=%v compactMsg1=%v compactMsg2=%v (msg0=%d msg1=%d msg2=%d msg3=%d)",
|
||||
breakdown.Total, breakdown.Budget, overBudget, needCompactMsg1, needCompactMsg2,
|
||||
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
|
||||
)
|
||||
|
||||
if !overBudget {
|
||||
// 未超限,记录 token 分布后直接返回
|
||||
saveTokenStats(ctx, input, flowState, breakdown)
|
||||
return messages
|
||||
}
|
||||
|
||||
// ---- msg1 压缩 ----
|
||||
if needCompactMsg1 {
|
||||
msg1 = compactMsg1IfNeeded(ctx, input, flowState, emitter, msg1)
|
||||
messages[1].Content = msg1
|
||||
// 压缩 msg1 后重算预算
|
||||
breakdown = pkg.EstimateExecuteMessagesTokens(msg0, msg1, msg2, msg3)
|
||||
}
|
||||
|
||||
// ---- msg2 压缩 ----
|
||||
if needCompactMsg2 || breakdown.Total > pkg.ExecuteTokenBudget {
|
||||
msg2 = compactMsg2IfNeeded(ctx, input, flowState, emitter, msg2)
|
||||
messages[2].Content = msg2
|
||||
breakdown = pkg.EstimateExecuteMessagesTokens(msg0, msg1, msg2, msg3)
|
||||
}
|
||||
|
||||
// 记录最终 token 分布
|
||||
saveTokenStats(ctx, input, flowState, breakdown)
|
||||
|
||||
log.Printf(
|
||||
"[COMPACT] after compaction: total=%d budget=%d (msg0=%d msg1=%d msg2=%d msg3=%d)",
|
||||
breakdown.Total, breakdown.Budget,
|
||||
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
|
||||
)
|
||||
return messages
|
||||
}
|
||||
|
||||
// compactMsg1IfNeeded 对 msg1(历史对话)执行 LLM 压缩。
|
||||
func compactMsg1IfNeeded(
|
||||
ctx context.Context,
|
||||
input ExecuteNodeInput,
|
||||
flowState *newagentmodel.CommonState,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
msg1 string,
|
||||
) string {
|
||||
compactionStore := input.CompactionStore
|
||||
if compactionStore == nil {
|
||||
log.Printf("[COMPACT] CompactionStore is nil, skip msg1 compaction")
|
||||
return msg1
|
||||
}
|
||||
|
||||
// 加载已有压缩摘要
|
||||
existingSummary, _, err := compactionStore.LoadCompaction(ctx, flowState.UserID, flowState.ConversationID)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT] load existing compaction failed: %v, proceed without cache", err)
|
||||
}
|
||||
|
||||
// SSE: 压缩开始
|
||||
tokenBefore := pkg.EstimateTextTokens(msg1)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg1", "context_compact_start",
|
||||
fmt.Sprintf("正在压缩对话历史(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
// 调用 LLM 压缩
|
||||
newSummary, err := newagentprompt.CompactMsg1(ctx, input.Client, msg1, existingSummary)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT] compact msg1 failed: %v", err)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg1", "context_compact_done",
|
||||
"对话历史压缩失败,使用原始文本",
|
||||
false,
|
||||
)
|
||||
return msg1
|
||||
}
|
||||
|
||||
// SSE: 压缩完成
|
||||
tokenAfter := pkg.EstimateTextTokens(newSummary)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg1", "context_compact_done",
|
||||
fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter),
|
||||
false,
|
||||
)
|
||||
|
||||
// 持久化压缩结果
|
||||
if err := compactionStore.SaveCompaction(ctx, flowState.UserID, flowState.ConversationID, newSummary, flowState.RoundUsed); err != nil {
|
||||
log.Printf("[COMPACT] save compaction failed: %v", err)
|
||||
}
|
||||
|
||||
return newSummary
|
||||
}
|
||||
|
||||
// compactMsg2IfNeeded 对 msg2(ReAct Loop 记录)执行 LLM 压缩。
|
||||
func compactMsg2IfNeeded(
|
||||
ctx context.Context,
|
||||
input ExecuteNodeInput,
|
||||
flowState *newagentmodel.CommonState,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
msg2 string,
|
||||
) string {
|
||||
// SSE: 压缩开始
|
||||
tokenBefore := pkg.EstimateTextTokens(msg2)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg2", "context_compact_start",
|
||||
fmt.Sprintf("正在压缩执行记录(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
// 调用 LLM 压缩
|
||||
compressed, err := newagentprompt.CompactMsg2(ctx, input.Client, msg2)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT] compact msg2 failed: %v", err)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg2", "context_compact_done",
|
||||
"执行记录压缩失败,使用原始文本",
|
||||
false,
|
||||
)
|
||||
return msg2
|
||||
}
|
||||
|
||||
// SSE: 压缩完成
|
||||
tokenAfter := pkg.EstimateTextTokens(compressed)
|
||||
_ = emitter.EmitStatus(
|
||||
executeStatusBlockID, "compact_msg2", "context_compact_done",
|
||||
fmt.Sprintf("执行记录已压缩:%d → %d tokens", tokenBefore, tokenAfter),
|
||||
false,
|
||||
)
|
||||
|
||||
return compressed
|
||||
}
|
||||
|
||||
// saveTokenStats 持久化当前 token 分布到 DB。
|
||||
func saveTokenStats(
|
||||
ctx context.Context,
|
||||
input ExecuteNodeInput,
|
||||
flowState *newagentmodel.CommonState,
|
||||
breakdown pkg.ExecuteTokenBreakdown,
|
||||
) {
|
||||
compactionStore := input.CompactionStore
|
||||
if compactionStore == nil {
|
||||
return
|
||||
}
|
||||
statsJSON, err := json.Marshal(breakdown)
|
||||
if err != nil {
|
||||
log.Printf("[COMPACT] marshal token stats failed: %v", err)
|
||||
return
|
||||
}
|
||||
if err := compactionStore.SaveContextTokenStats(ctx, flowState.UserID, flowState.ConversationID, string(statsJSON)); err != nil {
|
||||
log.Printf("[COMPACT] save token stats failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,10 @@ const (
|
||||
// 2. RuntimeState 提供 PendingInteraction;
|
||||
// 3. ChunkEmitter 负责推送收尾消息。
|
||||
type InterruptNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// RunInterruptNode 执行一轮中断节点逻辑。
|
||||
@@ -55,7 +56,7 @@ func RunInterruptNode(ctx context.Context, input InterruptNodeInput) error {
|
||||
|
||||
switch pending.Type {
|
||||
case newagentmodel.PendingInteractionTypeAskUser:
|
||||
return handleInterruptAskUser(ctx, pending, conversationContext, emitter)
|
||||
return handleInterruptAskUser(ctx, runtimeState, input.PersistVisibleMessage, pending, conversationContext, emitter)
|
||||
case newagentmodel.PendingInteractionTypeConfirm:
|
||||
return handleInterruptConfirm(pending, emitter)
|
||||
default:
|
||||
@@ -70,6 +71,8 @@ func RunInterruptNode(ctx context.Context, input InterruptNodeInput) error {
|
||||
// 写入历史,然后结束。用户体验和正常对话一样 — 助手问了问题,停下来等回复。
|
||||
func handleInterruptAskUser(
|
||||
ctx context.Context,
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
persist newagentmodel.PersistVisibleMessageFunc,
|
||||
pending *newagentmodel.PendingInteraction,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
@@ -89,7 +92,9 @@ func handleInterruptAskUser(
|
||||
}
|
||||
|
||||
// 写入对话历史,下一轮 resume 时 LLM 能看到这个上下文。
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(text, nil))
|
||||
msg := schema.AssistantMessage(text, nil)
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, persist, runtimeState.EnsureCommonState(), msg)
|
||||
|
||||
// 状态持久化已由 agent_nodes 层统一处理,此处不再需要自行存快照。
|
||||
|
||||
|
||||
121
backend/newAgent/node/llm_debug.go
Normal file
121
backend/newAgent/node/llm_debug.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package newagentnode
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -28,14 +28,16 @@ const (
|
||||
|
||||
// PlanNodeInput 描述单轮规划节点执行所需的最小依赖。
|
||||
type PlanNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
UserInput string
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ResumeNode string
|
||||
AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点
|
||||
ThinkingEnabled bool // 是否开启 thinking,由 config.yaml 的 agent.thinking.plan 注入
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
UserInput string
|
||||
Client *infrallm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ResumeNode string
|
||||
AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点
|
||||
ThinkingEnabled bool // 是否开启 thinking,由 config.yaml 的 agent.thinking.plan 注入
|
||||
CompactionStore newagentmodel.CompactionStore // 上下文压缩持久化
|
||||
PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc
|
||||
}
|
||||
|
||||
// RunPlanNode 执行一轮规划节点逻辑。
|
||||
@@ -68,6 +70,15 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
|
||||
// 2. 构造本轮规划输入。
|
||||
messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput)
|
||||
messages = compactUnifiedMessagesIfNeeded(ctx, messages, UnifiedCompactInput{
|
||||
Client: input.Client,
|
||||
CompactionStore: input.CompactionStore,
|
||||
FlowState: flowState,
|
||||
Emitter: emitter,
|
||||
StageName: planStageName,
|
||||
StatusBlockID: planStatusBlockID,
|
||||
})
|
||||
logNodeLLMContext(planStageName, "planning", flowState, messages)
|
||||
|
||||
// 3. 单轮深度规划:由配置决定是否开启 thinking,不做 token 上限约束。
|
||||
decision, rawResult, err := infrallm.GenerateJSON[newagentmodel.PlanDecision](
|
||||
@@ -95,6 +106,7 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
|
||||
// 4. 若模型先对用户说了话,且不是 ask_user(ask_user 交给 interrupt 收口),则先以伪流式推送,再写回 history。
|
||||
if strings.TrimSpace(decision.Speak) != "" && decision.Action != newagentmodel.PlanActionAskUser {
|
||||
msg := schema.AssistantMessage(decision.Speak, nil)
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
planSpeakBlockID,
|
||||
@@ -104,7 +116,8 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
); err != nil {
|
||||
return fmt.Errorf("规划文案推送失败: %w", err)
|
||||
}
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil))
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
|
||||
// 5. 按规划动作推进流程状态。
|
||||
@@ -139,6 +152,7 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
// 3. 推流后同步写入历史,确保后续 Execute 阶段的上下文也能看到这份计划。
|
||||
summary := strings.TrimSpace(buildPlanSummary(decision.PlanSteps))
|
||||
if summary != "" {
|
||||
msg := schema.AssistantMessage(summary, nil)
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
planSummaryBlockID,
|
||||
@@ -148,7 +162,8 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
); err != nil {
|
||||
return fmt.Errorf("自动执行前计划摘要推送失败: %w", err)
|
||||
}
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(summary, nil))
|
||||
conversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
|
||||
flowState.ConfirmPlan()
|
||||
|
||||
301
backend/newAgent/node/unified_compact.go
Normal file
301
backend/newAgent/node/unified_compact.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package newagentnode
|
||||
|
||||
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. 从 ExecuteNodeInput 中提取压缩所需的公共字段,消除对 Execute 的直接依赖;
|
||||
// 2. 各节点(Plan/Chat/Deliver)构造此参数时从自己的 NodeInput 中提取对应字段;
|
||||
// 3. StageName 和 StatusBlockID 用于区分日志来源和 SSE 状态推送。
|
||||
type UnifiedCompactInput struct {
|
||||
// Client 用于调用 LLM 压缩 msg1/msg2。
|
||||
Client *infrallm.Client
|
||||
// CompactionStore 用于持久化压缩摘要和 token 统计,为 nil 时跳过持久化。
|
||||
CompactionStore newagentmodel.CompactionStore
|
||||
// FlowState 提供 userID / chatID / roundUsed 等定位信息。
|
||||
FlowState *newagentmodel.CommonState
|
||||
// Emitter 用于推送压缩进度 SSE 事件。
|
||||
Emitter *newagentstream.ChunkEmitter
|
||||
// StageName 标识当前阶段(如 "execute"/"plan"/"chat"/"deliver"),用于日志和缓存 key。
|
||||
StageName string
|
||||
// StatusBlockID 是 SSE 状态推送的 block ID,各节点使用自己的 block ID。
|
||||
StatusBlockID string
|
||||
}
|
||||
|
||||
// compactUnifiedMessagesIfNeeded 检查统一消息结构的 token 预算,
|
||||
// 超限时对 msg1(历史对话)和 msg2(阶段工作区)执行 LLM 压缩。
|
||||
//
|
||||
// 消息布局约定(由 buildUnifiedStageMessages 返回):
|
||||
//
|
||||
// [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 压缩(历史对话 → LLM 摘要)。
|
||||
if needCompactMsg1 {
|
||||
msg1 = compactUnifiedMsg1(ctx, input, msg1)
|
||||
messages[1].Content = msg1
|
||||
// 压缩 msg1 后重算预算。
|
||||
breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3)
|
||||
}
|
||||
|
||||
// 6. msg2 压缩(阶段工作区 → LLM 摘要)。
|
||||
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 {
|
||||
// 1. CompactionStore 为 nil 时无法加载/保存摘要,跳过压缩。
|
||||
if input.CompactionStore == nil {
|
||||
log.Printf("[COMPACT:%s] CompactionStore is nil, skip msg1 compaction", input.StageName)
|
||||
return msg1
|
||||
}
|
||||
|
||||
// 2. 加载该阶段已有的压缩摘要(可能为空)。
|
||||
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)
|
||||
}
|
||||
|
||||
// 3. SSE: 压缩开始。
|
||||
tokenBefore := pkg.EstimateTextTokens(msg1)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_start",
|
||||
fmt.Sprintf("正在压缩对话历史(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
// 4. 调用 LLM 压缩:将 msg1 全文 + 已有摘要合并为一份紧凑摘要。
|
||||
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
|
||||
}
|
||||
|
||||
// 5. SSE: 压缩完成。
|
||||
tokenAfter := pkg.EstimateTextTokens(newSummary)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_done",
|
||||
fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter),
|
||||
false,
|
||||
)
|
||||
|
||||
// 6. 持久化压缩结果,下一轮可直接复用摘要。
|
||||
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 {
|
||||
// 1. SSE: 压缩开始。
|
||||
tokenBefore := pkg.EstimateTextTokens(msg2)
|
||||
_ = input.Emitter.EmitStatus(
|
||||
input.StatusBlockID, input.StageName, "context_compact_start",
|
||||
fmt.Sprintf("正在压缩执行记录(%d tokens)...", tokenBefore),
|
||||
false,
|
||||
)
|
||||
|
||||
// 2. 调用 LLM 压缩。
|
||||
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
|
||||
}
|
||||
|
||||
// 3. SSE: 压缩完成。
|
||||
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 分布到 DB。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 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/node/visible_message.go
Normal file
37
backend/newAgent/node/visible_message.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package newagentnode
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const chatRoutingSystemPrompt = `
|
||||
- route=direct_reply 时,控制码后的可见内容应直接回应用户问题,而不是先讲能力边界。
|
||||
- route=deep_answer 时,只输出控制码即可,不要补“让我想想”“这是个好问题”之类的占位话术。
|
||||
|
||||
粗排判断:当用户意图包含"批量安排/排课/把任务类排进日程",且上下文中有任务类 ID 时,设置 rough_build=true。
|
||||
粗排判断:当用户意图包含"批量安排/排课/把任务类排进日程"等批量调度需求时,可设置 rough_build=true;后端会结合真实请求范围决定是否真正进入粗排。
|
||||
二次粗排约束(强约束):
|
||||
- 若上下文已出现 rough_build_done,且用户未明确要求"重新粗排/从头重排",必须设置 rough_build=false。
|
||||
- "移动/微调/优化/均匀化/调顺序"等请求默认视为 refine,不得再次触发 rough build。
|
||||
@@ -83,40 +83,25 @@ func BuildChatRoutingSystemPrompt() string {
|
||||
|
||||
// BuildChatRoutingMessages 组装路由阶段的 messages。
|
||||
func BuildChatRoutingMessages(ctx *newagentmodel.ConversationContext, userInput string, state *newagentmodel.CommonState, nonce string) []*schema.Message {
|
||||
return buildStageMessages(
|
||||
BuildChatRoutingSystemPrompt(),
|
||||
return buildUnifiedStageMessages(
|
||||
ctx,
|
||||
BuildChatRoutingUserPrompt(ctx, userInput, state, nonce),
|
||||
StageMessagesConfig{
|
||||
SystemPrompt: BuildChatRoutingSystemPrompt(),
|
||||
Msg1Content: buildChatConversationMessage(ctx),
|
||||
Msg2Content: buildChatRoutingWorkspace(ctx),
|
||||
Msg3Suffix: BuildChatRoutingUserPrompt(userInput, nonce),
|
||||
Msg3Role: schema.User,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BuildChatRoutingUserPrompt 构造路由阶段的用户提示词。
|
||||
func BuildChatRoutingUserPrompt(ctx *newagentmodel.ConversationContext, userInput string, state *newagentmodel.CommonState, nonce string) string {
|
||||
func BuildChatRoutingUserPrompt(userInput string, nonce string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("nonce=%s\n", nonce))
|
||||
sb.WriteString(fmt.Sprintf("当前时间=%s\n", time.Now().In(time.Local).Format("2006-01-02 15:04")))
|
||||
sb.WriteString("\n请判断用户本轮意图的复杂度,选择最合适的路由,并输出控制码和对应内容。\n")
|
||||
|
||||
// 注入任务类上下文(供粗排判断参考)。
|
||||
if state != nil && len(state.TaskClassIDs) > 0 {
|
||||
parts := make([]string, len(state.TaskClassIDs))
|
||||
for i, id := range state.TaskClassIDs {
|
||||
parts[i] = fmt.Sprintf("%d", id)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\n本次请求涉及的任务类 ID:[%s]\n", strings.Join(parts, ", ")))
|
||||
}
|
||||
|
||||
if state != nil && len(state.TaskClasses) > 0 {
|
||||
sb.WriteString("任务类约束:\n")
|
||||
for _, tc := range state.TaskClasses {
|
||||
line := fmt.Sprintf("- [ID=%d] %s:策略=%s,总时段预算=%d", tc.ID, tc.Name, tc.Strategy, tc.TotalSlots)
|
||||
if tc.StartDate != "" || tc.EndDate != "" {
|
||||
line += fmt.Sprintf(",日期范围=%s ~ %s", tc.StartDate, tc.EndDate)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n请基于最近真实对话和本轮输入选择最合适的路由,并严格按系统约定输出控制码。\n")
|
||||
|
||||
trimmedInput := strings.TrimSpace(userInput)
|
||||
if trimmedInput != "" {
|
||||
@@ -146,10 +131,23 @@ func BuildDeepAnswerSystemPrompt() string {
|
||||
}
|
||||
|
||||
// BuildDeepAnswerMessages 组装深度回答阶段的 messages。
|
||||
func BuildDeepAnswerMessages(ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
|
||||
return buildStageMessages(
|
||||
BuildDeepAnswerSystemPrompt(),
|
||||
func BuildDeepAnswerMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
|
||||
return buildUnifiedStageMessages(
|
||||
ctx,
|
||||
userInput,
|
||||
StageMessagesConfig{
|
||||
SystemPrompt: BuildDeepAnswerSystemPrompt(),
|
||||
Msg1Content: buildChatConversationMessage(ctx),
|
||||
Msg2Content: buildDeepAnswerWorkspace(),
|
||||
Msg3Suffix: buildDeepAnswerUserPrompt(userInput),
|
||||
Msg3Role: schema.User,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func buildDeepAnswerUserPrompt(userInput string) string {
|
||||
trimmedInput := strings.TrimSpace(userInput)
|
||||
if trimmedInput != "" {
|
||||
return trimmedInput
|
||||
}
|
||||
return "请直接回答用户刚才的问题。"
|
||||
}
|
||||
|
||||
33
backend/newAgent/prompt/chat_context.go
Normal file
33
backend/newAgent/prompt/chat_context.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
// buildChatConversationMessage 生成 chat / deep_answer 共用的真实对话视图。
|
||||
func buildChatConversationMessage(ctx *newagentmodel.ConversationContext) string {
|
||||
return buildConversationHistoryMessage(ctx, "真实对话记录")
|
||||
}
|
||||
|
||||
// buildChatRoutingWorkspace 渲染 chat 路由节点的轻量补充区。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. chat 只保留与路由判断直接相关的最小流程标记;
|
||||
// 2. rough_build_done 仍需显式暴露,否则路由层会丢掉“不要重复粗排”的关键信号;
|
||||
// 3. 不再展示轮次、阶段锚点、ReAct 摘要等 execute 专属信息。
|
||||
func buildChatRoutingWorkspace(ctx *newagentmodel.ConversationContext) string {
|
||||
lines := []string{"路由补充:"}
|
||||
if hasExecuteRoughBuildDone(ctx) {
|
||||
lines = append(lines, "- 已存在 rough_build_done;除非用户明确要求重新粗排,否则不要再次触发 rough_build。")
|
||||
} else {
|
||||
lines = append(lines, "- 暂无额外流程标记。")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// buildDeepAnswerWorkspace 渲染 deep_answer 节点的轻量工作区。
|
||||
func buildDeepAnswerWorkspace() string {
|
||||
return "回答补充:请直接延续最近对话,聚焦回答用户本轮问题。"
|
||||
}
|
||||
37
backend/newAgent/prompt/conversation_view.go
Normal file
37
backend/newAgent/prompt/conversation_view.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
// buildConversationHistoryMessage 将“真实对话流”渲染成节点可直接复用的 msg1。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责把 user + assistant speak 组织成稳定文本;
|
||||
// 2. 不拼接 tool_call / tool observation,这些不属于“真实对话”;
|
||||
// 3. 不做长度裁剪,长度预算交给统一压缩层处理。
|
||||
func buildConversationHistoryMessage(ctx *newagentmodel.ConversationContext, title string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
title = "真实对话记录"
|
||||
}
|
||||
|
||||
lines := []string{title + ":"}
|
||||
if ctx == nil {
|
||||
lines = append(lines, "暂无。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
turns := CollectConversationTurns(ctx.HistorySnapshot())
|
||||
if len(turns) == 0 {
|
||||
lines = append(lines, "暂无。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
for _, turn := range turns {
|
||||
lines = append(lines, turn.Role+": \""+turn.Content+"\"")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -14,16 +14,16 @@ const deliverSystemPrompt = `
|
||||
|
||||
请遵守以下规则:
|
||||
1. 只基于已有历史和计划状态生成总结,不要编造未执行的操作。
|
||||
2. 如果所有步骤都已完成,简要总结每一步的成果。
|
||||
3. 如果是因轮次耗尽提前结束,如实告知用户当前进度和未完成的部分。
|
||||
4. 使用自然、友好的语气,不要机械地罗列步骤。
|
||||
5. 如果用户后续可能需要继续操作,给出简短的建议。
|
||||
6. 只输出总结文本,不要输出 JSON,不要输出 markdown 标题。
|
||||
2. 如果所有步骤都已完成,请自然概括每一步的主要成果。
|
||||
3. 如果流程因轮次耗尽或主动终止而提前结束,请如实说明当前进度与未完成部分。
|
||||
4. 使用自然、友好的语气,不要机械罗列工具过程。
|
||||
5. 如果用户后续还需要继续操作,可以给出一句简短建议。
|
||||
6. 只输出总结文本,不要输出 JSON,也不要输出 markdown 标题。
|
||||
|
||||
你会看到:
|
||||
- 原始计划步骤及完成判定
|
||||
- 当前执行进度
|
||||
- 执行阶段的对话历史
|
||||
- 原始计划步骤及完成进度
|
||||
- 最近真实对话
|
||||
- 当前流程的收口状态
|
||||
`
|
||||
|
||||
// BuildDeliverSystemPrompt 返回交付阶段系统提示词。
|
||||
@@ -31,37 +31,52 @@ func BuildDeliverSystemPrompt() string {
|
||||
return strings.TrimSpace(deliverSystemPrompt)
|
||||
}
|
||||
|
||||
// BuildDeliverMessages 组装交付阶段的 messages。
|
||||
// BuildDeliverMessages 组装交付阶段 messages。
|
||||
func BuildDeliverMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) []*schema.Message {
|
||||
return buildStageMessages(
|
||||
BuildDeliverSystemPrompt(),
|
||||
roughBuildPrefix := buildDeliverRoughBuildPrefix(ctx, state)
|
||||
return buildUnifiedStageMessages(
|
||||
ctx,
|
||||
BuildDeliverUserPrompt(state),
|
||||
StageMessagesConfig{
|
||||
SystemPrompt: BuildDeliverSystemPrompt(),
|
||||
Msg1Content: buildDeliverConversationMessage(ctx),
|
||||
Msg2Content: buildDeliverWorkspace(state),
|
||||
Msg3Prefix: roughBuildPrefix,
|
||||
Msg3Suffix: BuildDeliverUserPrompt(state, ctx),
|
||||
Msg3Role: schema.User,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BuildDeliverUserPrompt 构造交付阶段的用户提示词。
|
||||
func BuildDeliverUserPrompt(state *newagentmodel.CommonState) string {
|
||||
func BuildDeliverUserPrompt(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("请为当前任务生成完成总结。\n")
|
||||
sb.WriteString(renderStateSummary(state))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("请基于最近对话和交付工作区,生成一段自然、诚实的完成总结。\n")
|
||||
|
||||
if state == nil || !state.HasPlan() {
|
||||
sb.WriteString("当前没有正式计划,请基于对话历史简要总结本次交互。\n")
|
||||
if hasExecuteRoughBuildDone(ctx) {
|
||||
sb.WriteString("当前没有正式计划,但本轮已经完成粗排,请结合粗排补充和任务类详情总结粗排结果,不要把它说成正式完结。\n")
|
||||
} else {
|
||||
sb.WriteString("当前没有正式计划,请只概括本次互动,不要编造成果。\n")
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
current, total := state.PlanProgress()
|
||||
exhausted := state.Exhausted()
|
||||
completed := countCompletedPlanSteps(state)
|
||||
total := len(state.PlanSteps)
|
||||
|
||||
if exhausted {
|
||||
sb.WriteString(fmt.Sprintf("注意:任务因轮次耗尽提前结束,当前进度 %d/%d。\n", current, total))
|
||||
sb.WriteString("请如实说明已完成和未完成的部分,并建议用户如何继续。\n")
|
||||
} else {
|
||||
sb.WriteString("所有计划步骤已执行完毕,请总结整体成果。\n")
|
||||
if state.IsExhaustedTerminal() {
|
||||
sb.WriteString(fmt.Sprintf("注意:任务因轮次耗尽提前结束,当前已完成 %d/%d 步。\n", completed, total))
|
||||
sb.WriteString("请如实说明已完成与未完成的部分,并给出一句继续建议。\n")
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
if state.IsAborted() {
|
||||
sb.WriteString(fmt.Sprintf("注意:流程已被主动终止,当前已完成 %d/%d 步。\n", completed, total))
|
||||
sb.WriteString("请如实说明停在何处,以及用户若想继续应如何衔接。\n")
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
sb.WriteString("若计划已正常完成,请概括整体成果;若仍有未完成步骤,也必须如实说明。\n")
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
137
backend/newAgent/prompt/deliver_context.go
Normal file
137
backend/newAgent/prompt/deliver_context.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
// buildDeliverConversationMessage 生成 deliver 节点看到的真实对话视图。
|
||||
func buildDeliverConversationMessage(ctx *newagentmodel.ConversationContext) string {
|
||||
return buildConversationHistoryMessage(ctx, "执行对话记录")
|
||||
}
|
||||
|
||||
// buildDeliverRoughBuildPrefix 构造 deliver 在“粗排已完成”场景下的专属前缀。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只负责把粗排相关的任务类信息补进 msg3 前缀,不改写交付总结本身;
|
||||
// 2. 只有在上下文里明确存在 rough_build_done 时才注入,避免普通交付场景被额外信息污染;
|
||||
// 3. 这段前缀用于补齐第一次粗排没有正式计划时的任务类详情,优先让 deliver 看到 task_class_ids 和任务类约束。
|
||||
func buildDeliverRoughBuildPrefix(ctx *newagentmodel.ConversationContext, state *newagentmodel.CommonState) string {
|
||||
if !hasExecuteRoughBuildDone(ctx) {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"粗排补充信息:",
|
||||
"- 本轮已经完成粗排,相关任务类已进入 suggested/existing,不要把它们说成正式计划。",
|
||||
}
|
||||
|
||||
if taskClassIDs := renderPlanTaskClassIDs(state); taskClassIDs != "" {
|
||||
lines = append(lines, "- "+taskClassIDs)
|
||||
}
|
||||
if taskClassMeta := renderPlanTaskClassMeta(state); taskClassMeta != "" {
|
||||
lines = append(lines, "任务类详情:")
|
||||
lines = append(lines, taskClassMeta)
|
||||
}
|
||||
|
||||
if state == nil || !state.HasPlan() {
|
||||
lines = append(lines, "- 当前没有正式计划,请把这批任务类的粗排结果作为总结重点。")
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// buildDeliverWorkspace 渲染 deliver 节点自己的结果视图。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. deliver 只需要结果态信息:计划简表、完成进度、收口状态;
|
||||
// 2. 不再注入工具目录、任务类约束、ReAct 摘要等过程噪声;
|
||||
// 3. 没有正式计划时,明确退回“只基于对话做总结”。
|
||||
func buildDeliverWorkspace(state *newagentmodel.CommonState) string {
|
||||
lines := []string{"交付工作区:"}
|
||||
if state == nil {
|
||||
lines = append(lines, "- 当前缺少流程状态,请仅基于最近对话做诚实总结。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
lines = append(lines, renderDeliverTerminalSummary(state))
|
||||
if !state.HasPlan() {
|
||||
lines = append(lines, "- 当前没有正式计划,请只概括本次互动。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
total := len(state.PlanSteps)
|
||||
completed := countCompletedPlanSteps(state)
|
||||
lines = append(lines, fmt.Sprintf("- 计划进度:已完成 %d/%d 步。", completed, total))
|
||||
lines = append(lines, "计划步骤:")
|
||||
lines = append(lines, renderDeliverStepOutline(state, completed))
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// renderDeliverTerminalSummary 返回 deliver 节点需要知道的收口状态。
|
||||
func renderDeliverTerminalSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil || !state.HasTerminalOutcome() || state.TerminalOutcome == nil {
|
||||
return "- 当前没有正式终止结果,请按最近对话和计划进度自然总结。"
|
||||
}
|
||||
|
||||
outcome := state.TerminalOutcome
|
||||
line := fmt.Sprintf("- 收口状态:%s", outcome.Status)
|
||||
if stage := strings.TrimSpace(outcome.Stage); stage != "" {
|
||||
line += fmt.Sprintf(";阶段:%s", stage)
|
||||
}
|
||||
if msg := strings.TrimSpace(outcome.UserMessage); msg != "" {
|
||||
line += fmt.Sprintf(";用户提示:%s", msg)
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// renderDeliverStepOutline 生成 deliver 节点使用的步骤简表。
|
||||
func renderDeliverStepOutline(state *newagentmodel.CommonState, completed int) string {
|
||||
if state == nil || len(state.PlanSteps) == 0 {
|
||||
return "- 暂无。"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(state.PlanSteps))
|
||||
for i, step := range state.PlanSteps {
|
||||
status := "未完成"
|
||||
if i < completed {
|
||||
status = "已完成"
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(step.Content)
|
||||
if content == "" {
|
||||
content = "(步骤正文为空)"
|
||||
}
|
||||
line := fmt.Sprintf("%d. [%s] %s", i+1, status, content)
|
||||
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
|
||||
line += fmt.Sprintf(" | 完成判定:%s", doneWhen)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// countCompletedPlanSteps 统计当前已经完成的计划步骤数。
|
||||
func countCompletedPlanSteps(state *newagentmodel.CommonState) int {
|
||||
if state == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
total := len(state.PlanSteps)
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
if state.CurrentStep <= 0 {
|
||||
if state.IsCompleted() {
|
||||
return total
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if state.CurrentStep >= total {
|
||||
return total
|
||||
}
|
||||
return state.CurrentStep
|
||||
}
|
||||
@@ -12,20 +12,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// executeHistoryKindKey 用于在 history 中打运行态标记,供 prompt 分层识别。
|
||||
// 说明:loop_closed / step_advanced 等边界标记仍由节点层写入,但 prompt 层已不再消费它们——
|
||||
// 因为 msg1/msg2 已经按"真实对话流 + 当前活跃 ReAct 记录"重构,不再做 msg2→msg1 的归档搬运。
|
||||
executeHistoryKindKey = "newagent_history_kind"
|
||||
executeHistoryKindCorrectionUser = "llm_correction_prompt"
|
||||
executeHistoryKindLoopClosed = "execute_loop_closed"
|
||||
executeHistoryKindStepAdvanced = "execute_step_advanced"
|
||||
|
||||
// executeLoopWindowLimit 控制当轮 ReAct Loop 窗口最多保留多少条记录。
|
||||
executeLoopWindowLimit = 8
|
||||
|
||||
// executeTrimmedObservationText 是重复工具压缩后的 observation 占位文案。
|
||||
executeTrimmedObservationText = "当前工具调用结果已经被使用过,当前无需使用,为节省上下文空间,已折叠"
|
||||
|
||||
// executeConversationTurnLimit 控制 msg1 注入的最大对话轮数(user + assistant speak)。
|
||||
// 超出时保留最近的条目,早期部分由 ReAct 摘要兜底。
|
||||
executeConversationTurnLimit = 30
|
||||
)
|
||||
|
||||
type executeToolSchemaDoc struct {
|
||||
@@ -40,8 +31,6 @@ type executeLoopRecord struct {
|
||||
Observation string
|
||||
}
|
||||
|
||||
const executeMessage1MaxRunes = 1400
|
||||
|
||||
// buildExecuteStageMessages 组装 execute 阶段 4 条消息骨架。
|
||||
//
|
||||
// 消息结构(固定):
|
||||
@@ -82,87 +71,24 @@ func buildExecuteMessage0(stageSystemPrompt string, ctx *newagentmodel.Conversat
|
||||
return base + "\n\n" + toolCatalog
|
||||
}
|
||||
|
||||
// splitExecuteLoopRecordsByBoundary 按已收口标记拆分归档/活跃 ReAct 记录。
|
||||
//
|
||||
// 规则:
|
||||
// 1. 标记之前的记录归档到 msg1;
|
||||
// 2. 标记之后的记录作为活跃 loop 进入 msg2;
|
||||
// 3. 若没有标记,则全部视为活跃记录(兼容旧会话快照)。
|
||||
func splitExecuteLoopRecordsByBoundary(history []*schema.Message) (archived []executeLoopRecord, active []executeLoopRecord) {
|
||||
if len(history) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
boundary := findLatestExecuteBoundaryMarker(history)
|
||||
if boundary < 0 {
|
||||
return nil, collectExecuteLoopRecords(history)
|
||||
}
|
||||
|
||||
if boundary > 0 {
|
||||
archived = collectExecuteLoopRecords(history[:boundary])
|
||||
}
|
||||
if boundary+1 < len(history) {
|
||||
active = collectExecuteLoopRecords(history[boundary+1:])
|
||||
}
|
||||
return archived, active
|
||||
}
|
||||
|
||||
func findLatestExecuteBoundaryMarker(history []*schema.Message) int {
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
msg := history[i]
|
||||
if msg == nil || msg.Extra == nil {
|
||||
continue
|
||||
}
|
||||
kind, ok := msg.Extra[executeHistoryKindKey].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(kind) {
|
||||
case executeHistoryKindLoopClosed, executeHistoryKindStepAdvanced:
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func trimExecuteMessage1ByBudget(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(content)
|
||||
if len(runes) <= executeMessage1MaxRunes {
|
||||
return content
|
||||
}
|
||||
if executeMessage1MaxRunes <= 3 {
|
||||
return string(runes[:executeMessage1MaxRunes])
|
||||
}
|
||||
return string(runes[:executeMessage1MaxRunes-3]) + "..."
|
||||
}
|
||||
|
||||
// buildExecuteMessage1V3 负责把真实对话流 + 上一轮 loop 归档并入 msg1,并统一做长度裁剪。
|
||||
// buildExecuteMessage1V3 只渲染"真实对话流 + 阶段锚点"。
|
||||
//
|
||||
// 改造说明:
|
||||
// 1. msg1 从人工提炼的摘要变为真实对话流,只注入 user + assistant speak;
|
||||
// 2. tool_call / observation 不在 msg1 中重复(已由 msg2 承载);
|
||||
// 3. 超出 executeConversationTurnLimit 的早期对话不注入,由 ReAct 摘要兜底。
|
||||
// 1. msg1 只保留 user + assistant speak 组成的真实对话历史,全量注入;
|
||||
// 2. tool_call / observation 一律由 msg2 承载,这里不再重复;
|
||||
// 3. 不再从历史中"归档"上一轮 ReAct 结果到 msg1——归档搬运逻辑已随 splitExecuteLoopRecordsByBoundary 一并移除;
|
||||
// 4. token 预算由统一压缩层兜底,prompt 层不做提前裁剪。
|
||||
func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
|
||||
lines := []string{"历史上下文:"}
|
||||
if ctx == nil {
|
||||
lines = append(lines,
|
||||
"- 对话历史:暂无。",
|
||||
"- 阶段锚点:按当前工具事实推进执行。",
|
||||
"- 历史归档 ReAct 摘要:暂无。",
|
||||
"- 历史归档 ReAct 窗口:暂无。",
|
||||
"- 当前循环早期摘要:暂无。",
|
||||
)
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
history := ctx.HistorySnapshot()
|
||||
|
||||
// 注入真实对话流(user + assistant speak),全量放入,不再限制轮数和单条长度。
|
||||
turns := collectExecuteConversationTurns(history)
|
||||
turns := collectExecuteConversationTurns(ctx.HistorySnapshot())
|
||||
if len(turns) == 0 {
|
||||
lines = append(lines, "- 对话历史:暂无。")
|
||||
} else {
|
||||
@@ -180,16 +106,15 @@ func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
|
||||
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
|
||||
}
|
||||
|
||||
archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history)
|
||||
lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, executeLoopWindowLimit))
|
||||
lines = append(lines, renderArchivedExecuteLoopWindowForMessage1V3(archivedLoops))
|
||||
lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit))
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// buildExecuteMessage2V3 承载当前活跃 loop 的全部记录。
|
||||
// 若是新一轮刚开始(活跃 loop 为空),明确返回已清空状态。
|
||||
// 不再限制窗口大小,token 预算由 execute 层统一管理。
|
||||
// buildExecuteMessage2V3 承载当前会话中全部 ReAct Loop 记录。
|
||||
//
|
||||
// 改造说明:
|
||||
// 1. 不再按 execute_loop_closed / execute_step_advanced 边界切分"归档/活跃"两段;
|
||||
// 2. 直接从 history 提取全部 assistant tool_call + 对应 observation 作为当前 Loop 视图;
|
||||
// 3. 新一轮刚开始(尚未产生 tool_call)时返回明确占位,方便模型识别"干净起点"。
|
||||
func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
|
||||
lines := []string{"当轮 ReAct Loop 记录:"}
|
||||
if ctx == nil {
|
||||
@@ -197,31 +122,13 @@ func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
_, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot())
|
||||
if len(activeLoops) == 0 {
|
||||
loops := collectExecuteLoopRecords(ctx.HistorySnapshot())
|
||||
if len(loops) == 0 {
|
||||
lines = append(lines, "- 已清空(新一轮 loop 准备中)。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// 全量放入,不再限制窗口大小
|
||||
for i, loop := range activeLoops {
|
||||
lines = append(lines, fmt.Sprintf("%d) thought/reason:%s", i+1, loop.Thought))
|
||||
lines = append(lines, fmt.Sprintf(" tool_call:%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
|
||||
lines = append(lines, fmt.Sprintf(" observation:%s", loop.Observation))
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func renderArchivedExecuteLoopWindowForMessage1V3(records []executeLoopRecord) string {
|
||||
if len(records) == 0 {
|
||||
return "- 历史归档 ReAct 窗口:暂无。"
|
||||
}
|
||||
|
||||
windowLoops := tailExecuteLoops(records, executeLoopWindowLimit)
|
||||
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
|
||||
|
||||
lines := []string{"历史归档 ReAct 窗口(由上一轮 msg2 并入):"}
|
||||
for i, loop := range windowLoops {
|
||||
for i, loop := range loops {
|
||||
lines = append(lines, fmt.Sprintf("%d) thought/reason:%s", i+1, loop.Thought))
|
||||
lines = append(lines, fmt.Sprintf(" tool_call:%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
|
||||
lines = append(lines, fmt.Sprintf(" observation:%s", loop.Observation))
|
||||
@@ -525,51 +432,6 @@ func findExecuteThoughtBefore(history []*schema.Message, index int) string {
|
||||
return "(未记录)"
|
||||
}
|
||||
|
||||
func tailExecuteLoops(records []executeLoopRecord, limit int) []executeLoopRecord {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
if limit <= 0 || len(records) <= limit {
|
||||
result := make([]executeLoopRecord, len(records))
|
||||
copy(result, records)
|
||||
return result
|
||||
}
|
||||
result := make([]executeLoopRecord, limit)
|
||||
copy(result, records[len(records)-limit:])
|
||||
return result
|
||||
}
|
||||
|
||||
// compressExecuteLoopObservationsByTool 对窗口内重复工具做 observation 压缩。
|
||||
func compressExecuteLoopObservationsByTool(records []executeLoopRecord) []executeLoopRecord {
|
||||
if len(records) == 0 {
|
||||
return records
|
||||
}
|
||||
|
||||
latestIndexByTool := make(map[string]int, len(records))
|
||||
for i := len(records) - 1; i >= 0; i-- {
|
||||
key := strings.ToLower(strings.TrimSpace(records[i].ToolName))
|
||||
if key == "" {
|
||||
key = "unknown_tool"
|
||||
}
|
||||
if _, exists := latestIndexByTool[key]; !exists {
|
||||
latestIndexByTool[key] = i
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]executeLoopRecord, len(records))
|
||||
copy(result, records)
|
||||
for i := range result {
|
||||
key := strings.ToLower(strings.TrimSpace(result[i].ToolName))
|
||||
if key == "" {
|
||||
key = "unknown_tool"
|
||||
}
|
||||
if latestIndexByTool[key] != i {
|
||||
result[i].Observation = executeTrimmedObservationText
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func renderExecuteToolCallText(toolName, toolArgs string) string {
|
||||
toolName = strings.TrimSpace(toolName)
|
||||
if toolName == "" {
|
||||
@@ -582,38 +444,6 @@ func renderExecuteToolCallText(toolName, toolArgs string) string {
|
||||
return toolName + "(" + toolArgs + ")"
|
||||
}
|
||||
|
||||
func buildEarlyExecuteReactSummary(records []executeLoopRecord, windowLimit int) string {
|
||||
if len(records) == 0 {
|
||||
return "暂无。"
|
||||
}
|
||||
if len(records) <= windowLimit {
|
||||
return "无(当前窗口已覆盖全部 ReAct 记录)。"
|
||||
}
|
||||
|
||||
early := records[:len(records)-windowLimit]
|
||||
toolCounts := make(map[string]int, len(early))
|
||||
for _, record := range early {
|
||||
key := strings.TrimSpace(record.ToolName)
|
||||
if key == "" {
|
||||
key = "unknown_tool"
|
||||
}
|
||||
toolCounts[key]++
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(toolCounts))
|
||||
for name := range toolCounts {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
parts := make([]string, 0, len(names))
|
||||
for _, name := range names {
|
||||
parts = append(parts, fmt.Sprintf("%s×%d", name, toolCounts[name]))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("已折叠 %d 条旧记录,涉及:%s。", len(early), strings.Join(parts, "、"))
|
||||
}
|
||||
|
||||
func hasExecuteRoughBuildDone(ctx *newagentmodel.ConversationContext) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
@@ -725,3 +555,12 @@ func renderExecuteTaskClassIDs(state *newagentmodel.CommonState) string {
|
||||
}
|
||||
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ","))
|
||||
}
|
||||
|
||||
// renderExecuteMemoryContext 提取 execute 阶段要注入 msg3 的记忆文本。
|
||||
//
|
||||
// 1. 只读取统一的 memory_context,避免把其他 pinned block 误塞进 prompt。
|
||||
// 2. 为空时直接返回空串,保持 msg3 干净。
|
||||
// 3. 复用统一记忆渲染逻辑,保证各阶段记忆入口一致。
|
||||
func renderExecuteMemoryContext(ctx *newagentmodel.ConversationContext) string {
|
||||
return renderUnifiedMemoryContext(ctx)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
const executeMemoryContextKey = "memory_context"
|
||||
|
||||
// renderExecuteMemoryContext 提取 Execute 阶段需要补充到 msg3 的记忆文本。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 只白名单消费 memory_context,避免把 execution_context / current_step 等 Execute 自有块再次注入;
|
||||
// 2. 若 block 不存在或正文为空,直接返回空串,不给 msg3 留空段;
|
||||
// 3. 这里不重新渲染记忆,只消费 agentsvc 已经产出的最终文本,保证所有阶段口径一致。
|
||||
func renderExecuteMemoryContext(ctx *newagentmodel.ConversationContext) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
block, ok := ctx.PinnedBlockByKey(executeMemoryContextKey)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
content := strings.TrimSpace(block.Content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package newagentprompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
@@ -49,14 +48,19 @@ func BuildPlanSystemPrompt() string {
|
||||
// BuildPlanMessages 组装规划阶段的 messages。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 state + context 收敛成规划阶段模型输入;
|
||||
// 2. 负责把置顶上下文和工具摘要放在 history 前面,降低模型跑偏概率;
|
||||
// 3. 不负责解析模型输出,也不负责判断规划质量。
|
||||
// 1. 负责把 state + context 收敛成统一 4 段式规划阶段模型输入;
|
||||
// 2. 不负责解析模型输出,也不负责判断规划质量;
|
||||
// 3. msg3 中的状态文本由本函数显式传入,确保统一骨架下仍能看到完整计划与阶段信息。
|
||||
func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
|
||||
return buildStageMessages(
|
||||
BuildPlanSystemPrompt(),
|
||||
return buildUnifiedStageMessages(
|
||||
ctx,
|
||||
BuildPlanUserPrompt(state, userInput),
|
||||
StageMessagesConfig{
|
||||
SystemPrompt: BuildPlanSystemPrompt(),
|
||||
Msg1Content: buildPlanConversationMessage(ctx),
|
||||
Msg2Content: buildPlanWorkspace(state),
|
||||
Msg3Suffix: BuildPlanUserPrompt(state, userInput),
|
||||
Msg3Role: schema.User,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,21 +68,9 @@ func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.Conv
|
||||
func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("请继续当前任务的规划阶段。\n")
|
||||
sb.WriteString(renderStateSummary(state))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("本轮目标:围绕当前任务继续规划,直到形成一份稳定、可执行的自然语言 plan,或在信息不足时明确追问用户。\n\n")
|
||||
sb.WriteString("请继续当前任务的规划阶段,严格输出 JSON。\n")
|
||||
sb.WriteString("目标:围绕最近对话和规划工作区信息,产出一份稳定、可执行的自然语言计划;若关键信息不足,请明确 ask_user。\n\n")
|
||||
sb.WriteString(BuildPlanDecisionContractText())
|
||||
sb.WriteString("\n")
|
||||
|
||||
if state != nil && len(state.TaskClassIDs) > 0 {
|
||||
parts := make([]string, len(state.TaskClassIDs))
|
||||
for i, id := range state.TaskClassIDs {
|
||||
parts[i] = strconv.Itoa(id)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\n本次排课请求涉及的任务类 ID(前端传入):[%s]\n", strings.Join(parts, ", ")))
|
||||
sb.WriteString("规划时请结合上述任务类 ID 判断是否需要粗排(needs_rough_build),并在 plan_steps 中体现排课意图。\n")
|
||||
}
|
||||
|
||||
trimmedInput := strings.TrimSpace(userInput)
|
||||
if trimmedInput != "" {
|
||||
|
||||
133
backend/newAgent/prompt/plan_context.go
Normal file
133
backend/newAgent/prompt/plan_context.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
// buildPlanConversationMessage 生成 plan 节点看到的真实对话视图。
|
||||
func buildPlanConversationMessage(ctx *newagentmodel.ConversationContext) string {
|
||||
return buildConversationHistoryMessage(ctx, "规划参考对话")
|
||||
}
|
||||
|
||||
// buildPlanWorkspace 渲染 plan 节点自己的工作区。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. 这里只保留“规划真正需要知道的东西”:已有计划、当前步骤、task_class_ids、任务类约束;
|
||||
// 2. 不再复用通用胖状态摘要,避免把 execute / deliver 无关状态一起塞给 plan;
|
||||
// 3. 若当前没有正式计划,则明确告诉模型“从零开始规划”,避免继续误沿用旧上下文。
|
||||
func buildPlanWorkspace(state *newagentmodel.CommonState) string {
|
||||
lines := []string{"规划工作区:"}
|
||||
if state == nil {
|
||||
lines = append(lines, "- 当前缺少流程状态,请主要依据最近对话与本轮输入继续规划。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
if !state.HasPlan() {
|
||||
lines = append(lines, "- 当前还没有正式计划。")
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("- 已有计划:共 %d 步。", len(state.PlanSteps)))
|
||||
lines = append(lines, renderPlanCurrentStepSummary(state))
|
||||
lines = append(lines, "计划简表:")
|
||||
lines = append(lines, renderPlanStepOutline(state.PlanSteps))
|
||||
}
|
||||
|
||||
if taskClassIDs := renderPlanTaskClassIDs(state); taskClassIDs != "" {
|
||||
lines = append(lines, "- "+taskClassIDs)
|
||||
}
|
||||
if taskClassMeta := renderPlanTaskClassMeta(state); taskClassMeta != "" {
|
||||
lines = append(lines, "任务类约束:")
|
||||
lines = append(lines, taskClassMeta)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// renderPlanCurrentStepSummary 返回 plan 节点需要知道的当前步骤进度。
|
||||
func renderPlanCurrentStepSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil || !state.HasPlan() {
|
||||
return "- 当前步骤:暂无。"
|
||||
}
|
||||
|
||||
current, total := state.PlanProgress()
|
||||
step, ok := state.CurrentPlanStep()
|
||||
if !ok {
|
||||
return fmt.Sprintf("- 当前步骤:计划共 %d 步,当前没有可继续沿用的有效步骤。", total)
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(step.Content)
|
||||
if content == "" {
|
||||
content = "(当前步骤正文为空)"
|
||||
}
|
||||
|
||||
summary := fmt.Sprintf("- 当前步骤:第 %d/%d 步,%s", current, total, content)
|
||||
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
|
||||
summary += fmt.Sprintf(";完成判定:%s", doneWhen)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// renderPlanStepOutline 将完整计划压成 plan 节点可读的简表。
|
||||
func renderPlanStepOutline(steps []newagentmodel.PlanStep) string {
|
||||
if len(steps) == 0 {
|
||||
return "- 暂无。"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(steps))
|
||||
for i, step := range steps {
|
||||
content := strings.TrimSpace(step.Content)
|
||||
if content == "" {
|
||||
content = "(步骤正文为空)"
|
||||
}
|
||||
line := fmt.Sprintf("%d. %s", i+1, content)
|
||||
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
|
||||
line += fmt.Sprintf(" | 完成判定:%s", doneWhen)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// renderPlanTaskClassIDs 返回批量排课场景下的 task_class_ids 简表。
|
||||
func renderPlanTaskClassIDs(state *newagentmodel.CommonState) string {
|
||||
if state == nil || len(state.TaskClassIDs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := make([]string, len(state.TaskClassIDs))
|
||||
for i, id := range state.TaskClassIDs {
|
||||
parts[i] = strconv.Itoa(id)
|
||||
}
|
||||
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
// renderPlanTaskClassMeta 返回 plan 节点真正需要看的任务类边界。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 这里只保留名称、策略、总时段、日期范围这类规划相关信息;
|
||||
// 2. 不再把所有字段原样平铺,避免工作区过胖;
|
||||
// 3. 若某项字段为空,则直接省略,不制造噪声。
|
||||
func renderPlanTaskClassMeta(state *newagentmodel.CommonState) string {
|
||||
if state == nil || len(state.TaskClasses) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(state.TaskClasses))
|
||||
for _, tc := range state.TaskClasses {
|
||||
line := fmt.Sprintf("- [ID=%d] %s", tc.ID, strings.TrimSpace(tc.Name))
|
||||
if strategy := strings.TrimSpace(tc.Strategy); strategy != "" {
|
||||
line += fmt.Sprintf(";策略:%s", strategy)
|
||||
}
|
||||
if tc.TotalSlots > 0 {
|
||||
line += fmt.Sprintf(";总时段预算:%d", tc.TotalSlots)
|
||||
}
|
||||
if tc.StartDate != "" || tc.EndDate != "" {
|
||||
line += fmt.Sprintf(";日期范围:%s ~ %s", tc.StartDate, tc.EndDate)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
212
backend/newAgent/prompt/unified_context.go
Normal file
212
backend/newAgent/prompt/unified_context.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// ConversationTurn 表示对话历史中的一轮自然语言交互。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只承载 user 与 assistant speak,不承载 tool_call 和 tool observation;
|
||||
// 2. 供 chat / plan / deliver 等节点复用,避免各节点重复写一套提取逻辑;
|
||||
// 3. 不负责裁剪长度,长度预算统一交给压缩层处理。
|
||||
type ConversationTurn struct {
|
||||
Role string
|
||||
Content string
|
||||
}
|
||||
|
||||
// StageMessagesConfig 描述统一四段式骨架下,各节点自行提供的内容块。
|
||||
//
|
||||
// 设计目标:
|
||||
// 1. 统一层只负责“四条消息怎么拼”,不再替节点决定“每条消息里该放什么”;
|
||||
// 2. Msg1 / Msg2 / Msg3Prefix / Msg3Suffix 都由节点自己渲染,避免 chat / plan / deliver 继续套 execute 的内容模板;
|
||||
// 3. memory_context 仍由统一层单入口注入到 msg3,避免多处重复注入。
|
||||
type StageMessagesConfig struct {
|
||||
// SystemPrompt 是节点自己的系统提示词。
|
||||
SystemPrompt string
|
||||
|
||||
// Msg1Content 是第 2 条 assistant 消息,通常放“节点想看的历史视图”。
|
||||
Msg1Content string
|
||||
|
||||
// Msg2Content 是第 3 条 assistant 消息,通常放“节点自己的工作区/补充约束”。
|
||||
Msg2Content string
|
||||
|
||||
// Msg3Prefix 是第 4 条消息中位于 memory_context 之前的内容。
|
||||
// 常见放法:阶段状态、规划工作区摘要、交付收口约束等。
|
||||
Msg3Prefix string
|
||||
|
||||
// Msg3Suffix 是第 4 条消息中位于 memory_context 之后的内容。
|
||||
// 对 user-role 节点来说,这里通常放最终用户指令,保证“用户输入收尾”。
|
||||
Msg3Suffix string
|
||||
|
||||
// Msg3Role 指定第 4 条消息的角色。
|
||||
// Execute 继续使用 system,其余节点一般使用 user。
|
||||
Msg3Role schema.RoleType
|
||||
}
|
||||
|
||||
// buildUnifiedStageMessages 组装统一 4 段式消息骨架。
|
||||
//
|
||||
// 固定布局:
|
||||
// 1. msg0(system):系统规则 + 阶段规则 + 工具简表;
|
||||
// 2. msg1(assistant):节点自定义的历史视图;
|
||||
// 3. msg2(assistant):节点自定义的工作区;
|
||||
// 4. msg3(user/system):节点自定义前后缀 + 统一 memory_context。
|
||||
func buildUnifiedStageMessages(
|
||||
ctx *newagentmodel.ConversationContext,
|
||||
config StageMessagesConfig,
|
||||
) []*schema.Message {
|
||||
msg0 := buildUnifiedMsg0(config.SystemPrompt, ctx)
|
||||
msg1 := buildUnifiedMsg1(config.Msg1Content)
|
||||
msg2 := buildUnifiedMsg2(config.Msg2Content)
|
||||
msg3 := buildUnifiedMsg3(ctx, config)
|
||||
|
||||
return []*schema.Message{
|
||||
schema.SystemMessage(msg0),
|
||||
{Role: schema.Assistant, Content: msg1},
|
||||
{Role: schema.Assistant, Content: msg2},
|
||||
buildUnifiedMsg3Message(msg3, config.Msg3Role),
|
||||
}
|
||||
}
|
||||
|
||||
// buildUnifiedMsg3Message 根据配置决定第 4 条消息的角色。
|
||||
func buildUnifiedMsg3Message(content string, role schema.RoleType) *schema.Message {
|
||||
if role == schema.User {
|
||||
return schema.UserMessage(content)
|
||||
}
|
||||
return schema.SystemMessage(content)
|
||||
}
|
||||
|
||||
// buildUnifiedMsg0 合并系统提示 + 工具简表,生成 msg0。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先合并基础系统提示与节点系统提示,保证模型身份稳定;
|
||||
// 2. 若当前节点注入了工具 schema,则附加紧凑工具目录;
|
||||
// 3. 若两部分都为空,则回退到最小兜底提示,避免出现空消息。
|
||||
func buildUnifiedMsg0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
|
||||
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
|
||||
if base == "" {
|
||||
base = "你是 SmartMate 助手,请继续当前阶段。"
|
||||
}
|
||||
|
||||
toolCatalog := renderExecuteToolCatalogCompact(ctx)
|
||||
if toolCatalog == "" {
|
||||
return base
|
||||
}
|
||||
return base + "\n\n" + toolCatalog
|
||||
}
|
||||
|
||||
// buildUnifiedMsg1 返回节点自行提供的历史视图。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 统一层不再内置 execute 风格的 ReAct 摘要;
|
||||
// 2. 节点若未传入内容,则回退到最小占位,保证四段结构稳定;
|
||||
// 3. 压缩层仍会统一统计和压缩这条消息。
|
||||
func buildUnifiedMsg1(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
if content != "" {
|
||||
return content
|
||||
}
|
||||
return "历史上下文:暂无。"
|
||||
}
|
||||
|
||||
// buildUnifiedMsg2 返回节点自行提供的工作区。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 非 execute 节点也允许有自己的 msg2,不再被统一层硬塞“暂无”语义;
|
||||
// 2. 若节点暂时没有额外工作区,则回退到最小占位,保证结构稳定。
|
||||
func buildUnifiedMsg2(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
if content != "" {
|
||||
return content
|
||||
}
|
||||
return "阶段工作区:暂无。"
|
||||
}
|
||||
|
||||
// buildUnifiedMsg3 统一拼装 msg3:前缀 + memory_context + 后缀。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 前缀由节点决定,适合放轻量状态或阶段约束;
|
||||
// 2. memory_context 只在这里注入一次,避免 pinned block 多入口重复出现;
|
||||
// 3. 后缀由节点决定。对于 user-role 节点,通常把最终用户指令放在这里,保证消息末尾仍是用户输入。
|
||||
func buildUnifiedMsg3(ctx *newagentmodel.ConversationContext, config StageMessagesConfig) string {
|
||||
var sections []string
|
||||
|
||||
if prefix := strings.TrimSpace(config.Msg3Prefix); prefix != "" {
|
||||
sections = append(sections, prefix)
|
||||
}
|
||||
if memoryText := renderUnifiedMemoryContext(ctx); memoryText != "" {
|
||||
sections = append(sections, "相关记忆(仅在确有帮助时参考,不要机械复述):\n"+memoryText)
|
||||
}
|
||||
if suffix := strings.TrimSpace(config.Msg3Suffix); suffix != "" {
|
||||
sections = append(sections, suffix)
|
||||
}
|
||||
|
||||
if len(sections) == 0 {
|
||||
return "请继续当前阶段。"
|
||||
}
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
// renderUnifiedMemoryContext 提取需要补充到 msg3 的记忆文本。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 只消费 memory_context,避免把 execution_context / current_step 等阶段专属块混回 prompt;
|
||||
// 2. block 不存在或正文为空时直接返回空串;
|
||||
// 3. 这里只读取 agentsvc 已经产出的最终文本,不在这里重新拼装记忆。
|
||||
func renderUnifiedMemoryContext(ctx *newagentmodel.ConversationContext) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
block, ok := ctx.PinnedBlockByKey("memory_context")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
content := strings.TrimSpace(block.Content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// CollectConversationTurns 从历史消息中提取 user + assistant speak 对话流。
|
||||
//
|
||||
// 提取规则:
|
||||
// 1. 只保留 user 消息(排除 correction prompt)和 assistant 纯文本消息;
|
||||
// 2. assistant tool_call 消息与 tool observation 消息不纳入“真实对话”;
|
||||
// 3. 返回顺序保持与原始 history 一致。
|
||||
func CollectConversationTurns(history []*schema.Message) []ConversationTurn {
|
||||
if len(history) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
turns := make([]ConversationTurn, 0, len(history))
|
||||
for _, msg := range history {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(msg.Content)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
switch msg.Role {
|
||||
case schema.User:
|
||||
// 1. 跳过后端注入的 correction prompt,避免把纠错文案误判为用户真实意图。
|
||||
if isExecuteCorrectionPrompt(msg) {
|
||||
continue
|
||||
}
|
||||
turns = append(turns, ConversationTurn{Role: "user", Content: text})
|
||||
case schema.Assistant:
|
||||
// 2. 跳过工具调用消息,只保留真正面向用户的 speak/答复。
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
continue
|
||||
}
|
||||
turns = append(turns, ConversationTurn{Role: "assistant", Content: text})
|
||||
}
|
||||
}
|
||||
|
||||
return turns
|
||||
}
|
||||
Reference in New Issue
Block a user