Files
smartmate/docs/backend/ARCHITECTURE.md
Losita 66c06eed0a Version: 0.9.45.dev.260427
后端:
1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜
2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写
3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态
4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分
5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定

前端:
6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作

仓库:
7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件

PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
2026-04-27 01:09:37 +08:00

24 KiB
Raw Blame History

NewAgent 架构全景

本文档帮助读者建立对 newAgent 的完整心智模型,从宏观到微观逐层展开。


一、一句话概括

newAgent 是一个 状态机驱动的有向图:用户消息进入 Chat 节点,经过意图分类、计划生成、用户确认、工具执行、最终交付,每一步由 Phase 和 PendingInteraction 驱动路由。


二、宏观架构

2.1 目录结构

newAgent/
├── graph/           图骨架:节点注册、边连线、分支路由
├── model/           数据模型:状态、合约、接口定义
├── node/            节点实现:每个节点的业务逻辑
├── prompt/          提示词:每个阶段的 system prompt 和用户 prompt 构造
├── llm/             LLM 客户端文本生成、JSON 解析、流式适配
├── stream/          SSE 输出伪流式推送、OpenAI 兼容格式
├── tools/           工具层10 个排程工具 + 注册表
├── shared/          公共工具:重试、时区
├── router/          路由(当前为空,路由逻辑在 graph/ 中)
├── ROADMAP.md       改造计划
└── ARCHITECTURE.md  本文档

2.2 图结构

START
  │
  v
 Chat ──────┬── 意图=chat ────→ END直接回复
  │         │
  │         └── 意图=task ──→ Plan ──┬── continue ──→ Plan继续规划
  │                                │      │
  │                                │      ├── ask_user ──→ Interrupt ──→ END
  │                                │      │
  │                                │      └── plan_done ──→ Confirm
  │                                │                │
  │                                │                ├── 有计划 → 确认卡片
  │                                │                │          │
  │                                │                │          v
  │                                │                │     Interrupt ──→ END
  │                                │                │
  │                                │                └── 有 PendingConfirmTool → 确认卡片
  │                                                       │
  │                                                       v
  │                                                  Interrupt ──→ END
  │
  │  [用户确认后重新进入图]
  │
  v
 Chat(resume) ──┬── accept → 恢复 PendingConfirmTool → Execute
               │
               └── reject → 回到 planning 或 executing

Execute ──┬── continue(读工具) ──→ Execute继续 ReAct
          ├── continue(无工具) ──→ Execute
          ├── confirm(写工具) ──→ Confirm ──→ Interrupt ──→ END
          ├── ask_user ──→ Interrupt ──→ END
          ├── next_plan ──┬── 有剩余步骤 → Execute
          │                └── 无剩余步骤 → Deliver
          ├── done ──→ Deliver
          └── 轮次耗尽 ──→ Deliver强制

Deliver ──→ END最终总结清理持久化快照

2.3 一次完整排课的请求序列

请求1: 用户发 "帮我安排下周的复习"
  → Chat(intent=task) → Plan(plan_done, 2步计划) → Confirm → Interrupt → END
  前端展示确认卡片

请求2: 用户点 "确认"
  → Chat(resume, accept) → 确认通过 → Execute(读工具 get_overview)
  → Execute(读工具 find_free)
  → Execute(写工具 place → confirm) → Confirm → Interrupt → END
  前端展示写操作确认卡片

请求3: 用户点 "确认"
  → Chat(resume, accept) → Execute(执行 pending tool place) → 持久化
  → Execute(next_plan → 下一步)
  → Execute(done) → Deliver → END
  前端展示最终总结

三、状态模型

理解 newAgent 的关键在于理解 什么东西在什么时机变化

3.1 三个核心状态对象

