豆包 Agent Harness 工程师入门 | 第 3 章 TodoWrite 任务清单

54 阅读6分钟

S03 TodoWrite 任务清单

目前的 AI 在进行任务处理时使用的是注意力机制,一开始当我们输入一个需求的时候,AI 的注意力是全部放在我们输入的需求上的,这个时候 AI 能理解我们的需求。但是随着任务的进行,模型的返回内容、工具的输出之类的文本都会被放进 AI 的上下文之中,AI 就会将自己的注意力分配给所有的文字上去,这将导致 AI 逐渐的记不住我们当初提出的需求了!

解决办法也很简单,当我们最一开始提出我们的需求的时候,不要让 AI 一上来就直接开始干活,而是要求 AI,上来写列一个任务列表,然后按照任务列表来慢慢完成整个任务。

并且在任务的进行过程中也会要求 AI 不断的更新这个任务列表,在这种情况下, 任务列表本身也会被加入到上下文中,这样 AI 的注意力机制就可以反复的注意到任务列表,这样就不容易任务跑着跑着就忘记最一开始的需求了。

完整代码

代码现在已经越来越长了,就不整体粘贴在这里了,我放到 gitee 上了,方便国内的大佬们检阅:gitee.com/sanqiushu/D…

程序流程图

image.png

根据程序流程图可以看出 S03 的代码和 S02 的代码变化不大,主要是增加了一个任务列表的工具,并且让 AI 先调用这个工具来规划我们提出的需求,然后在后续的任务执行中继续跟踪 AI 的执行过程,如果 AI 三次调用工具后都没有更新任务列表,那么为了防止 AI 丧失记忆,就提醒 AI 赶紧更新任务列表。

我们不会自作聪明的帮 AI 规划任务,而是让 AI 自己规划任务、自己更新任务、自己完成任务,我们要做的只是建议、提醒、记录,仅此而已。

代码的变化

下面来看看代码方面有哪些变化。

# 系统顶级指令
SYSTEM = f"""你是一个位于 {WORKDIR} 的编程智能体。
请使用 todo 工具来规划多步骤任务。开始前标记为 'in_progress',完成后标记为 'completed'。
优先使用工具,而非纯文本描述。"""

首先是系统级指令进行了更改,一开始就让 AI 用任务列表工具来规划任务。

# 定义 TodoManager 工具的实现
class TodoManager:
    def __init__(self):
        self.items = []

    def update(self, items):
        if len(items) > 20:
            raise ValueError("最多允许20个任务列表")
        validated = []  # 存放校验后的任务列表
        in_progress_count = 0  # 记录当前有几个任务是进行中的
        # 将大模型返回的任务列表进行校验
        for i, item in enumerate(items):
            text = str(item.get("text", "")).strip()
            status = str(item.get("status", "pending")).lower()
            item_id = str(item.get("id", str(i +1)))  # i 从 0 开始的
            if not text:
                raise ValueError(f"任务 {item_id}: text 为必填项")
            if status not in ["pending", "completed", "in_progress"]:
                raise ValueError(f"任务 {item_id}: 错误的 status {status}")
            if status == "in_progress":
                in_progress_count += 1
            validated.append({"id": item_id, "text": text, "status": status})
        # 如果大模型同时执行多个任务,则报错
        if in_progress_count > 1:
            raise ValueError("同一时间只能有一个任务处于 in_progress 状态")
        # 没有问题的话就同意执行这个任务列表了
        self.items = validated
        return self.render()

    # 将 Ai 给出的任务列表展示出来给人看
    def render(self) -> str:
        if not self.items:
            return "没有任务需要进行"
        lines = []
        for item in self.items:
            marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[✅]"}[item.get("status")]
            lines.append(f"{marker}# {item['id']}: {item['text']}")
        done = sum(1 for t in self.items if t["status"] == "completed")
        lines.append(f"--------------\n({done}/{len(self.items)} 已完成)")
        return "\n".join(lines)

