Skip to content

通过消除 status 轮询和启用并行工具调用减少 token 浪费 - #48

Open
hufeide wants to merge 7 commits into
NanmiCoder:mainfrom
hufeide:fix/eliminate-captain-polling-and-dispatch
Open

通过消除 status 轮询和启用并行工具调用减少 token 浪费#48
hufeide wants to merge 7 commits into
NanmiCoder:mainfrom
hufeide:fix/eliminate-captain-polling-and-dispatch

Conversation

@hufeide

@hufeide hufeide commented Aug 18, 2026

Copy link
Copy Markdown

背景

通过对 AgentTeams 一次完整的"两首五言绝句 + 评审"任务的会话日志分析,发现 ReAct 循环中存在大量不必要的 step,每个 step 都要重读 ~15K 的 system prompt + 工具 schema(命中 cache_read),导致 token 消耗远超预期。

问题分析

会话日志实测数据(优化前)

会话 步数 input cache_read output 合计
captain 14 26,311 215,040 1,948 243K
poet-a 7 20,527 83,968 1,758 106K
poet-b 6 16,334 71,680 1,075 89K
critic 7 19,470 88,064 2,106 110K
总计 34 82,642 458,752 6,887 548K

88% 的 token 是 cache_read(每步重读固定前缀),而真正的工作(input+output)只占 12%。

三类浪费

1. status 轮询(captain 侧,~97K token)

captain 在创建任务后连续调用了 6 次 `agent_teams_status`,框架的 `repeat-tool-reminder` 插件已注入"Repeated tool call detected: consecutive_calls: 5"警告,但 captain 无视了:

```
s5: status ← 查一次(合理)
s6: status ← 没新消息,再查
s7: status ← 继续查
s8: status ← 框架警告:repeating
s9: status ← 仍然继续
s11: status ← sleep 之后又查
```

每次 cache_read ~15K,6 次共浪费 ~97K token。

根因:captain persona 写了"monitor with agent_teams_status"和"Poll status until every required task is terminal",直接鼓励轮询。

2. 串行工具调用(member 侧,~84K token)

member 把本可并行的工具调用拆成了串行步骤:

```
s3: claim_task ← 拿 attempt_id
s4: update_task(in_progress) ← 等 s3 返回后才调
s5: update_task(completed) ← 单独一步
s6: send_message ← 等 s5 返回后才调
```

实际上 `claim_task` 和 `update_task(in_progress)` 可以并行(DeepSeek API 支持单次返回多个 tool_calls,框架的 `runGroup` 也支持并行执行)。同理 `update_task(completed)` 和 `send_message` 也可以并行。

每个多余的 step 浪费 ~14K token,3 个 member 共浪费 ~84K。

根因:persona 用"Then mark in_progress"的措辞暗示了顺序执行;assignment prompt 说"call claim_task; it will return this same attempt_id"暗示需要先拿返回值。

3. 收尾总结(双端,~37K token)

captain 在 `delete` 后输出了两步纯文本总结;member 在 `send_message` 后也输出了一步总结。这些步骤没有工具调用,纯粹是模型"礼貌收尾"。

```
captain s13: (无工具) cache=18,432 output=364 ← 总结
captain s14: (无工具) cache=19,200 output=267 ← 二次总结
poet-a s7: (无工具) cache=14,336 output=286 ← 收尾总结
```

共浪费 ~37K token。


改动内容

4 个文件,10 行改动,分两层防御:

captain 侧

`src/index.ts` — captain persona(3 处)

规则
第 4 条 "monitor with agent_teams_status" "do NOT poll in a loop; use sleep; wait for member messages"
第 6 条 "Poll status until every required task is terminal" "Wait for member completion messages instead of polling"
第 7 条 "Present results, then delete" 追加"Do NOT output a recap or summary after delete"

`src/tools.ts` — 工具描述 + 输出(2 处)

位置
`status` 描述 "Poll this to watch progress" "do NOT poll in a loop; members report completion automatically"
`create_task` 输出 只返回 task 信息 追加"The assignee has been auto-notified; do NOT call agent_teams_status to check"

member 侧

`src/members.ts` — member persona(4 处)

改动 说明
禁止 status 轮询 "Do NOT call agent_teams_status to poll for tasks — wait for messages"
claim + in_progress 并行 "You may call claim_task and update_task(in_progress) in the SAME response"
completed + send_message 并行 "You may call update_task(completed) and send_message in the SAME response"
禁止收尾总结 "After send_message, end your turn immediately. Do NOT output a summary"

`src/scheduler.ts` — assignment prompt(1 处)

"call claim_task; it will return this same attempt_id" 追加"You already know the attempt_id from this message, so you MAY call claim_task and update_task(in_progress) in the SAME response"

关键点:assignment prompt 里已包含 `attempt_id`,member 不需要等 `claim_task` 返回就知道 attempt_id,因此可以并行调用。


防御层次

