从零开始构建基本AI代理:长任务规划
摘要
本教程解释了如何通过添加暂存器和待办事项列表工具来扩展基本AI代理的长任务规划能力,使代理能够通过逐步规划和跟踪来管理复杂任务。
暂无内容
查看缓存全文
缓存时间: 2026/06/11 13:55
# 从零开始构建一个基础AI Agent:长期任务规划
来源:https://medium.com/@rogi23696/build-a-basic-ai-agent-from-scratch-long-task-planning-14e803f9bd6d
作者:Roger Oriol (https://medium.com/@rogi23696?source=post_page---byline--14e803f9bd6d---------------------------------------)
在《从零开始构建一个基础AI Agent》(https://www.ruxu.dev/articles/ai/build-an-ai-agent-with-tools/)系列的上一个部分中,我们为Agent添加了必要的工具,使其能够自主为我们工作。我们让它具备了查找文件、读写文件、运行bash命令以及从网络获取内容的能力。仅凭这些工具,我们就得到了一个能力相当强的Agent。
## 当Agent执行长而复杂的任务时会发生什么?
目前的Agent工作得很好,但我们希望Agent能完成大量任务,这需要它在任务上保持长时间的专注。现在,如果我们试图让Agent执行长而复杂的任务,会发现它缺乏长期思考的能力,往往在取得一点点进展后就停止工作。这在意料之中,因为LLM的训练目标是对话式行为,它期望以问答的形式来回交流。这对于简单的聊天机器人来说没问题,但我们的Agent需要能够接收一个请求,然后长时间工作,最后返回结果。
## 长期任务规划
接下来我们要赋予Agent的能力是对长而复杂的任务进行规划。Agent需要具备的能力有:
- 理解任务目标
- 提前规划如何应对任务
- 将任务分解为具体步骤
- 持续跟踪待办、进行中和已完成的任务
- 如果当前计划出错,重新思考方案
- 在停止之前检查所有计划的事项是否真正完成
为了赋予Agent这些能力,我们将依赖上一部分新增的**工具**。我们还会在模型的**系统提示**中说明如何使用长期任务规划。
## 新工具:Scratchpad(便签)
这是一个非常简单但强大的工具。我们只是给模型一个地方来写下它的想法,并在稍后再次读取。这个工具的主要好处是,它迫使模型在开始工作之前先思考目标并规划整个方案。该工具将Scratchpad的内容保存在内存中,而不是文件或数据库,这没问题,因为我们不希望在不同会话之间共享Scratchpad的内容。
以下是Python实现:
```python
class Scratchpad:
"""读写内存中的便签"""
def __init__(self):
self._content = ""
def read(self) -> str:
if self._content == "":
return "(empty)"
return self._content
def write(self, content: str) -> str:
self._content = str(content).strip()
return self._content
scratchpad = Scratchpad()
def read_scratchpad():
"""读取便签的内容"""
return scratchpad.read()
def write_scratchpad(content: str):
"""写入便签。之前的内容将被覆盖。"""
scratchpad.write(content)
return "Successfully written content into scratchpad"
```
你可以在本博客系列的Github仓库中找到并克隆这些代码。
## 新工具:To-do List(待办事项列表)
待办事项列表允许Agent将工作分解为任务,并跟踪它们,以便了解还剩下什么要做(**待办**)、当前正在做什么(**进行中**)以及已经完成什么(**已完成**)。这个工具还强制执行一些良好实践:不允许同时有多个任务处于进行中,不允许无效的任务状态,也不允许重复的任务。
和Scratchpad一样,这个工具将待办事项列表保存在内存中而不是文件或数据库中。这也没问题,因为我们不希望在不同Agent会话之间共享待办事项列表。
```python
RETRY_LIMIT = 3
class ToDoList:
"""用于在内存中保存待办事项列表的辅助类"""
statuses = ["pending", "in_progress", "done", "cancelled", "failed"]
def __init__(self):
self._items = []
def read(self, include_completed=False):
"""读取待办事项列表"""
if include_completed:
return [item.copy() for item in self._items]
else:
return [item.copy() for item in self._items if item["status"] != "done" and item["status"] != "cancelled"]
def append(self, id, content, status):
if status not in ToDoList.statuses:
raise Exception(f"Invalid status {status}. "
"Valid to-do statuses: pending, in_progress, done, "
"cancelled, failed")
if self.contains(id):
raise Exception(f"To do item {id} already exists!")
new_item = {"id": id, "content": content, "status": status, "retries": 0}
self._items.append(new_item)
return new_item.copy()
def contains(self, id) -> bool:
"""检查待办事项列表中是否包含指定id的项"""
for item in self._items:
if item["id"] == id:
return True
return False
def update(self, id, content, status):
if status is not None and status not in ToDoList.statuses:
raise Exception(f"Invalid status {status}. "
"Valid to-do statuses: pending, in_progress, done, "
"cancelled, failed")
idx = 0
while idx < len(self._items):
if self._items[idx]["id"] == id:
if content is not None:
self._items[idx]["content"] = content
if status is not None:
prev_status = self._items[idx]["status"]
self._items[idx]["status"] = status
# 将失败的任务重新设为进行中属于重试尝试
if prev_status == "failed" and status == "in_progress":
self._items[idx]["retries"] += 1
return self._items[idx].copy()
idx += 1
raise Exception(f"To do item with id {id} not found")
todo_store = ToDoList()
def todo_append(id, content, status) -> str:
"""向待办事项列表追加一个新项"""
id_str = str(id)
content_str = str(content)
status_str = str(status)
try:
todo_store.append(id_str, content_str, status_str)
return f"Successfully appended to do item {id_str} in to do list!"
except Exception as e:
return f"Failed to append to do item: {e}"
def todo_list(include_completed=False) -> str:
"""列出待办事项列表中的所有项"""
items = todo_store.read(include_completed)
result = f"To Do List ({len(items)} items)\n"
for status in ToDoList.statuses:
count = sum(1 for i in items if i["status"] == status)
result += f"{count} {status} items\n"
result += "-----\n"
for item in items:
retry_note = f", {item['retries'] } retries" if item["retries"] > 0 else ""
result += f"- [{item['id']}] {item['content'] } ({item['status']}{retry_note})\n"
return result
def todo_update(id, content=None, status=None) -> str:
if content is None and status is None:
return "No content or status was given to update. Nothing to do."
try:
item = todo_store.update(id, content, status)
retries = item["retries"]
if item["status"] == "in_progress" and retries > 0:
if retries >= RETRY_LIMIT:
return (f"Updated to do item {id} to in_progress - "
f"but this is retry {retries} of {RETRY_LIMIT} (retry limit reached). "
f"Do not retry again. Escalate to the user instead.")
return (f"Successfully updated to do item {id}! "
f"Retry attempt {retries} of {RETRY_LIMIT}.")
return f"Successfully updated to do item {id}!"
except Exception as e:
return f"Failed to update to do item {id}: {e}"
```
## 新的系统提示
所有无法通过工具实现的长期任务规划策略都在系统提示中向模型进行说明。在这里,我们将向模型解释如何按照文章开头所述的过程进行规划,以及如何使用新工具来辅助规划过程。更多细节请阅读下面的系统提示。我还在系统提示中添加了一条小注释,告知模型如果未另行说明,它需要处理的项目位于当前目录中。
```json
{
"role": "system",
"content": (
"You are a capable coding and research assistant.\n\n"
"## Available tools\n\n"
"Action tools: read_file, write_file, edit_file, glob_files, grep, run_bash, webfetch\n\n"
"Planning tools:\n"
"- Scratchpad (read_scratchpad / write_scratchpad): your private working memory. "
"Use it to think through an approach, store intermediate findings, or draft content "
"before committing. Each write fully replaces the previous content.\n"
"- To-do list (todo_append / todo_list / todo_update): a persistent task tracker. "
"Items carry a status: pending, in_progress, done, cancelled, or failed.\n\n"
"## Working directory\n\n"
"The current working directory is always the user's project root. "
"When asked to work on a project or codebase without a specified path, "
"start by exploring '.' with glob_files or run_bash. "
"Never ask the user to supply a path.\n\n"
"## How to plan\n\n"
"For complex or multi-step tasks (roughly 3 or more distinct steps, or when the "
"path forward is unclear):\n"
"1. Write your initial thinking and approach to the scratchpad before acting.\n"
"2. Break the work into concrete steps and add each one to the to-do list with "
"todo_append (status: pending).\n"
"3. Before starting a step, mark it in_progress with todo_update. "
"Keep only one item in_progress at a time.\n"
"4. Mark items done immediately after completing them - do not batch completions.\n"
"5. Call todo_list to review remaining work before moving to the next step.\n"
"6. Mark tasks cancelled if they become unnecessary.\n\n"
"For simple, single-step tasks: act directly without creating todos.\n\n"
"Planning tool calls (write_scratchpad, todo_append, todo_update, todo_list) "
"are internal bookkeeping, not responses to the user. After any planning tool "
"call, always continue working immediately - make your next tool call or, once "
"the task is fully complete, give a substantive final answer. "
"Never emit an empty or whitespace-only message.\n\n"
"## Replanning\n\n"
"After every tool result, check whether the outcome matched your expectation. "
"If a tool returns an error, unexpected output, or reveals information that "
"changes your understanding of the task, do not move to the next planned step - "
"replan first.\n\n"
"When a step fails:\n"
"1. Diagnose in the scratchpad - is this a recoverable input error (wrong path, "
"typo, wrong argument) or a deeper problem (wrong approach, wrong assumption)?\n"
"2. Mark the task failed: todo_update(id, status='failed').\n"
"3. Choose a recovery action:\n"
" - Retry: the failure is correctable. Fix the input and set the task back to "
"in_progress. The tool will report which retry attempt this is.\n"
" - Replace: the approach is wrong. Cancel the task and add a revised one.\n"
" - Reorder: new information makes a different task more urgent. Update the "
"pending items before continuing.\n"
"4. If todo_update reports that the retry limit has been reached, stop retrying. "
"Write a clear diagnosis in the scratchpad - what you tried, what failed each "
"time, and what you need - then give the user a concise escalation message "
"and wait for their input.\n\n"
"When a tool succeeds but returns information that changes the picture, pause "
"before acting. Call todo_list, reassess all pending items in the scratchpad, "
"and cancel or replace any tasks that no longer make sense.\n\n"
"## How to use the scratchpad\n\n"
"Before each tool call during a complex task, update the scratchpad with your "
"current thinking. Structure each entry around these five steps:\n\n"
"1. Restate the goal - write what you understand the task to be, in your own words. "
"This catches misreads before they compound into wasted work.\n"
"2. Survey what you know - note which files you have seen, what the code structure "
"looks like, and what constraints or requirements apply.\n"
"3. Evaluate options - reason through at least two approaches and explain why you "
"are choosing one over the other (e.g. 'I could rewrite the middleware, or wrap it. "
"Wrapping is safer because it leaves the existing call sites untouched.').\n"
"4. Anticipate failure modes - write down what could go wrong with the chosen "
"approach and how you would diagnose it (e.g. 'If the tests fail after this, the "
"most likely cause is that the session cookie name changed.').\n"
"5. Decide the next single action - commit to exactly one tool call. "
"Do not plan several calls at once; decide the next step only.\n\n"
"Re-read the scratchpad whenever you resume after a tool result to keep your "
"reasoning grounded in what you have already learned.\n\n"
"## Done detection\n\n"
"Do not give a final answer based on the task list being empty alone. "
"Before declaring the task complete, verify all three of the following:\n\n"
"1. Structural completion - call todo_list and confirm there are no pending, "
"in_progress, or failed items.\n"
"2. Verification - check the output against the original goal. For code tasks: "
"run the tests or build with run_bash and confirm they pass. For research tasks: "
"re-read the scratchpad and confirm the assembled answer addresses what was "
"actually asked.\n"
"3. Uncertainty check - read the scratchpad and ask: are there unresolved "
"questions, assumptions that were never validated, or tasks that were cancelled "
"rather than properly completed?\n\n"
"If all three are satisfied, give your final answer. If any are not, re-enter "
"the planning loop - add the outstanding items to the todo list and continue."
),
}
```
## 让我们测试一下!
为了测试我们强大且新颖的Agent,我们需要给它一个真正困难的目标。在我的案例中,我要求它将我的静态网站从Eleventy迁移到Hugo:
```
$ python ../agent-harness/agent-planning/agent.py
You: Explore this static site project and migrate it from Eleventy to Hugo
[tool] write_scratchpad({'content': 'Goal: inspect the static site project, understand the current Eleventy setup, and migrate it to Hugo.
Survey: I have not yet inspected the repo. Need to identify project structure, config, templates, content, assets, and build scripts.
Options: I could try an incremental migration by reproducing the Eleventy output structure in Hugo, or do a clean Hugo reorganization. Incremental is safer because it preserves content and design while minimizing surprises.
Failure modes: Missing Eleventy-specific features (shortcodes, data files, filters, collections) could break the build. Hugo template syntax and content organization differ, so I need to map those carefully. Tests/build may fail if config or paths are wrong.
Next action: inspect the repository root to find the project files.'})
[tool result] Successfully written content into scratchpad
[tool] todo_append({'id': 'migrate-1', 'content': 'Inspect repository structure and identify Eleventy/Hugo-related files', 'status': 'pending'})
[tool result] Successfully appended to do item migrate-1 in to do list!
[tool] todo_append({'id': 'migrate-2', 'content': 'Map Eleventy templates/content/assets to Hugo equivalents', 'status': 'pending'})
[tool result] Successfully appended to do item migrate-2 in to do list!
[tool] todo_append({'id': 'migrate-3', 'content': 'Implement Hugo configuration, templates, and content migration', 'status': 'pending'})
[tool result] Successfully appended to do item migrate-3 in to do list!
[tool] todo_append({'id': 'migrate-4', 'content': 'Run build verification and fix any issues', 'status': 'pending'})
[tool result] Successfully appended to do item migrate-4 in to do list!
[tool] run_bash({'command': "pwd && echo '---' && find . -maxdepth 2 -type f | sed 's#^./##' | sort | head -200"})[........]
[tool] run_bash({'command': 'hugo --minify'})
[tool result] Start building sites ... hugo v0.152.2-6abdacad3f3fe944ea42177844469139e81feda6 darwin/arm64 BuildDate=2025-10-24T15:31:49Z VendorInfo=gohugoio│ EN ──────────────────┼──── Pages ...
[tool] todo_update({'id': 'migrate-4', 'status': 'done'})
[tool result] Successfully updated to do item migrate-4!
[tool] todo_list({'include_completed': True})
[tool result] To Do List (4 items)
0 pending items
0 in_progress items
4 done items
0 cancelled items
0 failed items
------ [migrate-1] Inspect repository structure and identify Eleventy/Hugo-related files (done)...
Assistant: Migrated the site from Eleventy to Hugo and verified the build passes.
What I changed:
```
相似文章
我写了一个实用的、无框架的构建首个AI代理的教程(仅包含循环,没有LangChain/CrewAI)
一个无框架的手动构建简单AI代理的教程,专注于目标、工具调用、观察和决策的核心循环,并提供关于提示词设计、工具暴露、步骤限制和日志记录等实用技巧。
从零开始用Python构建AI Agent
一篇关于从零开始用Python构建AI Agent的博客教程,寻求社区反馈。
构建小型AI代理处理我的零售规划琐碎工作——还有谁在这样做?
一位零售规划师描述使用基于Claude和ChatGPT构建的AI代理来自动化预测和报告等任务,提高日常工作效率。
@googledevs: 大多数智能体教程止步于无状态智能体。真正的工作流会持续数周。构建可暂停……
来自Google的教程,教你使用Agent Development Kit (ADK)构建能暂停数日、重启后恢复且不丢失上下文的长时间运行AI智能体,并附有代码和逐步指导,适用于新员工入职等企业工作流。
为会议前研究构建基于AI的辅助规划助手
作者分享了他们使用Claude Opus 4构建自主AI研究助手的经验,用于会前辅助规划任务,但在扩展到会后文档生成时,由于合规和模板问题遇到挑战。他们寻求建议:这两个阶段是否应保持分离,以及在受监管环境中如何衔接。