Files
smartmate/backend/agent2/graph/quicknote.go
LoveLosita f4ef6fb256 Version: 0.7.5.dev.260324
🐛 fix(agent/schedulerefine): 修复复合微调分支链路问题,并将 MinContextSwitch 重构为固定坑位重排语义

- 🔧 修复 `schedulerefine` 复合路由中参数透传不完整、缺少 deterministic objective 时错误降级,以及“复合工具执行成功”与“终审通过”语义混淆的问题
-  保证新的独立复合分支能够正确执行、正确出站,并统一交由 `hard_check` 裁决最终结果
- 🔍 排查时发现 `MinContextSwitch` 上游 `context_tag` 存在整体退化为 `General` 的风险,影响MinContextSwitch
- 🛡️ 为 `MinContextSwitch` 增加兜底策略:当标签整体退化时,按任务名关键词推断学科分组,避免分组能力失效
- ♻️ 将 `MinContextSwitch` 从“整周重新寻找新坑位”调整为“坑位不变,任务顺序改变”
- 🎯 将落地方式从顺序 `BatchMove` 改为固定坑位原子重写,避免出现远距离跳位、跨天错迁、异常嵌入课位及循环换位冲突
- 🧹 修复 `hard_check` 在 `MinContextSwitch` 成功后仍执行 `origin_rank` 顺序归位、并导致逆序终审误判的问题
- 🚦 命中该分支后跳过顺序归位与顺序硬校验,避免 `summary` / `hard_check` 将有效重排结果误判为失败

📈 当前连续微调规划涉及的全部功能已可以稳定运行;下一步将继续扩展能力边界,并进一步优化 `schedule_plan` 流程

♻️ refactor: 重整 agent2 架构,并迁移 quicknote/chat 新链路,目前还剩3个模块未迁移,后续迁移完成后会删除原agent并将此目录命名为agent

- 🏗️ 明确 `agent2` 采用“统一分层目录 + 文件分层 + 依赖注入”的重构方案,不再沿用模块目录多层嵌套结构
- 🧩 完善 `agent2` 基础骨架,统一收口 `entrance` / `router` / `llm` / `stream` / `shared` / `model` / `prompt` / `node` / `graph` 等层级职责
- 🚚 将通用路由能力迁移至 `agent2/router`,沉淀统一的 `Action`、`RoutingDecision`、控制码解析,以及 `Dispatcher` / `Resolver` 抽象
- 💬 将普通聊天链路迁移至 `agent2/chat`,复用 `stream` 的 OpenAI 兼容输出协议与 LLM usage 聚合能力
- 📝 将 `quicknote` 链路迁移到 `agent2` 新结构,拆分为 `model` / `prompt` / `llm` / `node` / `graph` 多层实现,替换对旧 `agent/quicknote` 的直接依赖
- 🔌 调整 `agentsvc` 对 `agent2` 的引用,普通聊天、通用分流与 `quicknote` 全部切换到新链路
- ✂️ 去除 graph 内部 `runner` 转接层,改为由 node 层直接持有请求级依赖,并向 graph 暴露节点方法
- 🧹 合并 `graph/quicknote` 与 `graph/quicknote_run`,删除冗余骨架文件,收敛为单一 `quicknote graph` 文件
- 📚 新增 `agent2`《通用能力接入文档》,明确公共能力边界、接入方式以及 graph/node 协作约定
- 📝 更新 `AGENTS.md`,要求后续扩展 `agent2` 通用能力时必须同步维护接入文档

♻️ refactor: 删除了现Agent目录内Chat模块的两条冗余Prompt
2026-03-24 21:35:22 +08:00