```
captain 侧 member 侧
┌───────────────────────┐ ┌───────────────────────┐
│ index.ts persona │ │ members.ts persona │
│ "不要轮询 status" │ │ "不要查 status" │
│ "用 sleep 等待" │ │ "并行 claim+progress" │
│ "delete 后不总结" │ │ "并行 completed+msg" │
└──────────┬────────────┘ │ "发完消息就结束" │
│ └──────────┬────────────┘
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ tools.ts 描述/输出 │ │ scheduler.ts prompt │
│ status: "不要循环" │ │ "已知 attempt_id" │
│ create_task: "已通知" │ │ "可并行调用" │
└───────────────────────┘ └───────────────────────┘
```

每个角色都有两层防御:persona 层 + 工具/prompt 层。即使模型忽略了某一层,另一层仍能阻止浪费行为。


实测效果

member 侧(已验证)

优化后重新运行了相同任务,对比日志:

member 旧版步数 新版步数 旧版 token 新版 token 降幅
poet-a 7 4 106K 60K -44%
poet-b 6 4 89K 60K -33%
judge 7 6 110K 78K -29%
合计 20 14 305K 198K -35%

三个优化全部生效:

  • ✅ welcome 后不再查 status(省 1-2 步)
  • ✅ claim + in_progress 并行(省 1 步)
  • ✅ completed + send_message 并行(省 1 步)
  • ⚠️ 收尾总结部分消除(output 从 286 降到 43,但仍有 1 步短收尾)

captain 侧(预期)

未重新测试,但根据改动逻辑预期:

旧版 预期新版
步数 14 7
token 241K ~107K
降幅 -55%

影响范围

场景 是否受影响 风险
AgentTeams 任务(captain) ✅ 行为变化 低:从轮询改为等消息,语义等价
AgentTeams 任务(member) ✅ 行为变化 低:并行调用不影响正确性,框架已支持
非 AgentTeams 会话 ✅ system prompt 增加 ~50 token 可忽略

`index.ts` 的 persona 是全局注入的,但只在用户主动要求用 AgentTeams 时生效(persona 开头有"When the user asks to run something with AgentTeams"守卫)。


测试

```bash
npm test
npm run lint
```

…arallel tool calls

- captain persona: replace status polling with sleep+wait-for-message
- member persona: encourage parallel claim+in_progress and completed+send_message
- scheduler: pre-disclose attempt_id to enable parallel calls
- tools: discourage status polling in description and create_task output
- create_task: allow forward dependency references (t3 can depend on t1/t2
  before they are created in the same parallel batch); validate id format
  instead of requiring pre-existence
- captain persona: instruct to create ALL tasks in one step (parallel);
  reduce sleep from 60s to 5s since member messages are delivered live
  via steerCaptainReport (no need for long waits)
- captain persona: allow only ONE sleep 5, then wait for messages;
  forbid sleep+status cycles and status after sleep
- member persona: forbid reading team.json/inbox files; prerequisite
  results are delivered inline; explicitly ban in_progress after completed
- scheduler: inject completed dependency outputs into assignmentPrompt
  so review/evaluation tasks get prerequisite results without file reads
- scheduler: add dependencyOutputs to DispatchTicket, collected from
  completed dependency tasks at dispatch time
Root cause (from session logs): captain slept 5s, woke up, and even
though both poet completion messages were already in its inbox, it
slept again for 15s — then again for 15s. Captain did not understand
that inbox messages = members are done.

Fix: persona now explicitly tells captain that after sleep, the next
step automatically contains member messages — if you see 'AgentTeams
message from member' in inbox, those members are DONE; process results
immediately, do NOT sleep again. Allow at most 2 sleeps total (in case
some members haven't reported yet).
Root cause: captain used sleep to 'wait for members', but sleep is
unnecessary because steerCaptainReport() calls captain.steer(), which
wakes an idle captain automatically (per agent-loop README: 'An idle
agent starts a turn synchronously' on steer).

Fix: persona now tells captain to simply END its turn after creating
tasks (stop calling tools). When members complete and call
send_message(to=captain), the steer() wake mechanism automatically
starts a new captain turn with the member's message in inbox.

This eliminates all sleep steps (~45K cache_read saved) and all
status polling.
Problems found in latest run (486K tokens, worse than v4):
1. captain manually called send_message to dispatch tasks after
   create_task already auto-notified members (redundant, +1 step each)
2. captain still called agent_teams_status 3 times despite ban
3. poet-yi split claim+in_progress into 2 steps instead of parallel
4. poet-jia sent duplicate completion report (re-waking captain)
5. two empty captain steps from spurious wake-ups

Fixes:
- index.ts: forbid send_message for task dispatch; only for guidance;
  tell captain to output brief text and end turn after create_task
- tools.ts: create_task output now says 'do NOT send_message to dispatch';
  status description strengthened with WARNING + ONCE limit
- members.ts: claim+in_progress now mandatory parallel (not optional);
  ban duplicate completion reports; strengthen status ban
- scheduler.ts: assignmentPrompt makes parallel claim+in_progress
  mandatory instead of 'MAY'
- members.ts: unify claim+in_progress wording (remove conflicting
  MAY/ALWAYS, use mandatory phrasing only)
- scheduler.ts: truncate dependency outputs at 2000 chars to prevent
  oversized assignment prompts
- index.ts: split captain rule 4 into two rules (delegation vs
  post-create behavior) for clarity
- tools.ts: add TODO comment for unvalidated forward dependency refs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant