Files
smartmate/backend/newAgent/ARCHITECTURE.md
Losita d8280cc647 Version: 0.9.26.dev.260417
后端:
1. Prompt 层从 execute 专属骨架重构为全节点统一四段式 buildUnifiedStageMessages
  - 新增 unified_context.go:定义 StageMessagesConfig + buildUnifiedStageMessages 统一骨架,所有节点(Chat/Plan/Execute/Deliver/DeepAnswer)共用同一套 msg0~msg3 拼装逻辑
  - 新增 conversation_view.go:通用对话历史渲染 buildConversationHistoryMessage,各节点复用,不再各自维护提取逻辑
  - 新增 chat_context.go / plan_context.go / deliver_context.go:各节点自行渲染 msg1(对话视图)和 msg2(工作区),统一层只负责"怎么拼",不再替节点决定"放什么"
  - Chat/Plan/Deliver/Execute 的 BuildXXXMessages 全部从 buildStageMessages 切到 buildUnifiedStageMessages,移除旧路径
  - 删除 execute_pinned.go:execute 记忆渲染合并到统一层 renderUnifiedMemoryContext
  - Plan prompt 不再在 user prompt 中拼装任务类 ID 列表和 renderStateSummary,改为依赖 msg2 规划工作区;Chat 粗排判断从"上下文有任务类 ID"改为"批量调度需求"
  - Deliver prompt 新增 IsAborted/IsExhaustedTerminal 区分,支持粗排收口和主动终止场景
2. Execute ReAct 上下文简化——移除归档搬运、窗口裁剪和重复工具压缩
  - 移除 splitExecuteLoopRecordsByBoundary、findLatestExecuteBoundaryMarker、tailExecuteLoops、compressExecuteLoopObservationsByTool、buildEarlyExecuteReactSummary、trimExecuteMessage1ByBudget 等六个函数
  - 移除 executeLoopWindowLimit / executeConversationTurnLimit / executeMessage1MaxRunes 等预算常量
  - msg1 不再从历史中归档上一轮 ReAct 结果,只保留真实对话流(user + assistant speak),全量注入
  - msg2 不再按 loop_closed / step_advanced 边界切分"归档/活跃",直接全量注入全部 ReAct Loop 记录
  - token 预算由统一压缩层兜底,prompt 层不再做提前裁剪
3. 压缩层从 Execute 专属提升为全节点通用 UnifiedCompact
  - 删除 execute_compact.go(Execute 专属压缩文件)
  - 新增 unified_compact.go:UnifiedCompactInput 参数化,各节点(Plan/Chat/Deliver/Execute)构造时从自己的 NodeInput 提取公共字段,消除对 Execute 的直接依赖
  - CompactionStore 接口扩展 LoadStageCompaction / SaveStageCompaction,各节点按 stageKey 独立维护压缩状态互不覆盖
  - 非 4 段式消息时退化成按角色汇总统计,确保 context_token_stats 仍然刷新
4. Retry 重试机制全面下线
  - dao/agent.go:saveChatHistoryCore / SaveChatHistory / SaveChatHistoryInTx 移除 retry_group_id / retry_index /
  retry_from_user_message_id / retry_from_assistant_message_id 四个参数,修复乱码注释
  - dao/agent-cache.go:移除 ApplyRetrySeed 和 extractMessageHistoryID 两个方法
  - conv/agent.go:ToEinoMessages 不再回灌 retry_* 字段到运行期上下文
  - service/agentsvc/agent.go:移除 chatRetryMeta 及 resolveRetryGroupID / buildRetrySeed 等全部重试逻辑
  - service/agentsvc/agent_quick_note.go:整个文件删除(retry 快速补写路径已无用)
  - service/events/chat_history_persist.go:移除 retry 参数传递
5. 节点层瘦身 + 可见消息逐条持久化
  - agent_nodes.go 大幅简化:Chat/Plan/Execute/Deliver 节点方法移除 ToolSchema 注入、状态摘要渲染等逻辑,只做参数转发和状态落盘
  - 新增 visible_message.go:persistVisibleAssistantMessage 统一处理可见 assistant speak 的实时持久化,失败仅记日志不中断主流程
  - 新增 llm_debug.go:logNodeLLMContext 统一打印 LLM 上下文调试日志
  - graph_run_state.go 新增 PersistVisibleMessageFunc 类型 + AgentGraphDeps.PersistVisibleMessage 字段
  - service/agentsvc/agent_newagent.go 精简主循环,注入 PersistVisibleMessage 回调;agent_history.go 精简历史构建
  - token_budget.go 移除 Execute 专属预算检查,统一到通用预算

前端:
1. 移除 retry 相关 UI 和类型
  - agent.ts 移除 retry_group_id / retry_index / retry_total 字段及 normalize 逻辑
  - AssistantPanel.vue 移除 retry 相关 UI 和交互代码(约 700 行精简)
  - dashboard.ts 移除 retry 相关类型定义
  - AssistantView.vue 微调
2. ContextWindowMeter 压缩次数展示和数值格式优化
  - 新增 formatCompactCount 工具函数,千位以上用 k 单位压缩(如 80k)
  - 新增压缩次数显示
3.修复了新对话发消息时,user和assistant消息被自动调换的bug

仓库:无
2026-04-17 22:19:38 +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