返回文章列表
ai2026年5月3日约 8 分钟阅读

从零理解 AI Agent:claw0 十讲学习笔记

用第一性原理拆解 Agent 架构——每一层为什么存在,会带来什么问题,怎么优化。

学完 claw0 后写的笔记。claw0 是 OpenClaw 的教学版,用十讲代码带你从零搭建一个完整的 AI Agent 网关。

这篇笔记不是逐章总结,而是用第一性原理的方式拆解每一讲:为什么需要这个机制?它解决了什么问题?又带来了什么新问题?

如果你同时在学 Claude Code 的内部架构,可以对照这篇笔记一起看——两个系统很多设计是相通的:Claude Code 架构学习笔记。


十讲的层叠关系

整个课程的结构很清晰:每一讲都在前一讲的基础上加一层,核心循环从始至终没有改变。

问题                          解法
─────────────────────────────────────────────
怎么让 LLM 持续对话?          01讲:while True + stop_reason
怎么让 LLM 执行外部操作?      02讲:TOOLS + TOOL_HANDLERS 分发表
对话历史怎么持久化/不爆炸?    03讲:JSONL 持久化 + ContextGuard
怎么接入多个平台?             04讲:Channel 抽象 + InboundMessage
一个 bot 怎么服务多个场景?    05讲:BindingTable 路由
怎么给 Agent 注入性格和记忆?  06讲:8层系统提示词 + 记忆搜索
怎么让 Agent 主动发起行为?    07讲:Heartbeat + Cron
消息发送失败怎么办?           08讲:DeliveryQueue + 指数退避
API 挂了怎么办?               09讲:三层重试洋葱 + Key 轮换
多个任务并发怎么不乱?         10讲:命名 Lane + Generation 计数器

01讲:Agent 循环

核心认知: LLM 不执行任何操作,它只返回"意图",你的代码负责执行。

while True:
    用户输入 → messages.append
             → LLM API
             → stop_reason == "end_turn"  → 打印,继续等待
             → stop_reason == "tool_use"  → 执行工具,结果还给 LLM

messages[] 是唯一的状态,每次 API 调用都带上完整数组,LLM 才能"记住"上下文。

这带来的问题: messages[] 会无限增长,最终超出上下文窗口。这是第03讲 ContextGuard 存在的根本原因。


02讲:工具使用

核心认知: 工具 = schema(告诉 LLM)+ 分发表(告诉代码)

TOOLS = [{"name": "bash", "description": "...", "input_schema": {...}}]
TOOL_HANDLERS = {"bash": tool_bash, "read_file": tool_read_file}

LLM 只返回一个 JSON 说"我要调用 bash,参数是 ls",实际执行是你的代码查表完成的。

新增工具只需两处各加一行,循环本身不动——这是 Dispatch Map 模式的优雅之处。

值得注意: 工具执行错误不要抛异常,而是返回错误字符串。这样 LLM 能看到错误并自行修正。


03讲:会话与上下文保护

核心问题: 对话历史会越来越长,进程重启会丢失,上下文会爆炸。

JSONL 持久化

每个会话是一个 .jsonl 文件,每行一条记录,追加写入:

{"type": "user", "content": "..."}
{"type": "assistant", "content": [...]}
{"type": "tool_use", "name": "bash", "input": {...}}
{"type": "tool_result", "content": "..."}

恢复时重放这些记录,重建为 API 格式。

三阶段保护(ContextGuard)

Attempt 0: 正常调用
    ↓ 溢出
Attempt 1: 裁剪过大的 tool_result
    ↓ 溢出
Attempt 2: 用 LLM 生成摘要替换旧消息
    ↓ 溢出
raise 异常

代价: 压缩会不可逆地丢失细节。这就是为什么重要信息要主动存到 MEMORY.md(06讲),而不是依赖对话历史。


04讲:通道抽象

核心思路: 不同平台格式各异,但 Agent 逻辑只看统一的 InboundMessage。

两种接入方式:

  • 长轮询(Telegram):你的代码主动每 30s 问一次"有没有新消息"
  • Webhook(飞书):平台主动推送到你的服务器

无论哪种,最终都归一化成同一个数据结构:

@dataclass
class InboundMessage:
    text: str
    sender_id: str
    channel: str    # "telegram" / "feishu" / "cli"
    peer_id: str    # 会话范围
    media: list

消息合并:两种场景,同一思路

等一个短暂的时间窗口,把属于"同一次表达"的碎片合并成一条。

判断依据等待时间
媒体组合并相同的 media_group_id500ms
文本合并同一用户无新消息1s 静默

Telegram 发一组图片时会拆成多条消息,必须等 500ms 攒齐再合并,否则 Agent 会对同一组图片响应多次。


05讲:网关与路由

核心问题: 一个 bot 收到消息,怎么决定交给哪个 Agent 处理?

五层路由,从最具体到最宽泛:

Tier 1: peer_id     → 特定用户(优先级最高)
Tier 2: guild_id    → 特定群组
Tier 3: account_id  → 特定 bot 账号
Tier 4: channel     → 整个平台
Tier 5: default     → 兜底(优先级最低)

本地单人使用时,只有 Tier 5 兜底规则,路由层形同透明。真正要用到多 Agent,需要公网 IP + 在绑定表里配置路由规则。


06讲:智能层

核心问题: 怎么给 Agent 注入稳定的性格,同时让它了解"这个具体的用户"?

两个时机,两件事

启动时(只跑一次,结果缓存): 加载 SOUL.md、IDENTITY.md、所有 SKILL.md → 这就是为什么安装了新 skill 需要重启

每轮(动态构建): _auto_recall() 搜索最相关记忆 → build_system_prompt() 拼 8 层

8 层系统提示词

Layer 1: IDENTITY.md  身份
Layer 2: SOUL.md      性格(靠前 = 影响强)
Layer 3: TOOLS.md     工具指南
Layer 4: 技能块
Layer 5: 记忆(常驻 + 本轮召回)
Layer 6: 引导上下文
Layer 7: 运行时上下文
Layer 8: 通道提示

个性化 = 性格 + 记忆

记忆注入不是加载所有记忆,而是每轮只注入当前最相关的片段——用 TF-IDF 搜索,加时间衰减(越近的记忆权重越高),加 MMR 去重。纯 Python 实现,不需要外部向量数据库。

与 03 讲的关系: 系统提示词越大,上下文压力越大,ContextGuard 越重要。


07讲:心跳与 Cron

核心问题: Agent 目前只能被动响应,怎么让它主动发起行为?

心跳 vs Cron

心跳Cron
触发频率高频持续低频精确
目的Agent 自检"有没有事做"执行预定任务
决策者LLM 自己判断时间到了就执行
类比每隔一会儿看手机设了一个闹钟

心跳的核心价值:把条件判断的决策权交给 LLM,不需要硬编码 if 超过30分钟 then 提醒,只需在 HEARTBEAT.md 里用自然语言描述,LLM 自己决定。

Lane 互斥(用户永远优先)

# 用户线程:阻塞获取,永远能进
lane_lock.acquire()
 
# 心跳线程:非阻塞获取,抢不到就跳过
acquired = lane_lock.acquire(blocking=False)
if not acquired:
    return

08讲:消息投递

核心问题: 消息发送可能失败,进程可能崩溃,怎么保证消息最终能送达?

消息发送有两种失败,需要分别应对:

网络抖动(发了但没成功) → 指数退避重试:[5s, 25s, 2min, 10min] ± 20% 抖动 → ±20% 随机抖动是为了打散重试时间,防止多条消息同时重试把 API 再次打挂(雷群效应)

进程崩溃(还没发就挂了) → 预写磁盘:消息入队时先写文件,发成功了才删文件

写入 .tmp.{pid}.{id}.json   → 崩溃 = 孤立临时文件,无害
os.fsync()                  → 数据已落盘
os.replace()                → 原子交换,绝不产生半写文件

这在工程上叫 at-least-once delivery(至少投递一次)。

类比:就像餐厅把订单写在纸上再通知厨房。服务员摔倒了,纸上的订单还在,换个人捡起来继续执行。


09讲:弹性

核心问题: API Key 限流、认证失败、上下文溢出,各种错误怎么自动恢复?

与 08 讲的区别:

  • 08讲:消息投递的可靠性(回复怎么送达用户)
  • 09讲:API 调用的可靠性(LLM 调用失败怎么恢复)

三层重试洋葱

Layer 1: 遍历所有 API Key,跳过冷却中的
    Layer 2: 上下文溢出 → 压缩历史后重试
        Layer 3: 标准工具调用循环

出了问题先分类,再按类型路由到对应层处理。 这个思维方式比具体代码更重要:

错误类型处理方式冷却时长
auth / billing换 Key300s
rate_limit换 Key120s
timeout换 Key60s
overflow压缩历史,原地重试不冷却

10讲:并发

核心问题: 用户输入、心跳、Cron 同时触发,怎么不互相干扰?

命名 Lane

每种任务进入独立的 FIFO 队列(Lane),Lane 内串行,Lane 间并行:

main      [用户消息处理中...]
cron      [定时任务处理中...]
heartbeat [心跳检查中...]

为什么 Lane 之间并行不会冲突? 因为每条 Lane 有自己独立的 messages[],根本不存在共享状态。冲突只会发生在共享状态上——07 讲需要锁是因为当时只有一个全局 messages[]。

这是对 07 讲锁机制的升级:

07讲(Lock)10讲(Lane)
模型同一时刻只有一个任务Lane 之间并行
扩展性任务类型多了难管理直接新增 Lane

Generation 计数器(防僵尸任务)

重启时所有 Lane 的 generation 递增。旧任务完成时 generation 不匹配,静默丢弃,不再泵送队列——防止过期状态污染新的运行。


总结

第01讲的 while True 循环在第10讲的核心依然清晰可辨。每一讲都是在这个循环外面加一层,而不是替换它。

AI Agent 的本质: 一个 while True 循环,加一张工具分发表,外面包裹着持久化、路由、智能、调度、可靠性和并发控制的层层机制。

对应 Claude Code 的架构可以对照这篇文章:Claude Code 架构学习笔记——你会发现两个系统在工具分发、上下文压缩、技能加载等设计上几乎是同构的。

目录 · 收起