Ctrl K
ComunidadGuías…

docs/plan/technical-design/delegate-followup-design.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 子代理追问(delegate_followup)技术方案

> 状态:需求已与用户逐项确认,本文为可执行方案(未改任何代码)
> 关联文档:[subagent-visibility.md](./subagent-visibility.md)、[2026-08-04-loop-compaction-reuse-session-design.md](../2026-08-04-loop-compaction-reuse-session-design.md)

## 1. 需求背景

当前主代理通过内置工具 `delegate(agent_type, task)` 委派子代理执行子任务。**每次调用都新建一个 `SUBAGENT` 子会话**:新会话只有本次 `task` 一条 USER 消息,子代理需要从零开始重新阅读项目文件、重新审查,无法复用上一次的审查结论与过程上下文。

典型痛点场景(代码审查闭环):

1. 主代理 `delegate` 一个 reviewer 子代理审查代码,子代理返回审查结果;
2. 主代理根据审查结果修复了 bug;
3. 主代理希望**同一个 reviewer 子代理**继续基于上次审查结论,检查修复情况并继续审查新问题(增量审查);
4. 现状只能再 `delegate` 一次 → 新建子会话 → 子代理丢掉上次全部结论,被迫全量重扫,浪费 token 且结论可能漂移。

需求目标:**在保留「新建子代理」能力的同时,新增「对已有子代理会话追问/续查」能力**,使「委派 → 修复 → 续查 → 再修复 → 再续查」成为连续过程。

## 2. 需求描述

### 2.1 功能定义

新增内置工具 `delegate_followup(child_session_id, task)`:

- 主代理指定一个**已存在且属于当前父会话**的子代理会话(`child_session_id` 来自上次 `delegate` 返回结果),向其追加一条追问 USER 消息,并**复用该会话的完整历史上下文**重新执行一轮 AgentLoop;
- 子代理因此能看到自己上次的审查结论、过程工具输出,结合主代理在 `task` 中描述的修复内容做**增量核查**,而非全量重扫;
- 追问结果以与普通聊天一致的形式(USER 消息 + ASSISTANT 输出)落库并呈现在子代理 Tab 内;
- `delegate`(新建)与 `delegate_followup`(追问)**并存**:全新任务走 `delegate`,基于上次结果续查走 `delegate_followup`。

### 2.2 目标用户流程

```
主代理 ──delegate(reviewer, 全量审查)──▶ 子会话A(审查完成,返回结果)
主代理 ──修复 bug──────────────────────▶ 工作区文件变化
主代理 ──delegate_followup(A, 已修复以上问题,请核查并继续审查)──▶ 子会话A 复用历史,增量核查
主代理 ──再次修复──────────────────────▶ ...
主代理 ──delegate_followup(A, ...)────▶ 子会话A 继续
```

### 2.3 已确认决策(与用户逐项确认)

| # | 决策点 | 结论 |
|---|--------|------|
| 1 | 使用入口 | 仅主代理工具层;子代理 Tab 保持只读,用户不直接输入 |
| 2 | 工具形态 | 新增独立工具 `delegate_followup`,`delegate` 不改动 |
| 3 | 追问边界 | 仅禁止 RUNNING(防御);COMPLETED / FAILED / CANCELLED 均可追问 |
| 4 | 归属校验 | `child_session_id` 必须属于当前父会话且 `session_type='SUBAGENT'`,否则拒绝 |
| 5 | 增量引导 | 主代理在 `task` 中描述修复内容与核查重点;子代理保留上次全部上下文,prompt 引导做增量核查、聚焦变更点、避免全量重扫;允许用文件/git 工具核实实际改动 |
| 6 | 轮次展示 | 追问内容以普通 USER 消息呈现,与聊天记录一致;前端不做特殊轮次视觉(无分隔条、无徽标) |
| 7 | 轮次治理 | 单个子会话追问次数不设上限,依赖现有 per-session 压缩机制控制上下文 |
| 8 | 审计 | 每轮(首轮 + 每次追问)在 `subagent_execution` 插入一条独立记录 |

## 3. 现状分析

### 3.1 现有链路(`DelegateTool`)

`backend/src/main/java/cn/etarch/mao/harness/tool/impl/DelegateTool.java` 执行流程:

1. 解析 `agent_type` / `task`,校验子代理定义存在;
2. `sessionService.createSession(...)` 新建子会话,置 `parentSessionId=当前会话`、`sessionType='SUBAGENT'`、`phase=null`;
3. LOCAL 模式注册 `LocalToolSessionRegistry`;
4. 插入 `subagent_execution` 记录(`status=RUNNING`,含 task、tokens、rounds 字段);
5. `sessionService.saveMessage(childId, "USER", task, ...)` 保存任务消息;
6. `visibilityService.notifySubagentCreated(...)` 通知前端并 auto-subscribe(带 toolCallId 便于并行委派绑定);
7. `buildSubContext(childSession, definition)` 构建子上下文:
   - 复用 `HarnessService.buildContext(childSessionId)`(**自动加载该会话 boundary 之后的全部消息**:USER 任务 + assistant 推理 + 工具调用/结果 + 终稿);
   - 覆盖 system prompt(`systemPromptOverride`)和 agentName;
   - 工具过滤:始终排除 `delegate`(防递归),再按白名单/排除表过滤;清除 skills;
8. `registerCancelFlag(childId)` 并继承父会话 cancel 标志;
9. `visibilityService.executeVisible(childSession, subContext, skip)`:置 phase=RUNNING、推送 WS 事件、组合 `WsStreamingEventListener` + `SubAgentResultCollector`,同步执行 `harnessService.executePrepared(...)`(AgentLoop 流式 + 过程落库);
10. 结果处理:成功(取 collector.result,空终稿补占位)/ 失败(补 ASSISTANT 错误消息)/ 取消,`markExecutionTerminal` 更新 `subagent_execution`,`finishSubagent` 推送终态;
11. 返回 JSON:`success / child_session_id / result / usage / rounds / tool_calls`。

### 3.2 关键机制(追问可复用的基础)

- **子会话历史全部落库**:首轮执行的所有消息(USER/ASSISTANT/TOOL)持久化在 `message` 表,`parent_session_id`、`session_type` 在 `session` 表;
- **上下文按会话重建**:`buildContext(childSessionId)` 每次都从 DB 加载该会话历史 → **对同一子会话再次执行,天然携带上次全部上下文**,这是增量审查的技术基础;
- **AgentLoop 终止条件**:LLM 某轮无 tool_call 即结束本轮执行,因此每次追问 = 在追加新 USER 消息后完整跑一轮循环;
- **子会话压缩**:子会话同样参与 per-session compaction(`session_compaction`),多次追问后历史会被摘要压缩,上下文可控;
- **取消传播**:`StreamingWsHandler` 的 `abortSubagentChildren` 按 `parent_session_id` 传播取消,追问复用同一子会话 id,现有取消机制天然适用;
- **workspace 共享**:子会话继承父会话 workspace,主代理修复后的文件状态,子代理通过文件/git 工具可直接读取,增量核查成立。

### 3.3 现状缺口

- `delegate` 只接受 `agent_type + task`,没有「复用既有子会话」的入口;
- `PromptEngine.appendDelegateToolHints` 只注入 `delegate` 的委派指引,无追问/续查指引;
- `buildSubContext` 的排除工具集只含 `delegate`,未排除追问工具(新增后必须一并排除防递归)。

## 4. 技术选型

| 维度 | 选型 | 理由 |
|------|------|------|
| 工具形态 | 新增内置工具 `delegate_followup`,实现 `Tool` 接口并标 `@Component`(与 `DelegateTool` 同机制自动注册) | 语义独立、schema 简单、不动 `delegate` 现有行为;主代理在历史工具结果中可见 `child_session_id`,续查意图明确 |
| 复用策略 | 完全复用 `buildContext` / `executeVisible` / AgentLoop / WS 可见性管线 | 增量上下文、流式、落库、取消、审批全部白拿,不新造执行链路 |
| 子代理类型定位 | 追问沿用子会话原 `AgentDefinition`(从该 child 最近一条 `subagent_execution.agent_type` 反查),**不提供**追问时更换 agent_type / 模型 | 子会话的工具集、prompt、maxRounds 由定义固定,更换属于另一需求 |
| 审计 | `subagent_execution` 每轮一条记录,不加列;轮次 = 该 `child_session_id` 下记录序数 | 避免无必要 schema 变更;按 id 排序即可得到「第 N 轮」 |
| 前端 | 零改动 | 追问 USER 消息 + 后续输出由现有 `SubagentChatPanel` 自然渲染;phase 徽标随 `session_status` 事件自动更新 |