┌─────────────────────┐   持久化到 Redis
│   AgentRuntimeState  │   ← StateStore.Save/Load
│                     │
│  ┌───────────────┐  │
│  │ CommonState   │  │   ← 每个节点都可能修改
│  │ - Phase       │  │
│  │ - PlanSteps   │  │
│  │ - CurrentStep │  │
│  │ - RoundUsed   │  │
│  └───────────────┘  │
│                     │
│  PendingInteraction │   ← 确认/追问 的交互快照
│  PendingConfirmTool │   ← Execute→Confirm 的临时邮箱
└─────────────────────┘

┌─────────────────────────┐   不持久化,每次请求重建
│  ConversationContext    │
│  - SystemPrompt         │   ← 各节点 prompt 函数构造
│  - History []*Message   │   ← 对话历史assistant+tool 配对)
│  - PinnedBlocks         │   ← 置顶上下文(计划、工具摘要)
│  - ToolSchemas          │   ← 工具 schema 注入
└─────────────────────────┘

┌─────────────────────────┐   懒加载,首次 Execute 时读取
│  ScheduleState          │
│  - Window (天数+映射)    │
│  - Tasks []ScheduleTask │   ← 工具操作的数据源
└─────────────────────────┘

3.2 Phase 状态转换

PhasePlanning       Plan 节点 plan_done + 用户确认后
    │
    v
PhaseExecuting      Execute 节点执行中
    │
    ├──→ PhaseWaitingConfirm   Execute 输出 action=confirm
    │        │
    │        v  用户确认
    │    PhaseExecuting          恢复继续执行
    │
    ├──→ PhaseDone              Execute 输出 done 或所有步骤完成
    │
    └──→ PhaseInterrupted       被中断(追问/确认等待用户输入)

3.3 PendingInteraction 生命周期

场景 A: 计划确认
  Plan → plan_done → Confirm 节点 → OpenConfirmInteraction(type="confirm")
  → Interrupt 展示 → END
  → 用户确认 → Chat(resume) → ResumeFromPending() → Phase=executing

场景 B: 写操作确认
  Execute → action=confirm → 设置 PendingConfirmTool → Confirm 节点
  → OpenConfirmInteraction(type="confirm", PendingTool=快照)
  → Interrupt 展示 → END
  → 用户确认 → Chat(resume) → PendingConfirmTool 从快照恢复 → Execute 执行工具

场景 C: 追问
  Execute → action=ask_user → OpenAskUserInteraction(question)
  → Interrupt 展示 → END
  → 用户回复 → Chat(resume) → Phase 回到 executing

3.4 PendingConfirmTool 临时邮箱

这个字段 不持久化,只在单次图运行中存在:

Execute(action=confirm)
  → PendingConfirmTool = {ToolName, ArgsJSON, Summary}
  → Phase = waiting_confirm
  → Confirm 节点读取 → 转入 PendingInteraction.PendingTool
  → PendingConfirmTool 被清空

用户确认后重新进入图:
  → Chat(resume) → 从 PendingInteraction.PendingTool 恢复到 PendingConfirmTool
  → Execute 发现 PendingConfirmTool 非空 → 直接执行工具 → 清空

四、各节点详解

4.1 Chat 节点 (node/chat.go)

职责:入口分流 + 中断恢复

两条路径

  1. 首次进入:调 LLM 做意图分类("chat" / "task"chat 直接回复task 转到 Plan
  2. 中断恢复:读取 PendingInteraction根据类型ask_user / confirm走不同恢复路径

关键逻辑

if HasPendingInteraction():
    handleChatResume()     // 不调 LLM
else:
    chatIntentDecision()   // 调 LLM 做意图分类

confirm resume 的 accept/reject 处理

  • accept从 PendingInteraction.PendingTool 恢复 PendingConfirmToolPhase=executing
  • reject有 PendingTool不恢复 PendingConfirmToolPhase=executingLLM 换方案)
  • reject无 PendingTool调用 RejectPlan()Phase=planning回到规划

4.2 Plan 节点 (node/plan.go)

职责LLM 生成结构化计划

