乐于分享
好东西不私藏

AgentScope 源码课:Agent 是怎么「想」的?—— ReAct 循环拆解

AgentScope 源码课:Agent 是怎么「想」的?—— ReAct 循环拆解

系列:Coach 别跑!AgentScope 源码拆解 本篇:第 4 课 · 拆开 reply(),里面藏着一个 while 循环 前情:第 3 课我们学了 context 压缩,但没讲它在什么时候触发


① 「reply() 里面到底长啥样?」

徒弟翻开前两课的笔记:

第 2 课说 reply() 做了四件事。第 3 课说 context 超过 80% 会压缩。 但我一直想问——这四件事是怎么组织起来的?压缩又是在哪一步触发的?

教练:你终于问到结构了。把 reply() 拆开,里面其实是一个 while 循环。


② 「想一下、动一下、再想一下」

教练在白板上画了一张图:

while 还没到最大轮数:    ┌─────────────────────────────────┐    │ ① 检查:需要退出吗?              │    │    LLM 直接给了回复 → 结束        │    │                                 │    │ ② REASONING(想)                │    │    compress_context()  ← 在这!   │    │    LLM 看上下文、思考             │    │    决定:直接回复 还是 调工具?    │    │                                 │    │ ③ ACTING(动)                   │    │    执行工具 → 结果塞进 context    │    │    回到 ① 再想一轮               │    └─────────────────────────────────┘

徒弟盯着图看了 10 秒,突然拍大腿:哦!这不就是「想一下→动一下→再想一下」的循环嘛!

教练:正是。ReAct 这个名字就是这么来的——Reasoning + Acting。


③ 「那压缩到底插在哪?」

徒弟追问:Day 3 的压缩机制,具体在循环里的哪个位置触发?

教练指着图里第 ② 步:每次「想」之前,都会检查一次。

用伪代码看更清楚:

while 还没到最大轮数:    action = 判断下一步做什么()    if action == "直接回复":        return 回复          # 结束了    if action == "需要推理":        compress_context()   # ← Day 3 的知识!每次推理前检查        result = LLM.推理(context)        if result.有最终回复:            return result    # 结束了        if result.要调工具:            continue         # 进入 acting    # acting:执行工具    工具结果 = 执行工具()    context.追加(工具结果)    # 回到循环开头,再想

所以压缩不是「定时触发」的,是「每次动脑子之前先看看抽屉是不是太满了」。

徒弟:懂了!这就解释了为什么 Day 3 说 compress_context() 在 _reply() 的入口附近——它就在循环的 reasoning 分支里。


④ 「不带工具的话,循环跑几轮?」

徒弟好奇:如果 Agent 没有配任何工具呢?

教练:那就简单了——

第 1 轮循环:    → reasoning:LLM 看了上下文    → LLM 直接给了回复    → 没有工具可调    → 结束

不带工具的 Agent,循环永远只跑 1 轮。 因为 LLM 唯一能做的事就是「直接回复」。


⑤ 「带了工具就不一样了」

教练:来看看带工具的典型流程——

第 1 轮循环:    → reasoning:用户问「123 + 456 等于多少?」    → compress_context():检查,没超    → LLM 说:我需要调 calculator 工具    → acting:执行 calculator(123, 456) → 579    → 结果塞进 context,回到循环开头第 2 轮循环:    → reasoning:LLM 看了 context(现在包含了工具结果 579)    → compress_context():检查,没超    → LLM 说:我知道了,123 + 456 = 579    → 这是最终回复,循环结束

徒弟:原来「多轮对话」不一定是你问我答很多次,一轮 reply() 里就可能跑多次循环!

教练点头:对。工具调用越多,循环跑得越长。这也是为什么 max_iters 默认是 20——防止死循环。


⑥ 「所以整个 reply() 的结构是……」

徒弟拿出笔记本,画了一张图:

agent.reply(用户消息)    │    ├─ 收消息,塞进 context    │    └─ 🔁 while 循环:         │         ├─ compress_context()   ← Day 3 的压缩在这触发         │         ├─ reasoning(LLM 想)         │    ├─ 直接回复 → 结束         │    └─ 要调工具 → 继续         │         └─ acting(执行工具)              └─ 结果塞 context → 回到循环开头

教练:满分。


⑦ 课后总结

知识点
一句话
reply() 的结构
收消息 → 进入 while 循环 → reasoning ⇄ acting
压缩触发时机
每次 reasoning 之前,检查 context 是否超 80%
循环何时结束
LLM 给了最终回复,且没有更多工具要调
不带工具
循环永远只跑 1 轮
max_iters
最多跑 20 轮,防止死循环

一图胜千言

本系列用师徒对话拆解 AgentScope 源码原理,欢迎转发给同样好奇的朋友。关注「黑夜向日葵」,每周拆一个源码零件。

下节预告:Day 5 · Tool 装饰器

循环骨架搭好了,工具是怎么「注册」进去的? 为什么 LLM 知道 calculator 能算加法,但不知道它能算乘法? ——答案是 Tool 的「说明书」。下节课拆。

相关学习资料