🚀 从零打造 AI Agent:核心闭环篇(S01-S06)

没有循环,就没有 Agent。这不是一句鸡汤,这是 AI 从"会说话"变成"会干活"的唯一途径。


🎬 开场:为什么你的 AI 总是"纸上谈兵"?

你有没有遇到过这种情况?

你让 AI 帮你写代码,它洋洋洒洒写了一千字,分析了需求,设计了架构,甚至画了流程图…

然后呢?

然后就没有然后了。代码只存在于它的"想象"里,你的文件夹空空如也。

问题出在哪?

问题出在:AI 只能"说话",不能"做事"。

它可以生成文本,但它不会自己去打开文件、运行命令、观察报错,把结果再喂回给模型继续推理。

那么,怎么让 AI 从"会说话"变成"会干活"呢?

答案就是这一系列文章的主题——打造真正的 AI Agent


📚 这个系列要讲什么

Learn Claude Code 将构建 Agent 系统分成了 19 个章节,我们将其整合成 4 个大部分

一、核心闭环(S01-S06)

基础能力建设,让 AI 从"说话"变"干活"

二、系统加固(S07-S11)

安全与稳定,让 AI 更可靠

三、任务运行时(S12-S14)

任务管理,让 AI 能协作

四、多 Agent 平台(S15-S19)

团队协作,让 AI 团队作战

今天,我们先从核心闭环开始。


🍳 先用做菜打个比方

想象你要做一道宫保鸡丁:

没有闭环的方式

  1. 你告诉厨师:“我要宫保鸡丁”
  2. 厨师说:“好的,需要去买鸡丁”
  3. 你说:“那去买吧”
  4. 厨师去买回来
  5. 厨师说:“鸡丁买回来了,还要花生”
  6. 你说:“那去买花生”
  7. …(无限循环,累死你)

有闭环的方式

  1. 你跟厨师说:“给我做一道宫保鸡丁,具体你自己看着办”
  2. 厨师自动去买鸡丁
  3. 回来告诉你"鸡丁买好了,要炒了"
  4. 你说"炒"
  5. 炒完说"该放花生了"
  6. 你说"放"
  7. 最后说"做好了"

Agent Loop 就是这个道理:任务 → 执行 → 反馈 → 下一步 → … → 完成。


🎯 一句话记住

Agent Loop 的本质,是把"模型的动作意图"变成"真实执行结果",再把结果送回模型继续推理。


� S01:Agent Loop - 核心闭环

什么是 Loop?

不是程序死循环,而是:只要任务还没做完,系统就继续重复同一套步骤

就像工厂流水线,产品没完成,传送带就一直转。

最小心智模型

用户消息
    ↓
   模型
    ↓
+-- 普通回答 → 结束
|
+-- 调用工具 → 执行
|
    ↓
工具结果
    ↓
写回消息历史
    ↓
下一轮继续

关键点只有一句:

工具结果必须重新进入消息历史,成为下一轮推理的输入。

少了这一步,模型就无法基于真实观察继续工作。

5个必须懂的名词

  1. Loop(循环):只要任务没完成,就继续重复同一套步骤
  2. Turn(一轮):发消息给模型 → 读回复 → 执行工具 → 把结果写回消息历史
  3. tool_result(工具结果):工具执行完返回的东西,必须写回消息历史
  4. State(状态):主循环继续往前走时,需要一直带着走的那份数据
  5. Messages(消息历史):不是聊天记录,而是模型下一轮要读的工作上下文

最小实现代码

# 第1步:准备初始消息
messages = [{"role": "user", "content": query}]

# 第2步:调用模型
response = client.messages.create(
    model=MODEL,
    system=SYSTEM,
    messages=messages,
    tools=TOOLS,
)

# 第3步:追加 assistant 回复(最容易被忘!)
messages.append({"role": "assistant", "content": response.content})

# 第4步:如果模型调用了工具,就执行
results = []
for block in response.content:
    if block.type == "tool_use":
        output = run_bash(block.input["command"])
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

# 第5步:把工具结果写回 messages
messages.append({"role": "user", "content": results})

新手最容易踩的5个坑

❌ 坑1:工具结果打印了,但不写回 messages
# 错误做法
result = run_bash("ls")
print(result)  # 打印了,但没写回去!

# 正确做法
messages.append({"role": "user", "content": [tool_result]})
❌ 坑2:只保存用户消息,不保存 assistant 消息
# 错误做法
response = client.messages.create(messages=messages)
# response 往哪放?没放!

# 正确做法
messages.append({"role": "assistant", "content": response.content})
❌ 坑3:不绑定 tool_use_id
# 错误做法
results.append({
    "type": "tool_result",
    "content": "文件内容..."  # 缺了 tool_use_id!
})