两阶段 LLM 调用

  1. Phase 1 快速评估temperature=0.2, max_tokens=1600, thinking=关闭
    • 输出 PlanDecision判断 Complexity(simple/moderate/complex)
  2. Phase 2 深度规划(仅 complex 任务触发thinking=开启, max_tokens=3200
    • 生成更详细的 PlanStep 列表(含 DoneWhen 完成判定条件)

三种 action

  • continue:继续规划(多轮对话中补充信息)
  • ask_user:追问用户
  • plan_done:规划完成,输出 PlanSteps

计划写入 PinnedBlocks:用 UpsertPinnedBlock 把计划文本注入 ConversationContext后续 Execute 阶段自动带入。

4.3 Confirm 节点 (node/confirm.go)

职责:创建确认卡片,不调 LLM

两种确认

  1. 计划确认Phase=waiting_confirm, PendingConfirmTool 为空):格式化计划摘要,创建 PendingInteraction
  2. 工具确认PendingConfirmTool 非空):格式化工具操作摘要,把 PendingTool 快照转入 PendingInteraction

关键Confirm 节点执行后PendingConfirmTool 被清空(数据已转移到 PendingInteraction.PendingTool

4.4 Execute 节点 (node/execute.go)

职责LLM 主导的 ReAct 循环,这是最复杂的节点。

入口判断优先级

1. PendingConfirmTool 非空 → executePendingTool() → 结束
2. 无有效 PlanStep → 报错
3. 正常 ReAct → 调 LLM → 处理决策

LLM 调用参数temperature=0.3, max_tokens=1200, thinking=开启

JSON 解析失败处理correction 机制):

LLM 输出非 JSON:
  → ConsecutiveCorrections++
  → 追加修正消息到历史
  → return nil图循环回来LLM 看到修正消息后重试)
  → 连续 3 次失败 → 返回硬错误,终止

五种 action 处理

action 行为 工具执行?
continue + tool_call 读工具直接执行 executeToolCall()
continue 无 tool 仅说话,继续循环
confirm 暂存 PendingConfirmTool 否,等用户确认
ask_user 打开追问
next_plan 推进步骤
done 结束所有步骤

工具执行后历史消息格式

assistant message: {Role: "assistant", ToolCalls: [{ID, Function: {Name, Arguments}}]}
tool message:     {Role: "tool", ToolCallID: <匹配ID>, Content: "工具结果"}

这对消息必须配对,否则 OpenAI 兼容 API 会拒绝请求。

轮次预算MaxRounds 默认 30耗尽强制进入 Deliver。

4.5 Interrupt 节点 (node/interrupt.go)

职责:向用户展示消息后暂停图执行

三种类型

  • ask_user伪流式展示 DisplayText
  • confirm展示确认状态
  • 默认:展示通用中断信息

4.6 Deliver 节点 (node/deliver.go)

职责:生成最终总结

  • 调 LLMtemperature=0.5, max_tokens=800生成总结
  • 失败时降级到机械格式化(逐条列出步骤 + 完成标记)
  • 完成后调用 deleteAgentState() 清理 Redis 快照

五、LLM 交互模式

5.1 统一 JSON 协议

所有 LLM 输出都是严格 JSON不是纯文本。每个阶段有自己的合约

Plan 合约 (model/plan_contract.go)

{
  "speak": "...",
  "action": "continue|ask_user|plan_done",
  "reason": "...",
  "complexity": "simple|moderate|complex",
  "need_thinking": false,
  "plan_steps": [{"content": "...", "done_when": "..."}]
}

Execute 合约 (model/execute_contract.go)

{
  "speak": "...",
  "action": "continue|ask_user|confirm|next_plan|done",
  "reason": "...",
  "goal_check": "next_plan/done 时必填)",
  "tool_call": {"name": "工具名", "arguments": {...}}
}

5.2 JSON 解析容错

llm/json.goParseJSONObject 能处理:

  • LLM 在 JSON 前后附带文字 → 提取中间的 JSON 对象
  • Markdown 代码块包裹(json ... )→ 剥离
  • 嵌套对象(大括号配对计数)

