S03 TodoWrite 任务清单
目前的 AI 在进行任务处理时使用的是注意力机制,一开始当我们输入一个需求的时候,AI 的注意力是全部放在我们输入的需求上的,这个时候 AI 能理解我们的需求。但是随着任务的进行,模型的返回内容、工具的输出之类的文本都会被放进 AI 的上下文之中,AI 就会将自己的注意力分配给所有的文字上去,这将导致 AI 逐渐的记不住我们当初提出的需求了!
解决办法也很简单,当我们最一开始提出我们的需求的时候,不要让 AI 一上来就直接开始干活,而是要求 AI,上来写列一个任务列表,然后按照任务列表来慢慢完成整个任务。
并且在任务的进行过程中也会要求 AI 不断的更新这个任务列表,在这种情况下, 任务列表本身也会被加入到上下文中,这样 AI 的注意力机制就可以反复的注意到任务列表,这样就不容易任务跑着跑着就忘记最一开始的需求了。
完整代码
代码现在已经越来越长了,就不整体粘贴在这里了,我放到 gitee 上了,方便国内的大佬们检阅:gitee.com/sanqiushu/D…
程序流程图
根据程序流程图可以看出 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 的文档,唉,真难。
# 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 执行过程的截图:
调用 AI 的整个请求消息历史我也加入到 gitee 了,想要看的可以去看一下。