然后定义 TodoManager 类,来更新任务的状态,和展示当前任务给人看。

其中主要的功能就是不允许将任务列的太详细、判断任务格式是否正确、AI 有没有同时标记多个任务为 in_progress 等。

将计划项限制在 20 条以内。这是对过度规划的刻意约束。不加限制时,模型倾向于将任务分解成越来越细粒度的步骤,例如产出 50 条的计划,但每一步都微不足道。

冗长的计划很脆弱:如果第 15 步失败,剩下的 35 步可能全部作废。20 条以内的短计划可以对任务进行合理的抽象规划,也更容易在现实偏离计划时做出调整。

任务规划工具强制要求任何时候最多只能有一个任务处于 in_progress 状态。如果模型想开始第二个任务,必须先完成或放弃当前任务。

如果允许同时进行多个项目会让智能体在不同任务间切换上下文,这看似更加灵活。但实际上,大型语言模型在处理上下文切换方面表现不佳——它们会忘记正在进行的任务,并在不同任务的细节上产生混淆。单一专注的限制是一种防护措施,有助于提升输出质量。

{
    "type": "function",
    "function": {
        "name": "todo",
        "description": "更新任务列表。追踪多步骤任务的进度。",
        "parameters": {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "id": { "type": "string"},
                            "text": { "type": "string"},
                            "status": {
                                "type": "string",
                                "enum": ["pending", "in_progress", "completed"]
                            }
                        },
                        "required": ["id", "text", "status"]
                    }
                }
            },
            "required": ["items"]
        }
    }
}

然后是工具定义这里新增了一个 todo 工具的定义,这里 items 的类型是 array 类型,当我去火山大模型文档中去查询 array 类型的定义如何编写的时候,火山大模型的文档让我自己去查 JSON 的文档,唉,真难。

image.png

# Agent 循环的核心方法
def agent_loop(messages: list):
    round_since_todo = 0  # 任务执行的轮次
    while True:
        response = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS, max_tokens=8000)
        # 将大模型返回的消息添加到消息列表中
        message = response.choices[0].message
        messages.append(message.model_dump())
        # 如果大模型没有调用工具,那么结束执行
        if response.choices[0].finish_reason != "tool_calls":
            # 打印 AI 的响应
            print(f"\n\033[36mAI: {message.content}\033[0m\n")
            return
        # 打印 AI 的响应
        print(f"\n\033[36mAI: \033[0m\033[36;9m思考: {message.content}\033[0m\n\033[36m回答:{message.reasoning_content} \033[0m\n")
        used_todo = False
        # if message.tool_calls:
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            call_id = tool_call.id
            # 3. 路由并执行本地函数
            handler = TOOL_HANDLERS.get(func_name)
            try:
                output = handler(**func_args) if handler else f"不存在 {func_name} 工具"
            except Exception as e:
                output = f"Error: {e}"
            print(f"\n\033[33mtools : \n{output}\033[0m\n")  # 打印工具的输出
            messages.append({"role": "tool", "content": output, "tool_call_id": call_id})
            if func_name == "todo":
                used_todo = True
        round_since_todo = 0 if used_todo else round_since_todo + 1  # 记录大模型多少轮没有调用 todo 工具了
        if round_since_todo >= 3:
            messages.append({"role": "user", "content": "<reminder>请更新你的任务列表。</reminder>"})

然后是 Agent 主循环部分。定义了一个 round_since_todo 参数来记录调用大模型的次数。

然后在函数的末尾判断:如果 AI 调用了任务列表工具,那么重制调用次数,如果调用其他工具,那就将 round_since_todo 加 1. 如果大模型调用了三次工具都没调用 任务列表工具更新当前的进度,那么在消息列表中插入提醒语句,让 AI 更新任务列表。

其他的变化就不大了,请自行查看代码就行。

然后下面是 AI 执行过程的截图: image.png

调用 AI 的整个请求消息历史我也加入到 gitee 了,想要看的可以去看一下。