## 5. 方案设计

### 5.1 工具定义

```
工具名:delegate_followup
参数:
  child_session_id (int, 必填)  要追问的子代理会话 id(取自上次 delegate 返回的 child_session_id)
  task (string, 必填)           追问任务描述:说明本次修复内容、期望核查重点、输出格式
返回(与 delegate 同构):
  success / child_session_id / result / usage / rounds / tool_calls
  + follow_up: true / round: <该子会话第 N 次执行>
```

工具描述(`getDescription` / `getToolPrompt`)要点:

- **何时使用**:已有子代理会话且需要基于其上次结论续查 / 增量审查(典型:审查后主代理已修复,需子代理核查修复情况并继续审查);
- **何时不用**:全新任务、与上次子代理主题无关、子代理历史已失效;
- **如何取 child_session_id**:从历史中最近一次 `delegate` 工具返回结果的 `child_session_id` 字段取得;
- **行为引导**:子代理保留上次全部上下文;本次为**增量核查**——聚焦主代理描述的变更点与上次结论中的待办项,**不要重新全量扫描项目**;可用文件/git 工具核实实际改动。

### 5.2 校验规则(顺序执行,任一失败即返回错误、不创建执行)

| 规则 | 实现 |
|------|------|
| 参数完整 | `child_session_id`、`task` 均非空 |
| 子会话存在 | `sessionMapper.selectById(childSessionId)` 非空 |
| 类型正确 | `sessionType == "SUBAGENT"` |
| 归属正确 | `parentSessionId == 当前会话 sessionId`(防跨会话越权) |
| 状态允许 | `phase != "RUNNING"`(防御;正常因 delegate 同步阻塞不会发生) |
| 定义可解析 | 查该 child 最近一条 `subagent_execution.agent_type` → `AgentDefinitionRegistry.getDefinition` 非空 |

### 5.3 执行流程(`DelegateFollowupTool.execute`)

与 `DelegateTool` 步骤 3 之后对齐,差异仅在「不建会话、改校验」:

1. 解析参数,执行 5.2 校验;
2. 保存追问 USER 消息:`sessionService.saveMessage(childSessionId, "USER", task, ...)` —— **与普通聊天一致,前端无需任何特殊处理**;
3. 插入新 `subagent_execution` 记录:`parentSessionId=当前会话`、`childSessionId`、`agentType`(该 child 原类型)、`taskDescription=本次追问内容`、`status=RUNNING`;
4. LOCAL 模式注册 `LocalToolSessionRegistry.setUserForSession(childId, userId)`;
5. `visibilityService.notifySubagentCreated(...)` **不重复发送**(Tab 已存在);但需确保 WS 订阅有效:执行前幂等调用 `registry.subscribe(userId, childSessionId)`(用户关掉 Tab 后重开也依赖「查看过程」入口,此步为兜底);
6. `buildSubContext(childSession, definition)` —— 复用 `DelegateTool` 的同名逻辑(见 5.4),上下文自动包含上次全部消息 + 本次追问消息;
7. `registerCancelFlag(childSessionId)` + 继承父会话 cancel 标志(与 delegate 一致);
8. `visibilityService.executeVisible(childSession, subContext, skip)` —— phase 置 RUNNING、WS 流式、过程落库;
9. 结果处理与 delegate 一致:成功 / 失败 / 取消三分支,`markExecutionTerminal` 更新**本次新记录**,`finishSubagent(childSessionId, userId, terminalPhase, executionId)` 推送终态;
10. 返回 JSON:delegate 同构字段 + `follow_up: true` + `round`(该 child 下 `subagent_execution` 记录数)。

### 5.4 代码复用与改动面

- 新建 `backend/src/main/java/cn/etarch/mao/harness/tool/impl/DelegateFollowupTool.java`,实现 `Tool`;
- 将 `DelegateTool.buildSubContext` 从 `private` 改为包内可见(或抽为共享方法),`DelegateFollowupTool` 复用;其余公共片段(cancel 注册/清理、executeVisible、markExecutionTerminal、结果拼装)按 delegate 现有实现对齐复制,**不做无谓抽象**(两处相似实现可接受,避免为单一复用点过度重构);
- `DelegateTool.buildSubContext` 的 `excludedNames` 增加 `"delegate_followup"`(与 `delegate` 一起,保证子代理工具集不含追问工具,防递归);
- `PromptEngine.appendDelegateToolHints`:当工具集含 `delegate_followup` 时,追加「追问/续查」小节(何时用、如何取 child_session_id、增量核查行为),使主代理在 system prompt 中获知该能力。

### 5.5 数据与审计

- 不新增表、不加列;`subagent_execution` 每轮(首轮 + 每次追问)一条记录,`task_description` 区分首轮任务与各轮追问内容;
- 轮次推导:`SELECT COUNT(*) FROM subagent_execution WHERE child_session_id = ?`(新记录插入后即为当前 round);
- `subagent_execution.status` 语义不变(RUNNING/COMPLETED/FAILED/CANCELLED),追问后子会话 phase 由 COMPLETED → RUNNING → COMPLETED 循环。

### 5.6 上下文与增量审查的保证

- 追问前保存 USER 消息,`buildContext` 在消息落库后执行 → 子代理本次可见「上次完整对话 + 本次追问」;
- 子代理上次的审查结论、发现的问题清单、工具输出(读过的文件内容)均在历史中,追问 prompt 引导其引用这些结论做增量核查;
- 主代理修复后 workspace 文件已更新,子代理可用文件/git 工具核实(`isGit` 环境信息已在上下文中);
- 多次追问导致的上下文增长由 per-session compaction 兜底(已确认不设轮次上限)。

## 6. 实现步骤

**后端(唯一改动端)**

1. 新建 `DelegateFollowupTool.java`:`getName()` 返回 `delegate_followup`;实现 `getInputSchema`(`child_session_id`、`task` 必填)、`getDescription`、`getToolPrompt`(见 5.1);实现 `execute(arguments, sessionId, workspace)`;
2. 校验逻辑(5.2):子会话存在 / SUBAGENT / 归属当前父会话 / 非 RUNNING / agent_type 可解析;
3. 追问执行体(5.3):保存 USER 消息 → 插 `subagent_execution` → LOCAL 注册 → subscribe 兜底 → 取 definition → buildSubContext → cancel 注册 → executeVisible → 结果三分支收尾 → 返回 JSON;
4. `DelegateTool.buildSubContext` 改包内可见,排除工具集增加 `delegate_followup`;
5. `PromptEngine.appendDelegateToolHints` 追加追问指引(5.1 行为引导);
6. 单测:`backend/src/test/` 补 `DelegateFollowupTool` 用例(见第 8 节)。

**前端 / 管理后台 / 安卓**:零改动。

**发版说明**:实现并验证后,在根 `CHANGELOG.md` 当前版本追加 `### 后端` 条目(主代理新增追问能力,用户可见)。

## 7. 落地清单

### 7.1 要做

- [ ] 新增内置工具 `delegate_followup(child_session_id, task)`(后端)
- [ ] 追问校验:SUBAGENT 类型 / 归属当前父会话 / 非 RUNNING / agent_type 可解析
- [ ] 追问执行:追加 USER 消息 + 复用子会话历史上下文跑一轮 AgentLoop + 结果落库与 WS 可见
- [ ] `subagent_execution` 每轮一条审计记录(含首轮与各轮追问)
- [ ] 子代理工具集排除 `delegate_followup`(防递归,与 `delegate` 一并排除)
- [ ] `PromptEngine` 注入追问/续查行为指引(增量核查、避免全量重扫)
- [ ] LOCAL 模式追问链路(LocalToolSessionRegistry 注册)与取消传播验证
- [ ] 后端单测 + CLOUD/LOCAL 手测清单(第 8 节)
- [ ] `CHANGELOG.md` 记录后端改动

### 7.2 明确不做

- 用户在子代理 Tab 手动输入追问(子代理面板保持只读,入口不变)
- 消息流分隔条 / 轮次徽标等任何前端特殊轮次视觉(追问内容即普通 USER 消息)
- 追问时更换 `agent_type`、模型或 `AgentDefinition`(沿用子会话原定义)
- 跨父会话复用子代理(`child_session_id` 仅限当前父会话创建的子代理)
- 子代理反向对主代理追问,或子代理再次 `delegate` / `delegate_followup`(工具集已排除)
- `delegate` 工具签名、schema、行为改动(新建能力原样保留)
- 追问轮次硬上限 / 软提示阈值(不设上限,依赖 compaction)
- 后台异步 `delegate` / 并行追问(保持同步阻塞语义)
- 管理后台 / 安卓端任何改动
- 新增数据库表或列

## 8. 测试与验收

### 8.1 单测(`backend/src/test/`)

- 参数缺失:无 `child_session_id` / 无 `task` → 明确错误返回;
- 非法目标:会话不存在 / 非 SUBAGENT / 归属其他父会话 → 拒绝且不插入执行记录;
- RUNNING 态拒绝(Mock 会话 phase);
- 追问成功后 `subagent_execution` 记录数 +1、`task_description` 为本轮追问内容、`round` 递增;
- 子代理上下文构建不包含 `delegate_followup` 工具。

### 8.2 手测清单(真实 LLM,沿用现有 delegate 手测方式)

1. CLOUD:主代理 `delegate` reviewer 审查 → 返回 `child_session_id` → 主代理修复 → `delegate_followup` 同一子代理 → 子代理 Tab 出现普通 USER 追问消息 + 流式输出,结果引用上次结论做增量核查(不在无关注释点全量重扫);
2. 连续多轮追问(≥3 轮)上下文正常,压缩机制生效;
3. FAILED / CANCELLED 子代理可追问;
4. 伪造/他人 `child_session_id` 被拒绝并返回明确错误;
5. 追问执行中主会话停止 → 子代理随之取消,Tab 进入取消态;
6. LOCAL 模式跑一遍同上主链路;
7. `delegate` 原有行为无回归(新建、并行委派、Tab 打开等)。

### 8.3 验收标准

1. 主代理可对已完成子代理发起 `delegate_followup`,返回成功结果;
2. 子代理 Tab 内追问内容以普通 USER 消息呈现,与聊天记录一致;
3. 追问中子代理能引用上次审查结论做增量核查(手测确认);
4. COMPLETED / FAILED / CANCELLED 均可追问,RUNNING 拒绝;
5. 越权 / 非法 `child_session_id` 被拒绝;
6. `subagent_execution` 每轮一条记录,首轮与各轮追问可审计;
7. `delegate` 无回归;子代理工具集不含 `delegate_followup`。

## 9. 风险与对策

| 风险 | 对策 |
|------|------|
| 多次追问历史膨胀 | 子会话参与 per-session compaction,自动摘要压缩;已确认不设硬上限 |
| 主代理忘记/丢失 `child_session_id` | 工具描述与 system prompt 引导从历史 delegate 结果中取;丢失则视为使用边界(无法追问,可新建) |
| 追问时 workspace 已被主代理修改,子代理读到修复后状态 | 这正是需求目标;如需对比改动前,子代理可用 git 工具(上下文含 isGit 信息) |
| 同一子会话并发执行 | delegate/delegate_followup 均同步阻塞父工具线程,天然串行;phase != RUNNING 校验兜底 |
| WS 订阅失效导致追问不可见 | 追问执行前幂等 `registry.subscribe`;前端「查看过程」入口可随时重开 Tab |
| 子代理全量重扫(违背增量目标) | 依赖 system prompt 追问指引 + 主代理 task 描述聚焦变更点;手测验收标准 3 把关 |
| 子代理递归追问/委派 | `buildSubContext` 排除 `delegate` 与 `delegate_followup`,结构上杜绝 |

## 10. 关键文件

**后端(改动/新增)**

- `harness/tool/impl/DelegateFollowupTool.java`(新增)
- `harness/tool/impl/DelegateTool.java`(`buildSubContext` 改包内可见、排除工具集增加 `delegate_followup`)
- `harness/core/PromptEngine.java`(追问指引注入)
- `harness/delegate/mapper/SubagentExecutionMapper.java`(如需按 child 计数查询,可用现有 mapper 扩展或直接查列表 count)
- `src/test/`(新增单测)

**前端 / 管理后台 / 安卓**:无改动。

## 11. 与既有文档的关系

- `subagent-visibility.md` 9.3「明确不做」中的「子会话追问」由本文档落地实现,其余冻结项(弹窗主方案、admin UI、子 Tab 独立取消、delegate 后台模式等)仍不做;
- `subagent-visibility.md` 中「只读中央 Tab,不可追问」的表述在本需求落地后更新为「只读中央 Tab,可被主代理追问(工具层)」;
- 复用 `2026-08-04-loop-compaction-reuse-session-design.md` 确认的子会话压缩机制,不新增压缩策略。