5.3 Correction 循环

当 LLM 输出非法 JSON 时:

1. 原始输出作为 assistant 消息追加到历史
2. 修正提示作为 user 消息追加到历史
3. return nil → 图循环回来
4. LLM 看到修正消息,下一轮输出合法 JSON
5. ConsecutiveCorrections 重置为 0
6. 连续 3 次失败 → 硬错误终止

六、工具系统

6.1 数据模型

ScheduleState 是工具操作的唯一数据源:

ScheduleState
├── Window                    时间窗口
│   ├── TotalDays             总天数(如 5 或 7
│   └── DayMapping[]          day_index → (week, day_of_week) 映射
└── Tasks[]                   扁平任务列表
    ├── source="event"        来自日程表的已有课程/任务
    │   ├── Slots[]           压缩的时段范围
    │   ├── CanEmbed          是否允许嵌入
    │   └── Locked            是否锁定(不可移动)
    └── source="task_item"    来自任务类的待安排任务
        ├── Duration          需要的连续时段数
        ├── CategoryID        所属 TaskClass.ID
        └── Status="pending"  待安排

6.2 10 个工具

读工具(直接执行,不需要确认)

工具 用途 典型调用时机
get_overview 全局概览:天数、占用统计、可嵌入、待安排 LLM 需要了解全局
query_range 查询指定天/时段的详情 LLM 需要具体位置信息
find_free 查找连续空闲时段 LLM 需要找空位放任务
list_tasks 按条件列出任务 LLM 需要筛选任务
get_task_info 单个任务详情(含嵌入关系) LLM 需要具体任务信息

写工具(需用户确认)

工具 用途 关键逻辑
place 放置 pending 任务到时段 自动检测嵌入CanEmbed=true 的宿主)
move 移动已有任务到新位置 冲突检测(排除自身)
swap 交换两个等时长任务的时段 冲突时自动回滚
batch_move 批量移动多个任务 原子性:任一冲突全部回滚
unplace 取消放置,恢复 pending 清理双向嵌入关系

6.3 工具执行流程

读工具action=continue + tool_call

Execute → executeToolCall() → registry.Execute() → 追加 assistant+tool 消息对 → return nil → 图循环

写工具action=confirm

Execute → handleExecuteActionConfirm() → 暂存 PendingConfirmTool
→ Confirm 节点 → Interrupt → 用户确认
→ Chat(resume) → 恢复 PendingConfirmTool
→ Execute → executePendingTool() → registry.Execute() + persistor + 追加消息对

6.4 持久化路径

工具执行成功
  → DiffScheduleState(original, modified) → []ScheduleChange
  → PersistScheduleChanges(事务)
    → applyPlaceChange / applyMoveChange / applyUnplaceChange

当前限制applyPlaceChange 只处理 source="event"source="task_item" 会报错。详见 ROADMAP.md P0 缺口。


七、SSE 输出系统

7.1 ChunkEmitter

所有节点通过 ChunkEmitter 向前端推送事件:

