一、背景问题:为什么需要可插拔会话存储?
1.1 Agent 会话管理的三大痛点
在开发 AI Agent 应用时,会话管理是每个开发者都必须面对的核心问题。让我从实际场景说起:
场景一:多轮对话中断恢复
用户正在和客服 Agent 沟通退款事宜,突然网络断开。重新连接后,Agent 完全不记得刚才的对话内容,用户只能重复描述问题------这种体验堪称灾难。
场景二: 分布式部署 会话共享
你的 Agent 应用部署了多个实例做负载均衡。用户第一次请求打到实例 A,第二次请求打到实例 B,结果 B 完全没有用户的会话历史------这需要用户每次请求都携带完整上下文,既不现实也不安全。
场景三:开发测试 vs 生产环境
开发阶段你只想快速测试功能,不想配置复杂的存储服务;生产环境却需要高可用、分布式的会话存储。同一套代码如何适配两种截然不同的需求?
这三个痛点的核心问题在于:会话存储策略应该是可插拔的,而不是硬编码在业务逻辑里。
1.2 传统方案的局限
传统会话管理方案往往存在这些问题:
# 传统硬编码会话存储方式
class CustomerServiceAgent:
# 问题1:存储实现与业务逻辑耦合严重
redis_client = redis.Redis(host='localhost', port=6379)
# 问题2:无法适应不同环境
def handle_chat(self, session_id, message):
# 问题3:无法轻松切换存储方式
history = self.redis_client.get(f"session:{session_id}:history")
# 问题4:分布式环境下需手动处理一致性
# ... 业务逻辑 ...
主要问题:
- 业务代码与存储实现紧密耦合
- 存储切换需改动业务逻辑
- 缺乏统一的抽象层支持多存储后端
- 无法优雅处理故障转移
1.3 为什么需要抽象存储层?
正如著名软件工程原则"关注点分离"所强调的:
"将问题的不同方面分离到不同的组件中,可以减少系统的复杂性,使其更容易理解和维护"
在会话管理场景中,我们需要分离:
- 何时存储(Agent生命周期钩子)
- 如何存储(具体存储实现)
- 存储什么(状态数据结构)
二、架构设计深度解析

2.1 存储抽象层设计
openJiuwen 的会话存储架构采用了经典的分层抽象 + 工厂模式设计。让我用一张类图来展示整体架构:
┌─────────────────────────────────────────────────────────────────────┐
│ Checkpointer(抽象层) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ - pre_workflow_execute() - post_workflow_execute() │ │
│ │ - pre_agent_execute() - interrupt_agent_execute() │ │
│ │ - post_agent_execute() - session_exists() │ │
│ │ - release() - graph_store() │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲
│ 继承
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌───────────────┐ ┌────────────────┐ ┌────────────────┐
│ InMemory │ │ Persistence │ │ Redis │
│ Checkpointer │ │ Checkpointer │ │ Checkpointer │
│ │ │ │ │ │
│ • 内存存储 │ │ • SQLite │ │ • Redis │
│ • 开发测试用 │ │ • Shelve │ │ • 分布式 │
│ • 重启丢失 │ │ • 单机生产 │ │ • 多实例共享 │
└───────────────┘ └────────────────┘ └────────────────┘
│ │ │
└─────────────────────────┼─────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Storage(存储接口层) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ AgentStorage │ WorkflowStorage │ GraphStore │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲
│ 依赖
▼
┌─────────────────────────────────────────────────────────────────────┐
│ BaseKVStore(KV 存储抽象) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ - set() / get() / delete() │ │
│ │ - exists() / mget() / batch_delete() │ │
│ │ - get_by_prefix() / delete_by_prefix() │ │
│ │ - pipeline() │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲
│ 实现
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌───────────────┐ ┌────────────────┐ ┌────────────────┐
│ InMemoryStore │ │ DbBasedKVStore │ │ RedisStore │
│ (图存储专用) │ │ (SQLite/MySQL) │ │ (Redis 集群) │
└───────────────┘ └────────────────┘ └────────────────┘
2.2 核心接口源码解析
2.2.1 Checkpointer 抽象基类
文件位置 : openJiuwen/core/session/checkpointer/base.py
class Checkpointer(ABC):
"""检查点抽象基类 - 定义会话状态管理的核心接口"""
@staticmethod
def get_thread_id(session: BaseSession) -> str:
"""获取线程ID,格式: session_id:workflow_id
这种命名空间设计确保了:
1. 同一个session可以包含多个workflow
2. 不同workflow的状态相互隔离
"""
return ":".join([session.session_id(), session.workflow_id()])
# ========== Workflow生命周期钩子 ==========
@abstractmethod
async def pre_workflow_execute(self, session: BaseSession, inputs: InteractiveInput):
"""工作流执行前 - 恢复状态或初始化新会话
典型场景:
- 用户重新进入对话时,从历史检查点恢复上下文
- 分布式环境下,从Redis加载其他实例保存的状态
"""
@abstractmethod
async def post_workflow_execute(self, session: BaseSession, result, exception):
"""工作流执行后 - 保存状态(异常时)或清理状态(成功时)
状态管理策略:
- 正常完成:清理检查点,释放存储空间
- 执行异常:保存检查点,支持从断点恢复
- 需要交互:保存检查点,等待用户输入后继续
"""
# ========== Agent生命周期钩子 ==========
@abstractmethod
async def pre_agent_execute(self, session: BaseSession, inputs):
"""Agent执行前 - 恢复状态并设置输入"""
@abstractmethod
async def interrupt_agent_execute(self, session: BaseSession):
"""Agent中断时 - 保存检查点用于后续恢复
关键场景:人机交互(Human-in-the-loop)
当Agent需要向用户提问时,保存当前状态,
用户回复后从检查点恢复继续执行
"""
@abstractmethod
async def post_agent_execute(self, session: BaseSession):
"""Agent执行后 - 保存最终状态"""
# ========== 会话管理 ==========
@abstractmethod
async def session_exists(self, session_id: str) -> bool:
"""检查会话是否存在 - 用于状态恢复判断"""
@abstractmethod
async def release(self, session_id: str):
"""释放会话资源 - 清理过期会话数据"""
@abstractmethod
def graph_store(self) -> Store:
"""获取图状态存储 - 支持 LangGraph 图状态持久化"""

这段抽象基类代码有几个值得注意的设计点:
- 为什么要把生命周期钩子分开? Agent和Workflow的执行时机不同。Agent可能在Workflow中多次被执行,比如一个客服流程中可能有"问题分类Agent"、"回答生成Agent"等多个节点。分开处理可以让状态管理更精细------Agent级别的状态保存粒度更细,Workflow级别则关注整体流程。
get_thread_id的命名空间设计 用session_id:workflow_id的组合作为唯一标识,这样设计的好处是:同一个用户会话可以包含多个工作流实例,而它们的状态互不干扰。比如用户同时在问天气和订机票,两个Workflow的状态是隔离的。interrupt_agent_execute是做什么的? 这是实现"人机交互"的关键。当Agent需要向用户提问时(比如"请问您的收货地址是哪里?"),会触发中断并保存当前状态。用户回答后,从这个检查点恢复继续执行。
设计亮点:
- 生命周期钩子分离:将 Agent 和 Workflow 的生命周期事件分开处理,符合单一职责原则
- 异步优先 :所有接口都是
async方法,充分利用 asyncio 并发能力 - 状态分离:Agent 状态、Workflow 状态、Graph 状态分别管理,避免耦合
2.2.2 Storage 抽象基类
文件位置 : openJiuwen/core/session/checkpointer/base.py
class Storage(ABC):
"""存储抽象基类 - 定义底层存储操作接口"""
@abstractmethod
async def save(self, session: BaseSession):
"""保存会话状态 - 序列化并写入存储"""
@abstractmethod
async def recover(self, session: BaseSession, inputs: InteractiveInput = None):
"""恢复会话状态 - 从存储读取并反序列化"""
@abstractmethod
async def clear(self, session_id: str):
"""清理会话状态 - 删除存储数据"""
@abstractmethod
async def exists(self, session: BaseSession) -> bool:
"""检查状态是否存在 - 用于恢复前验证"""

Storage接口的设计思路很清晰,就是把"怎么存"这件事单独抽出来。
- 为什么需要Storage和Checkpointer两层抽象?
这样分离的好处是:同一个Checkpointer实现可以组合不同的Storage后端。比如PersistenceCheckpointer可以搭配SQLite,也可以搭配Shelve。
- Checkpointer负责"什么时候存"------它定义的是生命周期钩子
- Storage负责"怎么存"------它定义的是具体的存取操作
save和recover为什么是核心? 这是状态持久化的两个基本操作。内部使用pickle序列化,可以把任意Python对象存下来。但要注意,pickle有版本兼容问题------如果代码升级了数据结构,老的状态可能反序列化失败。exists方法的用途 在恢复状态前先检查一下存在性,可以避免无效的IO操作。特别是在Redis场景下,如果key已经过期,exists会返回False,就不会去做无意义的读取了。
设计要点:
save/recover是状态持久化的核心操作,使用 pickle 序列化clear用于会话结束后清理资源,防止存储膨胀exists快速检查状态是否存在,避免无效恢复操作
2.2.3 命名空间设计:三层Key结构
文件位置 : openJiuwen/core/session/checkpointer/base.py
# Key namespace constants
# Namespace for agent state under session
SESSION_NAMESPACE_AGENT = "agent" # Agent 状态命名空间
# Namespace for workflow state under session (workflow's own state)
SESSION_NAMESPACE_WORKFLOW = "workflow" # 工作流状态命名空间
# Namespace for graph state under workflow (separated from workflow's own state)
WORKFLOW_NAMESPACE_GRAPH = "workflow-graph" # 图状态命名空间
def build_key(*parts: str) -> str:
"""使用冒号连接键的各个部分"""
return ":".join(parts)
def build_key_with_namespace(
session_id: str,
namespace: str,
entity_id: str,
*suffixes: str
) -> str:
"""构建带命名空间结构的键"""
parts = [session_id, namespace, entity_id] + list(suffixes)
return build_key(*parts)

代码讲解:
命名空间设计看起来简单,但它是整个存储系统的基础。
- 为什么要分三层命名空间?
分开存储的好处是:清理的时候可以精准删除。比如只需要重置Agent状态,不影响Workflow的执行进度。
- `agent`:Agent的运行时状态,比如记忆、上下文
- `workflow`:Workflow自身的状态,比如当前执行到哪个节点
- `workflow-graph`:图执行状态,这是给LangGraph用的
- Key的结构设计
{session_id}:{namespace}:{entity_id}:{field}这个结构支持前缀查询。比如想查某个session的所有数据,用session-001:*就行了;想查所有Agent状态,用*:agent:*。这在排查问题时特别有用。 - 实际使用中的坑
-
- session_id不要包含冒号,否则会破坏Key结构
- 如果用Redis,注意Key长度不要超过1024字节(虽然一般不会超)
Key结构示例:
|------------|---------------------------------------------------|--------------------|
| Key模式 | 示例 | 说明 |
| Agent状态 | session-001:agent:agent-001:agent_state_blobs | 特定Agent的运行时状态 |
| Workflow状态 | session-001:workflow:wf-001:workflow_state_blobs | 特定Workflow的执行状态 |
| Graph状态 | session-001:workflow-graph:wf-001:checkpoint_data | Workflow的图执行状态 |
| 所有Session | session-001:* | 获取session_001的所有状态 |
2.3 工厂模式与自动注册机制
文件位置 : openJiuwen/core/session/checkpointer/checkpointer.py
class CheckpointerFactory:
"""检查点工厂 - 支持自动注册的插件化架构
设计模式:工厂模式 + 装饰器注册
优势:新增存储类型无需修改工厂代码,符合开闭原则
"""
_registry: dict[str, CheckpointerProvider] = {}
@classmethod
def register(cls, name: str):
"""装饰器模式 - 实现自动注册
使用示例:
@CheckpointerFactory.register("redis")
class RedisCheckpointerProvider(CheckpointerProvider):
...
"""
def decorator(provider_class: Type[CheckpointerProvider]):
cls._registry[name] = provider_class()
return provider_class
return decorator
@classmethod
async def create(cls, name: str, config: dict) -> Checkpointer:
"""根据名称创建对应的Checkpointer实例
配置驱动的存储选型:
- 开发环境:create("in_memory", {})
- 单机生产:create("persistence", {"db_type": "sqlite"})
- 分布式:create("redis", {"url": "redis://..."})
"""
if name not in cls._registry:
raise ValueError(f"Unknown checkpointer type: {name}")
return await cls._registry[name].create(config)

