后端:
1. LLM 客户端从 newAgent/llm 提升为 infra/llm 基础设施层
- 删除 backend/newAgent/llm/(ark.go / ark_adapter.go / client.go / json.go)
- 等价迁移至 backend/infra/llm/,所有 newAgent node 与 service 统一改引用 infrallm
- 消除 newAgent 对模型客户端的私有依赖,为 memory / websearch 等多模块复用铺路
2. RAG 基础设施完成可运行态接入(factory / runtime / observer / service 四层成型)
- 新建 backend/infra/rag/factory.go / runtime.go / observe.go / observer.go /
service.go:工厂创建、运行时生命周期、轻量观测接口、检索服务门面
- 更新 infra/rag/config/config.go:补齐 Milvus / Embed / Reranker 全部配置项与默认值
- 更新 infra/rag/embed/eino_embedder.go:增强 Eino embedding 适配,支持 BaseURL / APIKey 环境变量 / 超时 /
维度等参数
- 更新 infra/rag/store/milvus_store.go:完整实现 Milvus 向量存储(建集合 / 建 Index / Upsert / Search /
Delete),支持 COSINE / L2 / IP 度量
- 更新 infra/rag/core/pipeline.go:适配 Runtime 接口,Pipeline 由 factory 注入而非手动拼装
- 更新 infra/rag/corpus/memory_corpus.go / vector_store.go:对接 Memory 模块数据源与 Store 接口扩展
3. Memory 模块从 Day1 骨架升级为 Day2 完整可运行态
- 新建 memory/module.go:统一门面 Module,对外封装 EnqueueExtract / ReadService / ManageService / WithTx /
StartWorker,启动层只依赖这一个入口
- 新建 memory/orchestrator/llm_write_orchestrator.go:LLM 驱动的记忆抽取编排器,替代原 mock 抽取
- 新建 memory/service/read_service.go:按用户开关过滤 + 轻量重排 + 访问时间刷新的读取链路
- 新建 memory/service/manage_service.go:记忆管理面能力(列出 / 软删除 / 开关读写),删除同步写审计日志
- 新建 memory/service/common.go:服务层公共工具
- 新建 memory/worker/loop.go:后台轮询循环 RunPollingLoop,定时抢占 pending 任务并推进
- 新建 memory/utils/audit.go / settings.go:审计日志构造、用户设置过滤等纯函数
- 更新 memory/model/item.go / job.go / settings.go / config.go / status.go:补齐 DTO 字段与状态常量
- 更新 memory/repo/item_repo.go / job_repo.go / audit_repo.go / settings_repo.go:补齐 CRUD 与查询能力
- 更新 memory/worker/runner.go:Runner 对接 Module 与 LLM 抽取器,任务状态机完整化
- 更新 memory/README.md:同步模块现状说明
4. newAgent 接入 Memory 读取注入与工具注册依赖预埋
- 新建 service/agentsvc/agent_memory.go:定义 MemoryReader 接口 + injectMemoryContext,在 graph
执行前统一补充记忆上下文
- 更新 service/agentsvc/agent.go:新增 memoryReader 字段与 SetMemoryReader 方法
- 更新 service/agentsvc/agent_newagent.go:调用 injectMemoryContext 注入 pinned block,检索失败仅降级不阻断主链路
- 更新 newAgent/tools/registry.go:新增 DefaultRegistryDeps(含 RAGRuntime),工具注册表支持依赖注入
5. 启动流程与事件处理器接线更新
- 更新 cmd/start.go:初始化 RAG Runtime → Memory Module → 注册事件处理器 → 启动 Worker 后台轮询
- 更新 service/events/memory_extract_requested.go:改用 memory.Module.WithTx(tx) 统一门面,事件处理器不再直接依赖
repo/service 内部包
6. 缓存插件与配置同步
- 更新 middleware/cache_deleter.go:静默忽略 MemoryJob / MemoryItem / MemoryAuditLog / MemoryUserSetting
等新模型,避免日志刷屏;清理冗余注释
- 更新 config.example.yaml:补齐 rag / memory / websearch 配置段及默认值
- 更新 go.mod / go.sum:新增 eino-ext/openai / json-patch / go-openai 依赖
前端:无 仓库:无
299 lines
10 KiB
Go
299 lines
10 KiB
Go
package newagentnode
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
|
||
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
|
||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||
"github.com/cloudwego/eino/schema"
|
||
)
|
||
|
||
const (
|
||
planStageName = "plan"
|
||
planStatusBlockID = "plan.status"
|
||
planSpeakBlockID = "plan.speak"
|
||
planSummaryBlockID = "plan.summary"
|
||
planPinnedKey = "current_plan"
|
||
planCurrentStepKey = "current_step"
|
||
planCurrentStepTitle = "当前步骤"
|
||
planFullPlanTitle = "当前完整计划"
|
||
)
|
||
|
||
// PlanNodeInput 描述单轮规划节点执行所需的最小依赖。
|
||
type PlanNodeInput struct {
|
||
RuntimeState *newagentmodel.AgentRuntimeState
|
||
ConversationContext *newagentmodel.ConversationContext
|
||
UserInput string
|
||
Client *infrallm.Client
|
||
ChunkEmitter *newagentstream.ChunkEmitter
|
||
ResumeNode string
|
||
AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点
|
||
}
|
||
|
||
// RunPlanNode 执行一轮规划节点逻辑。
|
||
//
|
||
// 步骤说明:
|
||
// 1. 先校验最小依赖,并推送一条”正在规划”的状态,避免用户空等;
|
||
// 2. Phase 1(快速评估):不开 thinking,让 LLM 同时产出复杂度评估和规划结果;
|
||
// 3. Phase 2(深度规划):若 LLM 自评需要深度思考且规划已完成,开 thinking 重跑;
|
||
// 4. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history;
|
||
// 5. 最后按 action 推进流程:
|
||
// 5.1 continue:继续停留在 planning;
|
||
// 5.2 ask_user:打开 pending interaction,后续交给 interrupt 收口;
|
||
// 5.3 plan_done:固化完整计划,刷新 pinned context,并进入 waiting_confirm。
|
||
func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||
runtimeState, conversationContext, emitter, err := preparePlanNodeInput(input)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
flowState := runtimeState.EnsureCommonState()
|
||
|
||
// 1. 先发一条阶段状态,让前端知道当前已经进入规划环节。
|
||
if err := emitter.EmitStatus(
|
||
planStatusBlockID,
|
||
planStageName,
|
||
"planning",
|
||
"正在梳理目标并补全执行计划。",
|
||
false,
|
||
); err != nil {
|
||
return fmt.Errorf("规划阶段状态推送失败: %w", err)
|
||
}
|
||
|
||
// 2. 构造本轮规划输入。
|
||
messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput)
|
||
|
||
// 3. Phase 1:快速评估(开 thinking),让 LLM 同时产出复杂度评估和规划结果。
|
||
decision, rawResult, err := infrallm.GenerateJSON[newagentmodel.PlanDecision](
|
||
ctx,
|
||
input.Client,
|
||
messages,
|
||
infrallm.GenerateOptions{
|
||
Temperature: 0.2,
|
||
MaxTokens: 1600,
|
||
Thinking: infrallm.ThinkingModeEnabled,
|
||
Metadata: map[string]any{
|
||
"stage": planStageName,
|
||
"phase": "assessment",
|
||
},
|
||
},
|
||
)
|
||
if err != nil {
|
||
if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" {
|
||
return fmt.Errorf("规划评估解析失败,原始输出=%s,错误=%w", strings.TrimSpace(rawResult.Text), err)
|
||
}
|
||
return fmt.Errorf("规划评估阶段模型调用失败: %w", err)
|
||
}
|
||
if err := decision.Validate(); err != nil {
|
||
return fmt.Errorf("规划评估决策不合法: %w", err)
|
||
}
|
||
|
||
// 4. Phase 2:若 LLM 自评需要深度思考且本轮规划已完成,则开启 thinking 重跑。
|
||
// 条件:NeedThinking=true + Action=plan_done → 说明 LLM 认为当前无 thinking 的计划质量不够。
|
||
// 其他 action(continue / ask_user)不需要 thinking,直接用 Phase 1 结果。
|
||
if decision.NeedThinking && decision.Action == newagentmodel.PlanActionDone {
|
||
if err := emitter.EmitStatus(
|
||
planStatusBlockID,
|
||
planStageName,
|
||
"deep_planning",
|
||
"正在深入思考,生成更完善的计划。",
|
||
false,
|
||
); err != nil {
|
||
return fmt.Errorf("深度规划状态推送失败: %w", err)
|
||
}
|
||
|
||
deepDecision, _, deepErr := infrallm.GenerateJSON[newagentmodel.PlanDecision](
|
||
ctx,
|
||
input.Client,
|
||
messages,
|
||
infrallm.GenerateOptions{
|
||
Temperature: 0.2,
|
||
MaxTokens: 3200,
|
||
Thinking: infrallm.ThinkingModeEnabled,
|
||
Metadata: map[string]any{
|
||
"stage": planStageName,
|
||
"phase": "deep_planning",
|
||
},
|
||
},
|
||
)
|
||
if deepErr == nil && deepDecision != nil {
|
||
if validateErr := deepDecision.Validate(); validateErr == nil {
|
||
decision = deepDecision
|
||
}
|
||
}
|
||
// 深度规划失败时静默降级到 Phase 1 结果,不中断流程。
|
||
}
|
||
|
||
// 5. 若模型先对用户说了话,且不是 ask_user(ask_user 交给 interrupt 收口),则先以伪流式推送,再写回 history。
|
||
if strings.TrimSpace(decision.Speak) != "" && decision.Action != newagentmodel.PlanActionAskUser {
|
||
if err := emitter.EmitPseudoAssistantText(
|
||
ctx,
|
||
planSpeakBlockID,
|
||
planStageName,
|
||
decision.Speak,
|
||
newagentstream.DefaultPseudoStreamOptions(),
|
||
); err != nil {
|
||
return fmt.Errorf("规划文案推送失败: %w", err)
|
||
}
|
||
conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil))
|
||
}
|
||
|
||
// 6. 按规划动作推进流程状态。
|
||
switch decision.Action {
|
||
case newagentmodel.PlanActionContinue:
|
||
flowState.Phase = newagentmodel.PhasePlanning
|
||
return nil
|
||
case newagentmodel.PlanActionAskUser:
|
||
question := resolvePlanAskUserText(decision)
|
||
runtimeState.OpenAskUserInteraction(uuid.NewString(), question, strings.TrimSpace(input.ResumeNode))
|
||
return nil
|
||
case newagentmodel.PlanActionDone:
|
||
// 4.1 直接把结构化 PlanStep 固化到 CommonState,避免 state 层丢失 done_when。
|
||
// 4.2 再把完整自然语言计划写入 pinned context,保证后续 execute 优先看到。
|
||
// 4.3 若 LLM 识别到批量排课意图,把 NeedsRoughBuild 标记写入 CommonState,
|
||
// Confirm 节点后的路由会据此决定是否跳入 RoughBuild 节点。
|
||
// 4.4 最后进入 waiting_confirm,等待用户确认整体计划。
|
||
flowState.FinishPlan(decision.PlanSteps)
|
||
writePlanPinnedBlocks(conversationContext, decision.PlanSteps)
|
||
if decision.NeedsRoughBuild {
|
||
flowState.NeedsRoughBuild = true
|
||
// 以 LLM 决策中的 task_class_ids 为准(若非空则覆盖前端传入值)。
|
||
if len(decision.TaskClassIDs) > 0 {
|
||
flowState.TaskClassIDs = decision.TaskClassIDs
|
||
}
|
||
}
|
||
// always_execute 开启时,计划层跳过确认闸门,直接进入执行阶段。
|
||
// 这样可以与 Execute 节点的“写工具跳过确认”语义保持一致。
|
||
if input.AlwaysExecute {
|
||
// 1. 自动执行模式不会经过 Confirm 卡片,因此这里先把完整计划明确展示给用户。
|
||
// 2. 摘要格式复用 Confirm 节点,保证“手动确认”和“自动执行”两条链路文案一致。
|
||
// 3. 推流后同步写入历史,确保后续 Execute 阶段的上下文也能看到这份计划。
|
||
summary := strings.TrimSpace(buildPlanSummary(decision.PlanSteps))
|
||
if summary != "" {
|
||
if err := emitter.EmitPseudoAssistantText(
|
||
ctx,
|
||
planSummaryBlockID,
|
||
planStageName,
|
||
summary,
|
||
newagentstream.DefaultPseudoStreamOptions(),
|
||
); err != nil {
|
||
return fmt.Errorf("自动执行前计划摘要推送失败: %w", err)
|
||
}
|
||
conversationContext.AppendHistory(schema.AssistantMessage(summary, nil))
|
||
}
|
||
|
||
flowState.ConfirmPlan()
|
||
_ = emitter.EmitStatus(
|
||
planStatusBlockID,
|
||
planStageName,
|
||
"plan_auto_confirmed",
|
||
"计划已自动确认,开始执行。",
|
||
false,
|
||
)
|
||
}
|
||
return nil
|
||
default:
|
||
// 1. LLM 输出了不支持的 action,不应直接报错终止,而应给它修正机会。
|
||
// 2. 使用通用修正函数追加错误反馈,让 Graph 继续循环。
|
||
// 3. LLM 下一轮会看到错误反馈并修正自己的输出。
|
||
llmOutput := decision.Speak
|
||
if strings.TrimSpace(llmOutput) == "" {
|
||
llmOutput = decision.Reason
|
||
}
|
||
AppendLLMCorrectionWithHint(
|
||
conversationContext,
|
||
llmOutput,
|
||
fmt.Sprintf("你输出的 action \"%s\" 不是合法的执行动作。", decision.Action),
|
||
"合法的 action 包括:continue(继续当前步骤)、ask_user(追问用户)、next_plan(推进到下一步)、done(任务完成)。",
|
||
)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func preparePlanNodeInput(input PlanNodeInput) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagentstream.ChunkEmitter, error) {
|
||
if input.RuntimeState == nil {
|
||
return nil, nil, nil, fmt.Errorf("plan node: runtime state 不能为空")
|
||
}
|
||
if input.Client == nil {
|
||
return nil, nil, nil, fmt.Errorf("plan node: plan client 未注入")
|
||
}
|
||
|
||
input.RuntimeState.EnsureCommonState()
|
||
if input.ConversationContext == nil {
|
||
input.ConversationContext = newagentmodel.NewConversationContext("")
|
||
}
|
||
if input.ChunkEmitter == nil {
|
||
input.ChunkEmitter = newagentstream.NewChunkEmitter(newagentstream.NoopPayloadEmitter(), "", "", time.Now().Unix())
|
||
}
|
||
return input.RuntimeState, input.ConversationContext, input.ChunkEmitter, nil
|
||
}
|
||
|
||
func resolvePlanAskUserText(decision *newagentmodel.PlanDecision) string {
|
||
if decision == nil {
|
||
return "我还缺一点关键信息,想先向你确认一下。"
|
||
}
|
||
if strings.TrimSpace(decision.Speak) != "" {
|
||
return strings.TrimSpace(decision.Speak)
|
||
}
|
||
if strings.TrimSpace(decision.Reason) != "" {
|
||
return strings.TrimSpace(decision.Reason)
|
||
}
|
||
return "我还缺一点关键信息,想先向你确认一下。"
|
||
}
|
||
|
||
func writePlanPinnedBlocks(ctx *newagentmodel.ConversationContext, steps []newagentmodel.PlanStep) {
|
||
if ctx == nil {
|
||
return
|
||
}
|
||
|
||
fullPlanText := buildPinnedPlanText(steps)
|
||
if strings.TrimSpace(fullPlanText) != "" {
|
||
ctx.UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||
Key: planPinnedKey,
|
||
Title: planFullPlanTitle,
|
||
Content: fullPlanText,
|
||
})
|
||
}
|
||
|
||
if len(steps) == 0 {
|
||
return
|
||
}
|
||
|
||
firstStep := strings.TrimSpace(steps[0].Content)
|
||
if strings.TrimSpace(steps[0].DoneWhen) != "" {
|
||
firstStep = fmt.Sprintf("%s\n完成判定:%s", firstStep, strings.TrimSpace(steps[0].DoneWhen))
|
||
}
|
||
ctx.UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||
Key: planCurrentStepKey,
|
||
Title: planCurrentStepTitle,
|
||
Content: firstStep,
|
||
})
|
||
}
|
||
|
||
func buildPinnedPlanText(steps []newagentmodel.PlanStep) string {
|
||
if len(steps) == 0 {
|
||
return ""
|
||
}
|
||
|
||
lines := make([]string, 0, len(steps))
|
||
for i, step := range steps {
|
||
content := strings.TrimSpace(step.Content)
|
||
if content == "" {
|
||
continue
|
||
}
|
||
|
||
line := fmt.Sprintf("%d. %s", i+1, content)
|
||
if strings.TrimSpace(step.DoneWhen) != "" {
|
||
line += fmt.Sprintf("\n完成判定:%s", strings.TrimSpace(step.DoneWhen))
|
||
}
|
||
lines = append(lines, line)
|
||
}
|
||
return strings.TrimSpace(strings.Join(lines, "\n\n"))
|
||
}
|