EmitPseudoAssistantText()  → 伪流式文本(分段推送,模拟打字效果)
EmitStatus()                → 状态推送("正在执行第2步"
EmitConfirmRequest()        → 确认卡片
EmitFinish() / EmitDone()   → 结束标记

7.2 伪流式

LLM 的一次性文本输出通过 SplitPseudoStreamText 拆分成多个 chunk

  • 按中英文标点断句
  • 每个 chunk 8~24 个字符
  • 间隔 40ms 推送

7.3 OpenAI 兼容格式

stream/openai.go 定义了 OpenAI 兼容的 SSE 格式,通过 ext 字段扩展:

  • reasoning_text:思考过程
  • assistant_text:正文
  • status:状态更新
  • tool_call / tool_result:工具调用
  • confirm_request:确认卡片
  • interrupt:中断消息

八、持久化模型

8.1 三个持久化层次

层级 机制 何时触发 存什么
快照 AgentStateStore (Redis) Plan/Confirm/Execute 节点后 AgentRuntimeState + ConversationContext
变更 SchedulePersistor (MySQL) 写工具执行后 ScheduleState 的 diff
历史 Redis + MySQL 图运行完成后 完整对话历史

8.2 快照恢复流程

用户发送新消息(图需要从中断恢复)
  → loadOrCreateRuntimeState()
    → StateStore.Load(conversationID)
    → 如果存在:恢复 RuntimeState + ConversationContext
    → 如果不存在:创建全新状态

快照在 Deliver 后被 deleteAgentState() 清理。


九、Prompt 体系

9.1 prompt 构造模式

所有阶段现在统一共享 buildUnifiedStageMessages() 函数:

msg0(system)    = 全局 system prompt + 阶段 system prompt + 工具简表
msg1(assistant) = 对话历史 + 归档摘要
msg2(assistant) = 阶段工作区
msg3(system)    = 阶段状态 + 记忆 + 本轮指令

统一构造由 StageMessagesConfig 驱动,具体阶段只负责填充各自的 Msg2ContentMsg3StageStateUserInstruction

9.2 各阶段 prompt 要点

阶段 核心指令 关键约束
Chat 分类意图chat vs task 保守默认为 task
Plan 两阶段:快速评估 + 深度规划 简单任务不开启 thinking
Execute ReAct思考→执行→观察 goal_check 为 next_plan/done 必填
Deliver 总结计划执行结果 失败降级到机械格式化

9.3 置顶上下文块

PinnedBlocks 是跨节点共享的上下文,通过 Key 去重:

execution_context  ← Execute 节点注入(当前步骤、完成判定等)
plan                ← Plan 节点注入(完整计划文本)
tool_summary        ← Execute 节点注入(可用工具摘要)

十、图路由逻辑 (graph/common_graph.go)

路由函数是图的核心控制逻辑,决定了每步之后走向哪个节点:

branchAfterChat

if PendingInteraction → Interrupt
else switch Phase:
  planning → Plan
  executing → Execute
  done → Deliver
  chatting → END

branchAfterPlan

if PendingInteraction → Interrupt
else switch Phase:
  waiting_confirm → Confirm
  planning → Plancontinue继续规划
  executing → Execute不应该发生但防御性路由

branchAfterConfirm

if PendingInteraction → Interrupt
else → Execute确认通过

branchAfterExecute

if PendingInteraction → Interrupt
else switch Phase:
  executing → Execute继续循环
  done → Deliver
  waiting_confirm → Confirm不应该发生防御性路由

关键保护机制

所有分支函数都以 branchIfInterrupted() 开头:

func branchIfInterrupted(st *AgentGraphState) string {
    if st.RuntimeState.HasPendingInteraction() {
        return "interrupt"
    }
    return ""
}

这确保任何节点设置了 PendingInteraction 后,图都会走向 Interrupt 节点展示给用户。


十一、Service 集成层 (service/agentsvc/agent_newagent.go)

入口函数runNewAgentGraph

1. 规范化 conversationID, modelName
2. 确保会话存在Redis 缓存 → DB
3. 构建重试元数据
4. 加载或创建 RuntimeState从 Redis 快照恢复)
5. 构建 AgentGraphRequestConfirmAction 从 extra 取)
6. 包装 Ark 客户端
7. 创建 SSE 适配器 + ChunkEmitter
8. 组装 AgentGraphDeps注入所有依赖
9. 调用 RunAgentGraph()
10. 持久化对话历史到 Redis + MySQL
11. 发送 [DONE] 标记,触发异步标题生成

依赖注入

cmd/start.go:
  → NewScheduleProvider(scheduleDAO, taskClassDAO)  → SetScheduleProvider()
  → NewSchedulePersistorAdapter(repoManager)         → SetSchedulePersistor()
  → NewDefaultRegistry()                              → SetToolRegistry()
  → NewRedisStateStore(cacheDAO)                      → SetAgentStateStore()

