Files
smartmate/backend/agent2/llm/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

171 lines
5.6 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 agentllm
import (
"context"
"fmt"
"strings"
agentprompt "github.com/LoveLosita/smartflow/backend/agent2/prompt"
"github.com/cloudwego/eino-ext/components/model/ark"
)
// QuickNoteIntentOutput 是“随口记意图识别”模型契约。
type QuickNoteIntentOutput struct {
IsQuickNote bool `json:"is_quick_note"`
Title string `json:"title"`
DeadlineAt string `json:"deadline_at"`
Reason string `json:"reason"`
}
// QuickNotePriorityOutput 是“随口记优先级评估”模型契约。
type QuickNotePriorityOutput struct {
PriorityGroup int `json:"priority_group"`
Reason string `json:"reason"`
UrgencyThresholdAt string `json:"urgency_threshold_at"`
}
// QuickNotePlanOutput 是“随口记单请求聚合规划”模型契约。
type QuickNotePlanOutput struct {
Title string `json:"title"`
DeadlineAt string `json:"deadline_at"`
UrgencyThresholdAt string `json:"urgency_threshold_at"`
PriorityGroup int `json:"priority_group"`
PriorityReason string `json:"priority_reason"`
Banter string `json:"banter"`
}
// IdentifyQuickNoteIntent 调用模型识别“是否随口记”。
func IdentifyQuickNoteIntent(ctx context.Context, chatModel *ark.ChatModel, nowText, userInput string) (*QuickNoteIntentOutput, error) {
prompt := fmt.Sprintf(`当前时间(北京时间,精确到分钟):%s
用户输入:%s
请仅输出 JSON不要 markdown不要解释字段如下
{
"is_quick_note": boolean,
"title": string,
"deadline_at": string,
"reason": string
}
字段约束:
1) deadline_at 只允许输出绝对时间,格式必须为 "yyyy-MM-dd HH:mm"。
2) 如果用户说了“明天/后天/下周一/今晚”等相对时间,必须基于上面的当前时间换算成绝对时间。
3) 如果用户没有提及时间deadline_at 输出空字符串。`,
nowText,
userInput,
)
parsed, _, err := CallArkJSON[QuickNoteIntentOutput](ctx, chatModel, agentprompt.QuickNoteIntentPrompt, prompt, ArkCallOptions{
Temperature: 0,
MaxTokens: 256,
Thinking: ThinkingModeDisabled,
})
return parsed, err
}
// PlanQuickNotePriority 调用模型评估优先级与紧急分界线。
func PlanQuickNotePriority(ctx context.Context, chatModel *ark.ChatModel, nowText, title, userInput, deadlineClue, deadlineText string) (*QuickNotePriorityOutput, error) {
prompt := fmt.Sprintf(`当前时间(北京时间,精确到分钟):%s
请对以下任务评估优先级:
- 任务标题:%s
- 用户原始输入:%s
- 时间线索原文:%s
- 归一化截止时间:%s
请仅输出 JSON不要 markdown不要解释
{
"priority_group": 1|2|3|4,
"reason": "简短理由",
"urgency_threshold_at": "yyyy-MM-dd HH:mm 或空字符串"
}
额外约束:
1) urgency_threshold_at 表示“何时从不紧急象限自动平移到紧急象限”;
2) 若该任务不需要自动平移,可输出空字符串;
3) 若任务已在紧急象限priority_group=1 或 3优先输出空字符串
4) 若输出非空时间,必须是绝对时间,且不晚于归一化截止时间(若有)。`,
nowText,
title,
userInput,
deadlineClue,
deadlineText,
)
parsed, _, err := CallArkJSON[QuickNotePriorityOutput](ctx, chatModel, agentprompt.QuickNotePriorityPrompt, prompt, ArkCallOptions{
Temperature: 0,
MaxTokens: 256,
Thinking: ThinkingModeDisabled,
})
return parsed, err
}
// PlanQuickNoteInSingleCall 一次性完成标题/时间/优先级/banter 聚合规划。
func PlanQuickNoteInSingleCall(ctx context.Context, chatModel *ark.ChatModel, nowText, userInput string) (*QuickNotePlanOutput, error) {
prompt := fmt.Sprintf(`当前时间(北京时间,精确到分钟):%s
用户输入:%s
请仅输出 JSON不要 markdown不要解释字段如下
{
"title": string,
"deadline_at": string,
"urgency_threshold_at": string,
"priority_group": 1|2|3|4,
"priority_reason": string,
"banter": string
}
约束:
1) deadline_at 只允许 "yyyy-MM-dd HH:mm" 或空字符串;
2) urgency_threshold_at 只允许 "yyyy-MM-dd HH:mm" 或空字符串;
3) 若用户给了相对时间(如明天/今晚/下周一),必须换算为绝对时间;
4) 若任务不需要自动平移,可让 urgency_threshold_at 为空;
5) banter 只允许一句中文不超过30字不得改动任务事实。`,
nowText,
strings.TrimSpace(userInput),
)
parsed, _, err := CallArkJSON[QuickNotePlanOutput](ctx, chatModel, agentprompt.QuickNotePlanPrompt, prompt, ArkCallOptions{
Temperature: 0,
MaxTokens: 220,
Thinking: ThinkingModeDisabled,
})
return parsed, err
}
// GenerateQuickNoteBanter 生成成功写入后的轻松跟进句。
func GenerateQuickNoteBanter(ctx context.Context, chatModel *ark.ChatModel, userMessage, title, priorityText, deadlineText string) (string, error) {
if chatModel == nil {
return "", fmt.Errorf("model is nil")
}
prompt := fmt.Sprintf(`用户原话:%s
已确认事实:
- 任务标题:%s
- %s
- %s
请输出一句轻松自然的跟进话术(仅一句)。`,
strings.TrimSpace(userMessage),
strings.TrimSpace(title),
strings.TrimSpace(priorityText),
strings.TrimSpace(deadlineText),
)
text, err := CallArkText(ctx, chatModel, agentprompt.QuickNoteReplyBanterPrompt, prompt, ArkCallOptions{
Temperature: 0.7,
MaxTokens: 72,
Thinking: ThinkingModeDisabled,
})
if err != nil {
return "", err
}
text = strings.TrimSpace(text)
text = strings.Trim(text, "\"'“”‘’")
if text == "" {
return "", fmt.Errorf("empty content")
}
if idx := strings.Index(text, "\n"); idx >= 0 {
text = strings.TrimSpace(text[:idx])
}
return text, nil
}