☰
手搓OpenClaw:SessionManager 会话管理模块拆解与 TaoToken 接入实践
2026/10/12 15:24:06 网站建设 项目流程

1. 为什么 SessionManager 是 OpenClaw 手搓路上第一个必须啃的模块

如果你正在搜 OpenClaw SessionManager 会话管理 怎么实现,大概率已经翻过官方仓库,被 Gateway、Agent、Tools、Sessions、Channels 这一堆模块绕晕了。我一开始也是这个状态:想跑一个最小可用的 Agent Loop,结果发现光是搞清楚一条消息从输入到回复经过哪些层,就花了大半天。所以这个系列我打算换个思路,用 Python 从零手搓一个简化版 OpenClaw,第一篇就死磕 SessionManager。

先说清楚它是什么。SessionManager 是 OpenClaw 里负责会话生命周期的那一层,管三件事:会话的创建与复用、同一会话内的并发串行化、全局并发限流。能做什么?它保证同一个 session_id 的两条消息不会交错写入上下文,避免回复串台、工具调用顺序错乱。适合谁?适合正在做 Agent 应用、多渠道机器人、或者任何需要多轮对话隔离的开发者。

为什么第一篇就做它,而不是先接大模型?因为真实系统里,先崩的往往不是模型调用,而是并发。你想想,同一个用户快速发两条消息,如果两条都进了同一个 Agent 实例,历史消息交错写入,模型拿到的上下文就是乱的,回复自然串台。这个问题在单机测试时几乎不会暴露,一上量就炸。所以骨架阶段就把这层打好,后面做记忆系统、多渠道接入才有地基。

这篇的目标很明确:用 FastAPI 搭一个能跑的服务,实现最小 /v1/chat 接口,具备会话隔离与并发控制,然后把模型调用 endpoint 改到 TaoToken 统一通道,最后完整验证一次会话创建、续接、过期的动作。全程可复制,不玩玄学。

2. TaoToken 前置准备:统一模型通道与 Key 获取

在动手写 SessionManager 之前,先把模型通道这件事定下来。手搓 OpenClaw 的过程中,Provider 抽象是绕不开的,而 TaoToken 提供的 OpenAI Compatible 接口正好可以作为一个统一入口,让你不用为每个模型厂商写一套适配。它的官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。

你需要先拿到一个 API Key。操作路径是:登录后进入控制台,找到 API Keys 页面创建一个新 Key。这个 Key 后面会写进 .env,作为 OPENAI_API_KEY 使用。注意,TaoToken 的接口是 OpenAI 兼容的,所以你的 Provider 代码里 base_url 填 https://taotoken.net/api 即可,不需要改任何请求结构。

这里有个细节要提醒:很多人在配 base_url 时会多写一个 /v1,导致路径变成 /v1/v1/chat/completions。TaoToken 的 API 根地址就是 https://taotoken.net/api ,SDK 内部会自己拼 /v1/chat/completions,所以你填根地址就行。我试过填错一次,报的是 404,排查了半天才发现是路径重复。

另外,如果你后面打算长期跑编码类 Agent,可以关注一下 Coding Plan,它更适合高频调用场景;如果只是想先验证模型能不能通,用模型对话页面手动发一条消息最快。这两个入口分别在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 和 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

把 Key 拿到手之后,先别急着写代码,我们先把项目骨架和配置片段准备好,这样后面接入时直接替换即可。

3. 可复制配置:项目结构、.env 与 SessionManager 核心代码

这一节是整篇的核心,所有片段都可以直接复制。先看项目结构,我按最小闭环来组织:

openclaw_py/ ├── app/ │ ├── main.py │ ├── config.py │ ├── api/ │ │ └── routes_chat.py │ ├── session/ │ │ └── manager.py │ └── core/ │ ├── agent.py │ └── provider.py ├── .env ├── requirements.txt └── run.py

先写 .env,把 TaoToken 的通道配进去:

PORT=7788 MAX_CONCURRENT_AGENTS=4 OPENAI_API_KEY=你的TaoTokenKey OPENAI_BASE_URL=https://taotoken.net/api OPENAI_MODEL=gpt-4o-mini SESSION_TTL_SECONDS=1800

