该系列文章基于
github.com/shareAI-lab/learn-claude-code写就,该仓库以大道至简的风格剖析了Claude Code的核心原理,值得大家学习。由于该仓库是基于Python语言,为方便.NET开发者学习,我已经将代码基于.NET 10的dotnet file重写,源码已上传至github,源码地址见文末。
本文是 Learn Claude Code (C# 版) 系列的第十一篇,对应代码文件
v10_autonomous_agents.cs。
v9 实现了结构化协议,但队友仍然是被动的——它们只在收到消息时才工作:
Lead: "fe-dev, implement the login page"
fe-dev: (完成后) "Done."
... fe-dev 空转, 不主动做任何事 ...
Lead: "fe-dev, now implement the dashboard"
fe-dev: (完成后) "Done."
... 再次空转 ...
这就像一个员工做完手头的事就坐着发呆,不会主动看看任务板上还有什么待办事项。
问题分两层:
- 无自驱力:队友完成一个任务后不会主动寻找下一个
- 身份丢失:上下文压缩后,队友忘记自己是谁、擅长什么
带任务看板轮询的空闲循环让队友能自己发现和认领工作,上下文压缩后通过身份重注入保持角色认知。
┌──────────────────────────┐
│ WORK 阶段 │
│ 正常 agent loop │
│ 执行工具 / 处理消息 │
│ │
│ 调用 idle 工具 ──────────┼──→ 转入 IDLE
└──────────────────────────┘
↑
│ (发现新任务)
│
┌──────────────────────────┐
│ IDLE 阶段 │
│ 每 5 秒轮询: │
│ 1. 检查收件箱 │
│ 2. 扫描任务看板 │
│ │
│ 找到 → claim → WORK │
│ 60秒超时 → 上报 Lead │
└──────────────────────────┘
让队友显式声明"我现在没事做":
"idle" => {
phase = Phase.IDLE;
return "Entering IDLE phase. Will poll for new tasks.";
}async Task IdleLoopAsync(string name)
{
var maxPolls = 12; // 12 × 5s = 60s 超时
var polls = 0;
while (polls < maxPolls)
{
await Task.Delay(5000);
polls++;
// 1. 检查收件箱
var inbox = messageBus.Read(name);
if (inbox.Count > 0)
{
// 有消息 → 注入到消息流, 回到 WORK
messages.Add(new() { Role = Role.User,
Content = FormatInboxMessages(inbox) });
phase = Phase.WORK;
return;
}
// 2. 扫描任务看板
var unclaimed = taskManager.ScanUnclaimed();
if (unclaimed.Count > 0)
{
var task = unclaimed.First();
if (taskManager.ClaimTask(task.Id, name))
{
// 成功认领 → 回到 WORK
messages.Add(new() { Role = Role.User,
Content = $"[Claimed task {task.Id}: {task.Title}]" });
phase = Phase.WORK;
return;
}
}
}
// 超时 → 通知 Lead
messageBus.Send(name, "lead", "IDLE timeout, no tasks found.", "message");
}多个队友可能同时看到同一个未认领的任务,需要原子操作:
class TaskManager
{
object claimLock = new();
public bool ClaimTask(string taskId, string owner)
{
lock (claimLock)
{
var task = Load(taskId);
if (task["status"] != "pending" || task["owner"] != null)
return false; // 已被认领
task["status"] = "in-progress";
task["owner"] = owner;
Save(taskId, task);
return true;
}
}
}上下文压缩会丢失 system prompt 和角色认知。解决方案:压缩后重新注入身份信息:
static ContentBlockParam MakeIdentityBlock(string name, string role)
{
return new TextBlockParam
{
Text = $"[Identity Reminder] You are {name}, role: {role}. " +
$"Use your specialized skills. Check inbox and task board regularly."
};
}在自动压缩后调用:
if (EstimateTokens(messages) > THRESHOLD)
{
messages = await AutoCompactAsync(messages);
// 重注入身份
messages.Add(new()
{
Role = Role.User,
Content = MakeIdentityBlock(name, role)
});
}| Agent | v9 工具数 | v10 新增 | v10 总计 |
|---|---|---|---|
| Lead | 12 | +2 (task_create, task_list) | 14 |
| Teammate | 8 | +2 (idle, claim_task) | 10 |
| 组件 | 之前 (v9) | 之后 (v10) |
|---|---|---|
| 队友行为 | 被动 (等待消息) | 主动 (轮询任务看板) |
| 生命周期 | 单一循环 | WORK/IDLE 双阶段 |
| 任务看板 | 无 | 集成 TaskManager |
| 身份管理 | 无 | 压缩后重注入 |
| 自治能力 | 无 | 自动认领 + 超时上报 |
WORK/IDLE 循环是基于拉取 (pull-based) 的任务分配模型:
- Push vs Pull:v8-v9 是 push(Lead 逐个分配),v10 增加了 pull(队友自己认领)
- 超时兜底:60 秒找不到工作就上报,避免无限空转
- 身份重注入:系统提示在压缩后被清除,显式注入保持角色一致性
- 乐观锁:ClaimTask 用 lock 保证只有一个 agent 能认领同一任务
这个模式在分布式系统中很常见:Kubernetes 的 Pod 调度、Celery 的 worker 都使用类似的"工作循环 + 空闲轮询"模式。
dotnet run v10_autonomous_agents.cs可以尝试的提示:
Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.Spawn a coder teammate and let it find work from the task board itselfCreate tasks with dependencies. Watch teammates respect the blocked order.- 输入
/tasks查看带 owner 的任务看板 - 输入
/team监控谁在工作、谁在空闲