S07 Tasks 任务系统
前面第三章的时候,我们就使用了 TodoWrite 任务清单,直接在 AI 对话上下文中记住 TodeWrite 列表。我们用这种方式来让 AI 列出解决长问题的步骤,和分步解决、跟踪问题。
但是如果上下文太长了,要进行上下文压缩了呢?压缩是有损的,这个在 AI 对话上下文中的 TodeWrite 大概率被 AI 压缩后就无法完整保留了。
所以我们不能继续使用这种在 AI 对话上下文中记录 TodeWrite 的方法了,我们可以将任务列表放到文件中去记录。
而且我们已经在前面几章使用了子 Agent 来完成任务,那么如果我们的任务可以被拆分的,分成多条支线任务交给多个子 Agent 去完成的话,就可以提升整体的效率了。主 Agent 只需要记录任务列表、任务分支、各任务的依赖关系即可。
程序流程图
代码
完整代码放在 gitee 了: gitee.com/sanqiushu/D…
下面是代码的具体解释:
先定义持久化任务的存放目录和系统提示词:
# 任务列表目录
TASKS_DIR = WORKDIR / ".tasks"
# 系统提示词
SYSTEM = f"You are a coding agent at {WORKDIR}. Use task tools to plan and track work."
# SYSTEM:您是位于{WORKDIR}的编码代理。请使用任务工具来规划和跟踪工作
然后是 TaskManager 的类,存放的是各种操作 task 文件的逻辑。
我们先看看 .tasks 文件夹里的基本结构:
.tasks/
|---task_1.json 文件内容: {"id":1, "subject":"...", "status":"completed", ...}
|---task_2.json 文件内容: {"id":2, "blockedBy":[1], "status":"pending", ...}
|---task_3.json 文件内容: {"id":3, "blockedBy":[2], "blocks":[], ...}
下面的 TaskManager 类的各种增删改查操作都是基于上面展示的这个结构的。
# TaskManager: 对任务图进行增删改查,用 json 文件来持久化
class TaskManager:
# 初始化:读取 dir 目录,创建目录,然后计算有几个 task_*.json 文件,假设有 5 个,将 _next_id 设为下一个:6
def __init__(self, task_dir: Path):
self.dir = task_dir
self.dir.mkdir(exist_ok=True)
self._next_id = self._max_id() + 1
# 读取 task_*.json 里的 * 有几个,取最大的一个。
def _max_id(self) -> int:
ids = [int(f1.stem.split("_")[1]) for f1 in self.dir.glob("task_*.json")]
return max(ids) if ids else 0
先进行初始化操作
# 读取 task_id 对应的文件内容
def _load(self, task_id: int) -> dict:
path = self.dir / f"task_{task_id}.json"
if not path.exists():
raise ValueError(f"Task {task_id} not found")
return json.loads(path.read_text())
# 保存 task 到文件夹
def _save(self, task: dict):
path = self.dir / f"task_{task['id']}.json"
path.write_text(json.dumps(task, indent=2))
读取 json 文件内容和保存 json 文件内容,比较简单。
# 创建一个新的 task:并将 _next_id 增加 1
def create(self, subject: str, description: str = "") -> str:
task = {
"id": self._next_id, "subject": subject, "description": description,
"status": "pending", "blockedBy": [], "blocks": [], "owner": "",
}
self._save(task)
self._next_id += 1
return json.dumps(task, indent=2)
创建一个新的 task 文件。需要注意的就是让 self._next_id += 1 就行了
# 调用 _load() 函数,读取对应的 task 文件
def get(self, task_id: int):
return json.dumps(self._load(task_id), indent=2)
# 更新 task 的状态,当然如果影响到了其他 task 也要把别的 task 也更新
def update(self, task_id: int, status: str= None, add_blocked_by: list = None,
add_blocks: list = None) -> str:
task = self._load(task_id)
if status:
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"Invalid status: {status}")
task["status"] = status
# 当任务完成时,将其从所有其他任务的 blockedBy 中移除
if status == "completed":
self._clear_dependency(task_id)
# 如果当前任务被某些任务阻塞,那么更新其 blockedBy 的值
if add_blocked_by:
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
# 如果当前任务会阻塞其他任务,那么读取对应的任务,然后更新其 blockedBy 的值
if add_blocks:
task["blocks"] = list(set(task["blocks"] + add_blocks))
# 双向:同时更新被阻塞任务的 blockedBy 列表
for blocked_id in add_blocks:
try:
# 读取被阻塞的 task
blocked_task = self._load(blocked_id)
# 如果被阻塞的 task 的 blockedBy 列表里没有当前的任务 task 的 id,那么才需要更新
if task_id not in blocked_task["blockedBy"]:
blocked_task["blockedBy"].append(task_id)
self._save(blocked_task)
except ValueError:
pass
self._save(task)
return json.dumps(task, indent=2)
这个 update() 函数稍微复杂点,但是做的事情也是很简单,就是修改每个 task 文件里的 blockedBy 和 blocks 数组里的值,其实不需要细看,逻辑很好理解,只是代码比较复杂。我也详细写了注释,看看就行。
# 从所有其他任务的 blockedBy 列表中移除 completed_id
def _clear_dependency(self, completed_id: int):
for f in self.dir.glob("task_*.json"):
task = json.loads(f.read_text())
if completed_id in task.get("blockedBy", []):
task["blockedBy"].remove(completed_id)
self._save(task)
# 展示所有任务的状态,类似这种:[ ] #5: 描述 (blocked by: [1 2])
def list_all(self):
tasks = []
for f in sorted(self.dir.glob("task_*.json")):
tasks.append(json.loads(f.read_text()))
if not tasks:
return "No tasks."
lines = []
for t in tasks:
marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[✅]"}.get(t["status"], "[?]")
blocked = f" (blocked by: {t['blockedBy']})" if t.get("blockedBy") else ""
lines.append(f"{marker} #{t['id']}: {t['subject']}{blocked}")
return "\n".join(lines)
然后下面是工具的索引:
还有工具的定义:太多了,就不全部展示了
主循环没有什么改变:
主程序入口 也没有改变
接下来让 AI 执行一个需求:
Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order. 翻译为中文就是:创建3个任务:“设置项目”、“编写代码”、“编写测试”。按顺序使它们相互依赖。
运行截图:
运行中的消息上下文,task 的结果都可以去 gitee 上查看: