Agent 开发进阶(十七):自治智能体系统,让 Agent 主动认领任务

0 阅读10分钟

Agent 开发进阶(十七):自治智能体系统,让 Agent 主动认领任务

本文是「从零构建 Coding Agent」系列的第十七篇,适合想让 Agent 主动工作的开发者。

先问一个问题

当你的团队规模越来越大,任务板上堆积了大量待处理任务时,你是否遇到过这样的问题:

  • Lead 需要手动分配每个任务,成为瓶颈
  • 空闲的队友不知道自己该做什么
  • 任务分配效率低下,团队协作不够流畅

如果你的答案是肯定的,那么你需要一个自治智能体系统。

手动分配的「协作瓶颈」问题

到了这一阶段,你的 Agent 已经具备了多种能力:

  • 核心循环运行
  • 工具使用与分发
  • 会话内规划(TodoWrite)
  • 子智能体机制(Subagent)
  • 技能加载
  • 上下文压缩
  • 权限系统
  • Hook 系统
  • Memory 系统
  • 系统提示词组装
  • 错误恢复
  • 任务系统
  • 后台任务系统
  • 定时调度系统
  • Agent 团队系统
  • 团队协议系统

但当团队规模变大时,手动分配任务会遇到明显问题:

  • 效率低下:Lead 需要手动分配每个任务
  • 资源浪费:空闲的队友不知道自己该做什么
  • 扩展性差:团队规模越大,分配任务越困难
  • 响应缓慢:任务分配不及时,影响整体进度

虽然 s16 的协议系统让队友可以协作,但很多事情仍然要靠 Lead 手动分配。

所以到了这个阶段,我们需要一个自治智能体系统:

让空闲队友自己扫描任务板,找到可做的任务并认领。

自治智能体系统的核心设计:WORK-IDLE 循环与自动认领

用一个图来表示自治智能体系统的工作流程:

WORK
  |
  | 当前轮工作做完,或者主动进入 idle
  v
IDLE
  |
  +-- 看邮箱,有新消息 -> 回到 WORK
  |
  +-- 看任务板,有 ready task -> 认领 -> 回到 WORK
  |
  +-- 长时间什么都没有 -> shutdown

关键点只有三个:

  1. WORK-IDLE 循环:队友在工作阶段和空闲阶段之间切换
  2. 自动认领:空闲时按规则扫描任务板并认领任务
  3. 原子操作:认领必须是原子的,避免重复抢任务

几个必须搞懂的概念

自治(Autonomy)

这里的自治,不是完全没人管。

这里说的自治是:

在提前给定规则的前提下,队友可以自己决定下一步接哪份工作。

认领(Claim)

认领,就是把一条原本没人负责的任务,标记成「现在由我负责」。

空闲阶段(IDLE)

空闲阶段不是关机,也不是消失。

它表示:

这个队友当前手头没有活,但仍然活着,随时准备接新活。

WORK-IDLE 循环

每个队友在工作阶段和空闲阶段之间切换:

  • WORK:正在执行任务
  • IDLE:检查邮箱和任务板,准备接新工作

最小实现

1. 增强的任务管理器

import os
import json
import threading
import time
import uuid
from pathlib import Path