代码讲解:
工厂模式 + 装饰器注册,这个组合实现了"开闭原则"------对扩展开放,对修改关闭。
-
装饰器注册是怎么工作的? 当Python解释器加载代码时,
@CheckpointerFactory.register("redis")装饰器就会执行。它把RedisCheckpointerProvider的实例存到_registry字典里。这个过程是自动的,不需要手动调用注册方法。 -
为什么要用 工厂模式 ? 创建一个Checkpointer实例可能涉及很多步骤:解析配置、建立连接、初始化存储组件。把这些逻辑封装在Provider里,调用方只需要传一个类型名称和配置字典就行了。
-
实际使用场景
开发环境:直接用内存,零配置
checkpointer = await CheckpointerFactory.create("in_memory", {})
生产环境:切换到Redis,只改类型名
checkpointer = await CheckpointerFactory.create("redis", {
"connection": {"url": "redis://localhost:6379"}
})
使用示例:
# Redis实现自动注册
@CheckpointerFactory.register("redis")
class RedisCheckpointerProvider(CheckpointerProvider):
async def create(self, conf: dict) -> Checkpointer:
config = RedisCheckpointerConfig.model_validate(conf)
# ... 创建RedisCheckpointer
return RedisCheckpointer(redis_store, ttl_dict)
# Persistence实现自动注册
@CheckpointerFactory.register("persistence")
class PersistenceCheckpointerProvider(CheckpointerProvider):
async def create(self, conf: dict) -> Checkpointer:
# ... 创建PersistenceCheckpointer
return PersistenceCheckpointer(kv_store)

2.4 KV 存储接口统一设计
文件位置 : openJiuwen/core/foundation/store/base_kv_store.py
class BaseKVStore(ABC):
"""KV 存储抽象接口 - 统一所有底层存储的操作方式"""
@abstractmethod
async def set(self, key: str, value: str | bytes):
"""存储键值对 - O(1)"""
@abstractmethod
async def exclusive_set(self, key: str, value: str | bytes, expiry: int | None = None) -> bool:
"""原子性设置 - 仅当键不存在时设置成功,用于分布式锁"""
@abstractmethod
async def get(self, key: str) -> str | bytes | None:
"""获取值 - O(1)"""
@abstractmethod
async def exists(self, key: str) -> bool:
"""检查键是否存在 - O(1)"""
@abstractmethod
async def delete(self, key: str):
"""删除键 - O(1)"""
@abstractmethod
async def get_by_prefix(self, prefix: str) -> dict[str, str | bytes]:
"""按前缀获取所有键值对 - O(N),使用 SCAN 避免阻塞"""
@abstractmethod
async def delete_by_prefix(self, prefix: str, batch_size: Optional[int] = None):
"""按前缀删除所有键 - O(N),分批删除避免内存问题"""
@abstractmethod
async def mget(self, keys: List[str]) -> List[str | bytes | None]:
"""批量获取 - O(N),单次网络往返"""
@abstractmethod
async def batch_delete(self, keys: List[str], batch_size: Optional[int] = None) -> int:
"""批量删除 - O(N)"""
@abstractmethod
def pipeline(self) -> Any:
"""创建管道用于批量操作 - 减少网络往返"""
代码讲解:
这个接口的设计目标是"统一存储操作",让上层代码不用关心底层是Redis还是SQLite。
-
为什么需要
exclusive_set? 这是一个原子操作,只有当key不存在时才能设置成功。它的典型用途是实现分布式锁:尝试获取锁
acquired = await store.exclusive_set("lock:session-001", "locked", expiry=30)
if not acquired:
# 锁被占用,等待或返回
pass -
get_by_prefix和delete_by_prefix的性能考量 这两个方法在清理会话数据时特别有用。但要注意:
-
- 在Redis中,它们使用SCAN命令,不会阻塞服务
- 在SQLite中,它们可能需要全表扫描,数据量大时要小心
-
pipeline的作用 批量操作时减少网络往返。比如保存一个Agent状态需要写两个key,用pipeline可以合并成一次网络请求:pipeline = store.pipeline()
await pipeline.set("key1", "value1")
await pipeline.set("key2", "value2")
await pipeline.execute() # 只有一次网络往返
接口设计要点:
- 统一接口设计允许底层存储自由替换(Redis/SQLite/Shelve)
exclusive_set支持原子操作,可用于实现分布式锁*_by_prefix方法支持会话级别的批量操作pipeline提供批量操作能力,显著提升性能

