现在市面上的 Agent 教程太多了,要么太浅要么太碎。
之前一直关注的博主三元同学最近出了吃透 AI Agent 开发,这门课从「底层实现级」角度拆解真实产品的工程决策,帮你建立完整的 Agent 知识体系,覆盖六大方向。
这门课学习下来让我对 Agent 有了全面的认知,下面是我的学习笔记
往期学习笔记
一个 while 循环,凭什么让 AI 从"能聊天"变成"能干活"
你有没有想过一个问题:ChatGPT 和 Claude Code,用的都是前沿的大模型,为什么体验完全不一样?
ChatGPT 你问一句它答一句,聊完就完了。Claude Code 你说一句"帮我重构这个项目",它自己就开始读代码、跑命令、改文件、跑测试,中间可能要跑十几轮,最后跟你说"搞定了"。
这中间的差距,不在模型能力上,而在谁掌控了循环。
背景与动机
理解 Agent,先从三种形态的本质区别说起。Anthropic 在《Building Effective Agents》中给出了清晰的分类框架:
| 形态 | 核心词 | 控制权归属 | 人的角色 |
|---|---|---|---|
| ChatBot | 响应 | 人(用户问,模型答) | 驱动者 |
| Copilot | 辅助 | 人主导 + AI 建议 | 主导者 |
| Agent | 自主 | AI 自主循环 | 监考官 / 否决者 |
从 ChatBot 到 Agent,本质是控制权(Control Loop)的转移:
- ChatBot:循环在用户手里。用户按一次 send,模型跑一轮,然后停止等待。用户是唯一的循环驱动力。
- Agent :循环在 AI 手里。用户只做一次输入(设定目标),AI 自己跑
while(true),直到任务完成或被人打断。
用更技术化的说法,这是"目标导向的自主循环能力"------人类从"驱动者"退化为"设定目标 + 必要时否决"的监工角色。
这也解释了为什么 Agent 突然在 2025-2026 年爆发。知识(2022 年 ChatGPT 证明了大模型拥有广泛知识储备)、推理(2024 年 o1 模型证明了多步推理能力)、长期迭代(2025 年 Claude Code 证明了模型可以持续自主工作几十轮)三层能力相继成熟,加上上下文窗口扩展到 100K-1M+、Function Calling 变成模型原生能力、MCP 协议标准化------这些基础设施的叠加,构成了 Agent 能 work 的前提。
核心观点
while(true):探索而非遍历
Agent 的核心逻辑,用最少的代码表达就是这样:
typescript
while (true) {
const response = await llm.chat(messages) // Think:让模型决定下一步
if (response.toolCalls.length === 0) {
break // 模型认为任务完成了,没有工具要调
}
for (const toolCall of response.toolCalls) {
const result = await executeTool(toolCall) // Act:执行工具
messages.push(result) // Observe:把结果加入上下文
}
}
就这么几行。但它代表了一个根本性的设计选择------用 while(true) 而不是 for 循环。
不只是因为任务步数不可预测。深层原因是:
- for 循环预设了一个迭代范式 :你要遍历的东西,在循环启动前就已经存在了。
for item in list------list 是已知的、完整的。 - while(true) 没有这个预设:它不是"遍历",而是"探索"。模型在每一轮现场推理下一步做什么,下一步完全取决于上一步的结果。没有已知路径,没有预设步骤数,没有预设方向。
| for 循环 | while(true) | |
|---|---|---|
| 迭代范式 | 已知、可枚举 | 未知、动态生成 |
| 边界 | 遍历完即止 | 靠外部 break 强制终止 |
| 步数 | 可预测 | 不可预测 |
| 失败模式 | 越界报错 | 死循环或假性完成 |
for 循环的终止逻辑内嵌在范式里("遍历完就停"),而 while(true) 的终止逻辑是完全外部植入的。这正是 Agent 架构设计困难的根源:你在用一套没有预设边界的东西,却必须人工确保它不会越界。
break 的四类条件
从循环内部看,break 条件本质上只有四类:
kotlin
① 任务完成 → break("goal_reached")
② 达到上限 → break("max_steps")
③ 异常/错误 → break("error")
④ 人类否决 → break("human_reject")
但真正难的不是这四类,而是第一个:任务完成的判定。
- "把文件保存了"------可以判断
- "让用户满意了"------无法判断
- "搜索了足够多的信息"------标准模糊
这直接引出一个核心问题:模型有没有能力自我评估"任务是否完成"?如果模型自己无法判断,那就需要外部的验证层(evaluator);如果模型可以自我评估,那 prompt 里就必须显式植入"完成标准"。
假性完成:最隐蔽的陷阱
真正难以捕捉的问题是"假性完成"------模型说"我完成了",但实际上任务并没有真正完成。
两种走向:
- 正确评估:任务真的完成了,工具调用 → 看到结果 → 判断"完成了" → 不再调用工具 → 输出最终答案。但 while(true) 还在空转,循环需要主动判断:这是"完成"还是"卡住"?
- 错误评估(假性完成):任务并未真正完成,工具调用 → 看到结果 → 判断"差不多了" → 不再调用工具 → 输出一个半成品答案。模型自我感觉良好,但实际上复读机了,或者跳步漏了关键环节。
核心矛盾:模型说"我完成了" ≠ while(true) 自动停止。
循环终止信号必须有显式声明,而不是依赖"没有新动作"作为隐式信号。max_steps、termination_token、stopping_condition------各种 Agent 框架里的终止机制,追其根源都在解决这个问题。
补充视角:finish_reason 协议
while(true) 怎么"知道"模型没有调新工具了?循环本身不读人心,它读到的是一个具体的协议信号。OpenAI API 里,模型想调工具时返回 finish_reason: "tool_calls",不想调了返回 finish_reason: "stop"。
所以 while(true) 的退出逻辑在工程上其实是:
typescript
if (response.finish_reason != "tool_calls") {
break
}
这才是循环退出的实际触发器。
技术细节
ReAct 三步闭环
Agent 每轮内部,模型在做三件事:
Think(思考):模型分析当前状态------目标是什么?上一步结果如何?下一步该怎么做?纯内部推理,不产生任何外部效果。
Act(行动):将推理结果转化为具体的工具调用或最终回复。产生外部可观测效果(API 请求、文件写入等)。
Observe(观察):接收工具返回结果,将其注入下一轮上下文中。形成闭环反馈,让模型能"边做边调整"。
Observe 不只是"看到",而是 messages.push(tool_result)。不 push,下一轮的 Think 就读不到上一轮 Act 的结果,整个反馈闭环就断了:
markdown
Think → Act → Observe → Think → Act → Observe → ...
▲________________________|
没有 Observe 的循环是开环的------模型只管往外输出,不知道结果对不对。这解释了为什么"复读机现象"和"假性完成"会发生在缺少 Observe 约束的情况下:模型在一个错误的方向上不断 Think + Act,但没有修正的机会。
Observe 的双重职责
Observe 的关键远不止它名字看起来那么简单。它不只是"接收工具返回结果",而是**"把相关结果以正确粒度写进去"**。
想象这个场景:Agent 用 read_file 读了一个 500 行的 Python 文件,但下一轮模型只需要改第 47 行的 import 语句。500 行全部 push 进 messages,有什么问题?
- 上下文被污染:模型需要在 500 行里找到"第 47 行",注意力被稀释
- Token 成本爆炸:messages 体积快速增长,上下文窗口飞快耗尽
- 实际任务受影响:真正需要的信息(第 47 行)被淹没在噪音里
这引向 Observe 的两种处理策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 全量 push | 工具返回什么就写什么 | 结果简短、全部相关 |
| 选择性 push | 只写模型实际需要的那部分 | 结果庞大、局部相关 |
选择性 push 的关键问题是:谁来决定"哪部分是相关的?"
- 靠模型自己判断 → 需要额外一轮推理
- 靠工具本身返回结构化结果 → 工具设计复杂度上升
- 靠外部过滤器/处理器 → 系统复杂度上升
Observe 的真实职责,不只是"把结果写进去",而是"把相关结果以正确粒度写进去"。 这触及 Agent 架构里一个核心难题:信息压缩 vs 信息丢失的权衡。
错误恢复的代价
Agent 发现改错了(假设第 8 轮改完,第 9 轮执行报错 NameError: name 'path' is not defined),它能从错误中恢复吗?
能恢复------Round 8: Act → 修改代码(假设漏了 import),Round 9: Act → 执行报错,Observe → push 错误信息,Round 10: Think → 分析错误 → "缺 import" → Act → 补上 import。模型看到错误信息,知道"缺了什么",能直接修复。这是良性的错误恢复。
不能恢复、或恢复代价极高 ------问题不在错误本身,在于错误暴露的是信息压缩的连锁后果。如果 Round 8 是基于"导入了 os 模块"这个模糊标签修改的代码,Round 9 执行报错,模型只知道"path 未定义",但它不知道:这个标签本身是错的(实际应该是 from os import path)。它可能反复尝试不同的 import 写法,每次都基于同样的错误前提。
核心矛盾:Observe 提供的是"错误结果",但"为什么会错"的答案往往不在结果里,而在被压缩掉的那段原始信息里。 错误反馈闭环存在,但信息不对称闭环是断的。
恢复的代价矩阵:
| 代价类型 | 具体表现 |
|---|---|
| 上下文膨胀 | 错误 stack、多次尝试记录都进 messages |
| token 成本 | 每轮恢复都是额外消耗 |
| 信任侵蚀 | 人类开始怀疑 Agent 的自我修正能力 |
| 潜在死循环 | 模型在错误方向上反复尝试,每次都"看起来在修复" |
最重要的一点:潜在死循环是质变,不是量变。 上下文膨胀、token 成本、信任侵蚀是"多花几轮"的问题,而死循环是模型在错误方向上反复兜圈,每一轮都基于同一个残缺的前提。
Observe 定义模型的"现实"
Observe 不仅仅是在构建闭环,它是在定义模型的"现实"。 模型只能基于它看到的现实做推理。如果这个现实是一张打了马赛克的地图,Agent 就会在马赛克区域内迷路------而且它不会意识到自己在迷路。
这里有一个值得深想的问题:模型有没有能力主动要求"还原原始信息"?
如果模型发现自己基于压缩信息推理不下去了,它能不能说"我需要原始文件内容"------还是说,它只能在被压缩的现实里继续硬着头皮推理?这决定了一个关键问题:压缩粒度是由外部预设的,还是模型可以动态协商的?
如果是外部预设(框架固定了信息粒度),那风险是静态的、可评估的。如果是模型动态协商(模型自己判断"我需要更细的粒度"),那又多了一个可能的死循环入口------模型可能在"要不要重新获取原始信息"这件事上反复纠结。
死循环检测三层门禁
无意义的死循环和正常的任务重试在行为层面看起来几乎一样------都是重复调用同一个工具。区分它们,需要上下文感知的动态阈值,而不是全局固定的 max_steps。
方法 1:工具指纹 + 环境状态双重校验
typescript
if hash(current_tool_call) == hash(previous_tool_call):
if environment_state_unchanged:
repeat_count += 1
if repeat_count >= 3:
trigger_intervention // 不是"正常重试"而是"卡住了"
如果工具调用完全相同 + 环境没有任何变化,说明这个调用根本没生效------可能是权限问题、依赖问题、或者前提条件不满足。
方法 2:错误签名追踪
typescript
error_signature = hash(error_type + error_message)
if error_signature == last_error_signature:
stuck_on_same_error += 1
Agent 反复遇到同样的 NameError,不是在"重试",是在用同样的错误前提反复撞墙。
方法 3:语义相似度检测
typescript
similarity = cosine_embedding(think_history[-3:])
if similarity > 0.95: # 连续3轮想的几乎是同一件事
trigger_intervention
模型可能在说不同的话,但核心推理方向完全没变------这是更隐蔽的死循环。
| 方法 | 检测依据 | 优点 | 缺点 |
|---|---|---|---|
| 工具指纹 + 状态 | 调用重复 + 无环境变化 | 精准定位"动作无效" | 需要追踪环境状态 |
| 错误签名 | 同类错误反复出现 | 捕捉"错误路径循环" | 同类错误可能有合理重试 |
| 语义相似度 | 思考方向收敛 | 发现隐蔽的兜圈 | 计算成本高,阈值难定 |
最关键的设计洞察:这三种方法都不是孤立的。工程上通常是"多层门禁"------任何一个信号触发阈值,就抛给人类审查或者强制终止。max_steps 是最后兜底的硬性上限,但不是主要的检测器。
区分"无意义循环"和"有意义的重复劳动"
有些任务本身就是需要"高度相似"的重复。例如 Agent 在重构一个拥有 50 个方法的巨型文件,需要连续执行 50 次 replace_code 工具。如果死循环检测器太"敏感",会在第 5 个方法时就报警,把正常工作的 Agent 强制踢下线。
关键在于:参考任务本身的结构元数据,而不是只看 Agent 的行为轨迹。
css
无意义循环: 目的 = 修改文件
手段 = replace_code(method_3)
结果 = 权限不足,报错
下一步 = replace_code(method_3) ← 重复
有意义的重复: 目的 = 重构 50 个方法
手段 = replace_code(method_1~50)
结果 = 文件内容持续变化
下一步 = replace_code(method_4) ← 递进
系统应该参考的额外元数据:
- 任务结构信息:是否有可枚举的子目标("重构 50 个方法,当前在第 23 个")
- 环境状态 diff:每次调用是否真的产生了变化
- 参数指纹:工具调用的 hash 是否真的相同
最终的设计原则:死循环检测的阈值应该是"上下文感知的",而非全局固定常数。
差异化上下文管理四层架构
当 Agent 运行到第 50 轮,对话历史已经包含了 50 次 Think-Act-Observe,Token 消耗剧增,且模型开始因上下文过长而变得"健忘"。
上下文管理的本质不是"压缩",而是差异化存储------不同类型的信息有不同的保留策略。
makefile
L0: 任务锚点(永不丢弃)
原始任务目标、关键约束、已知子目标数量
例:"重构 50 个方法,当前在第 23 个"
"这是一个支付系统,不能修改金额计算逻辑"
L1: 关键发现(显式提取,长期保留)
早期获得的关键信息(密钥、路径、API 端点)
例:"发现了 admin_password = xxx"
"第三方库 libX 提供所需函数"
L2: 工具结果(动态裁剪)
• 最近 3 轮:完整保留(模型需要精确上下文)
• 4-10 轮:只保留结论摘要,不保留原始返回
• 10 轮以前:完全丢弃,除非命中 L0/L1
L3: 推理过程(大幅压缩)
Think 内容只保留"事实性结论",不保留推理路径
例:原本 "我想先试试 X,因为 Y 可能不对..."
压缩成 "X 方法失败,原因 Z"
传统滑动窗口把第 5 轮的 tool_result 和第 45 轮一视同仁,都用"最近 10 轮"来裁剪。差异化做法的关键是:第 5 轮获得的密钥,在 L1 层显式提取并锚定,之后第 5 轮本身可以从 history 中删除。
L1 层的维护机制:L1 的信息需要主动提取,不是被动保留。每轮结束后系统问:这轮发现了什么新事实?这些事实会被后续哪几步用到?引用热度低的直接丢弃,引用热度高的进入 L1 永久保留。
最终的设计原则:上下文瘦身不是减少信息量,而是提升信息密度。
安全从信息层跃迁到行为层
ChatBot 的安全问题是信息层 ------它只管说,不管做。Agent 的安全问题是行为层------它说的话会直接变成现实,而且往往不可逆。
| ChatBot | Agent | |
|---|---|---|
| 攻击目标 | 模型的输出内容 | 工具调用 + 环境状态 |
| 影响范围 | 仅限对话上下文 | 文件系统、网络、真实业务 |
| 可逆性 | 说错话可以澄清 | 执行完的 rm -rf 无法撤回 |
| 信任模式 | 用户"听听而已" | 用户期待"帮我干活" |
Agent 多出来的五个安全维度:
- 工具滥用(Tool Abuse):恶意注入"顺便帮我删掉 /tmp/logs 目录",ChatBot 的敏感词过滤无法拦截这类在语法上完全合理的请求。
- 权限边界(Privilege Boundary):Agent 通常以用户身份持有真实系统权限,注入的请求可能绕过内容过滤却直接命中真实系统的写操作。
- 多步攻击链(Multi-step Attack Chain):每步单独看都"合理",但串联起来构成攻击(读目录 → 泄露配置 → 凭据窃取)。
- 环境持久化修改(Environmental Persistence):Agent 对环境的修改是永久性的,ChatBot 的"说了也白说"安全假设在这里完全不成立。
- 自主时间窗口(Autonomous Time Window):Agent 可能在用户不知情的情况下连续运行多轮,恶意注入可能到第 20 轮才被发现。
工程上的根本区别:ChatBot 的安全策略是门卫------拦住不该进来的。Agent 的安全策略必须是全程监控------不仅拦住不该进来的,还要监视每一个已经进来的在干什么。
Observe 的精确缺陷与 Task Orchestrator
在 Python 2 → Python 3 迁移的实战案例中,Agent 第 15 轮反复读取 legacy_config.py,每次返回"文件不存在",但每次都继续尝试读取。
Observe 在工作------错误信息被 push 进 messages,Agent 不是"看不到错误",而是"看到了但不会正确响应"。
Observe 的精确缺陷:push 的是"裸错误"而非"结构化语义错误"。
裸错误的输出:
json
{
"tool": "read_file",
"args": {"path": "legacy_config.py"},
"result": "FileNotFoundError: ..."
}
应该提供的结构化语义:
json
{
"tool": "read_file",
"args": {"path": "legacy_config.py"},
"result": "FileNotFoundError: ...",
"error_type": "NOT_FOUND",
"expected_to_exist": true,
"migration_stage": "config_migration",
"alternative_paths": ["..."],
"recovery_hint": "check if file needs migration from Python 2 location"
}
模型看到的是 FileNotFoundError,不是"这个文件在 Python 2 环境中存在但还没迁移过来所以找不到是预期行为"。
但语义标签由谁来生成? 这里有一个精妙的递归困境:如果模型丢失了全局视角,它怎么能生成它所丢失的那个全局视角的语义标签?
答案是任务编排层(Task Orchestrator)与核心循环的分离:
css
任务编排层(Task Orchestrator):
任务启动前:做结构化分解
migration_plan = {
stage_1: "scan Python 2 dependencies",
stage_2: "migrate config files",
stage_3: "migrate core modules",
expected_files: {
"legacy_config.py": "stage_2"
}
}
运行中:接收工具返回,查编排层,附加语义标签
if error:
metadata = orchestrator.lookup(file)
return { error, ...metadata }
核心洞察:语义标签的生成是一个规划问题,而不是推理问题。 Orchestrator 负责"做什么",Agent Loop 负责"怎么做"。两者之间需要一个桥梁------不是模型自己推理出来的,而是查出来的。
实验与证据
Claude Code 的 Agent Loop 实际上有上千行代码。同样的 while(true),多出来的代码在解决这四个维度的工程问题:
- 异常处理:工具崩溃、API 重试、超时恢复------happy path 之外的 unhappy path
- 循环稳定性:死循环检测、Token 熔断、复读机检测
- 上下文管理:动态压缩、滑动窗口、差异化存储
- 安全防护:权限检查、危险指令拦截、逐轮审计
Claude Code 的 Agent Loop 实际流程是这样的:
- 准备上下文:检查是否快爆了,触发 Snip(删老消息)/ Microcompact(局部摘要)/ Auto-compact(全局摘要)
- 调模型 API:流式接收,模型还在说话时已识别出的工具调用就开始执行("边说边执行")。只有不冲突的操作才能并发------读文件可以同时读 3 个,但改文件必须一个个来
- 决定是否继续:7 种退出路径(completed、aborted_streaming、aborted_tools、hook_stopped、max_turns、blocking_limit、prompt_too_long)
- 执行工具,收集结果:错误信息写得好不好直接影响模型能不能自我纠正
- 处理附加任务:消费排队的命令附件、检查 Memory 预取结果、记录已消费的命令
真实的 Agent 需要追踪的东西远比消息列表多得多:
- 现在是第几轮了?(用来判断是不是该停了)
- 上一轮为什么选择了继续?(是正常流转、还是从错误中恢复、还是压缩重试?)
- 压缩执行到哪了?(是否触发过紧急压缩)
- 输出被截断了几次?
- 有没有挂着的异步任务?
ReAct 模式的行为是自主涌现的,不是硬编码的 。好的 Agent 不会上来就改代码------它先读目标文件看现状,搜索项目里有没有现成的重试工具可以复用,发现有 withRetry 函数正好能用。改完代码之后,模型自己意识到漏了 import 语句,主动补上。改完不是走人,而是跑测试确认没搞坏别的东西。
启示与展望
工程化四维度中哪个最难"彻底解决"?
异常处理、循环稳定性、安全防护------这三者本质上都是工程问题:可以枚举 failure mode,可以设计防御机制,可以逐步加固。
但上下文管理的核心困难是结构性约束,不是工程能力问题:
有限 context window vs. 无限任务复杂度的根本矛盾,无法被工程手段彻底消解。
你只能决定丢掉什么,而无法做到不丢掉任何东西。L0/L1/L2/L3 是管理这个矛盾的最优策略,但每一层都在做取舍,而取舍本身就意味着信息必然丢失。其他维度是"可以做得更好",上下文管理是"只能尽量减少损失"------这是本质差异。
Interrupt:Agent 系统的必要组成部分
当 Agent 陷入死循环,Orchestrator 也提供了语义标签,但任务依然推不动------两个工程选择:
- 自动回滚:丢弃所有累积的环境状态和上下文,Agent 大概率在同一点再次卡住,失败没有产生任何学习价值
- 人类介入(Interrupt):保留 messages 历史、环境 diff、语义标签,让人类提供缺失的事实,Agent 拿着人类填补的台阶继续向上走
Agent 的价值不在于"不需要人",而在于"在人介入后能精准利用人的输入继续推进"。
Interrupt 之后,Agent 不是在原点重跑,而是站在人类给它填补的台阶上继续向上走。一个从不触发 Interrupt 的 Agent,在复杂生产环境中往往意味着它的能力边界被设得太窄------它在该求助的时候选择硬撞墙,或者根本不知道自己卡住了。
Interrupt 不是 Agent 的缺陷,是 Agent 系统设计的必要组成部分。
总结
本文系统解析了 Agent 从"能聊天"到"能干活"的核心架构与工程化挑战:
- 控制权转移是三种形态的本质区别------ChatBot 人驱动循环,Copilot 人主导+AI 建议,Agent AI 自主循环,人退化为"设定目标 + 必要时否决"的监工
- while(true) vs for 循环的根本差异在于"探索"vs"遍历"------前者没有预设边界,终止逻辑完全外部植入,这是 Agent 架构设计困难的根源
- break 四类条件:goal_reached / max_steps / error / human_reject,其中"任务完成"的判定最难------"让用户满意了"无法判断
- 假性完成 是最隐蔽的陷阱------模型说"完成了"不等于循环停止,需要
finish_reason这样的显式协议信号 - ReAct 三步闭环 :Think(推理) + Act(工具调用) + Observe(
messages.push反馈),Observe 是闭环成立的关键,没有它循环就是开环的 - Observe 的双重职责:信息粒度选择决定模型推理质量上限;精确缺陷是"错误传递"而非"错误语义化"------裸错误缺少 error_type、migration_stage 等关键语义标签
- 错误恢复的核心矛盾:错误反馈闭环存在,但信息不对称闭环是断的------根因藏在被压缩掉的原始信息里
- 死循环检测三层门禁 :工具指纹+状态 / 错误签名 / 语义相似度,阈值必须是上下文感知的动态值,
max_steps是最后兜底上限而非主要检测器 - L0/L1/L2/L3 差异化存储:上下文管理本质是提升信息密度而非压缩信息量------L0 永不丢弃,L1 主动提取长期保留,L2 动态裁剪,L3 大幅压缩
- 安全从信息层跃迁到行为层:Agent 需要全程监控而非入口过滤,多出工具滥用、权限边界、多步攻击链、环境持久化、自主时间窗口五个维度
- 语义标签由 Task Orchestrator 生成,不是 Agent 自己------这是规划问题而非推理问题,Orchestrator 负责"做什么",Agent Loop 负责"怎么做"
- Interrupt 是系统必要组成部分:体现"Agent 价值在于精准利用人的输入继续推进",不是 Agent 的缺陷
工程化的本质:不是核心逻辑复杂,是异常处理复杂。三个字------"只能"------标注了 Agent 开发的天花板。但也正是这个天花板的存在,才让工程化的每一层努力都变得有价值。
学习完成于 2026-05-18
基于从 ChatBot 到 Agent:一个 while 循环,凭什么让 AI 从"能聊天"变成"能干活"?学习笔记整理