class AutonomousTaskManager:
    """支持自治认领的任务管理器"""
    
    def __init__(self, tasks_dir=".tasks"):
        self.tasks_dir = Path(tasks_dir)
        self.tasks_dir.mkdir(exist_ok=True)
        
        # 认领锁
        self.claim_lock = threading.Lock()
        
        # 认领事件日志
        self.claim_events_file = self.tasks_dir / "claim_events.jsonl"
        
        # 加载所有任务
        self.tasks = {}
        self._load_all_tasks()
    
    def _load_all_tasks(self):
        """加载所有任务"""
        for task_file in self.tasks_dir.glob("task_*.json"):
            try:
                task = json.loads(task_file.read_text(encoding="utf-8"))
                self.tasks[task["id"]] = task
            except Exception as e:
                print(f"加载任务失败 {task_file}: {e}")
    
    def _save_task(self, task):
        """保存任务"""
        task_file = self.tasks_dir / f"task_{task['id']}.json"
        task_file.write_text(
            json.dumps(task, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def _log_claim_event(self, event):
        """记录认领事件"""
        with open(self.claim_events_file, "a", encoding="utf-8") as f:
            f.write(json.dumps(event, ensure_ascii=False) + "\n")
    
    def is_claimable(self, task_id, role=None):
        """判断任务是否可认领"""
        task = self.tasks.get(task_id)
        if not task:
            return False
        
        # 检查基本条件
        if task.get("status") != "pending":
            return False
        
        if task.get("owner"):
            return False
        
        if task.get("blockedBy"):
            return False
        
        # 检查角色条件
        if role:
            claim_role = task.get("claim_role")
            required_role = task.get("required_role")
            
            if claim_role and role != claim_role:
                return False
            
            if required_role and role != required_role:
                return False
        
        return True
    
    def claim_task(self, task_id, owner, role=None, source="manual"):
        """认领任务(原子操作)"""
        with self.claim_lock:
            task = self.tasks.get(task_id)
            if not task:
                return {"success": False, "message": "Task not found"}
            
            # 检查是否可认领
            if not self.is_claimable(task_id, role):
                return {"success": False, "message": "Task not claimable"}
            
            # 更新任务
            task["owner"] = owner
            task["status"] = "in_progress"
            task["claimed_at"] = time.time()
            task["claim_source"] = source
            
            self._save_task(task)
            
            # 记录认领事件
            event = {
                "event": "task.claimed",
                "task_id": task_id,
                "owner": owner,
                "role": role,
                "source": source,
                "ts": time.time(),
            }
            self._log_claim_event(event)
            
            return {"success": True, "task": task}
    
    def get_claimable_tasks(self, role=None):
        """获取可认领的任务"""
        claimable = []
        for task_id, task in self.tasks.items():
            if self.is_claimable(task_id, role):
                claimable.append(task)
        
        # 按优先级排序(这里简化为按 ID 排序)
        claimable.sort(key=lambda t: t["id"])
        return claimable

2. 自治队友循环

class AutonomousTeammate:
    """自治队友"""
    
    def __init__(self, name, role, task_manager, team_manager, prompt):
        self.name = name
        self.role = role
        self.task_manager = task_manager
        self.team_manager = team_manager
        self.prompt = prompt
        
        # 消息历史
        self.messages = [{
            "role": "system",
            "content": f"你是 {name},一个 {role}。请专注于你的职责,完成任务后等待新的指示。"
        }, {
            "role": "user",
            "content": prompt
        }]
        
        # 运行状态
        self.running = True
        self.idle_count = 0
        self.max_idle_count = 10  # 最多空闲 10 次
        self.poll_interval = 5  # 轮询间隔 5 秒
    
    def ensure_identity(self):
        """确保身份上下文存在"""
        # 检查是否有身份块
        has_identity = any(
            "<identity>" in msg.get("content", "") 
            for msg in self.messages
        )
        
        if not has_identity:
            # 注入身份块
            identity_msg = {
                "role": "user",
                "content": f"<identity>You are '{self.name}', role: {self.role}. Continue your work.</identity>",
            }
            self.messages.insert(0, identity_msg)
            
            # 添加确认语
            confirm_msg = {
                "role": "assistant",
                "content": f"I am {self.name}. Continuing."
            }
            self.messages.insert(1, confirm_msg)
    
    def work_phase(self):
        """工作阶段"""
        # 这里应该调用模型并执行任务
        # 简化版本模拟执行
        print(f"{self.name} 正在执行任务...")
        time.sleep(2)
        
        # 模拟完成当前任务
        # 实际应该根据任务状态判断
        return True
    
    def idle_phase(self):
        """空闲阶段"""
        # 1. 检查邮箱
        inbox = self.team_manager._read_inbox(self.name)
        if inbox:
            print(f"{self.name} 收到 {len(inbox)} 条消息")
            for msg in inbox:
                # 处理协议消息
                if msg.get("type") == "shutdown_request":
                    print(f"{self.name} 收到关机请求")
                    return False  # 退出循环
                
                # 普通消息
                self.messages.append({
                    "role": "user",
                    "content": f"来自 {msg['from']} 的消息: {msg['content']}"
                })
            
            self.idle_count = 0
            return True
        
        # 2. 扫描可认领任务
        claimable_tasks = self.task_manager.get_claimable_tasks(self.role)
        if claimable_tasks:
            task = claimable_tasks[0]
            print(f"{self.name} 发现可认领任务: #{task['id']} {task['subject']}")
            
            # 认领任务
            result = self.task_manager.claim_task(
                task["id"],
                self.name,
                role=self.role,
                source="auto"
            )
            
            if result["success"]:
                # 确保身份上下文
                self.ensure_identity()
                
                # 添加任务提示
                self.messages.append({
                    "role": "user",
                    "content": f"<auto-claimed>Task #{task['id']}: {task['subject']}</auto-claimed>",
                })
                
                self.messages.append({
                    "role": "assistant",
                    "content": f"Task #{task['id']} claimed. Working on it.",
                })
                
                self.idle_count = 0
                return True
        
        # 3. 没有任务,增加空闲计数
        self.idle_count += 1
        print(f"{self.name} 空闲 ({self.idle_count}/{self.max_idle_count})")
        
        # 超过最大空闲次数,退出
        if self.idle_count >= self.max_idle_count:
            print(f"{self.name} 长时间空闲,准备退出")
            return False
        
        return True
    
    def run(self):
        """主循环"""
        print(f"队友 {self.name} ({self.role}) 已启动")
        
        while self.running:
            # 工作阶段
            work_done = self.work_phase()
            
            # 空闲阶段
            should_continue = self.idle_phase()
            
            if not should_continue:
                break
            
            # 等待一段时间
            time.sleep(self.poll_interval)
        
        print(f"队友 {self.name} ({self.role}) 已退出")

3. 集成到团队系统

class AutonomousTeamManager:
    """支持自治的团队管理器"""
    
    def __init__(self, team_dir=".team"):
        self.team_dir = Path(team_dir)
        self.team_dir.mkdir(exist_ok=True)
        
        # 创建必要的目录
        self.config_dir = self.team_dir
        self.inbox_dir = self.team_dir / "inbox"
        self.inbox_dir.mkdir(exist_ok=True)
        
        # 初始化任务管理器
        self.task_manager = AutonomousTaskManager()
        
        # 加载配置
        self.config_path = self.config_dir / "config.json"
        self.config = self._load_config()
        
        # 存储队友
        self.teammates = {}
    
    def _load_config(self):
        """加载配置"""
        if self.config_path.exists():
            try:
                return json.loads(self.config_path.read_text(encoding="utf-8"))
            except Exception as e:
                print(f"加载配置失败: {e}")
        return {"team_name": "default", "members": []}
    
    def _save_config(self):
        """保存配置"""
        self.config_path.write_text(
            json.dumps(self.config, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def spawn(self, name, role, prompt):
        """创建队友"""
        # 检查是否已存在
        if name in self.teammates:
            return f"队友 {name} 已存在"
        
        # 添加到配置
        member = {
            "name": name,
            "role": role,
            "status": "working"
        }
        self.config["members"].append(member)
        self._save_config()
        
        # 创建队友
        teammate = AutonomousTeammate(
            name, role, self.task_manager, self, prompt
        )
        self.teammates[name] = teammate
        
        # 启动队友线程
        import threading
        thread = threading.Thread(target=teammate.run, daemon=True)
        thread.start()
        
        return f"队友 {name} ({role}) 已创建"
    
    def list(self):
        """列出所有队友"""
        if not self.config["members"]:
            return "团队暂无成员"
        
        lines = ["# 团队成员\n"]
        for member in self.config["members"]:
            name = member["name"]
            status = "运行中" if name in self.teammates else "已退出"
            lines.append(f"- **{member['name']}** ({member['role']}) [{status}]")
        
        return "\n".join(lines)

核心功能说明

1. 可认领任务判断

判断任务是否可认领

is_claimable = task_manager.is_claimable(task_id, role="frontend")

判断条件包括:

  • 任务状态是 pending
  • 没有被认领(owner 为空)
  • 没有前置阻塞(blockedBy 为空)
  • 角色匹配(如果有 claim_rolerequired_role

2. 自动认领

空闲时自动认领任务

result = task_manager.claim_task(
    task_id,
    owner="alice",
    role="frontend",
    source="auto"
)

认领是原子操作,使用锁避免重复认领。

3. 身份重注入

上下文压缩后重新注入身份

teammate.ensure_identity()

确保队友知道:

  • 我是谁
  • 我的角色是什么
  • 我属于哪个团队

4. WORK-IDLE 循环

队友在工作阶段和空闲阶段之间切换

  • WORK:执行当前任务
  • IDLE:检查邮箱和任务板,准备接新工作
  • 超时退出:长时间空闲后自动退出

认领机制的关键点

1. 原子操作

认领必须是原子的,避免重复抢任务:

with claim_lock:
    task = load(task_id)
    if task["owner"]:
        return "already claimed"
    task["owner"] = name
    task["status"] = "in_progress"
    save(task)

2. 角色过滤

不是所有任务都适合所有队友:

task = {
    "id": 7,
    "subject": "Implement login page",
    "claim_role": "frontend",
}

只有 frontend 角色的队友才能认领这个任务。

3. 事件日志

记录认领事件,便于追踪:

{
    "event": "task.claimed",
    "task_id": 7,
    "owner": "alice",
    "role": "frontend",
    "source": "auto",
    "ts": 1710000000.0,
}

新手最容易犯的 7 个错

1. 只看 pending,不看 blockedBy

# ❌ 错误
# 没有检查前置阻塞
def is_claimable(task):
    return task["status"] == "pending"

# ✅ 正确
# 检查所有条件
def is_claimable(task):
    return (
        task["status"] == "pending"
        and not task.get("owner")
        and not task.get("blockedBy")
    )

2. 只看状态,不看角色

# ❌ 错误
# 没有角色过滤
def get_claimable_tasks():
    return [task for task in tasks if task["status"] == "pending"]

# ✅ 正确
# 按角色过滤
def get_claimable_tasks(role):
    return [
        task for task in tasks
        if task["status"] == "pending"
        and not task.get("owner")
        and task.get("claim_role") == role
    ]

3. 没有认领锁

# ❌ 错误
# 没有锁,可能导致重复认领
def claim_task(task_id, owner):
    task = load(task_id)
    task["owner"] = owner
    save(task)

# ✅ 正确
# 使用锁保证原子性
def claim_task(task_id, owner):
    with claim_lock:
        task = load(task_id)
        if task["owner"]:
            return "already claimed"
        task["owner"] = owner
        save(task)

4. 空闲阶段只轮询任务板,不看邮箱

# ❌ 错误
# 只检查任务板
def idle_phase():
    tasks = get_claimable_tasks()
    if tasks:
        claim_task(tasks[0])

# ✅ 正确
# 先检查邮箱,再检查任务板
def idle_phase():
    inbox = read_inbox()
    if inbox:
        process_messages(inbox)
    else:
        tasks = get_claimable_tasks()
        if tasks:
            claim_task(tasks[0])

5. 认领了任务,但没有写 claim event

# ❌ 错误
# 没有记录事件
def claim_task(task_id, owner):
    task = load(task_id)
    task["owner"] = owner
    save(task)

# ✅ 正确
# 记录认领事件
def claim_task(task_id, owner):
    task = load(task_id)
    task["owner"] = owner
    save(task)
    
    log_claim_event({
        "event": "task.claimed",
        "task_id": task_id,
        "owner": owner,
        "ts": time.time(),
    })

6. 队友永远不退出

# ❌ 错误
# 永远不退出
def run(self):
    while True:
        work_phase()
        idle_phase()
        time.sleep(5)

# ✅ 正确
# 空闲超时后退出
def run(self):
    idle_count = 0
    max_idle = 10
    
    while idle_count < max_idle:
        work_phase()
        should_continue = idle_phase()
        if not should_continue:
            break
        idle_count += 1
        time.sleep(5)

7. 上下文压缩后不重注入身份

# ❌ 错误
# 没有身份重注入
def resume_from_compression(messages):
    # 直接恢复消息
    return messages

# ✅ 正确
# 重新注入身份
def resume_from_compression(messages, name, role):
    # 检查是否有身份块
    has_identity = any("<identity>" in msg.get("content", "") for msg in messages)
    
    if not has_identity:
        # 注入身份块
        identity_msg = {
            "role": "user",
            "content": f"<identity>You are '{name}', role: {role}. Continue your work.</identity>",
        }
        messages.insert(0, identity_msg)
    
    return messages

为什么这很重要

因为一个真正高效的团队,不应该依赖手动分配任务。

自治智能体系统让你能够:

  1. 自动分配:空闲队友自动认领任务,无需手动分配
  2. 提高效率:任务分配更快速,团队协作更流畅
  3. 角色匹配:按角色分配任务,确保合适的人做合适的事
  4. 可扩展性:团队规模变大时,分配机制仍然有效
  5. 可追踪性:记录认领事件,便于追踪和审计

推荐的实现步骤

  1. 第一步:实现可认领任务的判断逻辑
  2. 第二步:实现原子认领操作,使用锁避免重复认领
  3. 第三步:实现 WORK-IDLE 循环
  4. 第四步:在 IDLE 阶段实现邮箱检查和任务扫描
  5. 第五步:实现身份重注入机制
  6. 第六步:实现认领事件日志
  7. 第七步:集成到团队系统,支持自治队友

自治智能体系统与后续章节的关系

  • s17 自治智能体:解决 Agent 如何主动认领任务的问题
  • s18 Worktree:会利用自治智能体来管理工作区
  • s19 MCP 插件:会利用自治智能体来扩展能力

所以自治智能体系统是构建高级智能体系统的重要组件。

下一章预告

有了自治智能体系统,你的 Agent 已经能够主动认领任务。下一章我们将探讨 Worktree 系统,让 Agent 能够更好地管理工作区和代码隔离。


一句话总结:自治不是让 agent 乱跑,而是让它在清晰规则下自己接住下一份工作。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。