三、三种存储实现详解
3.1 实现概览
在深入细节之前,先帮你快速选型。如果你赶时间,看完这张表和下面的决策树就够了。
|-----------------|-------------------------------------------------------------------|-------------------------|-----------|---------------|--------|
| 存储类型 | 实现文件 | 核心类 | 适用场景 | 数据持久化 | 分布式支持 |
| InMemory | inmemory.py | InMemoryCheckpointer | 开发测试、单元测试 | ❌ 进程结束即丢失 | ❌ 不支持 |
| Persistence | persistence.py | PersistenceCheckpointer | 单机生产、中小规模 | ✅ 本地文件/SQLite | ❌ 不支持 |
| Redis | redis/checkpointer.py | RedisCheckpointer | 分布式部署、高并发 | ✅ Redis持久化 | ✅ 集群模式 |
3.1.1 选型决策树
你的应用场景是什么?
│
├── 本地开发/写单元测试?
│ └── 选 InMemory
│ • 零配置,开箱即用
│ • 进程重启数据丢失(测试时反而方便)
│
├── 单机部署,不需要多实例共享?
│ │
│ ├── 数据量小(<1GB)?
│ │ └── 选 Persistence (Shelve)
│ │ • Python内置,无需额外依赖
│ │
│ └── 需要查询/并发写入?
│ └── 选 Persistence (SQLite + WAL)
│ • 支持并发读
│ • WAL模式解决锁问题
│
└── 分布式部署/多实例?
│
├── 数据量中等,预算有限?
│ └── 选 Redis 单节点
│ • 配置简单
│ • 开启AOF持久化
│
├── 高可用要求?
│ └── 选 Redis Sentinel
│ • 主从自动切换
│ • 监控告警完善
│
└── 海量数据/超高并发?
└── 选 Redis Cluster
• 数据分片
• 横向扩展
3.1.2 三种存储对比
|-----------|----------|----------------------|----------------------|
| 对比维度 | InMemory | Persistence (SQLite) | Redis |
| 读取延迟 | ~0.1ms | ~2ms | ~1ms(本地)/ ~5ms(远程) |
| 写入延迟 | ~0.1ms | ~5ms | ~1ms |
| 并发能力 | 高(内存锁) | 中等(WAL优化) | 极高 |
| 数据可靠性 | ❌ 进程结束丢失 | ✅ 磁盘持久化 | ✅ AOF/RDB |
| 运维成本 | 零 | 低 | 中等 |
| 适用阶段 | 开发/测试 | 单机生产 | 分布式生产 |
3.2 InMemoryCheckpointer - 轻量快速
文件位置 : openJiuwen/core/session/checkpointer/inmemory.py
原理:基于Python字典的内存存储,速度最快,但进程结束数据即丢失。
class InMemoryCheckpointer(Checkpointer):
"""内存检查点实现 - 适用于开发和测试环境"""
def __init__(self):
self._agent_stores = {} # session_id -> AgentStorage
self._workflow_stores = {} # session_id -> WorkflowStorage
self._graph_store = InMemoryStore() # 图状态存储
self._session_to_workflow_ids = {} # session 到 workflow 的映射
self._lock = asyncio.Lock() # 并发安全锁
async def save(self, session: BaseSession):
"""保存状态到内存字典"""
key = self.get_thread_id(session)
# 深拷贝防止引用问题
async with self._lock:
self._storage[key] = copy.deepcopy(session.state().get_state())
async def recover(self, session: BaseSession, inputs: InteractiveInput = None):
"""从内存字典恢复状态"""
key = self.get_thread_id(session)
async with self._lock:
if key in self._storage:
session.state().set_state(self._storage[key])
这段代码有几个细节值得注意:
- 为什么要用深拷贝? Python的赋值是引用传递。如果直接存
session.state().get_state(),存的是引用。后面如果session状态变了,存储里的数据也会跟着变------这不是我们想要的。深拷贝确保存的是一份独立的快照。 asyncio.Lock()的作用 虽然字典操作本身是线程安全的,但在异步环境下,save和recover可能被并发调用。加锁是为了防止在保存过程中状态被其他协程修改。- 什么时候用InMemory?
千万别在生产环境用! 进程一重启,所有会话数据都没了。
- 写单元测试时,不想搭建Redis
- 本地调试时,需要快速迭代
- 一次性任务,不需要持久化
适用场景:
- 本地开发调试
- 单元测试(测试间相互隔离)
- 无状态、一次性任务
3.3 PersistenceCheckpointer - 本地持久化
文件位置 : openJiuwen/core/session/checkpointer/persistence.py
3.3.1 架构设计
class PersistenceCheckpointer(Checkpointer):
"""基于BaseKVStore的持久化检查点实现
组合模式:聚合三个专门的存储器,分别管理不同维度的状态
"""
def __init__(self, kv_store: BaseKVStore):
self._kv_store = kv_store
# 职责分离:每个存储器只负责一种状态类型
self._agent_storage = AgentStorage(kv_store) # Agent运行时状态
self._workflow_storage = WorkflowStorage(kv_store) # Workflow执行状态
self._graph_state = GraphStore(kv_store) # 图执行状态(Pregel)
代码讲解:
- 为什么用组合模式而不是继承? 这里把存储职责委托给了三个专门的Storage类。如果用继承,就需要为Agent、Workflow、Graph分别创建子类,会导致类爆炸。组合模式更灵活------换存储后端只需要换个KVStore实现。
- 三个Storage分别存什么?
-
AgentStorage:Agent的运行时状态,比如对话历史、记忆WorkflowStorage:工作流自身的执行状态,比如当前节点、变量GraphStore:LangGraph图执行的状态,用于断点续传
kv_store从哪来? 它是BaseKVStore的实现,可以是SQLite、Shelve或者其他数据库。这个参数是依赖注入的,创建逻辑在Provider里。
3.3.2 支持两种后端
|------------|-----------------|------------------|-------------|
| 后端 | 特点 | 配置方式 | 适用场景 |
| SQLite | 关系型数据库,支持WAL模式 | db_type="sqlite" | 需要复杂查询的生产环境 |
| Shelve | Python标准库,基于dbm | db_type="shelve" | 简单KV存储场景 |
3.3.3 WAL模式优化
def _enable_sqlite_wal(engine: AsyncEngine) -> None:
"""启用WAL模式解决'database is locked'问题
WAL (Write-Ahead Logging) 优势:
1. 读写不冲突:一个写操作和多个读操作可并发
2. 性能更好:写操作不阻塞读操作
3. 崩溃恢复:基于日志的恢复更可靠
"""
@event.listens_for(engine.sync_engine, "connect")
def _set_sqlite_pragma(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.close()

3.4 RedisCheckpointer - 分布式首选
文件位置 : openJiuwen/extensions/checkpointer/redis/checkpointer.py
3.4.1 架构特点
RedisCheckpointer
├── RedisConnectionConfig # 连接配置(支持集群)
│ ├── url: redis:// 或 redis+cluster://
│ ├── cluster_mode: 自动检测或显式指定
│ └── connection_args: 连接池等参数
│
├── RedisTTLConfig # TTL过期策略
│ ├── default_ttl: 默认过期时间(分钟)
│ └── refresh_on_read: 读取时刷新TTL
│
└── Storage Layer (Redis)
├── AgentStorage(Redis) # Agent状态存储
├── WorkflowStorage(Redis) # Workflow状态存储
└── GraphStore(Redis) # 图状态存储
3.4.2 Redis连接配置详解
class RedisConnectionConfig(BaseModel):
"""Redis连接配置 - 支持Standalone和Cluster两种模式
集群模式自动检测逻辑:
1. 如果提供了redis_client,根据类型判断(Redis vs RedisCluster)
2. 如果显式设置了cluster_mode,使用指定值
3. 如果从URL判断,redis+cluster://开头即为集群模式
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
redis_client: Optional[Union[Redis, RedisCluster]] = None
url: Optional[str] = "redis://localhost:6379"
cluster_mode: Optional[bool] = None
connection_args: dict = {}
@field_validator('url')
def validate_url(cls, v: str) -> str:
"""URL格式验证"""
valid_prefixes = (
"redis://", "rediss://",
"redis+cluster://", "rediss+cluster://"
)
if not any(v.startswith(p) for p in valid_prefixes):
raise ValueError(f"Invalid Redis URL format: {v}")
return v
def is_cluster_mode(self) -> bool:
"""判断是否为集群模式"""
if self.redis_client is not None:
return isinstance(self.redis_client, RedisCluster)
if self.cluster_mode is not None:
return self.cluster_mode
if self.url:
return self.url.startswith(("redis+cluster://", "rediss+cluster://"))
return False
代码讲解:
- 为什么要支持三种方式判断集群模式? 这是为了适应不同的使用场景:
-
- 传
redis_client:适合已经创建了Redis连接的情况,比如测试时mock一个 - 传
cluster_mode:显式指定,最明确 - 从URL判断:最方便,
redis+cluster://一眼就知道是集群
- 传
- URL 格式说明
-
redis://:普通连接rediss://:SSL加密连接redis+cluster://:集群模式rediss+cluster://:加密的集群模式
- Pydantic验证器的作用
@field_validator会在赋值时自动校验URL格式。如果传了非法的URL,在创建配置对象时就会报错,而不是等到运行连接时才发现问题。
3.4.3 TTL过期策略
class RedisTTLConfig(BaseModel):
"""TTL (Time To Live) 配置
设计目的:
1. 防止Redis无限增长导致内存溢出
2. 自动清理过期会话,无需手动维护
3. 活跃会话自动续期,避免使用过程中被清理
"""
default_ttl: Optional[float] = Field(
default=None,
description="默认过期时间(分钟)"
)
refresh_on_read: bool = Field(
default=False,
description="读取时是否刷新TTL(活跃会话保活)"
)
代码讲解:
TTL配置是Redis存储的关键,处理不好会导致内存爆炸或者会话丢失。
default_ttl设多少合适?
-
- 客服系统:24小时(1440分钟),用户可能隔天再问
- 短期任务:1-2小时,执行完就清理
- 长期分析:7天(10080分钟),复杂任务可能需要多天
refresh_on_read是什么意思? 设为True时,每次读取状态都会刷新TTL。这样活跃的会话不会被清理,而那些用户很久没操作的会话会自动过期。就像"正在使用中"的保活机制。- 不设 TTL 会怎样? Redis内存会无限增长,最终触发内存淘汰策略,可能把不该删的数据删了。生产环境强烈建议配置TTL。
实际应用场景:
# 场景1:客服系统 - 会话24小时过期,活跃会话自动续期
config = RedisCheckpointerConfig(
connection=RedisConnectionConfig(url="redis://localhost:6379"),
ttl=RedisTTLConfig(default_ttl=1440, refresh_on_read=True)
)
# 场景2:短期任务 - 会话1小时过期
config = RedisCheckpointerConfig(
connection=RedisConnectionConfig(url="redis://localhost:6379"),
ttl=RedisTTLConfig(default_ttl=60, refresh_on_read=False)
)
# 场景3:长期分析任务 - 7天过期
config = RedisCheckpointerConfig(
connection=RedisConnectionConfig(url="redis://localhost:6379"),
ttl=RedisTTLConfig(default_ttl=10080, refresh_on_read=True)
)

3.5 环境搭建指南
如果你想跟着本文动手实践,需要先搭建测试环境。这一节给你一份保姆级的搭建指南。
3.5.1 环境要求
|--------|-----------|--------------|
| 组件 | 版本要求 | 说明 |
| Python | >= 3.10 | 异步语法支持 |
| Redis | >= 7.0 | TTL和SCAN命令优化 |
| SQLite | >= 3.35 | JSON函数支持 |
| Docker | >= 20.10 | 容器化部署 |
3.5.2 Docker快速部署Redis
最简单的方式是用Docker跑一个Redis:
# 创建数据目录
mkdir -p ~/jiuwen-data/redis
# 启动Redis容器
docker run -d \
--name jiuwen-redis \
-p 6379:6379 \
-v ~/jiuwen-data/redis:/data \
redis:7-alpine \
redis-server --appendonly yes
# 验证Redis是否正常
docker exec -it jiuwen-redis redis-cli ping
# 输出: PONG
参数说明:
--appendonly yes:开启AOF持久化,重启后数据不丢失-v:挂载数据目录,持久化Redis数据
3.5.3 安装Python依赖
# 创建虚拟环境(推荐)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# 安装openJiuwen
pip install openJiuwen
# 如果要用Redis存储,需要额外安装
pip install "openJiuwen[redis]"
3.5.4 最小配置示例
创建一个test_checkpointer.py文件,用下面的代码验证环境:
import asyncio
from openJiuwen.core.session.checkpointer import (
CheckpointerFactory,
CheckpointerConfig,
)
async def test_redis_connection():
"""测试Redis连接是否正常"""
config = CheckpointerConfig(
type="redis",
conf={
"connection": {"url": "redis://localhost:6379"}
}
)
checkpointer = await CheckpointerFactory.create(config)
print("✅ Redis连接成功!")
# 测试基本功能
exists = await checkpointer.session_exists("test-session")
print(f"Session存在: {exists}")
asyncio.run(test_redis_connection())

四、Redis数据结构详解
这一章我们深入到Redis层面,看看数据到底是怎么存的。
4.1 Key命名空间设计
openJiuwen在Redis中使用结构化的Key设计,便于管理和查询:
{session_id}:{namespace}:{entity_id}:{field}
为什么要这样设计?
- 前缀查询方便 :想查某个session的所有数据?用
session-001:*就行。想查所有Agent状态?用*:agent:*。 - 清理方便 :用户注销时,删除
session-001:*就能清掉所有相关数据,不用一个个删。 - 避免冲突:不同类型的数据有各自的命名空间,不会撞车。
示例Key结构:
|---------------------------------------------------------|--------|-----------------|
| Key | 类型 | 说明 |
| sess_abc123:agent:agent_001:state_blobs | String | Agent状态二进制数据 |
| sess_abc123:agent:agent_001:state_blobs_dump_type | String | 序列化类型标识 |
| sess_abc123:workflow:wf_001:state_blobs | String | Workflow状态二进制数据 |
| sess_abc123:workflow:wf_001:update_blobs | String | Workflow增量更新数据 |
| sess_abc123:workflow-graph:wf_001:checkpoint_data_type | String | 图状态类型 |
| sess_abc123:workflow-graph:wf_001:checkpoint_data_value | String | 图状态数据 |
4.1.1 实际验证命令
光说不练假把式。我们用实际的Redis命令来验证:
# 查看Agent状态
docker exec -it jiuwen-redis-2giqn redis-cli KEYS "eval-test-redis-001:agent:*"
# 验证Redis中的Key结构
docker exec -it jiuwen-redis-2giqn redis-cli KEYS "*:agent:*"

4.2 数据存储格式
4.2.1 Agent状态存储结构
Agent的状态存成两个Key:一个存数据,一个存类型。为什么要分两个?因为反序列化时需要知道用什么方法(pickle、json等)。
# Agent状态存储使用两个Key:
# 1. {session}:agent:{agent_id}:agent_state_blobs_dump_type
# 存储序列化类型(如"pickle")
# 2. {session}:agent:{agent_id}:agent_state_blobs
# 存储序列化后的二进制数据(Base64编码)
class AgentStorage:
_STATE_BLOBS = "agent_state_blobs"
_STATE_BLOBS_DUMP_TYPE = "agent_state_blobs_dump_type"
async def save(self, session: BaseSession):
state = session.state().get_state()
state_blob = self._serialize_state(state) # (dump_type, blob)
dump_type_key = build_key_with_namespace(
session_id, SESSION_NAMESPACE_AGENT, agent_id,
self._STATE_BLOBS_DUMP_TYPE
)
blob_key = build_key_with_namespace(
session_id, SESSION_NAMESPACE_AGENT, agent_id,
self._STATE_BLOBS
)
# 使用Pipeline原子写入两个Key
pipeline = self._kv_store.pipeline()
await pipeline.set(dump_type_key, dump_type)
await pipeline.set(blob_key, blob)
await pipeline.execute()
代码讲解:
- 为什么用Pipeline? 两个Key需要同时写入成功。如果不用Pipeline,可能出现一个写入成功、另一个失败的情况。Pipeline保证了原子性。
- 序列化 类型为什么要单独存? pickle格式有版本兼容问题。单独存类型,以后可以扩展支持其他格式,比如json、msgpack。
- 数据量大的情况怎么办? Agent状态如果很大(比如包含了大量对话历史),会占用较多Redis内存。建议在业务层做截断,只保留最近N轮对话。
4.2.2 Workflow状态存储结构
Workflow比Agent复杂一点,它要存"主状态"和"增量更新"两部分。
# Workflow状态存储使用四个Key:
# 1. state_blobs / state_blobs_dump_type - 主状态
# 2. update_blobs / update_blobs_dump_type - 增量更新
class WorkflowStorage:
_STATE_BLOBS = "workflow_state_blobs"
_STATE_BLOBS_DUMP_TYPE = "workflow_state_blobs_dump_type"
_UPDATE_BLOBS = "workflow_update_blobs"
_UPDATE_BLOBS_DUMP_TYPE = "workflow_update_blobs_dump_type"
为什么要分主状态和增量更新?
Workflow在执行过程中会不断产生中间结果。如果把每次更新都合并到主状态,会导致:
- 序列化/反序列化开销大
- 并发修改时容易冲突
所以设计成:主状态存稳定的部分,增量更新存变化的部分。恢复时先加载主状态,再应用增量。
4.2.3 Graph状态存储结构
Graph状态是给LangGraph用的,存储工作流图的执行状态。
# 图状态存储使用命名空间隔离:
# {session}:workflow-graph:{workflow_id}:{field}
class GraphStore:
_DATA_TYPE = "checkpoint_data_type"
_DATA_VALUE = "checkpoint_data_value"
Graph状态和Workflow状态分开存,是因为它们的用途不同:
- Workflow状态:业务数据,比如对话历史、变量
- Graph状态:执行状态,比如当前节点、分支条件
这样分开的好处是:如果只需要重置执行进度,不影响业务数据。
4.3 批量操作与性能优化
4.3.1 Pipeline批量操作
当需要同时操作多个Key时,Pipeline能大幅提升性能。
async def save_workflow_batch(self, sessions: List[BaseSession]):
"""批量保存多个Workflow状态
使用Redis Pipeline减少网络往返次数:
- 单个操作:N次网络往返
- Pipeline批量:1次网络往返
"""
pipeline = self._kv_store.pipeline()
for session in sessions:
dump_type_key = build_key_with_namespace(
session.session_id(), SESSION_NAMESPACE_WORKFLOW,
session.workflow_id(), self._STATE_BLOBS_DUMP_TYPE
)
blob_key = build_key_with_namespace(
session.session_id(), SESSION_NAMESPACE_WORKFLOW,
session.workflow_id(), self._STATE_BLOBS
)
state_blob = self._serialize_state(session.state().get_state())
if state_blob:
dump_type, blob = state_blob
await pipeline.set(dump_type_key, dump_type)
await pipeline.set(blob_key, blob)
# 一次性执行所有命令
await pipeline.execute()
性能对比:
|----------|----------|-----------|
| 操作方式 | 10个Key耗时 | 100个Key耗时 |
| 单个操作 | ~50ms | ~500ms |
| Pipeline | ~5ms | ~8ms |
差距这么大,是因为网络往返成了瓶颈。Pipeline把多次往返合并成一次。
4.3.2 前缀删除优化
删除会话数据时,可能需要删除几十甚至上百个Key。直接用KEYS命令在生产环境是危险的------它会阻塞Redis。
async def release(self, session_id: str, agent_id: Optional[str] = None):
"""释放会话资源
优化策略:
1. 使用SCAN代替KEYS避免阻塞(Redis单线程)
2. 分批删除,每批500个key
3. 使用UNLINK代替DEL(异步删除,不阻塞)
"""
if agent_id is not None:
# 删除特定Agent
await self._agent_storage.clear(agent_id, session_id)
else:
# 批量删除整个session
prefix = f"{session_id}:"
await self._redis_store.delete_by_prefix(prefix, batch_size=500)
代码讲解:
- SCAN vs KEYS
KEYS *会扫描整个数据库,数据量大时会阻塞几秒。SCAN是增量式的,每次只返回一小批,不会阻塞。 - 分批删除 一次删除太多Key会导致Redis卡顿。每批500个是比较安全的值。
- UNLINK vs DEL
DEL是同步删除,会阻塞直到删除完成。UNLINK是异步的,在后台线程执行,不影响主线程。
五、故障转移配置与实战
5.1 高可用架构设计
5.1.1 Redis Sentinel架构
┌─────────────┐
│ Application │
└──────┬──────┘
│
┌──────▼──────┐
│ Sentinel │◄──── 监控多个Sentinel节点
│ (HA代理) │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌───▼───┐ ┌────▼───┐ ┌────▼───┐
│ Master│──►│ Slave1 │ │ Slave2 │
│ (主) │ │ (从) │ │ (从) │
└───────┘ └────────┘ └────────┘
配置示例:
# Sentinel模式配置
config = CheckpointerConfig(
type="redis",
config={
"connection": {
"url": "redis://sentinel-host:26379",
"connection_args": {
"sentinels": [
("sentinel1", 26379),
("sentinel2", 26379),
("sentinel3", 26379)
],
"sentinel_kwargs": {"password": "sentinel-pass"},
"min_other_sentinels": 2,
"socket_connect_timeout": 5,
"socket_timeout": 5,
"health_check_interval": 30 # 健康检查间隔
}
}
}
)
5.1.2 Redis Cluster架构
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Node 1 │◄───►│ Node 2 │◄───►│ Node 3 │ <-- Master节点
│ (0-5460)│ │(5461-10922) │(10923-16383)│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Replica 1│ │Replica 2│ │Replica 3│ <-- Slave节点
└─────────┘ └─────────┘ └─────────┘
配置示例:
# Cluster模式配置
config = CheckpointerConfig(
type="redis",
config={
"connection": {
"url": "redis://node1:7000",
"cluster_mode": True, # 显式启用集群模式
"connection_args": {
"startup_nodes": [
{"host": "node1", "port": 7000},
{"host": "node2", "port": 7000},
{"host": "node3", "port": 7000}
],
"skip_full_coverage_check": True, # 跳过全量检查,加快启动
"max_connections_per_node": 20,
"retry_on_timeout": True,
"retry_on_error": [ConnectionError, TimeoutError]
}
}
}
)
5.2 故障检测与恢复
5.2.1 连接超时配置
connection_args = {
# 连接超时
"socket_connect_timeout": 5, # 建立连接超时(秒)
"socket_timeout": 10, # 读写超时(秒)
# 重试策略
"retry_on_timeout": True, # 超时时重试
"retry_on_error": [
ConnectionError,
TimeoutError,
ConnectionRefusedError
],
"max_connections": 50, # 连接池大小
# 健康检查
"health_check_interval": 30, # 健康检查间隔(秒)
}
5.2.2 故障转移测试
下面是一个简化的测试框架,用于验证Redis故障恢复能力。完整代码见项目tests/目录。
# 故障转移测试核心逻辑
async def test_failover():
"""Redis故障转移测试"""
config = CheckpointerConfig(
type="redis",
conf={
"connection": {
"url": "redis://localhost:6379",
"connection_args": {
"socket_connect_timeout": 5,
"retry_on_timeout": True,
}
},
"ttl": {"default_ttl": 60}
}
)
checkpointer = await CheckpointerFactory.create(config)
# 测试1:正常操作
print("测试正常连接...")
exists = await checkpointer.session_exists("test-session")
print(f"✅ Redis连接正常,session存在: {exists}")
# 测试2:模拟Redis重启后恢复
# (需要手动停止/启动Redis来验证)
print("请手动执行: docker restart jiuwen-redis")
input("Redis重启后按回车继续...")
# 验证恢复
exists = await checkpointer.session_exists("test-session")
print(f"✅ Redis恢复后连接正常")
# 运行测试
asyncio.run(test_failover())
测试验证点:
- 正常情况下的连接和读写
- Redis重启后的自动重连
- 连接超时时的错误处理
完整的测试套件包含7个测试用例,位于:
tests/unit_tests/extensions/checkpointer/test_redis_checkpointer.py



5.3 降级策略设计
当Redis不可用时,应用层可以采取以下降级策略:
class ResilientCheckpointer:
"""弹性检查点 - 支持故障降级"""
def __init__(self, primary_config, fallback_config=None):
self.primary = None
self.fallback = None
self.primary_config = primary_config
self.fallback_config = fallback_config or {
"type": "in_memory" # 默认降级到内存存储
}
async def initialize(self):
"""初始化,尝试连接主存储,失败则使用备用"""
try:
self.primary = await CheckpointerFactory.create(
self.primary_config["type"],
self.primary_config["config"]
)
print("✅ 主存储(Redis)连接成功")
except Exception as e:
print(f"⚠️ 主存储连接失败: {e},切换到备用存储")
self.primary = None
self.fallback = await CheckpointerFactory.create(
self.fallback_config["type"],
self.fallback_config["config"]
)
async def save(self, session):
"""保存状态,主存储失败时记录日志但不阻塞"""
try:
if self.primary:
await self.primary.save(session)
elif self.fallback:
await self.fallback.save(session)
print("⚠️ 使用备用存储保存状态")
except Exception as e:
print(f"❌ 保存失败: {e}")
# 生产环境应发送到告警系统
async def recover(self, session):
"""恢复状态,主存储失败时尝试备用"""
try:
if self.primary:
return await self.primary.recover(session)
except Exception as e:
print(f"⚠️ 主存储恢复失败: {e}")
if self.fallback:
return await self.fallback.recover(session)
return None # 完全无法恢复
# 使用示例
resilient_config = {
"primary": {
"type": "redis",
"config": {
"connection": {"url": "redis://localhost:6379/0"}
}
},
"fallback": {
"type": "persistence",
"config": {
"db_type": "sqlite",
"db_path": "./fallback_checkpoints.db"
}
}
}
六、功能验证与测试
写了这么多代码,怎么验证它真的能工作?这一节带你过一遍测试验证流程。
6.1 测试文件清单
项目的测试覆盖比较全面,主要测试文件都在tests/unit_tests/extensions/checkpointer/目录下:
|-------------------------------------------------------------------------------------------------------------------------|-------------------|-----------------------|
| 文件 | 测试内容 | 关键测试点 |
| test_redis_checkpointer.py | Redis检查点核心功能 | Agent/Workflow状态保存与恢复 |
| test_redis_checkpointer_provider.py | Redis提供者配置 | 配置验证、集群模式检测 |
| test_agent_storage.py | AgentStorage组件 | 状态序列化、TTL刷新 |
| test_workflow_storage.py | WorkflowStorage组件 | 交互式输入处理、更新恢复 |
| test_graph_store.py | GraphStore组件 | 图状态持久化 |
| test_integration_workflow.py | 集成测试 | 完整工作流场景 |
| conftest.py | 测试Fixtures | Redis客户端、Mock Session |
6.2 运行测试
# 运行所有checkpointer测试
cd agent-core-develop
pytest tests/unit_tests/extensions/checkpointer/ -v
# 只运行Redis相关测试
pytest tests/unit_tests/extensions/checkpointer/test_redis_checkpointer.py -v
# 带覆盖率报告
pytest tests/unit_tests/extensions/checkpointer/ --cov=openJiuwen --cov-report=html
6.3 功能验证截图点
以下是验证功能时的关键截图位置:
6.3.1 命名空间Key结构
前置条件:运行一次Redis测试,生成测试数据
验证命令:
docker exec -it jiuwen-redis-2giqn redis-cli KEYS "*:agent:*"


6.3.2 TTL过期验证
验证命令:
docker exec -it jiuwen-redis-2giqn redis-cli TTL "eval-test-redis-001:agent:eval-agent-redis-001:agent_state_blobs"
预期输出:显示剩余秒数(如配置了60分钟TTL,应显示约3600秒)

6.3.3 测试执行结果
pytest tests/unit_tests/extensions/checkpointer/ -v

七、性能对比与选型建议
7.1 性能测试数据
|------------|----------|---------------------|-----------|-----------|
| 测试场景 | InMemory | Persistence(SQLite) | Redis(本地) | Redis(远程) |
| 单次读取延迟 | ~0.1ms | ~2ms | ~1ms | ~5ms |
| 单次写入延迟 | ~0.1ms | ~5ms | ~1ms | ~5ms |
| 并发能力 | 高(内存锁) | 中等(WAL优化) | 极高 | 高 |
| 数据可靠性 | 进程结束丢失 | 磁盘持久化 | Redis持久化 | Redis持久化 |
| 水平扩展 | ❌ | ❌ | ✅ 集群 | ✅ 集群 |

7.2 场景选型决策树
开始选型
│
├── 开发/测试环境?
│ └── 是 → InMemory(快速、无需配置)
│
├── 单机部署?
│ └── 是 → Persistence
│ ├── 需要SQL查询? → SQLite
│ └── 简单KV存储? → Shelve
│
└── 分布式/生产环境?
└── 是 → Redis
├── 数据量小/预算有限? → 单节点Redis
├── 高可用要求? → Redis Sentinel
└── 海量数据/高并发? → Redis Cluster
7.3 生产环境配置建议
配置1:中小型应用(单机+SQLite)
# 适合:日活<1万,单节点部署
CheckpointerConfig(
type="persistence",
config={
"db_type": "sqlite",
"db_path": "/data/openJiuwen/checkpoints.db",
"db_enable_wal": True, # 必须启用WAL
"db_timeout": 60 # 适当增加超时
}
)
配置2:中大型应用(Redis单节点)
# 适合:日活1-10万,多节点共享会话
CheckpointerConfig(
type="redis",
config={
"connection": {
"url": "redis://:password@redis-host:6379/0",
"connection_args": {
"socket_connect_timeout": 5,
"socket_timeout": 5,
"max_connections": 50
}
},
"ttl": {
"default_ttl": 10080, # 7天过期
"refresh_on_read": True # 活跃会话续期
}
}
)
配置3:大规模应用(Redis Cluster)
# 适合:日活>10万,需要横向扩展
CheckpointerConfig(
type="redis",
config={
"connection": {
"url": "redis+cluster://node1:7000,node2:7000,node3:7000",
"cluster_mode": True,
"connection_args": {
"skip_full_coverage_check": True,
"max_connections_per_node": 20
}
},
"ttl": {
"default_ttl": 4320, # 3天(大数据量缩短TTL)
"refresh_on_read": True
}
}
)
八、总结与展望
8.1 测评总结
openJiuwen v0.1.7的可插拔会话存储机制是一个设计精良、生产就绪的特性:
|-----------|-------|----------------------|
| 维度 | 评分 | 说明 |
| 功能完整性 | ⭐⭐⭐⭐⭐ | 覆盖内存/本地/分布式三种场景 |
| 架构设计 | ⭐⭐⭐⭐⭐ | 分层清晰,双接口设计优雅 |
| 易用性 | ⭐⭐⭐⭐⭐ | 一行配置切换,零代码侵入 |
| 性能 | ⭐⭐⭐⭐ | Redis性能优秀,SQLite有待优化 |
| 可观测性 | ⭐⭐⭐⭐ | 生命周期钩子便于监控 |
8.2 适用场景
强烈推荐使用:
- 需要断点续传的长任务场景
- 多轮对话的客服/助手类应用
- 分布式部署的高可用系统
- 需要人机交互(Human-in-the-loop)的工作流
8.3 核心要点回顾
- 存储 抽象层:Checkpointer + Storage双接口设计,职责分离清晰
- Redis数据结构 :
session:namespace:entity:field四层命名空间,便于管理 - 故障转移:支持Sentinel和Cluster高可用架构,配合监控告警
参考资源
- 项目主页 : https://openJiuwen.com?utm_source=csdn
- Agent Core : https://atomgit.com/openJiuwen/agent-core?utm_source=csdn
- Agent Studio : https://atomgit.com/openJiuwen/agent-studio?utm_source=csdn
- 版本发布 : v0.1.7 Release