Version: 0.9.11.dev.260409

后端:
1. conv 并行迁移与切流接线(旧目录下沉到 newAgent/conv)
   - 新建 newAgent/conv/schedule_provider.go、schedule_state.go、schedule_preview.go、schedule_persist.go,保持原有排程转换/预览/持久化能力;
   - 删除旧目录 conv/schedule_provider.go、schedule_state.go、schedule_preview.go、schedule_persist.go;
   - 更新 cmd/start.go 与 service/agentsvc/agent_newagent.go,ScheduleProvider/SchedulePersistor 与 preview 转换统一切到 newAgent/conv;
   - 删除旧 conv/schedule_state_test.go(迁移期测试文件清理)。
2. execute 循环上下文收口增强(历史归档 + 当前轮清晰化)
   - 更新 node/chat.go:仅在 completed 收口时写 execute_loop_closed marker,供后续 prompt 分层归档;
   - 更新 prompt/execute_context.go:msg1/msg2 升级为 V3,按收口标记拆分“历史归档 loop / 当前活跃 loop”,并增加 msg1 长度预算裁剪;
   - 更新 node/execute.go:新增 execute 置顶上下文同步(execution_context/current_step),在轮次开始与 next_plan 后即时刷新;
   - 更新 prompt/execute.go + execute_context.go:补齐“当前计划步骤 + done_when”强约束,禁止未达成判定时提前 next_plan。
3. 图路由与执行策略微调
   - 更新 graph/common_graph.go:Plan/Confirm 分支允许直接进入 Deliver 收口;
   - 更新 node/plan.go:always_execute 链路下补发计划摘要并写入历史,保证自动执行与手动确认文案一致;
   - 更新 model/common_state.go:DefaultMaxRounds 从 30 提升到 60。
4. 复合工具规划器重构(去重实现,复用 logic 公共能力)
   - 更新 tools/compound_tools.go:min_context_switch / spread_even 改为调用 backend/logic 规划器(PlanMinContextSwitchMoves / PlanEvenSpreadMoves);
   - 新增 state_id↔logic_id 映射层,统一入参与回填,避免工具层与规划层 ID 语义耦合;
   - 删除 compound_tools 内部重复的规划/归一化/分组/打分实现,减少第三份复制逻辑。
5. 同步调试与文档
   - 更新 newAgent/Log.txt 调试日志;
   - 新增 memory/记忆模块实施计划.md(面试优先版到产品可用版的落地路线)。
前端:无
仓库:无
This commit is contained in:
Losita
2026-04-09 22:20:30 +08:00
parent 821c2cde5d
commit 574d44c332
17 changed files with 9470 additions and 4539 deletions

View File

