This commit is contained in:
2026-09-22 15:42:25 +08:00
commit 8e86665096
60 changed files with 22339 additions and 0 deletions
@@ -0,0 +1,131 @@
# 阶段二 · 小点 1:Agent 基础认知
> 所属:阶段二 AI Agent 核心理论与基础范式
> 定位:这是整个学习计划最关键的"打地基"一讲。先建立 Agent 的心智模型:它不是一个"更聪明的聊天框",而是一个**自己会循环工作的小型系统**。理解了这个闭环,后面框架、组件、范式才都有落点。
## 精简大纲
1. Agent 的定义与运行闭环
2. Agent 形态演进:LLM 对话 → RAG → Agent
3. Agent 五大能力维度:规划 / 记忆 / 工具 / 反思 / 协作
4. 关键区分:单智能体 vs 多智能体、通用 vs 垂直
## 学习内容详情
### 1. Agent 的定义
一句话版:**Agent = 循环的 LLM 调用 + 工具执行 + 历史累积**。
```mermaid
graph TD
A[感知: 理解用户输入与当前状态] --> B[决策规划: 决定下一步做什么]
B --> C[工具执行: 搜索/数据库/计算器]
C --> D[反馈迭代: 把工具结果带回模型]
D -->|任务未完成 回到决策| B
D -->|任务完成| E[输出最终答案]
style E fill:#e6ffe6
```
- **感知** = 理解用户输入与当前状态
- **决策规划** = 决定下一步做什么
- **工具执行** = 调用外部能力(搜索 / 数据库 / 计算器)
- **反馈迭代** = 把工具结果带回模型再次推理,循环直到任务完成
> 理解关键:普通对话是"问一句答一句"就结束;Agent 是**在循环里跑**——模型推理一次 → 执行一个工具 → 把结果喂回去再推理,直到它认为任务完成。这个"循环"是贯穿整个学习计划的主线。
> 🔬 **深度理解 · Agent 运行闭环**:
> 1. **本质**:Agent 不是一个"更聪明的模型",而是一个"由模型当决策大脑、工具当手脚、上下文当短期记忆"的**循环执行系统**。只有先建立这个心智模型,后面看框架(LangGraph 的 Node/Edge)、看范式(ReAct)才都有落点。
> 2. **机制**:每一轮都走"感知 → 决策规划 → 工具执行 → 反馈迭代"。感知把用户输入与当前状态注入上下文;模型以此输出"下一步动作"——可能是"选工具 + 掏参数",也可能是"直接给最终答案";执行工具得到结构化结果;反馈把该结果当作新的"观察"追加回上下文,供下一轮决策。循环终止只有两种情况:模型判断任务已完成给出终态,或触发最大轮次熔断。
> 3. **为什么重要**:它把"一次性指令解码"升级为"多步自主求解"。代价是每一步都调一次模型、都花 token、都可能出错,因此后面强调的熔断、错误封装成消息、记忆修剪,全都是在为这个循环托底。
> 4. **易错点/常见误解**:新手常把"Agent 对话"当成"更长的一次性生成"。真正的区别不在长短,而在**是否在循环里调用工具并把结果喂回模型**。没有工具、没有反馈回路的"多轮聊天"不是 Agent。
### 2. 形态演进(三种形态)
| 形态 | 特征 | 对比 | 生产定位 |
|-|-|-|-|
| 普通 LLM 对话 | 只有 输入 → 输出,一步完成 | 单次问答 | 简单问答 |
| RAG | 输出前先查资料,但只查一次 | 检索增强 | 知识问答(阶段四细讲) |
| Agent | 思考 + 行动 + 观察,循环直到完成 | 自主闭环 | 复杂任务主力 |
```mermaid
graph LR
A[LLM对话] -->|+检索| B[RAG 查一次资料]
B -->|+工具+循环| C[Agent 思考-行动-观察 循环]
C --> D[可完成复杂多步任务]
```
### 3. 五大能力维度
| 能力 | 大白话 | 类比 |
|-|-|-|
| 规划 Planning | 把大目标拆成小步骤并动态调整 | 大脑指挥层 |
| 记忆 Memory | 保存历史/状态/偏好,跨轮次连续工作 | 笔记本 |
| 工具使用 Tool Use | 调用外部函数获得模型没有的能力 | 手脚 |
| 反思 Reflection | 失败后复盘原因并修正后续策略 | 复盘教练 |
| 协作 Collaboration | 多个 Agent 分工配合 | 团队分工 |
> 这五个词后面会反复出现。建议现在记一个口诀:"**规忆具反协**"(规划-记忆-工具-反思-协作)。阶段二第 2 讲会逐个展开,框架选型(第 4 讲)本质也是围绕它们做取舍。
### 4. 关键区分
```mermaid
graph LR
A[单智能体 vs 多智能体] --> A1[业务优先从单智能体做起]
B[通用 vs 垂直] --> B1[垂直只做一件事 工业落地主力]
C[符号Agent vs LLM Agent] --> C1[LLM用常识推理替代人工规则 成本大降]
```
- **单智能体 vs 多智能体(MAS)**:多智能体是"多个 Agent 各管一摊、互相协作",复杂度成倍上升。**业务优先从单智能体做起**,把单个做稳再考虑拆分。
- **通用 Agent vs 垂直领域 Agent**:通用想什么都做(落地难、不可控);垂直只做一件事(客服 / 数据分析 / 办公自动化),是工业落地主力。
- **传统符号 Agent / 专家系统**:靠人工 if-then 规则,规则一多就组合爆炸;LLM 原生 Agent 用常识与推理替代人工规则,成本大幅降低。
> 🔬 **深度理解 · 单智能体 vs 多智能体(MAS)**:
> 1. **本质**:单智能体是"在一个循环里塞进所有上下文自己干",多智能体是"把能力拆到多个独立循环,再用一套通信协议把它们接起来"。复杂度不是加法,是乘法。
> 2. **机制**:MAS 里每个 Agent 仍是"感知-决策-执行-反馈"循环,只是感知里额外包含其他 Agent 的输出。它会新增三类经典问题:**通信**(消息格式、答完谁发言)、**冲突**(两个 Agent 结论矛盾时谁来裁决)、**一致性**(共享事实/状态如何对齐,避免各说各话)。通常需要调度器(Orchestrator)负责分工,汇总器/仲裁负责收拢与判优。
> 3. **为什么重要**:业务上"先单后多"是铁律。单智能体做不稳时,暴露的往往是记忆、上下文、工具等单点问题,而不是协作问题;过早拆分会让原本可控的 bug 扩散成组合性爆炸。
> 4. **易错点/常见误解**:新手以为"多智能体 = 更强的智能"。它真正换来的主要是**模块化、并发、角色隔离**,而非更强的推理能力;一个单 Agent 做不好的任务拆成 5 个并不会自动变好,反而可能因通信损耗变得更差。
> ⏳ **短期可不深究**:传统符号 Agent / 专家系统(人工写 if-then 规则)是第一遍可以不学的历史对照物——你只需记住"LLM 用常识与推理替代了人工规则、成本大降"即可。它的推理库、前向/反向链等规则引擎细节跟本学习主线关系不大,见过即可,不必深入。
## 本节自检
- [ ] 能不看资料说清 Agent 与普通 LLM 对话、RAG 的本质区别
- [ ] 能画出"感知→决策→执行→反馈"闭环,并说出循环终止条件
- [ ] 能背出五大能力口诀并各举一个业务例子
## 本节配套思考题(快速入门的检验)
1. 普通 LLM 对话和 Agent 之间,真正多出来的那一步"循环"里,最核心的动作是什么?
2. 为什么说"多智能体"比"单智能体"复杂得多?从一个"拆分工"的场景想象它会引入哪些新问题(通信、冲突、一致性)?
3. 如果你要给老板解释"Agent 和 ChatGPT 有什么区别",你会用哪个例子最直观?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:Agent 和普通 LLM 对话 / RAG 的本质区别到底在哪?判断某系统是不是 Agent 的关键特征是什么?
- **面试官想考察**:是否真的理解"循环 + 工具 + 反馈",而不是浮于"Agent 就是聊天框升级版"的表象。
- **专业作答(含深度)**:
1. **普通 LLM 对话**:输入 → 输出,一步完成,模型只能基于自己学到的知识作答。
2. **RAG**:输出前先做一次检索、把资料注入上下文再回答——"查一次",但仍是一次性生成,没有自主决策和循环。
3. **Agent**:在循环里运行,核心是"思考 → 行动(调工具)→ 观察(看结果)→ 再思考",直到任务完成。
4. **关键特征**:①存在一个带终止条件的循环;②能主动选择并调用外部工具/能力;③工具结果被反馈回模型供下一轮决策。缺了③就退化成多轮聊天,缺了②就只是带检索的问答。
- **加分亮点 / 深度追问**:可主动补充"判断标准"——看它**是否在推理中产生了状态(上下文累积)并据此改变后续动作**;追问:RAG 也可以多次检索,边界在哪?答:判断依据不是"检了几次",而是"检索结果是否驱动模型主动改变下一步决策"。
### 面试题 2:五大能力(规划/记忆/工具/反思/协作)里,为什么"反思"最难做好?它和"重试"有什么区别?
- **面试官想考察**:对能力维度的理解深度,是否分清"复盘"和"瞎试"的本质差异。
- **专业作答(含深度)**:
1. 五大能力是 Agent 涉及的设计维度,各自解决不同问题:规划拆目标、记忆存状态、工具补能力、反思纠方向、协作做分工。
2. **重试(retry)**:失败后原封不动再来一次,不产生新的认知,只赌概率。
3. **反思(Reflection)**:失败后让模型显式回答"错在哪、为什么、下次怎么改",把**结论写回上下文/记忆**,下一轮带着这个教训执行——它改变的是"下一步策略"而非"重跑的次数"。
4. **难的原因**:反思依赖模型发现真因的能力,容易反思错方向(自圆其说);结论累积又占上下文、且可能污染后续推理,需要压缩与甄别。
- **加分亮点 / 深度追问**:可补一句"Reflexion 范式(阶段二第 3 讲)就是把反思显式做成一个环节";追问:反思结论会不会误导后续?答:会,所以许多实现给反思结论加来源/置信度,并限制保留数量。
### 面试题 3:多智能体听起来很强大,为什么工业界常说"优先从单智能体做起"?
- **面试官想考察**:是否有工程头脑,能否理性评估架构复杂度的成本,而不是跟风堆概念。
- **专业作答(含深度)**:
1. 单智能体是把所有能力放进一个循环的 Agent,多智能体是多个 Agent 通过通信/编排协作的系统。
2. 单智能体失效时,根因多半集中在**记忆溢出、上下文混乱、规划分不清主次、工具不可靠**——这些是单点问题,单靠加多个 Agent 解决不了,反而被掩盖。
3. 多智能体引入的新成本:**通信协议、冲突裁决、状态一致性**,加上更复杂的可观测性与调试难度,复杂度呈组合式上升。
4. 正确路径:先把单个 Agent 在真实数据上做稳,量化确认瓶颈再决定拆不拆;"一个 Agent 做不好"不是拆分的充要条件,"单点确实被某个子模块卡住、且拆分收益明确"才是。
- **加分亮点 / 深度追问**:可补充业界实践"先并联独立 Agent / 显式编排再谈自治协作";追问:什么时候多智能体真的更有性价比?答:子任务天然并行、角色/权限需要隔离(如安全边界)、或单个上下文已装不下完整任务时。
@@ -0,0 +1,248 @@
# 阶段二 · 小点 2:Agent 四大核心组件详解
> 所属:阶段二 AI Agent 核心理论与基础范式
> 定位:上一讲建立了"循环"的心智模型,这一讲把它拆成四大零件:**规划、记忆、工具、执行**。任何框架(LangGraph / CrewAI 等)本质都是把这四件事工程化。你先把每个零件"为什么存在、踩什么坑"想明白,后面看框架源码就像看拼装说明书。
## 精简大纲
1. 规划模块(Planning):任务分解 / 计划生成 / 动态调整 / 反思优化
2. 记忆模块(Memory):短期 / 工作 / 长期记忆
3. 工具调用模块(Tool Use):工具定义 / 调度 / 结果处理
4. 执行模块(Execution):状态机 / 追踪 / 终止 / 熔断 / 人在回路
## 学习内容详情
### 1. 规划模块(大脑指挥层)
```mermaid
graph TD
A[任务分解] --> B[计划生成: 有序步骤+里程碑]
B --> C[执行]
C --> D{执行结果?}
D -->|工具报错/结果不符| E[动态调整 Re-plan]
E --> B
D -->|失败| F[反思 Self-Reflection]
F --> G[修正策略 塞回上下文]
G --> B
D -->|完成| H[终止]
```
- **任务分解**:递归 / 分层分解。**粒度权衡是本节最重要的一条经验**:切太粗=子任务仍无法执行;切太细=每步都调模型、token 翻倍。经验值:子任务应满足「一句话能说清做什么、输入是什么、产出是什么」。
```python
# 一个"合格子任务"的判断模板: 三句话都能答上来才算拆好
def is_ok_subtask(t: str) -> bool:
return all([
len(t) > 0, # ① 一句话能说清"做什么"
"输入" in t or "使用" in t, # ② 输入/前置条件明确
"输出" in t or "返回" in t, # ③ 产出物明确
])
subtask = "读取 sales.csv,输出按区域分组的销售额汇总"
print(is_ok_subtask(subtask)) # True
```
> 🔬 **深度理解 · 规划(任务分解的粒度权衡)**:
> 1. **本质**:规划不是把目标简单切碎,而是生成一张"可按序/可并行执行、每步产出可验证"的动作清单;分解的粒度直接决定整条执行链路的成本与稳定性。
> 2. **机制**:递归地把大目标拆成子任务,每个子任务应满足"一句话能说清做什么、输入是什么、产出是什么"的最小自洽单元。执行中用 Re-plan 检测"工具报错、结果不符"等信号,动态增删改步骤;失败后再用反思把教训沉淀回上下文。
> 3. **为什么重要**:切太粗会导致子任务仍无法执行、被迫反复重规划;切太细则每步都调一次模型,token、延迟、出错面随步数线性上升——粒度决定了"跑得快不快、稳不稳、贵不贵"三个工程指标。
> 4. **易错点/常见误解**:新手以为"计划一次生成就够"。真实系统里计划几乎总会失效,规划的关键不在第一步拆得多完美,而在**有没有 Re-plan 闭环去纠偏**;否则一旦初始计划有误,就会沿着错误路径一路执行下去。
- **计划生成**:输出有序步骤清单 + 里程碑(关键节点)。
- **动态调整(Re-plan)**:执行中发现工具报错或结果不符,实时修改 / 新增 / 放弃子任务,不死守初始计划。
- **反思优化(Self-Reflection)**:失败后复盘「哪里错了、为什么、下次怎么做」,结论塞回上下文。
- **终止条件**:完成判定 / 异常判定 / 最大轮次熔断,防死循环烧钱。
### 2. 记忆模块(Memory)
```mermaid
graph LR
subgraph 记忆分级
S[短期记忆: 当前会话 messages] --> W[工作记忆: 本任务状态 LangGraph State]
W --> L[长期记忆: 跨会话持久化 用户偏好/经验/知识库]
end
L --> R[语义检索: 转向量按相似度召回]
L --> F[记忆遗忘: 按时间/重要度淘汰]
R --> M[重要度打分: 高分优先保留/召回]
M --> P[防记忆污染: 只召回高置信记忆+定期清理]
```
| 记忆类型 | 存活范围 | 存储位置 | 对应概念 |
|-|-|-|-|
| 短期记忆 | 当前会话 | 内存中的 messages 列表 | 上下文窗口 |
| 工作记忆 | 本任务期间 | 全局状态变量 | LangGraph 的 State |
| 长期记忆 | 跨会话 | 数据库 / 向量库 | 用户偏好 / 经验 / 知识库 |
- **语义检索**:查询和记忆都转向量按相似度召回,处理"意思相近但字面不同"。
> ⏳ **短期可不深究**:语义检索背后的"转向量 + 按相似度召回"机制第一遍不用细究——它是阶段四 RAG / 向量库的重头戏。这里你只需知道"长期记忆要靠它按语义而非字面来召回"即可,具体原理到时候再学。
- **记忆遗忘**:按时间 / 重要度淘汰,防止记忆库无限膨胀。
- **重要度打分**:高分优先保留、优先召回。
- **记忆污染(重点坑)**:历史错误信息被当事实混入当前推理。对策:只召回高置信记忆、定期清理。
> 🔬 **深度理解 · 记忆(分级结构与记忆污染)**:
> 1. **本质**:Agent 的记忆本质是"把需要跨步/跨会话保留的信息放到合适的位置",分级(短期/工作/长期)就是为了让不同寿命的信息各得其所,避免全都堆进上下文。
> 2. **机制**:短期记忆存活于当前会话的 messages 列表(即上下文窗口);工作记忆存活于当前任务的全局状态(如 LangGraph 的 State),随任务生命周期存在;长期记忆跨会话持久化到数据库/向量库,靠语义检索按相似度召回。召回前按置信度/重要度打分,只放行高置信条目,以降低污染。
> 3. **为什么重要**:它解决"上下文有限 vs 任务需要大量/长期信息"的根本矛盾,也支撑跨会话的个性化与连续工作。边界在于:检索召回的是"相关"而非"一定正确",因此需要置信度与清理机制兜底。
> 4. **易错点/常见误解**:新手常把"短期记忆"误当"模型的训练记忆"。短期记忆是每次请求都随 messages 传给模型的,根本不会写进模型参数;模型对历史毫无记忆,是上下文把历史搬运给它看的。一旦上下文混入错误信息(记忆污染),模型会当真且很难自我纠正。
> ⏳ **短期可不深究**:记忆遗忘(按时间/重要度淘汰)和重要度打分这类"记忆管理策略"属于锦上添花的工程优化点,第一遍知道它们存在即可,等真遇到"记忆库膨胀 / 检索噪声变高"再回来细调策略。
```python
# 一个带"置信度"的记忆条目: 低置信记忆不召回, 防止污染当前推理
class MemoryItem:
def __init__(self, content: str, confidence: float, created_at: float):
self.content = content
self.confidence = confidence # 0.0~1.0, 越高越可信
self.created_at = created_at
def is_confident(self, threshold: float = 0.8) -> bool:
return self.confidence >= threshold # 只召回高置信记忆
mem = MemoryItem("用户偏好用中文回复", confidence=0.95, created_at=1700000000)
if mem.is_confident():
print("可召回:", mem.content) # 只有高置信的才进上下文
else:
print("置信度不足, 不召回") # 防记忆污染
```
### 3. 工具调用模块(连接外部世界的「手」)
```mermaid
graph LR
A[工具定义规范] --> B[调度逻辑]
B -->|单工具| C[直接执行]
B -->|串行依赖| D[按顺序等结果]
B -->|并行| E[同时执行]
B -->|动态选择| F[运行时选工具集合]
C --> G[结果处理]
D --> G
E --> G
F --> G
G -->|正常| H[结果回传模型]
G -->|报错/超时/空| I[封装成消息回传 而非抛异常]
```
- **工具定义规范**:功能描述写清楚使用场景;参数 Schema 严谨;定义返回值格式(这些在阶段一第 4、7 讲已讲过 pydantic 与 JSON Schema)。
- **调度逻辑**:单工具调用 / 工具串行依赖 / 并行调用 / 运行时动态选择工具集合。
- **结果处理(关键工程习惯)**:正常结果回传;**报错 / 超时 / 空结果封装成消息给 LLM 而不是抛异常**——让模型"看到"错误并换方案,而不是让整个循环崩掉;超时时间要控制。
- **白名单 / 黑名单**:只允许白名单内工具;高危操作(删库 / 转账 / 发邮件)进黑名单。
- **高危工具**:执行后不可逆或有强副作用,必须加人工确认。
```python
# 结果处理: 错误封装成消息回传, 而不是 raise 中断循环
import time
def execute_tool(name: str, args: dict) -> str:
"""统一工具执行入口: 永远返回一个'能喂给模型的消息'。"""
try:
if name == "search":
return _search(args["query"]) # _search 为本项目自定义工具函数, 示意需自实现
if name == "send_email":
return _send_email(args) # 高危: 内部会先要人工确认; 函数本身示意需自实现
return f"未知工具: {name}"
except TimeoutError:
# 超时 → 封装成消息让模型决定重试或换思路, 而不是崩掉循环
return f"工具 {name} 超时, 请稍后重试或换一种方式"
except Exception as e:
return f"工具 {name} 执行失败: {e}" # 模型能看到原因, 自我修正
```
### 4. 执行模块(主循环与状态管理器)
```mermaid
stateDiagram-v2
[*] --> 待执行
待执行 --> 运行中: 开始
运行中 --> 成功: 得到最终答案
运行中 --> 失败: 抛错/超限
运行中 --> 暂停: 等待人工确认(高危节点)
暂停 --> 运行中: 人工确认后继续
运行中 --> 待执行: 动态新增子任务
```
- **任务状态机**:待执行 → 运行中 → 成功 / 失败 / 暂停;状态决定下一步行为。
- **执行链路追踪**:记录每轮思考、每次工具调用输入输出(生产用 LangSmith / LangFuse)。
- **终止条件**:模型给最终答案;或达到最大轮次 / 总时长 / 总预算强制熔断。
- **熔断(Circuit Breaker)**:达到最大轮次立即停止,防死循环、防 token 超支。**工程上必备**——下面用最小代码演示为什么必须加。
> 🔬 **深度理解 · 熔断(Circuit Breaker)**:
> 1. **本质**:熔断是对"不知道循环何时能结束"这件事加的一道硬性成本上限,本质是**把不确定性转化为可预支的预算**。
> 2. **机制**:在每轮循环入口检查计数器(轮次 / 时长 / 累计 token),任一指标超限立即强行终止并返回"已达上限"的兜底结果。它与终止条件正交:终止条件靠模型判断"任务完成",熔断靠硬编码的"资源上限"保证即使终止失效也不会无限烧钱。
> 3. **为什么重要**:模型可能永远不收敛(反复拿不准 → 再查一次),没有熔断就是无限烧 token、卡死生产任务;它同时充当降级保护,为"人在回路 / 兜底答案"留出触发点。
> 4. **易错点/常见误解**:新手以为熔断只在"明显死循环"时触发。其实模型不收敛是常态而非异常;阈值要按真实任务步数去调——太小任务常做不完,太大会烧较多钱,这是一个需要权衡的经验参数,不是设个安全值就一劳永逸。
- **Human-in-the-loop**:关键 / 高危节点暂停执行,等人工确认或修改后再继续。
```python
# 最小可运行的"执行模块"骨架: 用循环 + 熔断, 演示为什么要 max_turns
MAX_TURNS = 5 # 熔断阈值: 最多跑 5 轮
turns = 0
while True:
turns += 1
print(f"—— 第 {turns} 轮推理 ——")
# 模拟: 模型总是"再查一次"(没写好终止条件就死循环)
# 真实 Agent 这里可能是"模型调工具→回传→再调"无限循环
if turns >= MAX_TURNS: # 熔断!
print(f"已达最大轮次 {MAX_TURNS}, 强制终止, 防止 token 超支")
break
# 正常情况: 模型给出最终答案才 break
# 人机协作: 高危操作前暂停
def confirm(action: str) -> bool:
"""高危操作需要人工确认。生产里这里会弹一个审批。"""
# ⚠️ input() 是阻塞式交互, 自动化/无 TTY 环境会卡住; 生产请改走审批接口(如飞书/钉钉审批、Webhook)
return input(f"高危操作 {action}, 确认执行? (y/n) ") == "y"
if confirm("DELETE FROM users"):
print("执行删除")
else:
print("已取消") # 状态回到 运行中/暂停, 由调度器决定
```
## 本节自检
- [ ] 能举出四大组件各自解决的业务问题各一个
- [ ] 能说清工具「报错回传消息」而非「抛异常」的原因
- [ ] 能说出记忆三级结构,并解释"记忆污染"及对策
- [ ] 能画出任务状态机,并说明熔断 / 人工确认各自的触发场景
## 本节配套思考题(快速入门的检验)
1. 任务分解"切太细"到底浪费了什么?除了 token 翻倍,还有什么隐性成本(延迟 / 出错面)?
2. 为什么工具报错要"封装成消息回传"而不是 `raise`?试想 `raise` 一次会让整个 Agent 循环发生什么。
3. 如果会话的长期记忆里混进了一句错误的"用户喜欢英文",会发生什么?结合"记忆污染"说说你怎么防。
4. 熔断的阈值设太小会怎样(任务做不完),设太大又会怎样(可能烧钱)?你如何权衡?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:任务分解"切太细"除了 token 翻倍,还有什么损失?好的划分标准是什么?
- **面试官想考察**:是否理解粒度和工程成本的关系,能否给出可落地的评判依据,而非空谈"适度"。
- **专业作答(含深度)**:
1. **直接成本**:每步子任务都要调用一次模型,token 总量随步数线性增长。
2. **隐性成本**:a) 延迟——多轮串行调用显著拉长响应时间;b) 出错面——步骤越多,单步出错并失败重试的概率越大,整体成功率指数下降;c) 上下文累积——每步中间结果都写回上下文,长链路易触到窗口上限或被修剪。
3. **划分标准**:每个子任务应是一个"最小可执行自洽单元"——一句话能说清"做什么、输入是什么、产出是什么";过度拆分就违背了这个标准。
4. **权衡思路**:粒度不是越细越好,而是"恰好能一次性可靠完成、又不需要模型重新规划"。
- **加分亮点 / 深度追问**:可补"面向并行度拆分,可并发子任务用并行降低延迟";追问:拆太粗为什么也会贵?答:太粗常执行失败 → 触发 Re-plan 反复重规划,反而多轮调用,可能比拆细更贵。
### 面试题 2:工具报错为什么"封装成消息回传给模型"而不是 `raise` 抛异常?
- **面试官想考察**:是否理解 Agent 循环的容错哲学,以及异常处理对整个闭环的影响。
- **专业作答(含深度)**:
1. **目标不同**:普通程序里异常是"终止错误路径"的信号;Agent 循环里工具报错是"可据以换方案"的信息,应被当作普通观测喂回模型。
2. **机制**:把报错/超时/空结果统一转成字符串消息(含错误说明),以 `role: tool` 追加回上下文。模型"看到"后自行决定重试、换工具或改参数——错误变成了决策依据而非程序崩溃点。
3. **后果对比**:若 `raise`,异常一路上抛、外层循环中断,整个 Agent 的自主求解流程直接终止,回到工程逻辑而非模型逻辑,护栏全失效。
4. **边界**:并非所有错误都该回传——高危、不可重试的错位(如鉴权失败/越权)应硬中断或转人工,交由人在回路,不该让模型反复试。
- **加分亮点 / 深度追问**:补一句"这本质是'把不确定性留在模型侧而非代码侧'的容错设计";追问:会不会有"错误回传给模型反复浪费 token"?答:会,所以要配合超时熔断与最大重试次数上限。
### 面试题 3:长期记忆里的错误信息为什么危险?你如何设计防"记忆污染"?
- **面试官想考察**:是否真正懂"上下文即事实来源"的机制,而非只背"要清理"三个字。
- **专业作答(含深度)**:
1. **危险原因**:模型对会话历史没有真实记忆,每次都是把 messages 原样喂给它当"事实"。长期记忆一进上下文就被当作可靠事实参与推理,错误信息会系统性污染后续所有回答,且模型自身很难识别并纠正。
2. **机制原理**:检索召回靠"相关相似度",不等于"正确性";所以需要在召回侧加置信度门槛,在写入侧做来源/时效标注,在存储侧定期清理过期或反复被质疑的条目。
3. **设计要点**:只召回高置信记忆;对低置信/历史性/瞬时性信息做降权或拒绝召回;对冲突记忆做一致性裁决,必要时回源验证。
4. **边界**:仍无法杜绝"初始写入的内容本来就是错的",因此对敏感/易变信息要考虑从权威源实时校验,而非纯靠记忆。
- **加分亮点 / 深度追问**:可补"记忆污染在长链路 Agent 里会逐轮放大,越靠后的步骤越信以为真";追问:短期记忆里混入错误 offsets 怎么办?答:付之于上下文完整性校验 + 让模型显式标注不确定,必要时短路重问用户。
@@ -0,0 +1,265 @@
# 阶段二 · 小点 3:经典 Agent 工作范式
> 所属:阶段二 AI Agent 核心理论与基础范式(必懂)
> 定位:四大组件是"零件",工作范式是"装配图纸"——它决定循环怎么转。这一讲的 ReAct 是**理解一切 Agent 的最小单元**,一定要亲手复现一次;其他范式都是在这个基础上做增减。学完你会突然看懂 LangGraph 里那些 `Node / Edge` 到底在拼什么。
## 精简大纲
1. ReAct 范式:思考 → 行动 → 观察
2. Plan-and-Execute:先规划后执行
3. Reflexion:失败反思重试
4. AutoGPT 模式:自主目标拆解
5. 范式选型对比
## 学习内容详情
### 1. ReAct(Reason + Act)—— 最经典
#### 1.1 循环结构
```mermaid
graph LR
A[思考 Reason: 分析现状 决定下一步] --> B[行动 Act: 调用工具]
B --> C[观察 Observe: 看工具结果]
C -->|还没完成| A
C -->|完成| D[输出最终答案]
```
- 每轮先「想」(分析现状、决定下一步)→ 再「做」(调用工具)→ 再「观察」(看工具结果),三者循环。
- 优势:简单、通用、易实现。
- 局限:没有全局规划,长任务易「走一步算一步」循环跑偏。
- **思考-行动-观察** 是理解所有 Agent 循环的最小单元。
> 🔬 **深度理解 · ReAct(思考-行动-观察)**:
> 1. **本质**:ReAct 把"推理(Reason)"和"行动(Act)"耦合进同一个循环,让模型**用语言边想边做**——它的每一步思考都写在上下文里,天然形成可追溯的推理轨迹。
> 2. **机制**:每一轮:**思考**——模型基于当前上下文分析现状、决定下一步(输出"我要查什么"或动作计划);**行动**——据此调用一个工具并携带参数;**观察**——把工具返回的结果追加回上下文,作为下一轮思考的新信息。循环直到模型认为自己可以直接给出最终答案。
> 3. **为什么重要**:它是所有 Agent 框架抽掉外壳后的内核(LangGraph 的每个 Node 本质上都是"一次思考或一次行动")。它同时解决"如何规划下一步"和"如何获取外部信息"两个问题;局限是缺少整体前瞻,长任务易"走一步算一步"地跑偏。
> 4. **易错点/常见误解**:新手以为 ReAct 的"行动"是执行代码逻辑,其实模型只负责**决策与产出动作请求**,真正执行在工具层;也常误解"观察"可省——去掉观察,模型就失去外部反馈,退化成"无信息来源地自说自话"。
#### 1.2 最朴素的手写 ReAct(阶段二最重要练习)
下面这个 Demo 不依赖任何框架,只用 openai SDK + 一个工具函数,其余全部手写。请逐行读懂它——这就是所有 Agent 框架抽掉外壳后的内核。
```python
import json
import re
from openai import OpenAI
client = OpenAI(api_key="YOUR_KEY") # 替换为真实 Key, 或改用 os.environ["OPENAI_API_KEY"];也可直接跑仓库离线版 code/阶段二/react_agent.py
# ========== 第 1 步: 声明唯一一个工具 ==========
TOOLS = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
# ========== 第 2 步: 实现工具本体 ==========
def get_weather(city: str) -> str:
# 模拟真实天气查询
table = {"北京": "晴 26°C", "上海": "小雨 24°C", "广州": "多云 30°C"}
return table.get(city, "暂未收录该城市")
# ========== 第 3 步: 手写 ReAct 主循环 ==========
def run_react(user_question: str, max_turns: int = 5):
messages = [{"role": "user", "content": user_question}]
turn = 0
while turn < max_turns: # 熔断: 防死循环
turn += 1
print(f"\n===== 第 {turn} 轮 =====")
resp = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS, # 告诉模型有工具可用
)
msg = resp.choices[0].message
if msg.tool_calls: # 模型决定"行动": 调工具
messages.append(msg) # ① 追加模型的 tool_calls 请求
for tc in msg.tool_calls:
print(f"[行动] 调用 {tc.function.name}({tc.function.arguments})")
args = json.loads(tc.function.arguments)
result = get_weather(args["city"])
print(f"[观察] 工具返回: {result}")
# ② 用同一个 tool_call_id 回传工具结果 → 成对
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
else:
# 模型没调工具 = 给出最终答案 → 结束
print("[完成]", msg.content)
return msg.content
print(f"已达最大轮次 {max_turns}, 熔断终止") # 熔断
return None
run_react("北京今天天气怎么样?")
# 期望过程: [行动] get_weather("北京") → [观察] 晴 26°C → [完成] 回答
```
> **三个必观察现象**(来自原大纲,务必亲手复现):
> 1. 把工具改成返回 `{"error": "接口超时"}`,看模型如何自我纠错重试。
> 2. 把 `max_turns` 去掉,给一个模型反复拿不准的问题,观察无限循环烧钱。
> 3. 让工具返回空字符串,看模型会不会把"没结果"当成"没数据"而给出幻觉答案。
### 2. Plan-and-Execute
```mermaid
graph LR
A[整个任务] --> B[规划: 生成完整步骤清单 Plan]
B --> C[执行1] --> D[执行2] --> E[... 逐条执行 Execute]
E --> F{中途出问题?}
F -->|是| G[重新规划 Re-plan]
G --> C
F -->|否| H[完成]
```
- 先把整个任务拆成完整步骤清单(Plan),再逐条执行(Execute)。
- 优势:长周期任务有条理、token 更省(不必每步都规划)。
- 局限:初始计划错了后面跟着全错——**需要动态重规划补充机制**。
> 🔬 **深度理解 · Plan-and-Execute(先规划后执行)**:
> 1. **本质**:把"想"和"做"分离成两个阶段——先用一次较贵的推理生成完整步骤清单(Plan),再逐条机械执行(Execute),从而**把规划成本从"每步一次"摊薄为"整体一次"**。
> 2. **机制**:`planner` 一次性产出有序步骤清单;`executor` 逐条调用工具执行;一旦某步执行失败且还有重规划额度,就调用 `planner(task, hint=...)`,携带失败信息生成新计划,而非死守原计划。这相当于给"一次性规划"加了一个失败回退钩子。
> 3. **为什么重要**:相比 ReAct 每步都让模型思考"下一步做什么",Plan-and-Execute 只在规划时花大 token,执行步骤由确定性的调度完成,所以长任务普遍更省、更有条理。单点风险在于:**规划阶段的错会传导到所有执行步骤**,因此必须有 Re-plan。
> 4. **易错点/常见误解**:新手以为"先规划"就一定比 ReAct 聪明。其实计划生成本身会出错,且相比 ReAct 少了对中途新信息做出反应的窗口;在任务步骤之间强耦合、需要随时调整的场景,纯 Plan-and-Execute 反而更笨重。
```python
def plan_and_execute(task: str, planner, executor, max_replans: int = 2):
"""Plan-and-Execute 骨架: 先规划, 执行中出错可重新规划。"""
plan = planner(task) # 第一步: 让模型产出步骤清单
print("计划:", plan)
replans = 0
for step in plan:
ok = executor(step) # 逐条执行
if not ok and replans < max_replans:
# 执行失败 → 重新规划一次, 不死守原计划
replans += 1
print(f"步骤[{step}]失败, 重新规划({replans})")
plan = planner(task, hint=f"上一步 {step} 失败了, 请换路径")
break # 用新计划重跑
print("完成")
```
### 3. Reflexion(反思重试)
```mermaid
graph TD
A[普通Agent执行] --> B{失败?}
B -->|是| C[反思器: 复盘错在哪/怎么改]
C --> D[把反思结论存入记忆]
D --> A
B -->|否| E[成功输出]
```
- 普通 Agent + 失败反思器:失败后让模型复盘「错在哪、怎么改」,结论存记忆再重试。
- 优势:显著提升成功率;代价:多跑几轮、token 开销更高。
> 🔬 **深度理解 · Reflexion(失败反思重试)**:
> 1. **本质**:Reflexion 不同于"原地重试",它通过一个**显式的反思器**把每次失败沉淀成结构化的"教训",再带着教训重新开始——核心是把失败从"要重跑的次数"转译为"要修正的策略"。
> 2. **机制**:普通 Agent 执行失败后,反思器用另一段提示让模型回答"错在哪、为什么、下次怎么改",产出 `reflection` 文本;下一轮把这段教训拼进执行输入(常拼入 system prompt 或记忆),模型带着上次的错误认知重新决策。`max_attempts` 控制循环,不给死磕无限次。
> 3. **为什么重要**:很多失败是"同一个策略换种执行"兜不住的,Reflexion 让模型有机会跳出原来的错误路径,显著提升复杂任务成功率;代价是多轮调用带来更高的 token 与延迟,适合"成功率优先"的场景。
> 4. **易错点/常见误解**:新手以为"反思了就会变聪明"。反思能否奏效取决于模型是否找到**真因**,否则只会自圆其说地重复错误;且教训会持续占据上下文并可能"串味"到后续不相关任务,需要压缩与限定适用范围(这正是阶段二第 5 讲把教训拼 system_prompt 时的取舍)。
```python
def reflexion_agent(task: str, max_attempts: int = 3):
"""Reflexion 骨架: 失败 → 反思 → 带教训重试。"""
reflection = "" # 累积的反思结论
for attempt in range(1, max_attempts + 1):
print(f"\n--- 第 {attempt} 次尝试 ---")
result = run_react(task + (f"\n[上次教训] {reflection}" if reflection else ""))
if result is not None:
return result # 成功直接返回
# 失败: 让模型复盘, 把结论作为下一轮的"教训"
reflection = reflect(task, result) # reflect() 为本例未给出的复盘函数, 需自行实现
raise RuntimeError("多次尝试仍失败")
```
### 4. AutoGPT 模式
- 给一个目标(如「帮我建个网站」),Agent 自主拆解并无限循环执行直到自认为完成。
- 优势:Demo 惊艳;局限:容易死循环、任务发散、不可控——**生产慎用**。
> 一句话记住定位:AutoGPT 是"演示很酷、生产吓人"的反面教材。它暴露了"没有熔断、没有人在回路、没有白名单"会变成什么样——所以前面组件讲的熔断 / 高危确认在这是刚需。
> ⏳ **短期可不深究**:AutoGPT 模式第一遍不必深究其具体实现——它的价值主要在于当一个"反面教材",让你看到"缺少熔断、人在回路、白名单"会失控成什么样。你只需记住它的定位和教训即可,因为它生产基本用不上,学了细节也容易过时。
### 5. 范式选型对比
| 范式 | 特点 | 代价 / 风险 | 适用 |
|-|-|-|-|
| ReAct | 简单通用 | 长任务易跑偏 | 绝大多数日常任务 |
| Plan-and-Execute | 长任务有条理 | 初始计划错则全错 | 长周期复杂任务 |
| Reflexion | 失败自我修正 | 多消耗 token | 成功率优先场景 |
| AutoGPT | 自主拆解 | 不可控易死循环 | 仅原型演示 |
- **业务落地常混合范式**:如 Plan-and-Execute 定框架 + ReAct 执行子步骤 + Reflexion 兜底。
```mermaid
graph LR
A[Plan-and-Execute 定框架] --> B[ReAct 执行每个子步骤]
B --> C{子步骤失败?}
C -->|是| D[Reflexion 反思修正]
D --> B
C -->|否| E[完成]
```
### 6. 动手练习(阶段二最重要)
- 用最朴素的 Python 复现 Agent 核心循环:只用 openai 官方 SDK + 一个工具函数,其余全部手写。
- 三个必观察现象:工具返回「错误」时模型如何自我纠错;网络抖动时的重试必要性;去掉 max_turns 熔断后无限循环烧钱风险。
> 💡 **可直接跑起来的完整版**:进阶练习请直接使用 [阶段二综合实战:手写完整 Agent](05-阶段二综合实战-手写完整Agent.md) 的配套代码 [`react_agent.py`](../../code/阶段二/react_agent.py)。它把 ReAct / Plan-and-Execute / Reflexion 三种范式都写成了一个**无需 API Key、离线可运行**的完整 Agent(内置 MockLLM),跑完 `python3 code/阶段二/react_agent.py` 再对照文档改造即可。
## 本节自检
- [ ] 能口述 ReAct / Plan-and-Execute / Reflexion 三种范式的循环结构与代价
- [ ] 已完成一个不依赖框架的手写 ReAct Agent Demo,并复现过至少一种失败模式(格式解析失败 / 死循环 / 幻觉)
## 本节配套思考题(快速入门的检验)
1. ReAct 里"思考-行动-观察"三者的顺序为什么必须是循环而非一次性?如果把"观察"去掉会退化成什么?
2. Plan-and-Execute 相比 ReAct 省 token 的根本原因是什么?它的最大单点风险又是什么?
3. Reflexion 的"教训"如果一直攒下去,对上下文会有什么压力?你会怎么给它瘦身(参考阶段一的上下文压缩)?
4. 把上面的手写 ReAct Demo 跑起来,故意让工具返回错误,观察模型怎么自救——把过程用三句话记录下来。
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:为什么说 ReAct 是"理解一切 Agent 的最小单元"?它和普通多轮对话的核心差异是什么?
- **面试官想考察**:对 ReAct 机制本质的把握,能否讲清"循环 + 工具反馈"与普通对话的分野。
- **专业作答(含深度)**:
1. ReAct = 思考 → 行动(调工具)→ 观察(看结果)→ 再思考,三类动作循环,直到给出最终答案。
2. **最小单元的含义**:更高阶范式(Plan-and-Execute、Reflexion)都能被看成"在 ReAct 单个循环的某些环节上做扩展"——如 Plan-and-Execute 是把"思考"提前批量做掉,Reflexion 是在失败后往循环里注入教训;LangGraph 的每个 Node,也无非是"一次思考 Node / 一次工具 Node"。
3. **与多轮对话的差异**:多轮对话只是上下文延续,模型每一轮都端到端生成文本;ReAct 引入了"模型决策是否调用工具、工具结果作为`role: tool`回传"的结构化协议,行为由"外部观测 + 内部推理"共同驱动。
4. **定位**:正因为最朴素,它最容易手写复现,也最能暴露循环无熔断、错误不回传等工程问题——所以是打地基必练项。
- **加分亮点 / 深度追问**:补"ReAct 的局限恰是没有全局规划,这也是它给 Plan-and-Execute 留出的改进空间";追问:什么时候 ReAct 会明显不如 Plan-and-Execute?答:任务步骤多且需要整体条理、长期前后依赖时,ReAct 每步规划既贵又易跑偏。
### 面试题 2:Plan-and-Execute 比 ReAct 省 token 的根本原因是什么?它的最大单点风险如何缓解?
- **面试官想考察**:是否真懂"规划成本摊薄"与"DAG 式计划失效"两件事,而非只会背结论。
- **专业作答(含深度)**:
1. **省 token 的原因**:ReAct 每执行一步都要让模型重新推理"下一步做什么",规划成本随步数线性累积;Plan-and-Execute 用**一次**较贵的推理生成整体清单,执行步骤的"下一步"由确定性调度决定,不再消耗模型推理,故长任务普遍省。
2. **最大单点风险**:初始计划出错会传导到后续所有执行步骤(总体错在"源头")。
3. **缓解手段**:加 Re-plan 回退钩子——某步失败或结果不符时,携带失败上下文重新让 planner 产出新计划而不是硬执行原计划;再叠加"拆大计划为可重规划的分段"降低单点风险半径。
4. **权衡**:它对"步骤内部还要动态用外部信息"的任务并不友好,此时不如 ReAct 灵活。
- **加分亮点 / 深度追问**:可补"业界常混合使用:Plan-and-Execute 定框架、ReAct 执行每个子步骤、Reflexion 兜底失败";追问:只把计划拆大再执行一段,跟 ReAct 的分界线在哪?答:分界线在"执行步骤是否需要向模型重新决策",仍然是不需要则为 Plan-and-Execute。
### 面试题 3:Reflexion 和普通"失败就重试"的本质区别?它的反思结论应放哪、怎么避免污染?
- **面试官想考察**:能否把"反思"讲成改变策略的机制,而非玄学;以及是否意识到教训的适用范围管理。
- **专业作答(含深度)**:
1. **本质区别**:重试是"同一策略重跑赌概率没换策略";Reflexion 是在重试前**生成一段结构化教训**(错在哪、为什么、下次怎么改),下一轮带着教训执行——改变的是策略而非次数。所以它要求反思找真因,否则只会自圆其说地重复。
2. **机制**:反思器独立于执行器,失败后单独调一次模型产出 `reflection`;再通过 `max_attempts` 限制尝试次数,避免无限消耗 token。
3. **放哪**:可拼入 system prompt(更聚焦当前任务、不易串味)或写入对话历史/长期记忆(生存期更长)。放 system_prompt 利于"短时间内集中修正",但注意它对同任务内也可误伤其它步骤。
4. **防污染**:限定教训的作用域(只对本次失败相关路径生效)、对教训去重和压缩、超过窗口就截断,避免上一任务教训"串味"污染新任务。
- **加分亮点 / 深度追问**:补"Reflexion 多年跃生产力在于它是'自我纠错闭环',也是 Self-Refine、Self-Consistency 一类自纠正技术的简化模型";追问:教训拼在 system prompt vs 对话历史哪个更环保?答:system 更聚焦却生存期短、历史更持久却易污染,可视任务长在做取舍。
@@ -0,0 +1,181 @@
# 阶段二 · 小点 4:Agent 框架选型决策参考
> 所属:阶段二 AI Agent 核心理论与基础范式
> 定位:你已经知道 Agent 的零件(组件)和图纸(范式)了,这一讲回答一个现实问题:**我该用哪个框架,还是干脆手写?** 别急着追新框架——先按需求问 4 个问题,答案会自己浮现。这也是阶段三正式学 LangGraph / CrewAI 等之前的一次"路线预演"。
## 精简大纲
1. 框架的本质:封装「规划-记忆-工具-执行」
2. 主流框架定位速览(LangGraph / CrewAI / AutoGen / LlamaIndex / Dify)
3. 选型决策树:按业务需求问 4 个问题
## 学习内容详情
### 1. 框架的本质
```mermaid
graph LR
A[你的业务逻辑] --> B[框架]
B --> C[规划]
B --> D[记忆]
B --> E[工具]
B --> F[执行]
C --> G[你写代码]
D --> G
E --> G
F --> G
```
- 框架把阶段二讲的「规划-记忆-工具-执行」封装成可复用代码库,你写业务逻辑即可,不用从零造轮子。但**本质还是那些概念**——阶段二学的组件和范式,在框架里只是换了层皮(Node / State / Checkpointer 等名字)。
- 并不是「新项目必须上框架」:先想清楚需求再选型。需求很简单时,手写 ReAct 反而更可控、依赖更少。
> 🔬 **深度理解 · 框架如何封装四大组件(以 LangGraph 的 State / Node / Checkpointer 为例)**:
> 1. **本质**:框架不发明新概念,它只是把"规划-记忆-工具-执行"这套抽象变成可复用代码库,用一张运行图 / 状态机把它们串起来;你仍写业务逻辑,框架替你完成调度、持久化与追踪。
> 2. **机制**:`Node` 是一个执行单元(一次思考或一次工具调用),`Edge` 定义节点间的转移与条件分支,二者组成有向图;`State` 是跨节点共享的任务状态(对应四大组件里的"工作记忆");`Checkpointer` 在每一步把 State 持久化到外部存储,支持宕机 / 中断后的断点续跑(对应"持久化记忆")。
> 3. **为什么重要**:框架解决的是工程问题而非模型问题——状态怎么存、循环怎么保证不失控、错误 / 人工审批节点怎么编排、运行过程怎么可视化。它把阶段二反复强调的熔断、人在回路、记忆持久化变成开箱即用的构件。
> 4. **易错点/常见误解**:新手以为"用了框架就自动会规划、会反思"。其实框架只提供机制,不替你实现策略——模型是否规划、何时反思、拆多少层仍是你的 Prompt 与流程设计;框架一旦报错,若不懂底层循环几乎无从排查。
```python
# 判断该不该上框架的简单体检: 命中≥2条才值得
def should_use_framework(needs: list[str]) -> bool:
framework_worthy = [
"需要持久化状态/断点续跑", # 状态机 + Checkpointer
"需要复杂多Agent协作", # 编排与通信
"需要人类审批节点", # Human-in-the-loop
"需要可视化调试/追踪", # 运行图可视化
"团队协作、要长期维护", # 约定优于自研
]
hits = sum(1 for n in needs if n in framework_worthy)
return hits >= 2
print(should_use_framework(["简单问答", "一个工具"])) # False → 手写即可
print(should_use_framework(["需要断点续跑", "需要人类审批"])) # True → 上框架
```
> 🔬 **深度理解 · 手写 vs 框架的决策边界**:
> 1. **本质**:这不是"哪个技术更强"的比赛,而是"复杂度和可维护性放在哪个抽象层更划算"的工程决策;判断标准是"需求是否频繁触发框架能显著解决的问题"。
> 2. **机制**:框架的价值集中在几类非平凡需求——持久化状态 / 断点续跑(State + Checkpointer)、复杂多 Agent 编排、人工审批节点、可视化调试、团队长期维护。命中它们时,框架用约定与现成组件替你省下大量自研与踩坑;而需求只是"单轮问答 + 一个工具"时,引入框架反而带来学习成本、依赖体积与调试面上的额外负担。
> 3. **为什么重要**:选型应先于开发,在需求阶段就分清"该不该上框架",避免"为用框架而造复杂系统"或"硬手写导致后期难维护"两个极端。
> 4. **易错点/常见误解**:新手常把"框架报错"和"模型 / 循环设计问题"混为一谈——框架报错往往源于上层循环设计不当而非框架本身;也要警惕"多框架大杂烩",单一框架加必要插件通常更稳、可维护性更高。
### 2. 主流框架定位速览
| 框架 | 定位 | 对应概念 | 一句话记忆 |
|-|-|-|-|
| LangGraph | 以「图」(Node 节点 + Edge 边)组织流程,状态驱动、支持持久化与中断恢复,企业生产首选 | State≈工作记忆,Node≈执行动作,Checkpointer≈记忆持久化 | "把 Agent 画成一张流程图" |
| CrewAI | 强调「角色化」多 Agent:角色 / 目标 / 背景故事,Crew 编排协作,上手快 | 自定义状态流转弱 | "给 Agent 写角色卡" |
| AutoGen | Agent 之间用自然语言「对话」协作,内置代码沙箱,适合研究与原型 | 生产注意沙箱安全;⚠️ 已进入维护模式,官方新框架为 Microsoft Agent Framework | "让 Agent 们互相聊天" |
| LlamaIndex | 数据 + RAG 一体化框架,文档知识问答做检索层最顺 | 常配 LangGraph 编排 | "知识库的瑞士军刀" |
| Dify | 低代码平台,拖拽编排,适合快速验证 | 灵活性受平台限制 | "拖拽搭 Agent" |
```mermaid
graph LR
subgraph LangGraph[LangGraph 企业生产首选]
N1[Node1 理解问题] --> N2[Node2 调工具] --> N3[Node3 汇总答案]
end
subgraph CrewAI[CrewAI 角色化协作]
R1[策划Agent] --> R2[撰稿Agent] --> R3[校对Agent]
end
subgraph LlamaIndex[LlamaIndex 数据+RAG]
D1[文档] --> D2[索引] --> D3[检索]
end
```
> ⏳ **短期可不深究**:这张框架速览表里各框架的具体 API、上手命令和生态细节第一遍不必背——框架生态迭代快、细节容易过时,而且阶段三会正式上手。你只需记住它们各自"解决什么问题、适合什么场景"即可,真正选型时再打开官方文档看细节。
### 3. 选型决策树(按业务需求顺序问 4 个问题)
```mermaid
graph TD
A[开始选型] --> Q1{需要持久化状态/断点续跑?}
Q1 -->|是| L1[LangGraph]
Q1 -->|否| Q2{需要多Agent协作?}
Q2 -->|是, 简单| C1[CrewAI]
Q2 -->|是, 复杂控制| L1b[LangGraph]
Q2 -->|否| Q3{以文档知识为核心?}
Q3 -->|是| L2[LlamaIndex 可配LangGraph]
Q3 -->|否| Q4{只是快速验证?}
Q4 -->|是| D1[Dify 或手写Demo]
Q4 -->|否| R1[先手写ReAct 理解原理]
```
1. **是否需要持久化状态 / 中断恢复?** → 要则 **LangGraph** 优先。
2. **是否需要多 Agent 协作?** → 要且简单用 **CrewAI**,要复杂控制则 **LangGraph**。
3. **是否以文档知识为核心?** → 是则 **LlamaIndex**(可配 LangGraph 编排)。
4. **只是快速验证想法?** → **Dify** 或直接手写 Demo。
### 4. 场景示例
| 场景 | 关键需求 | 选型结论 |
|-|-|-|
| A 企业客服 | 保存会话、断线续聊、调知识库 | 持久化 + 多轮 + RAG → LlamaIndex 做检索层 + LangGraph 做编排层 |
| B 内容团队快速试做文案 | 快速原型 | Dify 或手写简单 Demo |
| C 内部 BI 数据分析助手 | 查询状态机与分步执行 | 先手写 ReAct 理解原理,再评估 LangGraph 落地 |
```python
# 一个"把决策树翻译成代码"的选型助手
def choose_framework(needs: dict) -> str:
"""按业务需求返回推荐框架。needs 形如 {persist: bool, multi_agent: str, doc: bool, quick: bool}"""
if needs["persist"]:
return "LangGraph" # 问题1: 要持久化
if needs["multi_agent"] == "complex":
return "LangGraph" # 问题2: 复杂多Agent
if needs["multi_agent"] == "simple":
return "CrewAI" # 问题2: 简单多Agent
if needs["doc"]:
return "LlamaIndex (+LangGraph 编排)" # 问题3: 文档知识
if needs["quick"]:
return "Dify 或手写 Demo" # 问题4: 快速验证
return "先手写 ReAct 理解原理" # 默认: 打牢根基
print(choose_framework({"persist": True, "multi_agent": "no", "doc": True, "quick": False}))
# LangGraph ← 企业客服场景
print(choose_framework({"persist": False, "multi_agent": "simple", "doc": False, "quick": False}))
# CrewAI ← 简单角色化协作
```
> **一条重要提醒**:阶段二建议先把"手写 ReAct"做扎实再接触框架。框架给你的是效率和工程化,但如果连"循环在干嘛"都不清楚,框架报错你根本无从下手。顺序:**先手写 → 再用框架**。
## 本节自检
- [ ] 能按业务条件在 4 个主流框架中给出选型结论并说明理由
- [ ] 能说出 LangGraph 的 State / Node / Checkpointer 各自对应四大组件里的谁
- [ ] 能判断"该不该上框架"的边界(手写 vs 框架)
## 本节配套思考题(快速入门的检验)
1. 为什么"要持久化 / 断点续跑"是选 LangGraph 的第一优先问题?它对应四大组件里的哪个能力?
2. CrewAI 的"角色化"和 LangGraph 的"图编排"分别适合什么协作复杂度?分界线在哪?
3. 一个只做"天气查询"的单轮问答,用框架还是手写?说说你的取舍依据。
4. 用第 3 节的决策树给"企业内部文档智能问答助手"走一遍,得出什么结论?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:项目什么时候该上手写、什么时候该上 Agent 框架?请给出清晰的判断思路。
- **面试官想考察**:是否有"按需求选技术"的工程判断力,而非"非框架不 Agent"或"啥都手写"的一刀切。
- **专业作答(含深度)**:
1. **先定性**:选型不是"框架更强",而是"把复杂度和可维护性放在哪个抽象层更划算"。
2. **触发上框架的需求**:需要持久化状态 / 断点续跑、复杂多 Agent 协作、人工审批节点、可视化调试、团队长期维护——命中越多越值得上。
3. **该手写的场景**:简单单轮问答、单一工具、团队/项目只想理解原理或快速验证 Demo——此时框架反增学习成本、依赖体积与调试面。
4. **顺序建议**:阶段二先手写 ReAct 打牢"循环"的理解,再引入框架;否则框架报错时无从定位。
- **加分亮点 / 深度追问**:可补"用决策树给'企业内部文档问答'走一步:以文档知识为核心 → LlamaIndex 做检索层 + LangGraph 做编排层";追问:如果需求现在简单、未来可能变复杂呢?答:优先选可平滑升级的轻量实现并预留抽象层,避免一开始就上重型框架。
### 面试题 2:LangGraph 里 State / Node / Checkpointer 分别对应四大组件里的"谁"?它为什么适合企业生产?
- **面试官想考察**:是否真正把"框架概念"映射回"Agent 原理",而非只会背名词。
- **专业作答(含深度)**:
1. **State** 对应"工作记忆"——它是跨节点共享、随任务生命周期存在并不断被读取/写入的任务状态(如当前进度、中间结果)。
2. **Node** 对应"执行动作 / 循环中的一个环节"(一次思考或一次工具调用),Edge 决定流转与条件分支,共同构成状态机。
3. **Checkpointer** 对应"持久化记忆"——把每步 State 落盘,支持宕机/中断后的断点续跑,这正是企业长任务与"人在回路能否跨时段恢复"的关键。
4. **为何生产**:它把 Agent 变成"图 + 状态"这种可观测、可恢复、可编排的结构,稳定性与可调试性都优于裸循环。
- **加分亮点 / 深度追问**:补"Agent 的规划与反思不是框架自动给的,仍是 Prompt 与流程设计的事";追问:Checkpointer 断点续跑在企业里有什么现实价值?答:长任务中途异常(超时/宕机/审计)可以从最后 Checkpoint 恢复,避免整体重跑、也便于人工介入审批。
### 面试题 3:多 Agent 协作选 CrewAI 还是 LangGraph?"简单 vs 复杂"的分界在哪?
- **面试官想考察**:能否清晰划分"低成本角色化协作"与"高精度图编排"两种需求,并说明依据。
- **专业作答(含深度)**:
1. **CrewAI 定位**:以"角色卡"(角色/目标/背景故事)驱动,Crew 内多个角色 Agent 协作,上手快,适合协作链路简单、强角色分工、不追求细粒度状态控制的场景。
2. **LangGraph 定位**:以"图 + 状态"显式编排,可精细控制分支、回溯、持久化与审批节点,适合需复杂控制流、需要精确状态或断点恢复的场景。
3. **分界依据**:看协作是否需要"对流转做程序化控制"——若只是几个 Agent 一段对话式协作,CrewAI 够;若需要条件分支、循环回溯、人审节点、可恢复执行、精细共享状态,则 LangGraph。
4. **另一层**:多智能体本身是"多个循环 + 通信与仲裁",无论选哪个框架,通信、冲突、一致性这三类问题都躲不开,只是框架替你承担了多少。
- **加分亮点 / 深度追问**:可补"简单多 Agent 也可用 LangGraph 显式编排,复杂控制场景可穿插人机协作节点";追问:什么时候多 Agent 不如单 Agent?答:任务本质串行、无明确子模块边界、单 Agent 上下文装得下时,多 Agent 只会增加通信损耗与一致性负担。
@@ -0,0 +1,227 @@
# 阶段二 · 综合实战:手写一个完整可运行的 Agent
> 所属:阶段二 AI Agent 核心理论与基础范式(动手收尾)
> 定位:前面 4 讲是"零件 + 图纸",这一讲把它们**焊成一个能跑的真家伙**。不依赖任何框架、也不需要 API Key,复制到本地 `python3 react_agent.py` 就能看到 Agent 循环完整跑起来。先跑通,再读懂,最后换上真模型。
## 配套文件
- 可运行代码:[`/workspace/code/阶段二/react_agent.py`](../../code/阶段二/react_agent.py)(复制到本地直接跑)
## 一次运行能学到什么
```bash
python3 react_agent.py
```
你会看到 4 个 Demo,分别演示阶段二的核心:
| Demo | 演示点 | 对应概念 |
|-|-|-|
| 1 查天气 | ReAct 主循环:思考→行动→观察→回答 | 经典范式 + 主循环 |
| 2 计算 10/0 | 工具报错被**封装成消息回传**而非崩溃 | 工具结果处理 |
| 3 多意图任务 | 先规划步骤清单再逐条执行 | Plan-and-Execute |
| 4 Reflexion | 失败后反思、带教训重试 | Reflexion 变体 |
## 代码结构(Part 1 ~ 7)
### Part 1:工具注册表(四大组件之「工具调用」)
```python
@dataclass
class Tool:
name: str # 工具名:模型靠它识别
description: str # 描述:决定模型何时触发
parameters_schema: dict # 参数 Schema:喂给模型的 JSON Schema
func: Callable # 真实函数:真正干活的代码
```
- 三个工具:`get_weather`(查天气)、`calculator`(四则运算)、`search_knowledge`(知识库)。
- 注意 `calculator` 的两个细节,都是阶段二强调的工程习惯:
- **白名单校验**:`re.fullmatch` 只允许数字和四则运算符号,防注入。
- **错误回传**:除零 / 非法表达式都 `return "错误: ..."` 而不是 `raise`——这样模型能看到错误并换方案,而不是整个循环崩掉。
> 🔬 **深度理解 · 工具(Tool)与工具抽象:为什么注册的是"描述和 Schema"而非直接传函数**:
> 1. **本质**:工具抽象是一种"把外部能力翻译成模型能理解和调用的语言"的桥——模型并不执行代码,它只"决定要不要用、用什么参数",所以注册的一定是**名字 + 描述 + 参数 Schema + 真实函数**这四元组,而非裸函数。
> 2. **机制**:`Tool(name, description, parameters_schema, func)` 构成注册表;`_tool_schema()` 把注册表转成 OpenAI 的 `tools`(JSON Schema)喂给模型;模型凭 `description` 判断何时触发、按 `parameters_schema` 生成参数;执行时 `_execute` 校验参数、绑定 `func`,再把返回结果统一封装成消息回传。
> 3. **为什么重要**:整个"模型-工具"协作的正确性建立在两件事上——描述是否足以让模型在正确时机选用、Schema 是否足以让模型填对参数;同时白名单与错误回传决定了单次失控的上限,属于安全边界的一部分。
> 4. **易错点/常见误解**:新手以为"描述越简越好",其实工具是否被触发完全取决于描述与 Schema 是否准确;也常把"工具定义"和"工具执行"混为一谈——定义是给模型看的说明书,执行才是真正干活的代码,二者解耦才便于安全插拔。
### Part 2:模拟 LLM(关键设计)
```python
class MockLLM:
def chat(self, messages, tools=None) -> LLMResponse: ...
```
- 用"规则"模仿大模型的行为:历史里已有工具结果 → 给最终答案;否则按用户提问关键词决定调用哪个工具。
- **为什么这么做**:让初学者**不花一分钱、不配 API Key** 也能看到 Agent 循环真实运转。真实模型和 MockLLM 的 `chat()` 接口完全一致,换模型只改一行(见 Part 3)。
> ⏳ **短期可不深究**:MockLLM 里"按关键词判断该调哪个工具、历史里有工具结果就给答案"这类 if-else 逻辑只是为了离线演示,不是真实模型的行为机制。第一遍看懂它的**接口 `chat(messages, tools)` 和数据流**即可,别被规则逻辑误导,以为真模型也是这么"判"的。
```python
@dataclass
class LLMResponse:
content: Optional[str] = None # 模型直接给答案
tool_calls: Optional[list] = None # 模型决定调用工具
```
> 这两个字段就是 OpenAI `chat.completions` 返回里最重要的两部分。看懂 `LLMResponse`,就看懂了所有框架的"模型层抽象"。
### Part 3:OpenAI 适配器(换一行接真模型)
```python
class OpenAILLM:
def __init__(self, api_key, model="gpt-4o", base_url=None): ...
def chat(self, messages, tools=None) -> LLMResponse: ...
```
- 把官方 SDK 的返回翻译成 `LLMResponse`,接口与 `MockLLM` 完全一致。
- 所以从 Mock 切到真实模型只需:
```python
from openai import OpenAI
llm = OpenAILLM(api_key="sk-xxx", model="gpt-4o") # 代替 MockLLM()
agent = Agent(llm=llm, max_turns=5)
agent.run("北京今天天气怎么样?")
```
### Part 4:ReAct 主循环(Agent 基类)
这是整个文件的灵魂,逐段拆解:
```python
def run(self, user_input: str) -> str:
messages = [
{"role": "system", "content": self.system_prompt}, # 立规矩,始终保留
{"role": "user", "content": user_input}, # 用户问题
]
turn = 0
while turn < self.max_turns: # 熔断:最多 N 轮,防死循环
messages = self._trim(messages) # 记忆修剪:超长丢最旧的
resp = self.llm.chat(messages, self._tool_schema())
if resp.tool_calls:
messages.append({...assistant tool_calls...}) # ① 追加"工具调用请求"
for call in resp.tool_calls:
result = self._execute(call) # 执行工具
messages.append({...role:tool, tool_call_id: call.id...}) # ② 回传结果
continue # 回到循环,继续"思考"
return resp.content # 模型给了最终答案 → 结束
return "【熔断】已达最大轮次"
```
配套的四个"工程习惯"方法:
| 方法 | 作用 | 对应概念 |
|-|-|-|
| `_tool_schema()` | 把注册表转成模型的 JSON Schema | 工具定义 |
| `_trim()` | 消息超长丢最旧、永远保留 system | 短期记忆修剪 |
| `_execute()` | 未知工具/参数错/执行错 → 封装成消息回传 | 工具结果处理 + 白名单 |
| `log()` | 每个回合/行动/观察都打日志 | 全链路追踪 |
> 运行 Demo 1 时观察 `[回合] → [行动] → [观察] → [回合] → [完成]` 的顺序,并看"模型调用 2 次"这个数字——这就是循环里每次 `chat()` 都在花 token 的直观证据。
> 🔬 **深度理解 · Agent 主循环的实现本质(run() 里发生了什么)**:
> 1. **本质**:`run()` 不是一次代码运行,而是一个"把每一轮对话追加进 messages、交给 LLM 判断、再根据判断执行工具并回传结果"的 while 循环;所谓"循环实操"就是这个 loop 加上一套消息协议。
> 2. **机制**:循环里的关键是一对成对的协议——先追加模型产出的 assistant 消息(含 `tool_calls`,每个带 `tool_call_id` 与动作),再用**同一个** `tool_call_id` 追加 `role: tool` 的对应结果。这样模型才能把"哪个动作的哪个结果"挂起来,继续下一轮推理,直到它返回不含工具调用的最终答案。
> 3. **为什么重要**:它把阶段二第 3 讲抽象的 ReAct 落成可执行代码;看懂这几十行,就等于看懂了任何框架"抽壳后的 Agent 内核"。循环里的 `_trim`、`_execute`、`log` 恰对应记忆修剪、工具容错、链路追踪。
> 4. **易错点/常见误解**:新手常把"一次 while 迭代"当成"一次 LLM 调用",其实一轮里可能既有工具请求又有多个工具结果;也常漏掉 `role: tool` 必须带 `tool_call_id` 且与请求一一对应的约束——漏了,模型就无法正确把结果关联到动作。
### Part 5:Plan-and-Execute 变体
```python
class PlanAndExecuteAgent(Agent):
def _plan(self, task): ... # 拆步骤:按意图数生成计划
def _execute_step(self, step): ... # 每步路由到对应工具
def run(self, task):
plan = self._plan(task) # ① 先规划
for step in plan: # ② 再逐条执行
results.append(self._execute_step(step))
return 汇总所有子结果
```
- 对比 ReAct:这里**先规划再执行**,一次性拆好步骤,不必每步都问模型"下一步做什么",所以 token 更省。
- 真实场景由模型生成计划;这里用关键词规则拆解,是为了离线可跑。
### Part 6:Reflexion 变体
```python
class ReflexionAgent(Agent):
def run(self, task):
reflection = ""
for attempt in range(self.max_attempts):
result = super().run(task) # 跑一轮普通 Agent
if 成功: return result
reflection = self._reflect(...) # 失败 → 让反思器复盘
# 把教训拼进 system_prompt 再重试
```
- 关键机制:失败后不是盲目重试,而是把"教训"拼进系统提示,让模型**带着上次的错误认知**重新开始。
- 代价:多跑几轮、token 更高;收益:成功率明显提升。
### Part 7:运行入口
```python
if __name__ == "__main__":
main()
```
`main()` 里跑了 4 个 Demo,方便你一次看到所有范式的行为差异。
## 进阶练习(把 Demo 改造成你的)
1. **换真模型**:把 `MockLLM()` 换成 `OpenAILLM(api_key=..., base_url=...)`,试试国内 DeepSeek / 本地 Ollama(只需改 base_url + model)。
2. **加新工具**:照着 `Tool(...)` 的格式往 `TOOL_REGISTRY` 里加一个"查汇率"或"查时间"工具,看 Agent 是否会自动调用它(记得在 `MockLLM.chat` 里加对应规则)。
3. **复现三种失败模式**(阶段二第 3 讲的动手任务):
- 让工具返回 `{"error": ...}` → 观察模型自救。
- 把 `max_turns` 调到 1 且问题需要 2 步 → 观察熔断。
- 让 `MockLLM` 故意返回错误 JSON → 观察解析兜底。
4. **数 token**:`llm.calls` 就是模型调用次数,把它乘以每次消息长度,就能估算一次任务的 token 成本——理解为什么"每轮都调模型"很贵。
> ⏳ **短期可不深究**:"数 token / 按调用次数估算成本"是进阶的省钱优化题,第一遍不必追求精确估量——当前你只需记住"每轮都调模型很贵"这个结论,等真正按 token 计费、需要成本精算的需求出现时再回来做。
## 本节自检
- [ ] 能不看提示,画出 `Agent.run()` 主循环的 `while` 逻辑(含 tool_calls 分支)
- [ ] 能说出 `_execute` 为什么把错误 return 成消息而不是 raise
- [ ] 已跑通 `react_agent.py` 的 4 个 Demo,并至少改造成:换真模型 或 加一个新工具
- [ ] 能说清 ReAct / Plan-and-Execute / Reflexion 在代码上的核心差异
## 本节配套思考题(快速入门的检验)
1. `Agent.run()` 里如果去掉 `_trim()`,跑一个特别长的多轮任务会发生什么?结合"上下文窗口"回答。
2. `_execute()` 对"未知工具"返回错误消息,这背后是"白名单"思想。如果换成"黑名单"(只禁高危),风险在哪?
3. Plan-and-Execute 用 `_plan` 一次性拆步骤,省在哪、险在哪?什么情况下它反而比 ReAct 更差?
4. Reflexion 把教训拼进 `system_prompt`,和直接拼进对话历史有什么区别?哪种更不容易"串味"到新任务?
5. 如果把 `max_turns` 设为 1,Demo 1 会发生什么?跑一下验证你的判断。
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:为什么要把 LLM 封装成 `LLMResponse` 与 MockLLM/OpenAILLM 两层?"换一行接真模型"背后靠什么做到?
- **面试官想考察**:是否理解"接口抽象"这一软件工程手段在 Agent 里的必要性,以及是否真的懂"依赖注入"而非只是看过这个 Demo。
- **专业作答(含深度)**:
1. **切的原因**:Agent 主循环只关心"给消息和工具、拿到'要不要调工具'的决策",并不关心底层是 Mock 还是真实 OpenAI;把差异收敛到统一 `chat(messages, tools) -> LLMResponse` 接口上,主循环与厂商 SDK 彻底解耦。
2. **`LLMResponse` 是抽象契约**:它只保留 `content`(最终答案)和 `tool_calls`(模型决定调哪些工具)两个字段——这正是 OpenAI `chat.completions` 返回里 Agent 层真正在乎的两部分,其余响应元数据被隔离在适配器内。
3. **机制**:`OpenAILLM.chat()` 把官方 SDK 的返回翻译成 `LLMResponse`,因此 `Agent(llm=...)` 通过注入不同对象即可切换实现,伪代码"换一行"实指"注入对象换一个",主循环零改动。
4. **价值**:便于测试(Mock 注入跑通流程)、便于切换/多模型(DeepSeek、Ollama 只需换 base_url+model)、便于下沉到框架时对齐"模型层抽象"。
- **加分亮点 / 深度追问**:补"这是策略模式 / 适配器模式的落地,也让 Agent 逻辑与模型厂商解耦";追问:MockLLM 会不会误导对真实模型的理解?答:会,它只是替身,判据只在"接口一致、数据流一致",真正理解了协议才是目的。
### 面试题 2:为什么 `role: tool` 必须用同一个 `tool_call_id` 回传?漏掉或乱配会发生什么?
- **面试官想考察**:是否真懂"工具调用的成对协议",这是手写 Agent(也是如实用官方 SDK)时最容易被坑的地方之一。
- **专业作答(含深度)**:
1. **协议要求**:当模型返回带 `tool_calls` 的 assistant 消息时,聊天协议要求对每个 `tool_call_id` 必须追加一条 `role: tool` 的结果消息,且 id 与请求一一对应。
2. **原因**:模型需要把"每个动作"与其"真实结果"精确配对,才能基于正确反馈继续推理;id 就是关联二者并维持有序性的键。
3. **漏掉/乱配的后果**:要么校验报错(缺结果),要么结果挂到错误动作上,导致模型基于错乱信息推理出幻觉或错误结论;在支持并行多工具调用时尤其致命。
4. **与本 Demo 的关系**:主循环里"先追加 assistant 的 tool_calls,再 for 循环逐一追加 tool 结果"这两步就是该协议的完整落地,也是最该手写吃透的一段。
- **加分亮点 / 深度追问**:可补"多工具并行调用时,每个 id 仍要各自成对,顺序可乱、配对不能错";追问:收到 `role: tool` 但模型并不再返回 tool_calls 会出现什么?答:说明 Agent 判定任务完成、给出最终答案,循环正常终止。
### 面试题 3:ReAct / Plan-and-Execute / Reflexion 在这份代码里各自的核心差异在哪?为什么说它们是同一内核的不同变体?
- **面试官想考察**:能否从"类继承 + 策略"角度讲清三种范式的代码差异,而非停留在概念复述。
- **专业作答(含深度)**:
1. **共用内核**:三者都复用 `Agent` 基类的 `_tool_schema / _execute / _trim / log` 等"循环基础设施",差异只在于"如何决策下一步"这一步的策略。
2. **ReAct**:基类本身——每轮都让 LLM 判断"下一步调哪个工具",`while` 内一问一执行,出现 `tool_calls` 就继续、否则返回最终答案。
3. **Plan-and-Execute**:在基类外套一层"先 `_plan` 一次性拆步骤、再逐条 `_execute_step`"的变体,把"每步征询模型"变成"执行定好的清单",这就是 token 更省的代码出处。
4. **Reflexion**:在 `run()` 外层用 `for attempt` 循环,失败后让反思器生成 `reflection` 并把它拼进 system_prompt 再重跑——把"教训注入"体现在 `super().run()` 的输入构造上。
- **加分亮点 / 深度追问**:可补"三者都缺不了熔断与错误回传,AutoGPT 的教训正是少了这些护栏";追问:ReAct 基类同时被三者复用,会不会让变体出现耦合?答:会,所以规范化时要把"决策策略"再抽象一层(如执行器/规划器/反思器接口),本 Demo 为入门做了简化。