AgentScope 框架源码深度解读:ReAct = Reasoning + Acting 的实现原理
🎯 本文目标 :通过深入 AgentScope 框架源码,揭示 ReAct 模式如何做到"推理 + 行动"的自动循环
📦 源码版本 :AgentScope 1.0.16.dev0
🔍 阅读建议 :结合代码示例,理解每个核心方法的职责
⏱️ 预计时间:30-45 分钟
📖 目录
-
核心问题:ReAct 如何实现?\](#核心问题 react 如何实现)
-
ReAct 循环的核心引擎\](#react 循环的核心引擎)
-
行动(Acting)过程详解\](#行动 acting 过程详解)
- 完整执行流程图解
-
实战:在 alioo-agent 中的应用\](#实战在 alioo-agent 中的应用)
核心问题:ReAct 如何实现?
从一个实际场景开始
当用户对 alioo-agent 说:
"帮我读一下这个 PDF:/path/to/file.pdf"
黑盒过程:
- Agent "思考":用户想读取 PDF → 需要调用
read_pdf工具 - Agent "行动":执行
read_pdf("/path/to/file.pdf") - Agent "观察":获取 PDF 内容
- Agent "再思考":内容太长,需要总结
- Agent "再行动":生成总结并返回
关键问题:
- 这个"思考 - 行动"循环是如何自动进行的?
- 谁在控制循环的开始和结束?
- LLM 的输出如何被解析成工具调用指令?
- 工具执行结果如何反馈给 LLM?
让我们深入源码寻找答案。
类的继承体系
三层继承结构
┌─────────────────────────────────────┐
│ AgentBase (基类) │
│ - 状态管理 │
│ - Hook 机制 │
│ - 打印功能 │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ ReActAgentBase (抽象基类) │
│ - 定义_pre_reasoning 接口 │
│ - 定义_acting 接口 │
│ - Hook 类型声明 │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ ReActAgent (具体实现) │
│ - reply() 主入口 │
│ - _reasoning() 推理实现 │
│ - _acting() 行动实现 │
│ - _summarizing() 总结实现 │
└─────────────────────────────────────┘
源码位置
python
# _agent_base.py
class AgentBase:
"""所有 Agent 的基类"""
# _react_agent_base.py
class ReActAgentBase(AgentBase, metaclass=_ReActAgentMeta):
"""ReAct 代理的抽象基类"""
# _react_agent.py
class ReActAgent(ReActAgentBase):
"""ReAct 模式的具体实现"""
元类的作用
_ReActAgentMeta 元类自动为 _reasoning 和 _acting 方法包裹 Hook:
python
# _react_agent_base.py
class _ReActAgentMeta(type):
"""元类,自动为 reasoning 和 acting 添加 Hook 包装"""
def __new__(mcs, classname, bases, class_dict):
# 获取父类的 hook 包装函数
pre_reasoning_hook = get_hook_wrapper("pre_reasoning")
post_reasoning_hook = get_hook_wrapper("post_reasoning")
pre_acting_hook = get_hook_wrapper("pre_acting")
post_acting_hook = get_hook_wrapper("post_acting")
# 如果子类定义了_reasoning 方法,用 Hook 包装它
if "_reasoning" in class_dict:
class_dict["_reasoning"] = pre_reasoning_hook(
post_reasoning_hook(class_dict["_reasoning"])
)
# 同理包装_acting 方法
if "_acting" in class_dict:
class_dict["_acting"] = pre_acting_hook(
post_acting_hook(class_dict["_acting"])
)
return type.__new__(mcs, classname, bases, class_dict)
作用:允许开发者在 reasoning/acting 前后插入自定义逻辑,无需修改框架代码。
ReAct 循环的核心引擎
reply() 方法:ReAct 循环的总控开关
这是整个 ReAct 模式最核心的方法,位于 _react_agent.py 第 376-537 行。
python
# _react_agent.py
@trace_reply
async def reply(
self,
msg: Msg | list[Msg] | None = None,
structured_model: Type[BaseModel] | None = None,
) -> Msg:
"""Generate a reply based on the current state and input arguments."""
# Step 1: 记录用户输入到记忆
await self.memory.add(msg)
# Step 2: 从长期记忆和知识库检索相关信息
await self._retrieve_from_long_term_memory(msg)
await self._retrieve_from_knowledge(msg)
# Step 3: 管理结构化输出
tool_choice = None
if structured_model:
self.toolkit.register_tool_function(
getattr(self, self.finish_function_name),
)
tool_choice = "required"
# ★★★★★★★★ Step 4: ReAct 循环主体 ★★★★★★★★
structured_output = None
reply_msg = None
for _ in range(self.max_iters): # 最多迭代 max_iters 次
# 4.1 记忆压缩(如果需要)
await self._compress_memory_if_needed()
# 4.2 【Reasoning】推理过程
msg_reasoning = await self._reasoning(tool_choice)
# 4.3 【Acting】行动过程
futures = [
self._acting(tool_call)
for tool_call in msg_reasoning.get_content_blocks("tool_use",)
]
# 并行或串行执行工具
if self.parallel_tool_calls:
structured_outputs = await asyncio.gather(*futures)
else:
structured_outputs = [await _ for _ in futures]
# 4.4 检查退出条件
if self._required_structured_model:
# 处理结构化输出的情况
if structured_outputs:
structured_output = structured_outputs[-1]
# ... 处理逻辑
break
elif not msg_reasoning.has_content_blocks("tool_use"):
# ★★★ 关键退出条件 ★★★
# 如果没有工具调用,说明 LLM 决定直接回答
msg_reasoning.metadata = structured_output
reply_msg = msg_reasoning
break
# Step 5: 超过最大迭代次数时的总结
if reply_msg is None:
reply_msg = await self._summarizing()
reply_msg.metadata = structured_output
return reply_msg
循环的关键要素
1. 循环控制器
python
for _ in range(self.max_iters): # 默认 10 次
- 防止无限循环的安全机制
- 每次迭代 = 一次 Reasoning + 一次(或多次)Acting
2. 退出条件
条件 A:完成结构化输出
python
if self._required_structured_model:
if structured_outputs:
# 成功生成结构化输出 → 退出
break
条件 B:LLM 决定直接回答
python
elif not msg_reasoning.has_content_blocks("tool_use"):
# LLM 没有调用工具 → 直接返回文本响应
reply_msg = msg_reasoning
break
条件 C:达到最大迭代次数
python
if reply_msg is None:
# 循环结束但未生成回复 → 强制总结
reply_msg = await self._summarizing()
推理(Reasoning)过程详解
_reasoning() 方法源码
位于 _react_agent.py 第 540-648 行。
python
async def _reasoning(
self,
tool_choice: Literal["auto", "none", "required"] | None = None,
) -> Msg:
"""Perform the reasoning process."""
# Step 1: 如果有计划笔记本,添加推理提示
if self.plan_notebook:
hint_msg = await self.plan_notebook.get_current_hint()
await self.memory.add(hint_msg, marks=_MemoryMark.HINT)
# Step 2: 准备发送给 LLM 的消息
prompt = await self.formatter.format(
msgs=[
Msg("system", self.sys_prompt, "system"),
*await self.memory.get_memory(),
],
)
# 清除临时提示消息
await self.memory.delete_by_mark(mark=_MemoryMark.HINT)
# Step 3: ★★★ 调用 LLM,获取推理结果 ★★★
res = await self.model(
prompt,
tools=self.toolkit.get_json_schemas(), # ← 关键:传入工具列表
tool_choice=tool_choice,
)
# Step 4: 处理 LLM 响应
msg = Msg(name=self.name, content=[], role="assistant")
if self.model.stream:
# 流式处理
async for content_chunk in res:
msg.content = content_chunk.content
await self.print(msg, False)
else:
# 非流式:直接获取完整响应
msg.content = list(res.content)
# Step 5: 将推理结果加入记忆
await self.memory.add(msg)
return msg
关键点解析
1. 工具信息的传递
python
res = await self.model(
prompt,
tools=self.toolkit.get_json_schemas(), # ← 这里!
tool_choice=tool_choice,
)
tools 参数的作用:
- 告诉 LLM 有哪些工具可用
- 每个工具的参数格式
- LLM 基于这些信息决定调用哪个工具
工具 Schema 示例:
json
{
"type": "function",
"function": {
"name": "read_pdf",
"description": "读取 PDF 文件内容",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "PDF 文件路径"
},
"max_pages": {
"type": "integer",
"description": "最大读取页数"
}
},
"required": ["file_path"]
}
}
}
2. LLM 的响应格式
LLM 可能返回两种内容:
A. 纯文本响应(不需要工具)
json
{
"content": [
{
"type": "text",
"text": "我已经理解了您的需求..."
}
]
}
B. 工具调用(需要执行工具)
json
{
"content": [
{
"type": "tool_use",
"id": "call_abc123",
"name": "read_pdf",
"input": {
"file_path": "/path/to/file.pdf"
}
}
]
}
3. 格式化器的作用
python
prompt = await self.formatter.format(
msgs=[...],
)
不同模型提供商需要不同的消息格式:
DashScopeChatFormatter→ 通义千问格式OpenAIChatFormatter→ OpenAI 格式
行动(Acting)过程详解
_acting() 方法源码
位于 _react_agent.py 第 650-708 行。
python
async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
"""Perform the acting process."""
# Step 1: 创建工具结果消息
tool_res_msg = Msg(
"system",
[
ToolResultBlock(
type="tool_result",
id=tool_call["id"],
name=tool_call["name"],
output=[],
),
],
"system",
)
try:
# Step 2: ★★★ 执行工具函数 ★★★
tool_res = await self.toolkit.call_tool_function(tool_call)
# Step 3: 处理流式结果
async for chunk in tool_res:
tool_res_msg.content[0]["output"] = chunk.content
await self.print(tool_res_msg, chunk.is_last)
# 处理中断
if chunk.is_interrupted:
raise asyncio.CancelledError()
# 如果是 finish 函数且成功,返回结构化输出
if (
tool_call["name"] == self.finish_function_name
and chunk.metadata
and chunk.metadata.get("success", False)
):
return chunk.metadata.get("structured_output")
return None
finally:
# Step 4: 将工具结果加入记忆
await self.memory.add(tool_res_msg)
关键点解析
1. 工具执行的委托
python
tool_res = await self.toolkit.call_tool_function(tool_call)
Toolkit 的职责:
- 根据
tool_call.name找到对应的函数 - 使用
tool_call.input作为参数调用函数 - 返回
ToolResponse对象
2. 工具结果的格式
python
ToolResultBlock(
type="tool_result",
id=tool_call["id"], # 与调用 ID 匹配
name=tool_call["name"], # 工具名称
output=..., # 工具返回的内容
)
3. 结果反馈给 LLM
python
await self.memory.add(tool_res_msg)
为什么重要:
- 工具执行结果会被加入对话记忆
- 下一轮 Reasoning 时,LLM 能看到之前的工具执行结果
- 形成完整的"思考 - 行动 - 观察"闭环
工具调用的自动化机制
Toolkit:工具管理的核心
工具注册
python
# agent.py
def _create_toolkit(self) -> Toolkit:
toolkit = Toolkit()
# 注册工具函数
toolkit.register_tool_function(read_pdf)
toolkit.register_tool_function(fetch_news_summary)
return toolkit
工具 Schema 自动生成
当你注册一个函数时,Toolkit 会自动分析:
python
def read_pdf(
file_path: Union[str, Path],
max_pages: Optional[int] = None
) -> ToolResponse:
"""读取 PDF 文件内容
Args:
file_path: PDF 文件路径
max_pages: 最大读取页数
"""
自动生成的 Schema:
json
{
"name": "read_pdf",
"description": "读取 PDF 文件内容",
"parameters": {
"file_path": {"type": "string", "description": "PDF 文件路径"},
"max_pages": {"type": "integer", "description": "最大读取页数"}
}
}
工具执行流程
python
# toolkit.py (简化版)
async def call_tool_function(self, tool_call: ToolUseBlock):
# 1. 根据名称查找函数
func = self.tools[tool_call["name"]]
# 2. 准备参数
args = tool_call["input"]
# 3. 调用函数
result = await func(**args)
# 4. 返回结果
return result
自动化闭环
┌──────────────────────────────────────────┐
│ 1. LLM 生成工具调用指令 │
│ { │
│ "name": "read_pdf", │
│ "input": {"file_path": "..."} │
│ } │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 2. Framework 解析指令 │
│ - 提取工具名称 │
│ - 提取参数 │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 3. Toolkit 执行工具函数 │
│ - 查找函数 │
│ - 调用函数 │
│ - 获取返回值 │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 4. 结果加入记忆 │
│ Msg( │
│ role="system", │
│ content=tool_result │
│ ) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 5. 下一轮 Reasoning │
│ - LLM 看到工具结果 │
│ - 决定下一步行动 │
└──────────────────────────────────────────┘
完整执行流程图解
从用户输入到最终响应的全链路
需要工具
直接回答
有 tool_use
无 tool_use
用户输入
帮我读一下这个 PDF
memory.add
记录到记忆
ReAct 循环开始
_reasoning
推理过程
准备 Prompt
sys_prompt + memory
调用 LLM
传入 tools 参数
LLM 决策
生成 tool_use
read_pdf 调用
生成 text
直接返回
_acting
行动过程
Toolkit 执行
read_pdf 函数
获取工具结果
PDF 内容
memory.add
记录工具结果
检查退出条件
退出循环
返回 reply_msg
多轮迭代示例
假设用户要求:"读一下这个 PDF,然后总结主要内容,再翻译成英文"
第 1 轮迭代:
Reasoning:
Thought: 用户要读取 PDF → 调用 read_pdf
Action: read_pdf("/path/to/file.pdf")
Acting:
执行:read_pdf("/path/to/file.pdf")
结果:【第 1 页】...【第 2 页】...(共 15 页内容)
第 2 轮迭代:
Reasoning:
Thought: PDF 已读取,但内容太长,需要总结
Action: finish(response="这份 PDF 的核心内容是...")
Acting:
执行:finish(...)
结果:Successfully generated response.
第 3 轮迭代(如果还需要翻译):
Reasoning:
Thought: 用户还要求翻译 → 调用 translate_text
Action: translate_text("这份 PDF 的核心内容是...", target_lang="en")
Acting:
执行:translate_text(...)
结果:The core content of this PDF is...
实战:在 alioo-agent 中的应用
AliooAgent 的初始化
python
# agent.py
class AliooAgent(ReActAgent):
def __init__(self, name: str = "Alioo"):
config = load_config()
# 1. 创建工具包
toolkit = self._create_toolkit()
# 2. 构建系统提示词
sys_prompt = self._build_system_prompt()
# 3. 创建模型和格式化器
model, formatter = create_model_and_formatter()
# 4. 调用父类初始化
super().__init__(
name=name,
model=model,
sys_prompt=sys_prompt,
toolkit=toolkit,
formatter=formatter,
max_iters=config.agent.max_iters, # 默认 50 次
)
系统提示词的设计
python
def _build_system_prompt(self) -> str:
return """你是一个简洁高效的个人智能助手,名叫 Alioo。
你的核心能力:
1. **PDF 阅读**:帮助用户读取 PDF 文件,提取关键信息,总结内容
- 当用户要求解读、阅读或分析 PDF 时,使用 read_pdf(file_path) 函数读取
- 【重要】读取 PDF 后,必须主动总结核心要点,而不是直接返回原文
2. **新闻摘要**:为用户提供实时热门新闻
- 使用 fetch_news_summary(sources) 函数获取新闻
工作要求:
- 回答简洁明了,重点突出
- 对于工具返回的长文本,必须进行归纳总结
"""
作用:
- 定义 Agent 的角色和职责
- 明确告知哪些工具可用
- 指导 LLM 如何使用工具(包括何时使用、如何使用)
实际执行日志
查看 logs/model_interaction.log:
================================================================================
模型请求
模型:qwen-turbo
[system] : 你是一个简洁高效的个人智能助手...
[user] : 帮我读一下这个 PDF:/Users/mac/report.pdf
================================================================================
模型响应
{
"content": [
{
"type": "tool_use",
"id": "call_abc123",
"name": "read_pdf",
"input": {
"file_path": "/Users/mac/report.pdf"
}
}
]
}
Usage:
Input Tokens: 1250
Output Tokens: 45
Time: 0.8s
================================================================================
[工具执行] read_pdf("/Users/mac/report.pdf")
[工具结果] 【第 1 页】AI Agent 技术报告...
================================================================================
模型请求
模型:qwen-turbo
[system] : 你是一个简洁高效的个人智能助手...
[user] : 帮我读一下这个 PDF:/Users/mac/report.pdf
[assistant] : [tool_use: read_pdf]
[system] : [tool_result: PDF 内容...]
================================================================================
模型响应
{
"content": [
{
"type": "text",
"text": "这份文档是一份 AI Agent 技术报告,共 15 页。\n\n核心要点:\n1. 介绍了 AI Agent 的基本概念\n2. 详细讲解了 ReAct 模式的工作原理\n3. 提供了多个实践案例"
}
]
}
================================================================================
总结与启示
核心要点回顾
1. ReAct 循环的本质
Reasoning(推理) → Acting(行动) → Observation(观察) → 循环
- Reasoning:LLM 基于当前上下文决定下一步行动
- Acting:执行 LLM 选择的工具
- Observation:将工具结果反馈给 LLM
2. 自动化的关键
| 组件 | 作用 |
|---|---|
reply() |
控制循环的开始和结束 |
_reasoning() |
调用 LLM,获取工具调用指令 |
_acting() |
执行工具函数 |
Toolkit |
管理工具函数,执行调用 |
Memory |
存储对话历史和工具结果 |
3. LLM 的核心作用
LLM 不是被动执行命令,而是:
- 理解决策:分析用户意图
- 自主选择:决定使用哪个工具
- 评估结果:判断是否完成任务
- 调整策略:根据工具结果调整下一步行动
与传统编程的对比
| 特性 | 传统方式 | ReAct 方式 |
|---|---|---|
| 流程控制 | 硬编码 if-else | LLM 自主决策 |
| 工具路由 | 手动编写路由逻辑 | 自动匹配工具名称 |
| 错误处理 | try-catch + 预定义规则 | LLM 理解错误并调整 |
| 可扩展性 | 需修改路由代码 | 注册新工具即可 |
| 灵活性 | 低(固定流程) | 高(动态适应) |
设计启示
1. 关注点分离
LLM → 决策层(做什么)
Framework → 执行层(怎么做)
Tools → 能力层(具体能力)
2. 透明化设计
- 所有工具调用对 LLM 透明
- LLM 知道有哪些工具、参数格式、返回值
- LLM 能看到工具执行结果
3. 循环而非线性
传统:输入 → 处理 → 输出(单向)
ReAct:输入 → [推理 → 行动 → 观察]×N → 输出(循环)
进一步学习
源码阅读建议
- 第一遍 :理解
reply()的主循环逻辑 - 第二遍 :深入
_reasoning()和_acting()细节 - 第三遍:研究 Hook 机制和扩展点
实践方向
- 添加自定义工具
- 实现自定义 Hook
- 优化系统提示词
- 调试工具调用过程
❓ 常见问题 FAQ
Q1: 为什么 LLM 知道要调用哪个工具?
A:
- 在每次调用 LLM 时,会通过
tools参数传入所有可用工具的 Schema - LLM 基于工具的名称、描述和参数,理解每个工具的用途
- 系统提示词也会指导 LLM 在什么场景下使用什么工具
Q2: 如果 LLM 选择了错误的工具怎么办?
A:
- 工具执行结果会反馈给 LLM
- LLM 可以看到错误信息(如"文件不存在")
- LLM 会在下一轮 Reasoning 中调整策略
- 可以通过优化系统提示词减少错误
Q3: 如何调试 ReAct 循环?
A:
- 查看
logs/model_interaction.log - 启用 debug 日志级别
- 在
_reasoning()和_acting()中添加断点 - 观察每次迭代的输入输出
Q4: 支持并行工具调用吗?
A : 支持!通过 parallel_tool_calls=True 开启:
python
agent = ReActAgent(
...,
parallel_tool_calls=True, # 并行执行多个工具
)
Q5: 如何限制最大迭代次数?
A : 在初始化时设置 max_iters:
python
agent = ReActAgent(
...,
max_iters=10, # 最多迭代 10 次
)