Version: 0.9.3.dev.260407
后端:
1.Execute 上下文修复(无限循环 / 重复确认根治)
- 更新node/execute.go:speak 写入历史(修复旧 TODO);confirm 动作 speak 不再丢失;
continue 无工具调用时写 reason 保证上下文推进;区分 tool_call 数组/JSON损坏两种
correction hint;goal_check hint 区分 plan/ReAct 模式
- 更新node/execute.go:新增 AlwaysExecute 字段,extra.always_execute=true 时写工具
跳过确认闸门直接执行并持久化
- 更新model/graph_run_state.go:AgentGraphRequest 新增 AlwaysExecute;新增
WriteSchedulePreviewFunc 类型和 WriteSchedulePreview Dep
- 更新service/agentsvc/agent.go:新增 readAgentExtraBool 辅助
2.粗排全链路修复
- 更新service/agentsvc/agent_newagent.go:makeRoughBuildFunc 改用 HybridScheduleEntry
而非 TaskClassItem.EmbeddedTime,普通时段放置不再被丢弃
- 更新conv/schedule_provider.go:LoadScheduleState 从 task class 日期范围推算多周
规划窗口,不再硬编码当前周 7 天;DayMapping 覆盖全部相关周,粗排跨周结果不再
被 WeekDayToDay 静默丢弃
- 更新node/rough_build.go:pinned block 区分有/无未覆盖 pending 任务两种情况,
有 pending 时明确操作顺序(find_free→place)和完成判定,防止 LLM 重复调
list_tasks;新增 countPendingTasks 辅助(只统计 Slots 为空的真正未覆盖任务)
- 更新model/common_state.go:新增 StartDirectExecute(),Chat 直接路由 execute 时
清空旧 PlanSteps,修复跨会话 HasPlan() 误判导致 ReAct 走 plan 模式的 bug
- 更新node/chat.go:handleRouteExecute 改用 StartDirectExecute()
3.排程预览缓存迁移至 Deliver 节点
- 更新node/agent_nodes.go:Deliver 节点完成后调用 WriteSchedulePreview,只有任务
真正完成才写预览缓存,中断路径不写中间态
- 更新service/agentsvc/agent_newagent.go:注入 makeWriteSchedulePreviewFunc;移除
graph 结束后的内联写入;makeRoughBuildFunc 注释修正
- 更新conv/schedule_preview.go:ScheduleStateToPreview 补设 GeneratedAt
- 更新model/agent.go:GetSchedulePlanPreviewResponse 新增 HybridEntries 字段
- 更新service/agentsvc/agent_schedule_preview.go:GET handler Redis/MySQL 两条路径
均透传 HybridEntries
4.Execute thinking 模式修复
- 更新newAgent/llm/ark_adapter.go:thinking 开启时强制 temperature=1,MaxTokens 自
动托底至 16000,调用方与适配层行为对齐
- 更新node/execute.go:调用参数同步改为 temperature=1.0 / MaxTokens=16000
undo:
1.流式推送换行未修复(undo)
2.上下文依然待审视
前端:无
仓库:无
This commit is contained in:
@@ -2,6 +2,7 @@ package conv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/LoveLosita/smartflow/backend/model"
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||||
@@ -105,5 +106,6 @@ func ScheduleStateToPreview(
|
|||||||
Summary: summary,
|
Summary: summary,
|
||||||
HybridEntries: entries,
|
HybridEntries: entries,
|
||||||
TaskClassIDs: taskClassIDs,
|
TaskClassIDs: taskClassIDs,
|
||||||
|
GeneratedAt: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package conv
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/LoveLosita/smartflow/backend/dao"
|
"github.com/LoveLosita/smartflow/backend/dao"
|
||||||
@@ -31,39 +32,106 @@ func NewScheduleProvider(scheduleDAO *dao.ScheduleDAO, taskClassDAO *dao.TaskCla
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadScheduleState 实现 model.ScheduleStateProvider 接口。
|
// LoadScheduleState 实现 model.ScheduleStateProvider 接口。
|
||||||
// 加载用户当前周的日程和所有待安排任务,构建 ScheduleState。
|
//
|
||||||
|
// 窗口策略:
|
||||||
|
// 1. 优先从 task class 的 StartDate/EndDate 推算规划窗口,覆盖粗排所需的完整日期范围;
|
||||||
|
// 2. task class 无日期信息时,降级到当前周 7 天(兼容普通查询场景)。
|
||||||
|
//
|
||||||
|
// 日程加载策略:对窗口内每周分别调用 GetUserWeeklySchedule 并合并结果。
|
||||||
func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) {
|
func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) {
|
||||||
// 1. 确定当前周。
|
// 1. 加载用户所有任务类(含 Items 预加载)。
|
||||||
now := time.Now()
|
|
||||||
week, _, err := RealDateToRelativeDate(now.Format(DateFormat))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("解析当前日期失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 加载当前周的所有日程(含 Event + EmbeddedTask 预加载)。
|
|
||||||
schedules, err := p.scheduleDAO.GetUserWeeklySchedule(ctx, userID, week)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("加载用户周日程失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 加载用户所有任务类(含 Items 预加载)。
|
|
||||||
// 两步:先拿 ID 列表,再批量获取完整数据(含 Items)。
|
|
||||||
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID)
|
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 构建 WindowDay 列表(当前周 7 天)。
|
// 2. 确定规划窗口:优先使用 task class 日期范围,降级到当前周。
|
||||||
windowDays := make([]WindowDay, 7)
|
windowDays, weeks := buildWindowFromTaskClasses(taskClasses)
|
||||||
for i := 0; i < 7; i++ {
|
if len(windowDays) == 0 {
|
||||||
windowDays[i] = WindowDay{Week: week, DayOfWeek: i + 1}
|
now := time.Now()
|
||||||
|
currentWeek, _, err := RealDateToRelativeDate(now.Format(DateFormat))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析当前日期失败: %w", err)
|
||||||
|
}
|
||||||
|
windowDays = make([]WindowDay, 7)
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
windowDays[i] = WindowDay{Week: currentWeek, DayOfWeek: i + 1}
|
||||||
|
}
|
||||||
|
weeks = []int{currentWeek}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 构建额外 item category 映射(已加载全部 taskClass,通常为空)。
|
// 3. 按周加载日程(含 Event + EmbeddedTask 预加载)。
|
||||||
extraItemCategories := buildExtraItemCategories(schedules, taskClasses)
|
var allSchedules []model.Schedule
|
||||||
|
for _, w := range weeks {
|
||||||
|
weekSchedules, err := p.scheduleDAO.GetUserWeeklySchedule(ctx, userID, w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("加载用户周日程失败 week=%d: %w", w, err)
|
||||||
|
}
|
||||||
|
allSchedules = append(allSchedules, weekSchedules...)
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 调用已有的 LoadScheduleState 构建内存状态。
|
// 4. 构建额外 item category 映射。
|
||||||
return LoadScheduleState(schedules, taskClasses, extraItemCategories, windowDays), nil
|
extraItemCategories := buildExtraItemCategories(allSchedules, taskClasses)
|
||||||
|
|
||||||
|
// 5. 调用已有的 LoadScheduleState 构建内存状态。
|
||||||
|
return LoadScheduleState(allSchedules, taskClasses, extraItemCategories, windowDays), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWindowFromTaskClasses 从 task class 的 StartDate/EndDate 推算规划窗口。
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - windowDays:窗口内每天的 (week, dayOfWeek) 有序列表;
|
||||||
|
// - weeks:窗口覆盖的周号(去重、升序),供按周加载日程使用;
|
||||||
|
// - 若无有效日期信息,返回空切片,调用方应降级到默认窗口。
|
||||||
|
func buildWindowFromTaskClasses(taskClasses []model.TaskClass) (windowDays []WindowDay, weeks []int) {
|
||||||
|
var minDate, maxDate *time.Time
|
||||||
|
for _, tc := range taskClasses {
|
||||||
|
if tc.StartDate != nil && (minDate == nil || tc.StartDate.Before(*minDate)) {
|
||||||
|
t := *tc.StartDate
|
||||||
|
minDate = &t
|
||||||
|
}
|
||||||
|
if tc.EndDate != nil && (maxDate == nil || tc.EndDate.After(*maxDate)) {
|
||||||
|
t := *tc.EndDate
|
||||||
|
maxDate = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if minDate == nil || maxDate == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
startWeek, startDay, err := RealDateToRelativeDate(minDate.Format(DateFormat))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
endWeek, endDay, err := RealDateToRelativeDate(maxDate.Format(DateFormat))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
weeksSet := make(map[int]bool)
|
||||||
|
w, d := startWeek, startDay
|
||||||
|
for {
|
||||||
|
windowDays = append(windowDays, WindowDay{Week: w, DayOfWeek: d})
|
||||||
|
weeksSet[w] = true
|
||||||
|
if w == endWeek && d == endDay {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d++
|
||||||
|
if d > 7 {
|
||||||
|
d = 1
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
if w > endWeek+1 { // 防止因日期转换异常导致无限循环
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
weeks = make([]int, 0, len(weeksSet))
|
||||||
|
for wk := range weeksSet {
|
||||||
|
weeks = append(weeks, wk)
|
||||||
|
}
|
||||||
|
sort.Ints(weeks)
|
||||||
|
return windowDays, weeks
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCompleteTaskClasses 批量加载用户所有任务类(含 Items 预加载)。
|
// loadCompleteTaskClasses 批量加载用户所有任务类(含 Items 预加载)。
|
||||||
|
|||||||
@@ -249,11 +249,12 @@ type SchedulePlanPreviewCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetSchedulePlanPreviewResponse struct {
|
type GetSchedulePlanPreviewResponse struct {
|
||||||
ConversationID string `json:"conversation_id"`
|
ConversationID string `json:"conversation_id"`
|
||||||
TraceID string `json:"trace_id,omitempty"`
|
TraceID string `json:"trace_id,omitempty"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
CandidatePlans []UserWeekSchedule `json:"candidate_plans"`
|
CandidatePlans []UserWeekSchedule `json:"candidate_plans"`
|
||||||
GeneratedAt time.Time `json:"generated_at"`
|
HybridEntries []HybridScheduleEntry `json:"hybrid_entries,omitempty"`
|
||||||
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSEResponse struct {
|
type SSEResponse struct {
|
||||||
|
|||||||
@@ -50,24 +50,36 @@ func WrapArkClient(arkChatModel *ark.ChatModel) *Client {
|
|||||||
|
|
||||||
// buildArkStreamOptions 将 newAgent 的 GenerateOptions 转换为 ark 的流式调用选项。
|
// buildArkStreamOptions 将 newAgent 的 GenerateOptions 转换为 ark 的流式调用选项。
|
||||||
func buildArkStreamOptions(options GenerateOptions) []einoModel.Option {
|
func buildArkStreamOptions(options GenerateOptions) []einoModel.Option {
|
||||||
|
thinkingEnabled := options.Thinking == ThinkingModeEnabled
|
||||||
|
|
||||||
// Thinking
|
// Thinking
|
||||||
thinkingType := arkModel.ThinkingTypeDisabled
|
thinkingType := arkModel.ThinkingTypeDisabled
|
||||||
if options.Thinking == ThinkingModeEnabled {
|
if thinkingEnabled {
|
||||||
thinkingType = arkModel.ThinkingTypeEnabled
|
thinkingType = arkModel.ThinkingTypeEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []einoModel.Option{
|
opts := []einoModel.Option{
|
||||||
ark.WithThinking(&arkModel.Thinking{Type: thinkingType}),
|
ark.WithThinking(&arkModel.Thinking{Type: thinkingType}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature
|
// Temperature:thinking 模型强制要求 temperature=1,否则 API 静默忽略 thinking。
|
||||||
if options.Temperature > 0 {
|
if thinkingEnabled {
|
||||||
|
opts = append(opts, einoModel.WithTemperature(1.0))
|
||||||
|
} else if options.Temperature > 0 {
|
||||||
opts = append(opts, einoModel.WithTemperature(float32(options.Temperature)))
|
opts = append(opts, einoModel.WithTemperature(float32(options.Temperature)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaxTokens
|
// MaxTokens:thinking 模式下 thinking token 占用 max_tokens 预算,
|
||||||
if options.MaxTokens > 0 {
|
// 调用方设定的值仅代表"期望输出长度",实际预算需留出思考空间。
|
||||||
opts = append(opts, einoModel.WithMaxTokens(options.MaxTokens))
|
// 最低保障 16000,避免思考链被截断导致输出为空或非 JSON。
|
||||||
|
maxTokens := options.MaxTokens
|
||||||
|
if thinkingEnabled {
|
||||||
|
const minThinkingBudget = 16000
|
||||||
|
if maxTokens < minThinkingBudget {
|
||||||
|
maxTokens = minThinkingBudget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxTokens > 0 {
|
||||||
|
opts = append(opts, einoModel.WithMaxTokens(maxTokens))
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ func (s *CommonState) ConfirmPlan() {
|
|||||||
s.Phase = PhaseExecuting
|
s.Phase = PhaseExecuting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartDirectExecute 进入无 plan 的直接执行(ReAct)模式。
|
||||||
|
// Chat 节点路由到 execute 时必须调用此方法,而非直接赋值 Phase,
|
||||||
|
// 否则上一次任务残留的 PlanSteps 会被 HasPlan() 误判为仍有计划,
|
||||||
|
// 导致 Execute 节点用旧步骤跑 plan 模式而非 ReAct 模式。
|
||||||
|
func (s *CommonState) StartDirectExecute() {
|
||||||
|
s.PlanSteps = nil
|
||||||
|
s.CurrentStep = 0
|
||||||
|
s.Phase = PhaseExecuting
|
||||||
|
}
|
||||||
|
|
||||||
// RejectPlan 表示用户拒绝当前计划,清空计划并回退到 planning。
|
// RejectPlan 表示用户拒绝当前计划,清空计划并回退到 planning。
|
||||||
func (s *CommonState) RejectPlan() {
|
func (s *CommonState) RejectPlan() {
|
||||||
s.PlanSteps = nil
|
s.PlanSteps = nil
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
type AgentGraphRequest struct {
|
type AgentGraphRequest struct {
|
||||||
UserInput string
|
UserInput string
|
||||||
ConfirmAction string // "accept" / "reject" / "",仅 confirm 恢复场景由前端传入
|
ConfirmAction string // "accept" / "reject" / "",仅 confirm 恢复场景由前端传入
|
||||||
|
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行,适合前端已展示预览、用户无需逐步确认的场景
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize 统一清洗请求级输入中的字符串字段。
|
// Normalize 统一清洗请求级输入中的字符串字段。
|
||||||
@@ -43,6 +44,10 @@ type RoughBuildPlacement struct {
|
|||||||
// 由 service 层封装 HybridScheduleWithPlanMulti 后注入,newAgent 层不直接依赖外层 model。
|
// 由 service 层封装 HybridScheduleWithPlanMulti 后注入,newAgent 层不直接依赖外层 model。
|
||||||
type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([]RoughBuildPlacement, error)
|
type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([]RoughBuildPlacement, error)
|
||||||
|
|
||||||
|
// WriteSchedulePreviewFunc 是排程预览写入的依赖注入签名。
|
||||||
|
// 由 service 层封装 cacheDAO 后注入,deliver 节点在任务完成时调用,保证只有真正完成的结果才写入缓存。
|
||||||
|
type WriteSchedulePreviewFunc func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error
|
||||||
|
|
||||||
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。
|
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。
|
||||||
//
|
//
|
||||||
// 设计目的:
|
// 设计目的:
|
||||||
@@ -50,16 +55,17 @@ type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([
|
|||||||
// 2. Chat/Plan/Execute/Deliver 允许分别挂不同 client,但也允许先复用同一个 client;
|
// 2. Chat/Plan/Execute/Deliver 允许分别挂不同 client,但也允许先复用同一个 client;
|
||||||
// 3. ChunkEmitter 统一承接阶段提示、正文、工具事件、确认请求等 SSE 输出。
|
// 3. ChunkEmitter 统一承接阶段提示、正文、工具事件、确认请求等 SSE 输出。
|
||||||
type AgentGraphDeps struct {
|
type AgentGraphDeps struct {
|
||||||
ChatClient *newagentllm.Client
|
ChatClient *newagentllm.Client
|
||||||
PlanClient *newagentllm.Client
|
PlanClient *newagentllm.Client
|
||||||
ExecuteClient *newagentllm.Client
|
ExecuteClient *newagentllm.Client
|
||||||
DeliverClient *newagentllm.Client
|
DeliverClient *newagentllm.Client
|
||||||
ChunkEmitter *newagentstream.ChunkEmitter
|
ChunkEmitter *newagentstream.ChunkEmitter
|
||||||
StateStore AgentStateStore
|
StateStore AgentStateStore
|
||||||
ToolRegistry *newagenttools.ToolRegistry
|
ToolRegistry *newagenttools.ToolRegistry
|
||||||
ScheduleProvider ScheduleStateProvider // 按 DAO 注入,Execute 节点按需加载 ScheduleState
|
ScheduleProvider ScheduleStateProvider // 按 DAO 注入,Execute 节点按需加载 ScheduleState
|
||||||
SchedulePersistor SchedulePersistor // 按 DAO 注入,用于写工具执行后持久化变更
|
SchedulePersistor SchedulePersistor // 按 DAO 注入,用于写工具执行后持久化变更
|
||||||
RoughBuildFunc RoughBuildFunc // 按 Service 注入,粗排算法入口
|
RoughBuildFunc RoughBuildFunc // 按 Service 注入,粗排算法入口
|
||||||
|
WriteSchedulePreview WriteSchedulePreviewFunc // 按 Service 注入,排程预览写入入口
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureChunkEmitter 保证 graph 运行时始终有一个可用的 chunk 发射器。
|
// EnsureChunkEmitter 保证 graph 运行时始终有一个可用的 chunk 发射器。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||||
@@ -214,6 +215,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
|||||||
ScheduleState: scheduleState,
|
ScheduleState: scheduleState,
|
||||||
SchedulePersistor: st.Deps.SchedulePersistor,
|
SchedulePersistor: st.Deps.SchedulePersistor,
|
||||||
OriginalScheduleState: st.OriginalScheduleState,
|
OriginalScheduleState: st.OriginalScheduleState,
|
||||||
|
AlwaysExecute: st.Request.AlwaysExecute,
|
||||||
},
|
},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -247,6 +249,16 @@ func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphSt
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 任务完成后写排程预览缓存:只有走到 Deliver 才代表排程结果已稳定,
|
||||||
|
// 中断(confirm/ask_user)路径不写,避免把中间态暴露给前端。
|
||||||
|
if st.Deps.WriteSchedulePreview != nil && st.ScheduleState != nil {
|
||||||
|
flowState := st.EnsureFlowState()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveAgentState(ctx, st)
|
saveAgentState(ctx, st)
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ func handleRouteExecute(
|
|||||||
// 推送轻量状态通知,让前端知道请求已接收。
|
// 推送轻量状态通知,让前端知道请求已接收。
|
||||||
_ = emitter.EmitStatus(chatStatusBlockID, chatStageName, "accepted", speak, false)
|
_ = emitter.EmitStatus(chatStatusBlockID, chatStageName, "accepted", speak, false)
|
||||||
|
|
||||||
flowState.Phase = newagentmodel.PhaseExecuting
|
// 清空旧 PlanSteps 并设 PhaseExecuting,避免上一次任务残留的步骤被 HasPlan() 误判。
|
||||||
|
flowState.StartDirectExecute()
|
||||||
|
|
||||||
// 安全兜底:只有真正持有 task_class_ids 时才开粗排。
|
// 安全兜底:只有真正持有 task_class_ids 时才开粗排。
|
||||||
if decision.NeedsRoughBuild && len(flowState.TaskClassIDs) > 0 {
|
if decision.NeedsRoughBuild && len(flowState.TaskClassIDs) > 0 {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func generateDeliverSummary(
|
|||||||
return buildMechanicalSummary(flowState)
|
return buildMechanicalSummary(flowState)
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(result.Text)
|
return normalizeSpeak(result.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildMechanicalSummary 在 LLM 不可用时,机械拼接一份最小可用总结。
|
// buildMechanicalSummary 在 LLM 不可用时,机械拼接一份最小可用总结。
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ type ExecuteNodeInput struct {
|
|||||||
ScheduleState *newagenttools.ScheduleState
|
ScheduleState *newagenttools.ScheduleState
|
||||||
SchedulePersistor newagentmodel.SchedulePersistor
|
SchedulePersistor newagentmodel.SchedulePersistor
|
||||||
OriginalScheduleState *newagenttools.ScheduleState
|
OriginalScheduleState *newagenttools.ScheduleState
|
||||||
|
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteRoundObservation 记录执行阶段每轮的关键观察。
|
// ExecuteRoundObservation 记录执行阶段每轮的关键观察。
|
||||||
@@ -141,9 +143,9 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
input.Client,
|
input.Client,
|
||||||
messages,
|
messages,
|
||||||
newagentllm.GenerateOptions{
|
newagentllm.GenerateOptions{
|
||||||
Temperature: 0.3,
|
Temperature: 1.0, // thinking 模式强制要求 temperature=1
|
||||||
MaxTokens: 1200,
|
MaxTokens: 16000, // 需为 thinking chain 留出足够预算
|
||||||
Thinking: newagentllm.ThinkingModeDisabled,
|
Thinking: newagentllm.ThinkingModeEnabled,
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"stage": executeStageName,
|
"stage": executeStageName,
|
||||||
"step_index": flowState.CurrentStep,
|
"step_index": flowState.CurrentStep,
|
||||||
@@ -166,12 +168,18 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
return fmt.Errorf("连续 %d 次输出非 JSON,终止执行: 原始输出=%s",
|
return fmt.Errorf("连续 %d 次输出非 JSON,终止执行: 原始输出=%s",
|
||||||
flowState.ConsecutiveCorrections, rawText)
|
flowState.ConsecutiveCorrections, rawText)
|
||||||
}
|
}
|
||||||
AppendLLMCorrectionWithHint(
|
// 区分两种常见失败:
|
||||||
conversationContext,
|
// 1. tool_call 是数组(LLM 想批量调工具)→ 告知只能单次调用,保留已有上下文;
|
||||||
rawText,
|
// 2. 真正的 JSON 格式损坏 → 要求重新输出合法 JSON。
|
||||||
"你的输出不是合法 JSON,无法解析。",
|
var errorDesc, optionHint string
|
||||||
"你必须输出严格的 JSON 格式,不要使用 [NEXT_PLAN] 等纯文本标记。合法格式示例:{\"speak\":\"...\",\"action\":\"next_plan\",\"goal_check\":\"...\",\"reason\":\"...\"}",
|
if strings.Contains(rawText, `"tool_call": [`) || strings.Contains(rawText, `"tool_call":[`) {
|
||||||
)
|
errorDesc = "你在 tool_call 字段传入了数组,但每轮只能调用一个工具,不支持批量格式。"
|
||||||
|
optionHint = "请把多个工具调用拆开,每轮只调一个,拿到结果后再继续下一步。示例:{\"speak\":\"...\",\"action\":\"continue\",\"reason\":\"...\",\"tool_call\":{\"name\":\"get_task_info\",\"arguments\":{\"task_id\":1}}}"
|
||||||
|
} else {
|
||||||
|
errorDesc = "你的输出不是合法 JSON,无法解析。"
|
||||||
|
optionHint = "你必须输出严格的 JSON 格式。合法格式示例:{\"speak\":\"...\",\"action\":\"continue\",\"reason\":\"...\",\"tool_call\":{\"name\":\"工具名\",\"arguments\":{}}}"
|
||||||
|
}
|
||||||
|
AppendLLMCorrectionWithHint(conversationContext, rawText, errorDesc, optionHint)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +231,9 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
// 决策合法,重置连续修正计数。
|
// 决策合法,重置连续修正计数。
|
||||||
flowState.ConsecutiveCorrections = 0
|
flowState.ConsecutiveCorrections = 0
|
||||||
|
|
||||||
|
// speak 后处理:补列表序号换行 + 末尾加 \n 防止连续 speak 在前端粘连。
|
||||||
|
decision.Speak = normalizeSpeak(decision.Speak) // 末尾已含 \n
|
||||||
|
|
||||||
// 自省校验:next_plan / done 必须附带 goal_check,否则不推进,追加修正让 LLM 重试。
|
// 自省校验:next_plan / done 必须附带 goal_check,否则不推进,追加修正让 LLM 重试。
|
||||||
if decision.Action == newagentmodel.ExecuteActionNextPlan ||
|
if decision.Action == newagentmodel.ExecuteActionNextPlan ||
|
||||||
decision.Action == newagentmodel.ExecuteActionDone {
|
decision.Action == newagentmodel.ExecuteActionDone {
|
||||||
@@ -231,31 +242,52 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
if flowState.ConsecutiveCorrections >= maxConsecutiveCorrections {
|
if flowState.ConsecutiveCorrections >= maxConsecutiveCorrections {
|
||||||
return fmt.Errorf("连续 %d 次 goal_check 为空,终止执行", flowState.ConsecutiveCorrections)
|
return fmt.Errorf("连续 %d 次 goal_check 为空,终止执行", flowState.ConsecutiveCorrections)
|
||||||
}
|
}
|
||||||
|
// hint 区分有 plan / ReAct 两种模式:
|
||||||
|
// - 有 plan:要求对照 done_when 逐条验证;
|
||||||
|
// - ReAct:没有 done_when,只要求总结完成事实。
|
||||||
|
var goalCheckHint string
|
||||||
|
if flowState.HasPlan() {
|
||||||
|
goalCheckHint = fmt.Sprintf("输出 %s 时,必须在 goal_check 中对照 done_when 逐条说明完成依据。", decision.Action)
|
||||||
|
} else {
|
||||||
|
goalCheckHint = fmt.Sprintf("输出 %s 时,必须在 goal_check 中总结任务已完成的事实证据(调用了哪些工具、得到了什么结果)。", decision.Action)
|
||||||
|
}
|
||||||
AppendLLMCorrectionWithHint(
|
AppendLLMCorrectionWithHint(
|
||||||
conversationContext,
|
conversationContext,
|
||||||
decision.Speak,
|
decision.Speak,
|
||||||
fmt.Sprintf("你输出了 action=%s,但 goal_check 为空。", decision.Action),
|
fmt.Sprintf("你输出了 action=%s,但 goal_check 为空。", decision.Action),
|
||||||
fmt.Sprintf("输出 %s 时,必须在 goal_check 中对照 done_when 逐条说明完成依据。", decision.Action),
|
goalCheckHint,
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 若 LLM 先对用户说话,且不是 ask_user / confirm(二者交给下游节点收口),则伪流式推送。
|
// 6. speak 推流与历史写入。
|
||||||
if strings.TrimSpace(decision.Speak) != "" &&
|
//
|
||||||
decision.Action != newagentmodel.ExecuteActionAskUser &&
|
// AlwaysExecute=true 时,confirm 动作不走确认卡片,speak 和 continue 一样直接推流;
|
||||||
decision.Action != newagentmodel.ExecuteActionConfirm {
|
// AlwaysExecute=false 时,confirm 的 speak 不推流(由确认卡片展示),但仍写入历史,
|
||||||
if err := emitter.EmitPseudoAssistantText(
|
// 防止 LLM 下一轮忘记自己的计划,形成重复确认循环。
|
||||||
ctx,
|
speakText := decision.Speak // 已由 normalizeSpeak 处理,末尾含 \n
|
||||||
executeSpeakBlockID,
|
if speakText != "" {
|
||||||
executeStageName,
|
isConfirmWithCard := decision.Action == newagentmodel.ExecuteActionConfirm && !input.AlwaysExecute
|
||||||
decision.Speak,
|
isAskUser := decision.Action == newagentmodel.ExecuteActionAskUser
|
||||||
newagentstream.DefaultPseudoStreamOptions(),
|
|
||||||
); err != nil {
|
if !isConfirmWithCard && !isAskUser {
|
||||||
return fmt.Errorf("执行文案推送失败: %w", err)
|
// 推流给前端
|
||||||
|
if err := emitter.EmitPseudoAssistantText(
|
||||||
|
ctx,
|
||||||
|
executeSpeakBlockID,
|
||||||
|
executeStageName,
|
||||||
|
speakText,
|
||||||
|
newagentstream.DefaultPseudoStreamOptions(),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("执行文案推送失败: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 将 LLM 的话追加到对话历史,保证下一轮上下文连续。
|
// 始终写入历史(confirm 卡片场景下也写,保证上下文连续)
|
||||||
// TODO: 后续需要把工具调用结果也追加到历史,这里先留占位。
|
conversationContext.AppendHistory(&schema.Message{
|
||||||
|
Role: schema.Assistant,
|
||||||
|
Content: speakText,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 按 LLM 决策执行动作,后端信任 LLM 判断,不做语义校验。
|
// 7. 按 LLM 决策执行动作,后端信任 LLM 判断,不做语义校验。
|
||||||
@@ -266,7 +298,15 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
if decision.ToolCall != nil {
|
if decision.ToolCall != nil {
|
||||||
return executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter, input.ToolRegistry, input.ScheduleState)
|
return executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter, input.ToolRegistry, input.ScheduleState)
|
||||||
}
|
}
|
||||||
// 无工具调用,仅对话,继续下一轮。
|
// 无工具调用且 speak 为空(speak 非空时已在步骤 6 写入历史)。
|
||||||
|
// 若 history 本轮完全没有更新,下一轮 LLM 会收到完全相同的上下文,容易死循环。
|
||||||
|
// 把 reason 写入历史,保证上下文向前推进。
|
||||||
|
if strings.TrimSpace(decision.Speak) == "" && strings.TrimSpace(decision.Reason) != "" {
|
||||||
|
conversationContext.AppendHistory(&schema.Message{
|
||||||
|
Role: schema.Assistant,
|
||||||
|
Content: decision.Reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case newagentmodel.ExecuteActionAskUser:
|
case newagentmodel.ExecuteActionAskUser:
|
||||||
@@ -276,8 +316,20 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case newagentmodel.ExecuteActionConfirm:
|
case newagentmodel.ExecuteActionConfirm:
|
||||||
// LLM 申报了写操作意图,需要用户确认后才能真正执行。
|
// AlwaysExecute=true:跳过确认闸门,直接执行写工具并持久化,不走 confirm 节点。
|
||||||
// 步骤:1) 把 ToolCallIntent 转成快照暂存;2) 设 Phase → 下游 confirm 节点接管。
|
if input.AlwaysExecute && decision.ToolCall != nil {
|
||||||
|
if err := executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter, input.ToolRegistry, input.ScheduleState); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if input.SchedulePersistor != nil && input.OriginalScheduleState != nil {
|
||||||
|
cs := runtimeState.EnsureCommonState()
|
||||||
|
if persistErr := input.SchedulePersistor.PersistScheduleChanges(ctx, input.OriginalScheduleState, input.ScheduleState, cs.UserID); persistErr != nil {
|
||||||
|
log.Printf("[WARN] execute always-execute 持久化失败: %v", persistErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// AlwaysExecute=false(默认):暂存工具意图,设 Phase → 下游 confirm 节点接管。
|
||||||
return handleExecuteActionConfirm(decision, runtimeState, flowState)
|
return handleExecuteActionConfirm(decision, runtimeState, flowState)
|
||||||
|
|
||||||
case newagentmodel.ExecuteActionNextPlan:
|
case newagentmodel.ExecuteActionNextPlan:
|
||||||
@@ -588,6 +640,24 @@ func executePendingTool(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listItemRe 匹配被粘连在一起的列表序号(如 ")2. " "水课3. "),用于自动补换行。
|
||||||
|
// 规则:非换行字符后紧跟 2-9 的序号("2. " "3、" 等),说明 LLM 漏写了换行。
|
||||||
|
var listItemRe = regexp.MustCompile(`([^\n])([2-9][\.、]\s)`)
|
||||||
|
|
||||||
|
// normalizeSpeak 对 LLM 输出的 speak 做后处理:
|
||||||
|
// 1. 在列表序号(2. 3. …)前补 \n,防止列表项粘连;
|
||||||
|
// 2. 统一补尾部 \n,防止多轮 speak 推流时文字头尾粘连。
|
||||||
|
func normalizeSpeak(speak string) string {
|
||||||
|
speak = strings.TrimSpace(speak)
|
||||||
|
if speak == "" {
|
||||||
|
return speak
|
||||||
|
}
|
||||||
|
if !strings.Contains(speak, "\n") {
|
||||||
|
speak = listItemRe.ReplaceAllString(speak, "$1\n$2")
|
||||||
|
}
|
||||||
|
return speak + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
// truncateText 截断文本到指定长度。
|
// truncateText 截断文本到指定长度。
|
||||||
//
|
//
|
||||||
// 用于状态推送时避免超长文本影响前端展示。
|
// 用于状态推送时避免超长文本影响前端展示。
|
||||||
|
|||||||
@@ -80,17 +80,33 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 8. 把粗排完成信息写入 pinned context,让 Execute 阶段的 LLM 直接跳过"触发粗排",
|
// 8. 把粗排完成信息写入 pinned context,让 Execute 阶段的 LLM 直接进入验证和微调。
|
||||||
// 进入验证和微调,避免 LLM 误以为需要自己运行算法而浪费一轮工具调用。
|
stillPending := countPendingTasks(scheduleState)
|
||||||
st.EnsureConversationContext().UpsertPinnedBlock(newagentmodel.ContextBlock{
|
var pinnedContent string
|
||||||
Key: "rough_build_done",
|
if stillPending > 0 {
|
||||||
Title: "粗排已完成",
|
pinnedContent = fmt.Sprintf(
|
||||||
Content: fmt.Sprintf(
|
|
||||||
"后端已自动运行粗排算法,初始排课方案已写入日程状态(共 %d 个任务已预排)。\n"+
|
"后端已自动运行粗排算法,初始排课方案已写入日程状态(共 %d 个任务已预排)。\n"+
|
||||||
|
"注意:仍有 %d 个任务未被粗排覆盖,处于待安排(pending)状态,必须在微调阶段手动安排完毕。\n\n"+
|
||||||
|
"处理 pending 任务的正确操作顺序:\n"+
|
||||||
|
"1. 调用 get_overview 或 find_free 确认可用空位(不要反复调用 list_tasks,list_tasks 只能看任务列表,看不出空位)\n"+
|
||||||
|
"2. 调用 place 将 pending 任务放入空位\n"+
|
||||||
|
"3. 重复上述步骤,直到 get_overview 显示待安排任务剩余为 0\n\n"+
|
||||||
|
"微调完成的判定标准:所有 pending 任务均已 place(待安排任务剩余=0),且现有排课无明显失衡。\n"+
|
||||||
|
"无需再次触发粗排。",
|
||||||
|
len(placements), stillPending,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pinnedContent = fmt.Sprintf(
|
||||||
|
"后端已自动运行粗排算法,初始排课方案已写入日程状态(共 %d 个任务已预排,无待安排任务)。\n"+
|
||||||
"请直接调用 get_overview 查看预排结果,然后用 move/swap 微调不合理的位置。\n"+
|
"请直接调用 get_overview 查看预排结果,然后用 move/swap 微调不合理的位置。\n"+
|
||||||
"无需再次触发粗排,也不要在 plan_steps 里描述触发粗排相关的操作。",
|
"无需再次触发粗排。",
|
||||||
len(placements),
|
len(placements),
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
st.EnsureConversationContext().UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||||||
|
Key: "rough_build_done",
|
||||||
|
Title: "粗排已完成",
|
||||||
|
Content: pinnedContent,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 9. 清除标记,进入执行阶段。
|
// 9. 清除标记,进入执行阶段。
|
||||||
@@ -99,6 +115,24 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// countPendingTasks 统计粗排后仍无位置的待安排任务数。
|
||||||
|
//
|
||||||
|
// 粗排只设 Slots,不改 Status(仍为 "pending"),
|
||||||
|
// 所以"真正未覆盖"= pending 且 Slots 为空,需要手动 place。
|
||||||
|
func countPendingTasks(state *newagenttools.ScheduleState) int {
|
||||||
|
if state == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for i := range state.Tasks {
|
||||||
|
t := &state.Tasks[i]
|
||||||
|
if t.Status == "pending" && len(t.Slots) == 0 {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// applyRoughBuildPlacements 把粗排结果写入 ScheduleState 对应任务的 Slots。
|
// applyRoughBuildPlacements 把粗排结果写入 ScheduleState 对应任务的 Slots。
|
||||||
//
|
//
|
||||||
// 设计说明:
|
// 设计说明:
|
||||||
|
|||||||
@@ -10,26 +10,28 @@ import (
|
|||||||
|
|
||||||
// buildStageMessages 组装某个阶段通用的 messages。
|
// buildStageMessages 组装某个阶段通用的 messages。
|
||||||
//
|
//
|
||||||
// 步骤说明:
|
// 消息排列策略(利用 LLM 近因效应):
|
||||||
// 1. 先合并 context 自带 system prompt 与阶段 prompt,保证通用约束和阶段约束都生效;
|
// 1. system prompt(角色 + 阶段规则)— 始终最顶部,定义基本身份;
|
||||||
// 2. 再把置顶上下文块和工具摘要补成 system message,尽量顶在 history 前面;
|
// 2. tool schemas(能力边界)— 稳定参考信息,放在 history 前即可;
|
||||||
// 3. 最后追加历史消息与本轮 user prompt,保持"新约束在前、历史在后"的稳定顺序。
|
// 3. history(对话历史、工具调用、修正反馈)— 按时间顺序排列;
|
||||||
|
// 4. pinned blocks(当前计划、当前步骤、粗排结果等最新约束)— 紧贴 user prompt,
|
||||||
|
// 利用近因效应让 LLM 优先关注本轮最相关的约束,而非被历史消息分散注意力;
|
||||||
|
// 5. user prompt(阶段性指令)— 始终在末尾,是本轮回答的核心触发。
|
||||||
func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) []*schema.Message {
|
func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) []*schema.Message {
|
||||||
messages := make([]*schema.Message, 0, 4)
|
messages := make([]*schema.Message, 0, 4)
|
||||||
|
|
||||||
|
// 1. 合并 system prompt:基础角色约束 + 阶段规则,始终在最顶部。
|
||||||
mergedSystemPrompt := mergeSystemPrompts(ctx, stageSystemPrompt)
|
mergedSystemPrompt := mergeSystemPrompts(ctx, stageSystemPrompt)
|
||||||
if mergedSystemPrompt != "" {
|
if mergedSystemPrompt != "" {
|
||||||
messages = append(messages, schema.SystemMessage(mergedSystemPrompt))
|
messages = append(messages, schema.SystemMessage(mergedSystemPrompt))
|
||||||
}
|
}
|
||||||
|
|
||||||
if pinnedText := renderPinnedBlocks(ctx); pinnedText != "" {
|
// 2. 工具摘要:稳定参考信息,放在 history 前即可。
|
||||||
messages = append(messages, schema.SystemMessage(pinnedText))
|
|
||||||
}
|
|
||||||
|
|
||||||
if toolText := renderToolSchemas(ctx); toolText != "" {
|
if toolText := renderToolSchemas(ctx); toolText != "" {
|
||||||
messages = append(messages, schema.SystemMessage(toolText))
|
messages = append(messages, schema.SystemMessage(toolText))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 对话历史:按时间顺序,包含工具调用结果和修正反馈。
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
history := ctx.HistorySnapshot()
|
history := ctx.HistorySnapshot()
|
||||||
if len(history) > 0 {
|
if len(history) > 0 {
|
||||||
@@ -48,6 +50,13 @@ func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.Conversatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 置顶上下文块:当前计划、当前步骤、粗排结果等最新约束。
|
||||||
|
// 放在 history 之后、user prompt 之前,利用 LLM 近因效应提升对最新约束的注意力。
|
||||||
|
if pinnedText := renderPinnedBlocks(ctx); pinnedText != "" {
|
||||||
|
messages = append(messages, schema.SystemMessage(pinnedText))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 阶段性用户提示词:始终在末尾,是本轮回答的核心触发。
|
||||||
runtimeUserPrompt = strings.TrimSpace(runtimeUserPrompt)
|
runtimeUserPrompt = strings.TrimSpace(runtimeUserPrompt)
|
||||||
if runtimeUserPrompt != "" {
|
if runtimeUserPrompt != "" {
|
||||||
messages = append(messages, schema.UserMessage(runtimeUserPrompt))
|
messages = append(messages, schema.UserMessage(runtimeUserPrompt))
|
||||||
|
|||||||
@@ -196,9 +196,9 @@ func BuildExecuteUserPrompt(state *newagentmodel.CommonState) string {
|
|||||||
return strings.TrimSpace(sb.String())
|
return strings.TrimSpace(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentStep, ok := state.CurrentPlanStep(); ok {
|
if _, ok := state.CurrentPlanStep(); ok {
|
||||||
sb.WriteString("执行要求:\n")
|
sb.WriteString("执行要求:\n")
|
||||||
sb.WriteString("1. 始终围绕下面这个当前步骤行动。\n")
|
sb.WriteString("1. 始终围绕上方「当前步骤内容」行动。\n")
|
||||||
sb.WriteString("2. 若当前步骤未完成,请继续思考-执行-观察循环。\n")
|
sb.WriteString("2. 若当前步骤未完成,请继续思考-执行-观察循环。\n")
|
||||||
sb.WriteString("3. 若当前步骤已完成,请输出 action=next_plan,并填写 goal_check 说明完成依据。\n")
|
sb.WriteString("3. 若当前步骤已完成,请输出 action=next_plan,并填写 goal_check 说明完成依据。\n")
|
||||||
sb.WriteString("4. 若整个任务已完成,请输出 action=done,并填写 goal_check 总结整体证据。\n")
|
sb.WriteString("4. 若整个任务已完成,请输出 action=done,并填写 goal_check 总结整体证据。\n")
|
||||||
@@ -206,14 +206,6 @@ func BuildExecuteUserPrompt(state *newagentmodel.CommonState) string {
|
|||||||
sb.WriteString("6. 输出 next_plan 或 done 时,goal_check 不能为空,必须对照 done_when 逐条验证。\n")
|
sb.WriteString("6. 输出 next_plan 或 done 时,goal_check 不能为空,必须对照 done_when 逐条验证。\n")
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
sb.WriteString(BuildExecuteDecisionContractText())
|
sb.WriteString(BuildExecuteDecisionContractText())
|
||||||
sb.WriteString("\n当前步骤正文:\n")
|
|
||||||
sb.WriteString(strings.TrimSpace(currentStep.Content))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
if strings.TrimSpace(currentStep.DoneWhen) != "" {
|
|
||||||
sb.WriteString("\n当前步骤完成判定:\n")
|
|
||||||
sb.WriteString(strings.TrimSpace(currentStep.DoneWhen))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("当前 plan 已存在,但当前步骤索引无效;请不要擅自执行其他步骤。\n")
|
sb.WriteString("当前 plan 已存在,但当前步骤索引无效;请不要擅自执行其他步骤。\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -473,7 +473,8 @@ func (e *ChunkEmitter) emitPseudoText(ctx context.Context, text string, options
|
|||||||
if emitChunk == nil {
|
if emitChunk == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
text = strings.TrimSpace(text)
|
// 只剥首尾空格和制表符,保留结尾 \n,让上层加的段落分隔符能作为内容的一部分推出。
|
||||||
|
text = strings.TrimRight(strings.TrimLeft(text, " \t\r\n"), " \t\r")
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -499,7 +500,8 @@ func (e *ChunkEmitter) emitPseudoText(ctx context.Context, text string, options
|
|||||||
// 2. 若长时间遇不到合适边界,则在 MaxChunkRunes 处强制切块,避免整段卡太久;
|
// 2. 若长时间遇不到合适边界,则在 MaxChunkRunes 处强制切块,避免整段卡太久;
|
||||||
// 3. 对中文文本优先按 rune 长度处理,避免多字节字符被截断。
|
// 3. 对中文文本优先按 rune 长度处理,避免多字节字符被截断。
|
||||||
func SplitPseudoStreamText(text string, options PseudoStreamOptions) []string {
|
func SplitPseudoStreamText(text string, options PseudoStreamOptions) []string {
|
||||||
text = strings.TrimSpace(text)
|
hasTrailingNewline := strings.HasSuffix(strings.TrimRight(text, " \t"), "\n")
|
||||||
|
text = strings.TrimRight(strings.TrimLeft(text, " \t\r\n"), " \t\r")
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -507,6 +509,9 @@ func SplitPseudoStreamText(text string, options PseudoStreamOptions) []string {
|
|||||||
options = normalizePseudoStreamOptions(options)
|
options = normalizePseudoStreamOptions(options)
|
||||||
runes := []rune(text)
|
runes := []rune(text)
|
||||||
if len(runes) <= options.MaxChunkRunes {
|
if len(runes) <= options.MaxChunkRunes {
|
||||||
|
if hasTrailingNewline {
|
||||||
|
return []string{text + "\n"}
|
||||||
|
}
|
||||||
return []string{text}
|
return []string{text}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,8 +548,14 @@ func SplitPseudoStreamText(text string, options PseudoStreamOptions) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(chunks) == 0 {
|
if len(chunks) == 0 {
|
||||||
|
if hasTrailingNewline {
|
||||||
|
return []string{text + "\n"}
|
||||||
|
}
|
||||||
return []string{text}
|
return []string{text}
|
||||||
}
|
}
|
||||||
|
if hasTrailingNewline {
|
||||||
|
chunks[len(chunks)-1] += "\n"
|
||||||
|
}
|
||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,25 @@ func readAgentExtraInt(extra map[string]any, key string) int {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readAgentExtraBool(extra map[string]any, key string) bool {
|
||||||
|
if len(extra) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
raw, ok := extra[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case bool:
|
||||||
|
return v
|
||||||
|
case float64:
|
||||||
|
return v != 0
|
||||||
|
case string:
|
||||||
|
return strings.ToLower(strings.TrimSpace(v)) == "true"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// readAgentExtraIntSlice 从 extra 中提取 []int。
|
// readAgentExtraIntSlice 从 extra 中提取 []int。
|
||||||
// 支持 JSON 数组格式([]any,每个元素为 float64/int)。
|
// 支持 JSON 数组格式([]any,每个元素为 float64/int)。
|
||||||
func readAgentExtraIntSlice(extra map[string]any, key string) []int {
|
func readAgentExtraIntSlice(extra map[string]any, key string) []int {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/LoveLosita/smartflow/backend/conv"
|
"github.com/LoveLosita/smartflow/backend/conv"
|
||||||
"github.com/LoveLosita/smartflow/backend/model"
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
"github.com/LoveLosita/smartflow/backend/pkg"
|
"github.com/LoveLosita/smartflow/backend/pkg"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/respond"
|
||||||
eventsvc "github.com/LoveLosita/smartflow/backend/service/events"
|
eventsvc "github.com/LoveLosita/smartflow/backend/service/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,18 +102,23 @@ func (s *AgentService) runNewAgentGraph(
|
|||||||
conversationContext = s.loadConversationContext(requestCtx, chatID, userMessage)
|
conversationContext = s.loadConversationContext(requestCtx, chatID, userMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5.5 若 extra 携带 task_class_ids,写入 CommonState(仅首轮/尚未设置时生效,跨轮持久化)。
|
// 5.5 若 extra 携带 task_class_ids,校验后写入 CommonState(仅首轮/尚未设置时生效,跨轮持久化)。
|
||||||
|
// 校验:通过 LoadTaskClassMetas → GetCompleteTaskClassesByIDs 检查所有 ID 是否存在且属于当前用户;
|
||||||
|
// 校验失败时向 errChan 推送 WrongTaskClassID(code=40040),前端收到 SSE 错误事件。
|
||||||
if taskClassIDs := readAgentExtraIntSlice(extra, "task_class_ids"); len(taskClassIDs) > 0 {
|
if taskClassIDs := readAgentExtraIntSlice(extra, "task_class_ids"); len(taskClassIDs) > 0 {
|
||||||
cs := runtimeState.EnsureCommonState()
|
cs := runtimeState.EnsureCommonState()
|
||||||
if len(cs.TaskClassIDs) == 0 {
|
if len(cs.TaskClassIDs) == 0 {
|
||||||
cs.TaskClassIDs = taskClassIDs
|
if s.scheduleProvider == nil {
|
||||||
if s.scheduleProvider != nil {
|
pushErrNonBlocking(errChan, respond.WrongTaskClassID)
|
||||||
if metas, metaErr := s.scheduleProvider.LoadTaskClassMetas(requestCtx, userID, taskClassIDs); metaErr != nil {
|
return
|
||||||
log.Printf("加载任务类约束元数据失败 chat=%s err=%v", chatID, metaErr)
|
|
||||||
} else {
|
|
||||||
cs.TaskClasses = metas
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
metas, metaErr := s.scheduleProvider.LoadTaskClassMetas(requestCtx, userID, taskClassIDs)
|
||||||
|
if metaErr != nil {
|
||||||
|
pushErrNonBlocking(errChan, respond.WrongTaskClassID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cs.TaskClassIDs = taskClassIDs
|
||||||
|
cs.TaskClasses = metas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ func (s *AgentService) runNewAgentGraph(
|
|||||||
graphRequest := newagentmodel.AgentGraphRequest{
|
graphRequest := newagentmodel.AgentGraphRequest{
|
||||||
UserInput: userMessage,
|
UserInput: userMessage,
|
||||||
ConfirmAction: confirmAction,
|
ConfirmAction: confirmAction,
|
||||||
|
AlwaysExecute: readAgentExtraBool(extra, "always_execute"),
|
||||||
}
|
}
|
||||||
graphRequest.Normalize()
|
graphRequest.Normalize()
|
||||||
|
|
||||||
@@ -139,16 +146,17 @@ func (s *AgentService) runNewAgentGraph(
|
|||||||
|
|
||||||
// 9. 构造 AgentGraphDeps(由 cmd/start.go 注入的依赖)。
|
// 9. 构造 AgentGraphDeps(由 cmd/start.go 注入的依赖)。
|
||||||
deps := newagentmodel.AgentGraphDeps{
|
deps := newagentmodel.AgentGraphDeps{
|
||||||
ChatClient: chatClient,
|
ChatClient: chatClient,
|
||||||
PlanClient: planClient,
|
PlanClient: planClient,
|
||||||
ExecuteClient: executeClient,
|
ExecuteClient: executeClient,
|
||||||
DeliverClient: deliverClient,
|
DeliverClient: deliverClient,
|
||||||
ChunkEmitter: chunkEmitter,
|
ChunkEmitter: chunkEmitter,
|
||||||
StateStore: s.agentStateStore,
|
StateStore: s.agentStateStore,
|
||||||
ToolRegistry: s.toolRegistry,
|
ToolRegistry: s.toolRegistry,
|
||||||
ScheduleProvider: s.scheduleProvider,
|
ScheduleProvider: s.scheduleProvider,
|
||||||
SchedulePersistor: s.schedulePersistor,
|
SchedulePersistor: s.schedulePersistor,
|
||||||
RoughBuildFunc: s.makeRoughBuildFunc(),
|
RoughBuildFunc: s.makeRoughBuildFunc(),
|
||||||
|
WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. 构造 AgentGraphRunInput 并运行 graph。
|
// 10. 构造 AgentGraphRunInput 并运行 graph。
|
||||||
@@ -181,23 +189,8 @@ func (s *AgentService) runNewAgentGraph(
|
|||||||
eventsvc.PublishAgentStateSnapshot(requestCtx, s.eventPublisher, snapshot, chatID, userID)
|
eventsvc.PublishAgentStateSnapshot(requestCtx, s.eventPublisher, snapshot, chatID, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11.6. 将排程结果写入 Redis 预览缓存,复用旧 agent 的 SchedulePlanPreviewCache 格式。
|
// 排程预览缓存由 Deliver 节点负责写入(通过注入的 WriteSchedulePreview func),
|
||||||
// 前端通过 GET /agent/schedule-preview 获取,无需改动。
|
// 保证只有任务真正完成时才写,中断路径不写中间态。
|
||||||
if finalState != nil && finalState.ScheduleState != nil {
|
|
||||||
flowState := finalState.EnsureFlowState()
|
|
||||||
preview := conv.ScheduleStateToPreview(
|
|
||||||
finalState.ScheduleState,
|
|
||||||
userID,
|
|
||||||
chatID,
|
|
||||||
flowState.TaskClassIDs,
|
|
||||||
"", // summary 由转换函数自动生成
|
|
||||||
)
|
|
||||||
if preview != nil && s.cacheDAO != nil {
|
|
||||||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(requestCtx, userID, chatID, preview); err != nil {
|
|
||||||
log.Printf("[WARN] 写入排程预览缓存失败 chat=%s: %v", chatID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. 发送 OpenAI 兼容的流式结束标记,告知客户端 stream 已完成。
|
// 12. 发送 OpenAI 兼容的流式结束标记,告知客户端 stream 已完成。
|
||||||
_ = chunkEmitter.EmitDone()
|
_ = chunkEmitter.EmitDone()
|
||||||
@@ -425,34 +418,54 @@ func (s *AgentService) persistChatAfterGraph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// makeRoughBuildFunc 把 AgentService 上的 HybridScheduleWithPlanMultiFunc 封装成
|
// makeRoughBuildFunc 把 AgentService 上的 HybridScheduleWithPlanMultiFunc 封装成
|
||||||
// newAgent 层的 RoughBuildFunc,完成外层 model.TaskClassItem → RoughBuildPlacement 的转换。
|
// newAgent 层的 RoughBuildFunc,将 HybridScheduleWithPlanMultiFunc 的结果转换为 RoughBuildPlacement。
|
||||||
// HybridScheduleWithPlanMultiFunc 未注入时返回 nil,RoughBuild 节点会静默跳过粗排。
|
// HybridScheduleWithPlanMultiFunc 未注入时返回 nil,RoughBuild 节点会静默跳过粗排。
|
||||||
|
//
|
||||||
|
// 修复说明:
|
||||||
|
// 旧实现使用第二个返回值 []TaskClassItem,只有 EmbeddedTime != nil 的条目(嵌入水课)才生成
|
||||||
|
// placement,普通时段放置的任务全部被丢弃。
|
||||||
|
// 正确做法:使用第一个返回值 []HybridScheduleEntry,过滤 Status="suggested" 且 TaskItemID>0 的条目,
|
||||||
|
// 这样嵌入和非嵌入的粗排结果都能正确写入 ScheduleState。
|
||||||
func (s *AgentService) makeRoughBuildFunc() newagentmodel.RoughBuildFunc {
|
func (s *AgentService) makeRoughBuildFunc() newagentmodel.RoughBuildFunc {
|
||||||
if s.HybridScheduleWithPlanMultiFunc == nil {
|
if s.HybridScheduleWithPlanMultiFunc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return func(ctx context.Context, userID int, taskClassIDs []int) ([]newagentmodel.RoughBuildPlacement, error) {
|
return func(ctx context.Context, userID int, taskClassIDs []int) ([]newagentmodel.RoughBuildPlacement, error) {
|
||||||
_, items, err := s.HybridScheduleWithPlanMultiFunc(ctx, userID, taskClassIDs)
|
entries, _, err := s.HybridScheduleWithPlanMultiFunc(ctx, userID, taskClassIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
placements := make([]newagentmodel.RoughBuildPlacement, 0, len(items))
|
placements := make([]newagentmodel.RoughBuildPlacement, 0, len(entries))
|
||||||
for _, item := range items {
|
for _, entry := range entries {
|
||||||
if item.EmbeddedTime == nil {
|
if entry.Status != "suggested" || entry.TaskItemID == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
placements = append(placements, newagentmodel.RoughBuildPlacement{
|
placements = append(placements, newagentmodel.RoughBuildPlacement{
|
||||||
TaskItemID: item.ID,
|
TaskItemID: entry.TaskItemID,
|
||||||
Week: item.EmbeddedTime.Week,
|
Week: entry.Week,
|
||||||
DayOfWeek: item.EmbeddedTime.DayOfWeek,
|
DayOfWeek: entry.DayOfWeek,
|
||||||
SectionFrom: item.EmbeddedTime.SectionFrom,
|
SectionFrom: entry.SectionFrom,
|
||||||
SectionTo: item.EmbeddedTime.SectionTo,
|
SectionTo: entry.SectionTo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return placements, nil
|
return placements, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeWriteSchedulePreviewFunc 封装 cacheDAO 写排程预览缓存的操作,供 Deliver 节点注入。
|
||||||
|
func (s *AgentService) makeWriteSchedulePreviewFunc() newagentmodel.WriteSchedulePreviewFunc {
|
||||||
|
if s.cacheDAO == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error {
|
||||||
|
preview := conv.ScheduleStateToPreview(state, userID, conversationID, taskClassIDs, "")
|
||||||
|
if preview == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, conversationID, preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 依赖注入字段 ---
|
// --- 依赖注入字段 ---
|
||||||
|
|
||||||
// toolRegistry 由 cmd/start.go 注入
|
// toolRegistry 由 cmd/start.go 注入
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, c
|
|||||||
TraceID: strings.TrimSpace(preview.TraceID),
|
TraceID: strings.TrimSpace(preview.TraceID),
|
||||||
Summary: strings.TrimSpace(preview.Summary),
|
Summary: strings.TrimSpace(preview.Summary),
|
||||||
CandidatePlans: plans,
|
CandidatePlans: plans,
|
||||||
|
HybridEntries: cloneHybridEntries(preview.HybridEntries),
|
||||||
GeneratedAt: preview.GeneratedAt,
|
GeneratedAt: preview.GeneratedAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -212,6 +213,7 @@ func snapshotToSchedulePlanPreviewResponse(snapshot *model.SchedulePlanStateSnap
|
|||||||
TraceID: strings.TrimSpace(snapshot.TraceID),
|
TraceID: strings.TrimSpace(snapshot.TraceID),
|
||||||
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
||||||
CandidatePlans: plans,
|
CandidatePlans: plans,
|
||||||
|
HybridEntries: cloneHybridEntries(snapshot.HybridEntries),
|
||||||
GeneratedAt: generatedAt,
|
GeneratedAt: generatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user