【源码笔记-Agentscope】快速开始与整体概览
📚 阅读动机
为什么想要阅读 Agentscope 的源码?主要是好奇这几个问题:
- 一个 Agent 框架是如何组织”推理 - 行动”循环的?
- 工具调用是怎么让大模型”感知”并正确执行的?
- 对话记忆是怎么管理和持久化的?
- Hook 系统如何优雅地插入到各个执行环节?
带着这些问题,先从官方文档入手,了解一下整体架构。
🎯 整体印象
先放一张我理解的调用流程图(基于文档描述):

我的理解:
整个流程其实就是一个双循环结构:
- Reasoning(推理环):从 Memory 读取历史 → Formatter 格式化 → Model 调用 LLM → 得到响应
- Acting(行动环):如果需要调用工具 → Toolkit 执行 → 结果存回 Memory → 回到推理环
这个设计思想跟 ReAct 论文的思路是一致的:思考(Reasoning)和行动(Acting)交替进行,直到完成任务。
📄 ReAct 论文原文:ReAct: Synergizing Reasoning and Acting in Language Models
- 作者:Yao et al.
- 发表:ICLR 2023
- 核心思想:LLM 通过交替进行推理(Reasoning)和行动(Acting)来更好地完成复杂任务
接下来逐一拆解各个模块,看看它们各自扮演什么角色。
💬 消息(Message)
我的疑问
Agent 之间怎么通信?对话历史怎么存储?跟 LLM API 交互用什么数据结构?
答案:Msg 对象
核心设计
Msg 是贯穿整个框架的数据载体,我觉得有几个关键点:
- 统一数据格式:无论是用户输入、AI 响应还是工具调用结果,都用同一个
Msg封装 - 原生多模态支持:从设计上看,
Msg可以承载文本、图片、文件等多种内容 - 结构化字段:通过 name、content 等字段区分消息来源和类型
代码示例
1 | // 创建简单的文本消息 |
🤔 待验证的点
Msg在内部传递过程中会不会被修改或增强?- 多模态内容在不同 LLM 提供商之间怎么保持兼容性?
🤖 智能体(Agent)
我的理解
如果把 Agent 看作一个函数,那它的签名应该是怎样的?
Agentscope 给了一个清晰的答案:
1 | public interface Agent { |
关键观察
- 响应式编程:用了 Reactor 的
Mono和Flux,这很符合现代 Java 的异步风格 - 有状态对象:每个
agent实例都是独立的,不能在线程间共享(并发安全问题⚠️) - 中断机制:
interrupt()方法的存在说明框架考虑了长时间运行的场景
默认实现:ReActAgent
1 | ReActAgent agent = ReActAgent.builder() |
🔍 重点关注
- ReAct 算法的具体实现:怎么实现”推理 - 行动”循环的?
- interrupt 怎么用:什么时候会触发中断?资源怎么清理?
- 并发安全:为什么不能共享?有没有保护机制?
🛠️ 工具(Toolkit)
核心问题
LLM 只能生成文本,但我想让它查数据库、调 API、做计算,怎么办?
答案:把 Java 方法包装成工具
设计亮点
Agentscope 的工具定义方式:
- 用
@Tool注解标记方法 - 支持实例方法、静态方法、类方法
- 支持同步/异步、流式/非流式返回
使用示例
1 | public class WeatherService { |
⚠️ 注意事项
@ToolParam 必须显式指定 name 属性,因为 Java 运行时不保留参数名(这是 Java 的坑😅)
🔍 我的疑问
这才是我最关心的部分:
工具是怎么让 LLM 知道的?
- 是通过 prompt 注入工具描述?
- 还是用了 Function Calling 之类的特性?
调用流程是怎样的?
- LLM 返回工具调用请求后,框架怎么解析?
- 怎么找到对应的 Java 方法并执行?
- 执行结果怎么反馈给 LLM?
这些得看源码才能知道。
🧠 记忆(Memory)
为什么需要记忆?
如果没有记忆,每轮对话都是独立的,那就成了”金鱼记忆”,根本没法进行有意义的对话。
框架的处理
ReActAgent 会自动管理记忆:
- ✅ 用户消息 → 加入记忆
- ✅ 工具调用和结果 → 加入记忆
- ✅ 智能体响应 → 加入记忆
- ✅ 推理时 → 读取记忆作为上下文
存储策略
- 默认实现:
InMemoryMemory(纯内存,进程重启就没了) - 持久化方案:可以用
MysqlSession等基于数据库的实现
🤔 我的猜测
从之前看过的会话管理代码来看,我猜框架可能是这样做的:
- 用一个列表或队列存储历史消息
- 每次推理前,把历史消息拼接到 prompt 里
- 可能有上下文长度限制,需要裁剪或摘要
但具体实现还得看源码验证,特别是:
- 记忆的增删改查是怎么封装的?
- 跨会话持久化是怎么做到的?
- 上下文窗口超限了怎么处理?
🔄 格式化器(Formatter)
为什么要格式化?
不同的 LLM 提供商,API 格式千差万别:
- OpenAI 用 messages 数组
- 阿里云百炼有自己的格式
- 其他厂商又不一样
难道要为每个厂商写一套代码?当然不是!
适配器模式
Agentscope 用 Formatter 做了适配层:
1 | AgentScope 消息 → Formatter → 特定 LLM API 格式 |
内置实现
DashScopeFormatter- 阿里云百炼(通义千问)OpenAIFormatter- OpenAI 及兼容 API
功能范围
不仅仅是格式转换,还包括:
- 📝 提示词工程(添加系统提示、格式化多轮对话)
- ✅ 消息验证(确保符合 API 要求)
- 👥 多智能体身份处理(区分不同角色的消息)
使用体验
对用户来说,通常是无感的:
1 | .model(DashScopeChatModel.builder()...) |
💡 我的理解
总流程图中提到的 Formatter,本质上就是一个协议转换器,让上层应用不用关心底层 LLM 的差异。
后面可以看看它是如何设计统一抽象的。
🪝 钩子(Hook)
使用场景
如果我想在智能体执行时做一些额外的事:
- 📝 记录日志
- 📊 性能监控
- 🔧 修改消息内容
- 🚨 异常告警
怎么办?Hook 系统就是干这个的
事件驱动设计
Hook 通过事件机制在 ReAct 循环的关键节点提供扩展点:
| 事件类型 | 触发时机 | 能否修改 |
|---|---|---|
PreCallEvent |
智能体开始处理前 | ✅ |
PostCallEvent |
智能体处理完成后 | ✅ |
PreReasoningEvent |
调用 LLM 前 | ✅ |
PostReasoningEvent |
LLM 返回后 | ✅ |
ReasoningChunkEvent |
LLM 流式输出时 | ❌ |
PreActingEvent |
执行工具前 | ✅ |
PostActingEvent |
工具执行后 | ✅ |
ActingChunkEvent |
工具流式输出时 | ❌ |
ErrorEvent |
发生错误时 | ❌ |
优先级控制
Hook 按优先级执行,数值越小优先级越高(默认 100)
代码示例
1 | Hook loggingHook = new Hook() { |
🔍 我的核心疑问
这也是我最好奇的地方:
Hook 是怎么注入到 LLM 调用流程中的?
- 是通过 AOP 切面?
- 还是在关键位置手动调用?
- 事件是怎么产生和传播的?
这个必须看源码才能明白。
Action
基于以上概览,接下来的行动计划:
第一阶段:核心机制探索
ReAct 算法实现分析
- 追踪
ReActAgent.call()方法的完整调用链 - 分析推理 - 行动循环的具体实现逻辑
- 研究循环终止条件和状态流转
- 追踪
工具调用机制
- 查看
Toolkit如何注册和管理工具 - 分析 LLM 如何感知可用工具(prompt 注入 or 其他方式)
- 追踪工具调用的解析和执行流程
- 研究工具结果的反馈机制
- 查看
Hook 系统注入方式
- 查找 Hook 在 ReAct 循环中的注册点
- 分析事件触发和传播机制
- 理解 Hook 如何影响消息流转
第二阶段:记忆与状态管理
记忆管理实现
- 分析
InMemoryMemory的内存存储策略 - 研究
MysqlSession等持久化实现 - 探索记忆的增删改查操作封装
- 理解上下文窗口的管理机制
- 分析
Formatter 适配器模式
- 对比
DashScopeFormatter和OpenAIFormatter的实现差异 - 分析消息转换的统一抽象层设计
- 对比
第三阶段:并发与中断
interrupt 中断机制
- 研究中断信号的产生和传递
- 分析正在进行的操作如何响应中断
- 探索资源清理和状态恢复机制
并发安全性
- 验证 Agent 实例的线程安全问题
- 查看是否有并发控制或保护机制
预期产出
- 绘制详细的源码流程图
- 总结关键设计模式和架构思想
- 输出各模块的深度分析文章