# 正确做法
results.append({
    "type": "tool_result",
    "tool_use_id": block.id,  # 必须绑定!
    "content": "文件内容..."
})
❌ 坑4:一上来就把流式、并发、恢复、压缩全塞进来

第一关只做一件事——把最小回路跑通。其他的,慢慢加。

❌ 坑5:以为 messages 只是聊天展示

Messages 是模型的输入原料,告诉模型"现在处于什么状态,下一步该做什么"。


🛠️ S02:工具使用 - 把意图路由成动作

核心问题

只有 bash 时,所有操作都走 shell,容易出问题:

  • cat 截断不可预测
  • sed 遇到特殊字符就崩
  • 每次 bash 调用都是不受约束的安全面

解决方案

加工具不需要改循环。只要定义好 handler 和 schema,注册到 dispatch map,主循环自然知道怎么调用。

三张图理解工具系统

1. Tool Schema(给模型看的说明书)
{
    "name": "read_file",
    "description": "读取文件内容",
    "input_schema": {
        "properties": {
            "path": {"type": "string", "description": "文件路径"}
        },
        "required": ["path"]
    }
}

2. Handler Function(真正的执行者)
def run_read(path: str) -> str:
    text = safe_path(path).read_text()
    return text

3. Dispatch Map(路由表)
TOOL_HANDLERS = {
    "bash": lambda **kw: run_bash(kw["command"]),
    "read_file": lambda **kw: run_read(kw["path"]),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
}

🍳 外卖点餐的比喻

  1. 你(模型)想点宫保鸡丁
  2. App(dispatch map)查表找到"川菜厨师"
  3. 川菜厨师(handler)真正去做菜
  4. 结果返回给你

关键是:你不需要告诉 App “去找哪个厨师”,它自己查表匹配。

核心代码

# 路径安全检查
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

# 工具执行
for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS.get(block.name)
        output = handler(**block.input) if handler else f"Unknown tool"

新手最容易踩的3个坑

  1. 每个工具都写 if/elif → 用 dispatch map,一行解决
  2. 不加路径安全检查 → 用户可能读取任意文件
  3. handler 参数不一致 → schema 和 handler 参数名要保持一致

📝 S03:待办写入 - 会话级计划

核心问题

多步任务容易走一步忘一步,明明做过的检查会重复再做。

解决方案

让 Agent 把"当前计划"显式写出来:

[ ] 凉拌黄瓜  ← 刚做完
[x] 红烧肉    ← 正在做
[ ] 番茄蛋汤  ← 待做

🍳 做饭记事本的比喻

没有记事本: 做完一道,你问助手"接下来做什么?" 助手一脸懵。

有记事本: 你做完一道,就在记事本上打个勾,下一道要做什么,一目了然。

核心数据结构

# 待办条目
PlanItem = {
    "content": "Read the failing test",
    "status": "pending",  # pending / in_progress / completed
    "activeForm": "Reading the failing test",
}

# 强制规则:同一时间最多一个 in_progress
def update(self, items: list) -> str:
    in_progress_count = sum(1 for i in items if i.status == "in_progress")
    if in_progress_count > 1:
        raise ValueError("Only one item can be in_progress")

新手最容易踩的5个坑

  1. 计划写得过长 → 一次最多列 3-5 项
  2. 同时多个 in_progress → 焦点散掉
  3. 只在开始写一次,后面从不更新 → 计划变成摆设
  4. 把会话计划当成长期任务系统 → S03 只是当前会话,不负责跨阶段持久化
  5. 以为 reminder 是可有可无的装饰 → 主循环不仅要执行动作,还要维护动作过程中的结构化状态

👥 S04:子代理 - 子任务使用全新上下文

核心问题

大任务的所有中间过程都堆在主上下文里,真正的结论反而没地方放。

解决方案

把局部任务放进独立上下文里做,做完只把必要结果带回来。

工作流程

Parent agent
    |
    | 1. 决定把局部任务外包出去
    v
Subagent(全新上下文)
    |
    | 2. 在自己的上下文里工作
    v
Summary(摘要返回)
    |
    | 3. 只把结果带回父 Agent
    v
Parent agent continues

🍳 老板与助理的比喻

没有子代理: 助理读了 50 个文件,全记在脑子里,真正的结论装不下了。

有子代理: 助理派一个审计员去读文件,审计员只把结论带回来。

核心代码

def run_subagent(prompt: str) -> str:
    # 关键:从一份新的消息列表开始,不是共享父 Agent 的 messages
    sub_messages = [{"role": "user", "content": prompt}]
    
    # 只返回摘要,不返回全量历史
    return {"type": "tool_result", "content": summary_text}