@@ -14,6 +14,7 @@ import (
const (
executeHistoryKindKey = "newagent_history_kind"
executeHistoryKindCorrectionUser = "llm_correction_prompt"
executeHistoryKindLoopClosed = "execute_loop_closed"
// executeLoopWindowLimit 控制“当轮 ReAct Loop 窗口”最多保留多少条记录。
// 采用固定窗口能避免上下文无上限增长,且可保持“最近行为”可追踪。
@@ -36,6 +37,8 @@ type executeLoopRecord struct {
Observation string
}
const executeMessage1MaxRunes = 1400
// buildExecuteStageMessages 组装 execute 阶段 4 条消息骨架。
//
// 消息结构(固定):
@@ -50,8 +53,8 @@ func buildExecuteStageMessages(
runtimeUserPrompt string,
) []*schema.Message {
msg0 := buildExecuteMessage0(stageSystemPrompt, ctx)
msg1 := buildExecuteMessage1(ctx)
msg2 := buildExecuteMessage2(ctx)
msg1 := buildExecuteMessage1V3(ctx)
msg2 := buildExecuteMessage2V3(ctx)
msg3 := buildExecuteMessage3(state, ctx, runtimeUserPrompt)
return []*schema.Message{
@@ -140,6 +143,209 @@ func buildExecuteMessage2(ctx *newagentmodel.ConversationContext) string {
}
// buildExecuteMessage3 生成当前执行状态与执行锚点。
// buildExecuteMessage1V2 生成历史摘要:
// 1. 已收口的 loop 归档到 msg1
// 2. 当前活跃 loop 只保留“早期摘要”;
// 3. 最终对 msg1 做统一长度裁剪,控制 token 开销。
func buildExecuteMessage1V2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 历史归档 ReAct 摘要:暂无。",
"- 当前循环早期摘要:暂无。",
)
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history)
lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, 0))
lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit))
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
// buildExecuteMessage2V2 仅展示“当前活跃 loop”的窗口记录。
func buildExecuteMessage2V2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
_, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot())
if len(activeLoops) == 0 {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(activeLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
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")
}
// 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 := findLatestExecuteLoopClosedMarker(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 findLatestExecuteLoopClosedMarker(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
}
if strings.TrimSpace(kind) == executeHistoryKindLoopClosed {
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并统一做长度裁剪。
func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 历史归档 ReAct 摘要:暂无。",
"- 历史归档 ReAct 窗口:暂无。",
"- 当前循环早期摘要:暂无。",
)
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
// 1. 通过收口标记拆分“归档 loop / 当前活跃 loop”。
// 2. 归档 loop 的窗口条目直接并入 msg1满足“上一轮 msg2 挪入 msg1”。
// 3. 当前活跃 loop 在 msg1 只保留早期摘要,详细窗口交给 msg2。
archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history)
lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, executeLoopWindowLimit))
lines = append(lines, renderArchivedExecuteLoopWindowForMessage1V3(archivedLoops))
lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit))
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
// buildExecuteMessage2V3 仅承载“当前活跃 loop”的窗口。
// 若是新一轮刚开始(活跃 loop 为空),明确返回“已清空”状态。
func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
_, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot())
if len(activeLoops) == 0 {
lines = append(lines, "- 已清空(新一轮 loop 准备中)。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(activeLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
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 {
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 buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) string {
lines := []string{"当前执行状态:"}
@@ -169,6 +375,32 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
if initialGoal != "" && initialGoal != currentGoal {
lines = append(lines, "- 首轮目标来源:"+initialGoal)
}
// 1. 有 plan 时,把当前步骤与完成判定强制写入 msg3。
// 2. 该锚点用于约束模型“只推进当前步骤”,避免退化成泛化 ReAct。
// 3. 当前步骤不可读时给出兜底指引,避免引用旧步骤。
if state != nil && state.HasPlan() {
current, total := state.PlanProgress()
lines = append(lines, "计划步骤锚点(强约束):")
if step, ok := state.CurrentPlanStep(); ok {
stepContent := strings.TrimSpace(step.Content)
if stepContent == "" {
stepContent = "(当前步骤内容为空)"
}
doneWhen := strings.TrimSpace(step.DoneWhen)
if doneWhen == "" {
doneWhen = "(未提供 done_when需基于步骤目标给出可验证完成证据"
}
lines = append(lines, fmt.Sprintf("- 当前步骤:第 %d/%d 步", current, total))
lines = append(lines, "- 当前步骤内容:"+stepContent)
lines = append(lines, "- 当前步骤完成判定(done_when)"+doneWhen)
lines = append(lines, "- 动作纪律1未满足 done_when 时,只能 continue / confirm / ask_user禁止 next_plan")
lines = append(lines, "- 动作纪律2满足 done_when 时,优先 next_plan并在 goal_check 对照 done_when 给证据")
lines = append(lines, "- 动作纪律3禁止跳到后续步骤执行")
} else {
lines = append(lines, "- 当前计划步骤不可读;请先判断是否已完成全部计划")
lines = append(lines, "- 若已完成全部计划,输出 done 并给出 goal_check 证据")
}
}
if taskClassText := renderExecuteTaskClassIDs(state); taskClassText != "" {
lines = append(lines, "- 目标任务类:"+taskClassText)
}