后端: 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永远只适合做选择题、判断题,不适合做开放创新题。
675 lines
24 KiB
Markdown
675 lines
24 KiB
Markdown
# 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 恢复 PendingConfirmTool,Phase=executing
|
||
- reject(有 PendingTool):不恢复 PendingConfirmTool,Phase=executing(LLM 换方案)
|
||
- 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`)
|
||
|
||
**职责**:生成最终总结
|
||
|
||
- 调 LLM(temperature=0.5, max_tokens=800)生成总结
|
||
- 失败时降级到机械格式化(逐条列出步骤 + 完成标记)
|
||
- 完成后调用 deleteAgentState() 清理 Redis 快照
|
||
|
||
---
|
||
|
||
## 五、LLM 交互模式
|
||
|
||
### 5.1 统一 JSON 协议
|
||
|
||
所有 LLM 输出都是严格 JSON,不是纯文本。每个阶段有自己的合约:
|
||
|
||
**Plan 合约** (`model/plan_contract.go`):
|
||
```json
|
||
{
|
||
"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`):
|
||
```json
|
||
{
|
||
"speak": "...",
|
||
"action": "continue|ask_user|confirm|next_plan|done",
|
||
"reason": "...",
|
||
"goal_check": "(next_plan/done 时必填)",
|
||
"tool_call": {"name": "工具名", "arguments": {...}}
|
||
}
|
||
```
|
||
|
||
### 5.2 JSON 解析容错
|
||
|
||
`llm/json.go` 的 `ParseJSONObject` 能处理:
|
||
- 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` 驱动,具体阶段只负责填充各自的 `Msg2Content`、`Msg3StageState` 和 `UserInstruction`。
|
||
|
||
### 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 → Plan(continue,继续规划)
|
||
executing → Execute(不应该发生,但防御性路由)
|
||
```
|
||
|
||
### branchAfterConfirm
|
||
```
|
||
if PendingInteraction → Interrupt
|
||
else → Execute(确认通过)
|
||
```
|
||
|
||
### branchAfterExecute
|
||
```
|
||
if PendingInteraction → Interrupt
|
||
else switch Phase:
|
||
executing → Execute(继续循环)
|
||
done → Deliver
|
||
waiting_confirm → Confirm(不应该发生,防御性路由)
|
||
```
|
||
|
||
### 关键保护机制
|
||
|
||
所有分支函数都以 `branchIfInterrupted()` 开头:
|
||
```go
|
||
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. 构建 AgentGraphRequest(ConfirmAction 从 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` |
|