新手最容易踩的4个坑

  1. 把子代理当成"炫技的并发" → 首先是为了解决上下文问题
  2. 把父历史全部原样灌回去 → 只带摘要,不带全量历史
  3. 一上来就做复杂的角色系统 → 先把"干净上下文的子任务执行器"做对
  4. 忘记给子代理设置停止条件 → 可能无限转

fork 是什么

普通子代理: 从空白上下文开始

sub_messages = [{"role": "user", "content": prompt}]

fork: 先复制父 Agent 的上下文,再追加子任务

sub_messages = list(parent_messages)  # 继承上下文
sub_messages.append({"role": "user", "content": prompt})

📚 S05:技能系统 - 先轻发现,再深加载

核心问题

不同任务需要不同领域知识,全部塞进 system prompt 会臃肿。

解决方案

平时只展示目录(发现),需要时才加载正文(按需)

两层结构

第1层:轻量目录(system prompt 里)
 - skill 名称
 - skill 描述
 - 让模型知道"有哪些可用"

第2层:按需正文(真正需要时才加载)
 - 只有模型真正需要时才加载
 - 通过 tool_result 注入当前上下文

🍳 图书馆目录的比喻

  • 大厅目录牌写着"医学区在左转、烹饪区在右转…"
  • 你想找做菜的书,系统把《烹饪全书》拿出来
  • 用完又放回去,不占用大厅空间

四类 Memory 的区别

概念 定义 判断方法
skill 可选知识包 某类任务才需要的做法?→ skill
memory 跨会话记住的信息 长期事实需要记住?→ memory
CLAUDE.md 稳定规则说明 更全局的规则?→ CLAUDE.md

新手最容易踩的5个坑

  1. 把所有 skill 正文永远塞进 system prompt → 会臃肿
  2. skill 目录信息写得太弱 → 没有描述,模型不知道什么时候该加载
  3. 把 skill 当成"绝对规则" → skill 更像"可选工作手册"
  4. 把 skill 和 memory 混成一类 → skill 解决"怎么做",memory 解决"记住什么"
  5. 一上来就讲太多多源加载细节 → 重点是"轻发现 + 按需加载"

📦 S06:上下文压缩 - 保持活跃上下文小而稳

核心问题

上下文越来越长,模型注意力被淹没,最终撞上上限。

三层压缩策略

第1层:大结果不直接塞进上下文
 → 写到磁盘,只留预览

第2层:旧结果不一直原样保留
 → 替换成简短占位

第3层:整体历史太长时
 → 生成一份连续性摘要

🍳 手机内存的比喻

没有压缩: 桌面上堆满上周、上上月、半年前的文件,要找东西得翻遍整个桌子

有压缩: 最近用的文件放桌上,旧文件放抽屉,再旧的放档案柜

压缩后必须保住的4类信息

必须保住 解释
当前任务目标 模型要知道"我在做什么"
已完成的关键动作 模型要知道"做过什么"
关键决定与约束 模型要知道"有什么限制"
下一步应该做什么 模型要知道"接下来该干嘛"

新手最容易踩的5个坑

  1. 以为压缩等于删除 → 是换一种表示,不是删掉
  2. 只在撞到上限后才临时乱补 → 从一开始就有三层思路
  3. 摘要只写成一句空话 → 必须保住关键信息
  4. 把压缩和 memory 混成一类 → 压缩解决"当前会话太长",memory 解决"跨会话保留"
  5. 一上来就给初学者讲过多产品化层级 → 先讲清最小正确模型

🎯 核心公式总结

核心闭环 = Agent Loop + 工具路由 + 待办写入 + 子代理 + 技能系统 + 上下文压缩
Agent Loop = 发消息 → 看回复 → 执行工具 → 写回结果 → 下一轮
工具路由 = dispatch map = {tool_name: handler}
待办写入 = pending → in_progress → completed
子代理 = 新上下文 + 摘要返回
技能系统 = 轻量目录 + 按需加载
上下文压缩 = 大结果落盘 + 旧结果缩短 + 整体摘要

🎯 一句话带走

记住这句话就够了:Agent 的核心不是"模型很聪明",而是"系统持续把现实结果喂回模型"。工具结果必须写回消息历史,成为下一轮的输入,没有这条回路,AI 就只是"会说话",不会"会干活"。


▶️ 下一步

下一篇文章我们将进入系统加固部分,学习如何给 Agent 加上安全闸门、扩展能力、长期记忆,让它更稳定,更安全。

相关文章:


关注我,一起探索 AI Agent 的世界!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