150 lines
5.4 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 agentgraph
import (
"context"
"errors"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/agent2/model"
agentnode "github.com/LoveLosita/smartflow/backend/agent2/node"
agentshared "github.com/LoveLosita/smartflow/backend/agent2/shared"
"github.com/cloudwego/eino/compose"
)
const (
// QuickNoteGraphName 是“随口记”图编排的稳定标识。
// 保留这个名字的目的:
// 1. 让 compile 后的 graph 名称在日志、调试、可视化工具里有固定口径;
// 2. 后续如果接入更多技能图,可以统一按技能名识别。
QuickNoteGraphName = "quick_note"
)
// RunQuickNoteGraph 执行“随口记”图编排。
//
// 职责边界:
// 1. 这里只负责 graph 连线与运行时装配,不负责节点内部业务细节;
// 2. graph 层只挂 node 层对外暴露的方法,不再维护额外 runner 适配层;
// 3. 工具注册、时间基准补齐、compile 参数收口都在这里统一完成。
func RunQuickNoteGraph(ctx context.Context, input agentnode.QuickNoteGraphRunInput) (*agentmodel.QuickNoteState, error) {
// 1. 启动前先做硬校验。
// 1.1 model 为空时无法调模型,直接失败;
// 1.2 state 为空时图无法承载共享上下文,也必须直接拦截;
// 1.3 tool deps 不完整时,后续 persist 节点必然失败,因此这里提前收口。
if input.Model == nil {
return nil, errors.New("quick note graph: model is nil")
}
if input.State == nil {
return nil, errors.New("quick note graph: state is nil")
}
if err := input.Deps.Validate(); err != nil {
return nil, err
}
// 2. 统一补齐本次请求的时间基准。
// 2.1 RequestNow 只在整条 quicknote 链路入口确定一次,避免同一次请求里相对时间口径漂移;
// 2.2 RequestNowText 是 prompt 注入用文本,缺失时也在这里统一补齐。
if input.State.RequestNow.IsZero() {
input.State.RequestNow = agentshared.NowToMinute()
}
if strings.TrimSpace(input.State.RequestNowText) == "" {
input.State.RequestNowText = agentshared.FormatMinute(input.State.RequestNow)
}
// 3. 构建工具包并提取“创建任务”工具。
// 3.1 graph 层只关心“拿到一个可执行工具”,不关心工具内部如何注册;
// 3.2 失败时直接返回,避免把半残依赖继续交给 node 层。
toolBundle, err := agentnode.BuildQuickNoteToolBundle(ctx, input.Deps)
if err != nil {
return nil, err
}
createTaskTool, err := agentnode.GetInvokableToolByName(toolBundle, agentnode.ToolNameQuickNoteCreateTask)
if err != nil {
return nil, err
}
// 4. 在 node 层创建节点容器。
// 4.1 这一步就是“请求级依赖注入”的唯一收口点;
// 4.2 graph 后续只认 `nodes.Intent / nodes.Priority / nodes.Persist` 这些方法,不再额外造 runner。
nodes, err := agentnode.NewQuickNoteNodes(input, createTaskTool)
if err != nil {
return nil, err
}
// 5. 创建状态图容器,输入输出统一都是 *QuickNoteState。
graph := compose.NewGraph[*agentmodel.QuickNoteState, *agentmodel.QuickNoteState]()
// 6. 注册节点。
if err = graph.AddLambdaNode(agentnode.QuickNoteGraphNodeIntent, compose.InvokableLambda(nodes.Intent)); err != nil {
return nil, err
}
if err = graph.AddLambdaNode(agentnode.QuickNoteGraphNodeRank, compose.InvokableLambda(nodes.Priority)); err != nil {
return nil, err
}
if err = graph.AddLambdaNode(agentnode.QuickNoteGraphNodePersist, compose.InvokableLambda(nodes.Persist)); err != nil {
return nil, err
}
if err = graph.AddLambdaNode(agentnode.QuickNoteGraphNodeExit, compose.InvokableLambda(nodes.Exit)); err != nil {
return nil, err
}
// 7. 所有请求统一从 intent 节点开始。
if err = graph.AddEdge(compose.START, agentnode.QuickNoteGraphNodeIntent); err != nil {
return nil, err
}
// 8. intent 后分支:
// 8.1 命中随口记且时间合法 -> priority
// 8.2 非随口记,或时间校验失败 -> exit。
if err = graph.AddBranch(agentnode.QuickNoteGraphNodeIntent, compose.NewGraphBranch(
nodes.NextAfterIntent,
map[string]bool{
agentnode.QuickNoteGraphNodeRank: true,
agentnode.QuickNoteGraphNodeExit: true,
},
)); err != nil {
return nil, err
}
// 9. 显式 exit 节点仍然保留。
// 这样后续若要统一加日志、埋点、收尾逻辑,不需要再改 branch 结构。
if err = graph.AddEdge(agentnode.QuickNoteGraphNodeExit, compose.END); err != nil {
return nil, err
}
// 10. priority 后固定进入 persist。
if err = graph.AddEdge(agentnode.QuickNoteGraphNodeRank, agentnode.QuickNoteGraphNodePersist); err != nil {
return nil, err
}
// 11. persist 后分支:
// 11.1 已成功写入 -> END
// 11.2 仍可重试 -> 回到 persist
// 11.3 重试耗尽 -> END由 state 中的失败文案兜底。
if err = graph.AddBranch(agentnode.QuickNoteGraphNodePersist, compose.NewGraphBranch(
nodes.NextAfterPersist,
map[string]bool{
agentnode.QuickNoteGraphNodePersist: true,
compose.END: true,
},
)); err != nil {
return nil, err
}
// 12. 为 persist 重试预留运行步数余量,避免异常状态把图跑成死循环。
maxSteps := input.State.MaxToolRetry + 10
if maxSteps < 12 {
maxSteps = 12
}
// 13. 编译并执行图。
runnable, err := graph.Compile(ctx,
compose.WithGraphName(QuickNoteGraphName),
compose.WithMaxRunSteps(maxSteps),
compose.WithNodeTriggerMode(compose.AnyPredecessor),
)
if err != nil {
return nil, err
}
return runnable.Invoke(ctx, input.State)
}