十二、如何调试

12.1 日志关键字

搜索关键字 含义
[DEBUG] execute LLM Execute 节点的 LLM 原始输出和解析结果
[DEBUG] plan LLM Plan 节点的 LLM 输出
[WARN] execute 决策不合法 LLM 输出合法 JSON 但 action 不合法
[DEBUG] execute LLM 输出解析失败 JSON 解析失败,触发 correction
PersistScheduleChanges 持久化调用
loadOrCreateRuntimeState 状态恢复/创建

12.2 常见问题排查

SSE 断开

  1. 检查 [DEBUG] execute LLM 日志,看 LLM 输出是否为合法 JSON
  2. 如果输出 [NEXT_PLAN] 等纯文本 → prompt 问题(已修复,参考 execute.go 的 correction 机制)
  3. 如果输出合法 JSON 但 action 不对 → 检查 prompt 的合约文本

工具不执行

  1. 检查 PendingConfirmTool 是否被正确设置和恢复
  2. 检查 ScheduleState 是否为 nil可能 ScheduleProvider 未注入)
  3. 检查 history 中 assistant+tool 消息是否配对ToolCallID 是否匹配)

图循环不退出

  1. 检查 ConsecutiveCorrections 计数(可能 LLM 反复输出非法 JSON
  2. 检查 RoundUsed 是否耗尽MaxRounds 默认 30
  3. 检查 Phase 是否卡在某个状态

12.3 单元测试

node/execute_confirm_flow_test.go   → 7 个测试,覆盖完整 confirm 回路
node/llm_tool_orchestration_test.go → 5 个测试,覆盖真实排课场景

测试使用 mock LLM预定义 JSON 响应序列)和 mock 工具注册表,不依赖外部服务。


十三、关键设计决策及理由

决策 理由
Phase 驱动路由而非硬编码序列 同一个图支持多种流程直接聊天、排课、追问恢复Phase 是最小状态信号
PendingInteraction 作为中断快照 图是无状态的(每次请求重新运行),需要一种机制跨请求传递"等用户回复"的上下文
PendingConfirmTool 作为临时邮箱 Execute 和 Confirm 之间不能直接传参(中间隔了 Interrupt+END+Chat用运行态字段传递
JSON 协议而非文本标记 LLM 输出结构化数据,后端用泛型解析,避免正则匹配的不确定性
Correction 机制 LLM 不是 100% 可靠,需要给修正机会,但限制最大连续次数避免死循环
伪流式而非真流式 LLM API 的一次性返回更适合分段推送,真流式实现复杂且收益低
工具操作扁平 ScheduleState 避免嵌套数据结构,工具只需关心"在哪里放什么"
Diff 持久化 只持久化变更部分,减少 DB 操作,支持原子性
PinnedBlocks 注入上下文 计划、工具摘要等信息不需要每轮都重复,用置顶块注入一次即可

十四、关键文件速查

想了解... 看这个文件
图怎么连的 graph/common_graph.go
每个节点怎么被调用的 node/agent_nodes.go
Chat 怎么分类意图的 prompt/chat.go + node/chat.go
Plan 怎么生成计划的 prompt/plan.go + node/plan.go
Execute 的 ReAct 循环 node/execute.go
confirm 回路怎么转的 node/confirm.go + node/chat.go(handleConfirmResume)
LLM 输出什么格式 model/plan_contract.go + model/execute_contract.go
JSON 解析怎么容错的 llm/json.go
correction 怎么追回的 node/correction.go
工具怎么注册和执行的 tools/registry.go
工具操作什么数据 tools/state.go
SSE 输出什么格式 stream/openai.go
状态怎么持久化的 model/state_store.go + conv/schedule_persist.go
日程数据怎么加载的 conv/schedule_provider.go + conv/schedule_state.go
Service 怎么组装的 service/agentsvc/agent_newagent.go
API 怎么调用的 api/agent.go
距离全链路还差什么 ROADMAP.md