然后是 config.py,用 pydantic-settings 读取:

from pydantic_settings import BaseSettings class Settings(BaseSettings): port: int = 7788 max_concurrent_agents: int = 4 openai_api_key: str = "" openai_base_url: str = "https://taotoken.net/api" openai_model: str = "gpt-4o-mini" session_ttl_seconds: int = 1800 class Config: env_file = ".env" settings = Settings()

接下来是 SessionManager 本体。相比 excerpt 里的版本,我加了 TTL 过期和 last_active 记录,这样会话生命周期才完整:

from __future__ import annotations import asyncio import time from contextlib import asynccontextmanager from typing import AsyncIterator from app.config import settings from app.core.agent import Agent class SessionManager: def __init__(self, max_concurrent: int = 4, ttl: int = 1800) -> None: self._sessions: dict[str, Agent] = {} self._locks: dict[str, asyncio.Lock] = {} self._last_active: dict[str, float] = {} self._semaphore = asyncio.Semaphore(max_concurrent) self._ttl = ttl def get_or_create(self, session_id: str) -> Agent: if session_id not in self._sessions: self._sessions[session_id] = Agent(session_id=session_id) self._last_active[session_id] = time.time() return self._sessions[session_id] def get_lock(self, session_id: str) -> asyncio.Lock: if session_id not in self._locks: self._locks[session_id] = asyncio.Lock() return self._locks[session_id] @asynccontextmanager async def acquire(self, session_id: str) -> AsyncIterator[None]: lock = self.get_lock(session_id) async with lock: async with self._semaphore: yield def is_expired(self, session_id: str) -> bool: last = self._last_active.get(session_id) if last is None: return True return (time.time() - last) > self._ttl def drop(self, session_id: str) -> None: self._sessions.pop(session_id, None) self._locks.pop(session_id, None) self._last_active.pop(session_id, None) def __len__(self) -> int: return len(self._sessions) _session_manager = SessionManager( max_concurrent=settings.max_concurrent_agents, ttl=settings.session_ttl_seconds, ) def get_session_manager() -> SessionManager: return _session_manager

Provider 层用 OpenAI SDK 指向 TaoToken:

from openai import AsyncOpenAI from app.config import settings client = AsyncOpenAI( api_key=settings.openai_api_key, base_url=settings.openai_base_url, ) async def chat_completion(messages: list[dict]) -> str: resp = await client.chat.completions.create( model=settings.openai_model, messages=messages, ) return resp.choices[0].message.content

Agent 里维护自己的历史消息,这样会话隔离才真正生效:

class Agent: def __init__(self, session_id: str) -> None: self.session_id = session_id self.history: list[dict] = [] async def run(self, message: str) -> str: self.history.append({"role": "user", "content": message}) from app.core.provider import chat_completion reply = await chat_completion(self.history) self.history.append({"role": "assistant", "content": reply}) return reply

路由层把 SessionManager 串起来:

from fastapi import APIRouter from pydantic import BaseModel from app.session.manager import get_session_manager router = APIRouter() class ChatRequest(BaseModel): session_id: str message: str @router.post("/v1/chat") async def chat(req: ChatRequest): sm = get_session_manager() if sm.is_expired(req.session_id): sm.drop(req.session_id) agent = sm.get_or_create(req.session_id) async with sm.acquire(req.session_id): reply = await agent.run(req.message) return {"session_id": req.session_id, "reply": reply}

这套配置下来,Base URL、Key、Model ID 三件套齐全,SessionManager 的创建、复用、过期、并发控制也都在了。requirements.txt 里记得加上 fastapi、uvicorn、openai、pydantic-settings、python-dotenv。

4. 验证请求:会话创建、续接与过期的一次完整动作

代码写完,跑起来验证。先启动服务:

python -m venv .venv .venv\Scripts\activate pip install -r requirements.txt python run.py

服务起来后,健康检查打一下 http://127.0.0.1:7788/health ,返回 ok 就说明 FastAPI 正常。然后做第一次会话创建,用 PowerShell:

curl -X POST "http://127.0.0.1:7788/v1/chat" ^ -H "Content-Type: application/json" ^ -d "{\"session_id\":\"demo-001\",\"message\":\"你好,我叫小明\"}"

预期返回类似:

{"session_id":"demo-001","reply":"你好小明,有什么可以帮你?"}

这一步验证的是会话创建 + 首次模型调用。接着做续接,用同一个 session_id 再发一条:

curl -X POST "http://127.0.0.1:7788/v1/chat" ^ -H "Content-Type: application/json" ^ -d "{\"session_id\":\"demo-001\",\"message\":\"我叫什么名字?\"}"

如果 Agent 的 history 正确维护,模型应该能答出「你叫小明」。这一步验证的是会话续接,也就是同一 session_id 复用了同一个 Agent 实例,历史消息没有丢。如果这里答不出来,说明你的 get_or_create 每次都在新建 Agent,历史没接上。

最后验证过期。把 .env 里的 SESSION_TTL_SECONDS 临时改成 5,重启服务,发一条消息后等 6 秒,再发第二条。因为 is_expired 返回 True,SessionManager 会先 drop 掉旧会话,再新建一个,所以模型不会再记得之前的名字。这一步验证的是会话生命周期里的过期清理逻辑。

并发验证也顺手做一下:开两个终端,同时用同一个 session_id 发消息,观察返回顺序。因为同一会话有 asyncio.Lock,两条消息会串行处理,不会交错。你可以把 Agent.run 里加一行 sleep 模拟慢调用,效果更明显。

5. 本篇常见报错排查:401、local proxy failed 与 reading choices

接入过程中最容易撞的几个错,我按真实报错对照着说。

第一个是 401 Unauthorized。报错长这样:

openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}

原因基本是 .env 里的 OPENAI_API_KEY 没读到,或者 Key 复制时带了空格。排查顺序:先确认 config.py 里 env_file 路径对,再打印 settings.openai_api_key 看是不是空字符串。如果是空,说明 pydantic-settings 没加载到 .env,检查文件名是不是 .env 而不是 env。

第二个是 local proxy failed 或 connection error。报错类似:

openai.APIConnectionError: Connection error.

这个通常是 base_url 写错,或者本机网络环境有干扰。先确认 OPENAI_BASE_URL 是 https://taotoken.net/api ,不要带多余路径。如果还是连不上,用 curl 直接打一下 https://taotoken.net/api/v1/models 看能不能通,排除是代码问题还是网络问题。

第三个是 reading choices 相关报错:

TypeError: 'NoneType' object is not subscriptable

出现在 resp.choices[0] 这一行。原因是模型返回体里 choices 为空,常见于请求参数不合法,比如 model 名写错、messages 格式不对。检查 OPENAI_MODEL 是不是有效模型 ID,messages 里每条是不是都有 role 和 content。还有一种情况是超时后返回了空体,可以在 provider 里加 try/except 打印原始 resp。

第四个是 OAuth 相关报错,如果你用的是某些需要 OAuth 的客户端,可能会看到 token 过期提示。TaoToken 的 API Key 模式不走 OAuth,所以如果你在代码里混用了 OAuth 流程,直接换成 api_key 参数即可。

排障时建议把日志级别调到 DEBUG,openai SDK 会打印请求 URL 和状态码,定位很快。如果上面这些都没解决,去接入文档页面翻一下最新说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

6. 下一步:把 SessionManager 接进真实 Agent Loop

到这里,一个能跑、能隔离、能过期的 SessionManager 就成型了。它现在管的是会话创建、复用、TTL 和并发,模型调用走的是 TaoToken 统一通道。下一步自然是把 Agent Loop 补全:工具调用、多轮推理、记忆系统。到那时,SessionManager 就是这些能力的挂载点——每个会话一个 Agent,每个 Agent 有自己的 history 和工具上下文。

如果你打算继续跟这个系列,建议现在就把 Key 和配置固定下来,后面每篇都在这个骨架上加东西。API Keys 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,需要长期跑编码 Agent 的话 Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。先把这一篇的代码跑通,下一篇我们接真实 Provider 的超时、重试和错误映射。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询