Files
smartmate/backend/agent/scheduleplan/react.go
LoveLosita 059b25872a Version: 0.7.1.dev.260320
🧠 agent智能编排:删除了落库相关逻辑。再次重申:agent智能编排旨在为用户预览排程结果,实际的落库由用户决定,并通过按钮触发常规接口进行落库。目前仅保留 ReAct 精排循环链路(待改进)。
📄 修改了 ReAct 智能精排决策文档相关内容。
🔄 undo:当前 agent 智能排程逻辑待改进。
2026-03-20 19:40:11 +08:00

210 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package scheduleplan
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/agent/chat"
"github.com/cloudwego/eino-ext/components/model/ark"
"github.com/cloudwego/eino/schema"
arkModel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
)
// reactRoundTimeout 是单轮 ReAct 的超时时间。
// 深度思考模式下 reasoning 阶段可能耗时较长,需要给足时间。
const reactRoundTimeout = 15 * time.Minute
// runReactRefineNode 执行 ReAct 精排循环。
//
// 核心流程(最多 ReactMaxRound 轮):
// 1. 构造 messagessystem prompt + 混合日程 JSON + 上轮 tool 结果)
// 2. 调用 chatModel.Stream() + ThinkingTypeEnabled
// 3. reasoning_content 实时推送到 outChan前端可见思考过程
// 4. content 累积后解析done=true 则退出tool_calls 则执行
// 5. tool 结果拼入下一轮 messages
func runReactRefineNode(
ctx context.Context,
st *SchedulePlanState,
chatModel *ark.ChatModel,
outChan chan<- string,
modelName string,
emitStage func(stage, detail string),
) (*SchedulePlanState, error) {
if st == nil {
return nil, fmt.Errorf("schedule plan graph: nil state in reactRefine node")
}
if chatModel == nil {
return nil, fmt.Errorf("schedule plan graph: model is nil in reactRefine node")
}
if len(st.HybridEntries) == 0 {
st.ReactDone = true
st.ReactSummary = "无可优化的排程条目。"
return st, nil
}
// 准备 SSE 流式输出的基础参数
if strings.TrimSpace(modelName) == "" {
modelName = "smartflow-worker"
}
// 构造混合日程 JSON只在首轮构造后续轮次复用
hybridJSON, err := json.Marshal(st.HybridEntries)
if err != nil {
return nil, fmt.Errorf("序列化混合日程失败: %w", err)
}
// 用户约束文本
constraintsText := "无"
if len(st.Constraints) > 0 {
constraintsText = strings.Join(st.Constraints, "、")
}
// 对话历史:跨轮次累积
messages := []*schema.Message{
schema.SystemMessage(SchedulePlanReactSystemPrompt),
schema.UserMessage(fmt.Sprintf(
"以下是当前混合日程JSON\n%s\n\n用户约束%s\n\n请分析并优化 suggested 任务的时间安排。",
string(hybridJSON), constraintsText,
)),
}
// ── ReAct 主循环 ──
for st.ReactRound < st.ReactMaxRound {
st.ReactRound++
emitStage("schedule_plan.react.round", fmt.Sprintf("第 %d 轮优化思考...", st.ReactRound))
// 1. 带超时的 context
roundCtx, cancel := context.WithTimeout(ctx, reactRoundTimeout)
// 2. 调用模型(流式 + 深度思考)
content, streamErr := streamReactRound(roundCtx, chatModel, modelName, messages, outChan)
cancel()
if streamErr != nil {
emitStage("schedule_plan.react.error", fmt.Sprintf("第 %d 轮模型调用失败: %s", st.ReactRound, streamErr.Error()))
// 明确标记为失败,不伪装成功
st.ReactDone = true
st.ReactSummary = fmt.Sprintf("排程优化未完成:第 %d 轮模型调用超时或失败,使用粗排结果。", st.ReactRound)
break
}
// 3. 解析 LLM 输出
parsed, parseErr := parseReactLLMOutput(content)
if parseErr != nil {
// 解析失败,把原始输出当作摘要,结束循环
emitStage("schedule_plan.react.parse_error", "LLM 输出格式异常,结束优化。")
st.ReactSummary = "排程优化已完成LLM 输出格式异常,使用当前结果)。"
st.ReactDone = true
break
}
// 4. 检查是否完成
if parsed.Done {
st.ReactSummary = parsed.Summary
st.ReactDone = true
emitStage("schedule_plan.react.done", "优化完成。")
break
}
// 5. 执行 tool calls
if len(parsed.ToolCalls) == 0 {
// 没有 tool 调用也没有 done视为完成
st.ReactSummary = "排程优化已完成。"
st.ReactDone = true
break
}
results := make([]reactToolResult, 0, len(parsed.ToolCalls))
for _, call := range parsed.ToolCalls {
var result reactToolResult
st.HybridEntries, result = dispatchReactTool(st.HybridEntries, call)
results = append(results, result)
statusMark := "OK"
if !result.Success {
statusMark = "FAIL"
}
emitStage("schedule_plan.react.tool_call",
fmt.Sprintf("[%s] %s: %s", statusMark, result.Tool, result.Result))
}
// 6. 将 tool 结果拼入下一轮 messages
// 先追加 assistant 的输出
messages = append(messages, schema.AssistantMessage(content, nil))
// 再追加 tool 结果作为 user message
resultsJSON, _ := json.Marshal(results)
messages = append(messages, schema.UserMessage(
fmt.Sprintf("工具执行结果:\n%s\n\n请继续优化或输出 {\"done\":true,\"summary\":\"...\"} 完成。", string(resultsJSON)),
))
}
// 循环结束兜底
if !st.ReactDone {
st.ReactDone = true
if strings.TrimSpace(st.ReactSummary) == "" {
st.ReactSummary = fmt.Sprintf("排程优化已达最大轮次(%d 轮),使用当前结果。", st.ReactRound)
}
emitStage("schedule_plan.react.max_round", "已达最大优化轮次,使用当前结果。")
}
return st, nil
}
// streamReactRound 执行单轮 ReAct 模型调用:
// - 流式推送 reasoning_content 到 outChan前端可见思考过程
// - 累积 content 并返回(包含 tool_calls 或 done 信号)
func streamReactRound(
ctx context.Context,
chatModel *ark.ChatModel,
modelName string,
messages []*schema.Message,
outChan chan<- string,
) (string, error) {
// 开启深度思考
reader, err := chatModel.Stream(ctx, messages,
ark.WithThinking(&arkModel.Thinking{Type: arkModel.ThinkingTypeEnabled}),
)
if err != nil {
return "", fmt.Errorf("模型 Stream 调用失败: %w", err)
}
defer reader.Close()
requestID := "react-" + fmt.Sprintf("%d", time.Now().UnixMilli())
created := time.Now().Unix()
var contentBuilder strings.Builder
for {
chunk, recvErr := reader.Recv()
if recvErr == io.EOF {
break
}
if recvErr != nil {
return contentBuilder.String(), fmt.Errorf("流式接收失败: %w", recvErr)
}
if chunk == nil {
continue
}
// 推送 reasoning_content 到前端(实时思考过程)
if chunk.ReasoningContent != "" && outChan != nil {
payload, fmtErr := chat.ToOpenAIStream(
&schema.Message{ReasoningContent: chunk.ReasoningContent},
requestID, modelName, created, false,
)
if fmtErr == nil && payload != "" {
outChan <- payload
}
}
// 累积 contenttool_calls 或 done 信号)
if chunk.Content != "" {
contentBuilder.WriteString(chunk.Content)
}
}
return strings.TrimSpace(contentBuilder.String()), nil
}