Agent多用户并发下 CURRENT_USER_ID 竞态问题
## 1. 问题背景
by 于成季
·
知与行
CloudBase
1. 问题背景
神秘人 Agent 部署在 CloudBase 云托管(容器模式),同一容器进程处理多用户并发请求。Agent 通过全局变量 CURRENT_USER_ID(tools.py)和 USER_ID(agent/runtime.py)在工具执行时获取当前用户身份。
当两个用户请求同时到达同一进程时:
请求A: set_user("user_a") → 写 CURRENT_USER_ID = "user_a"
请求B: set_user("user_b") → 写 CURRENT_USER_ID = "user_b"
请求A: tools.create_plan() → 读 CURRENT_USER_ID = "user_b" ← 错误!
CURRENT_USER_ID 是模块级全局变量,不具备任何并发隔离能力。用户 A 的请求可能执行了用户 B 的操作,导致数据跨用户污染。
2. 问题列表
2.1 多用户并发下 CURRENT_USER_ID 竞态
根因:
- why1: 为什么工具执行时拿到错误的用户 ID?因为
CURRENT_USER_ID全局变量在请求 A 写入后、工具执行前,被请求 B 的写入覆盖了 - why2: 为什么会被请求 B 覆盖?因为
CURRENT_USER_ID是进程级全局变量,没有任何隔离机制 - why3: 为什么使用全局变量传递用户 ID?因为早期设计时未考虑并发场景,直接复用了模块级变量作为"上下文"
方案:
短期方案:引入
threading.Lock,在set_user_id()和get_user_id()调用处加锁保护- 优点:改动最小,改动点集中
- 缺点:所有请求串行化执行,并发能力退化;Lock 无法防护
asyncio协程切换
长远方案:使用
contextvars.ContextVar实现协程级别的上下文本地变量- 优点:asyncio 任务之间完全隔离,不影响并发性能;Python 标准库,无需引入新依赖
- 缺点:需要改造所有读取/写入
user_id的代码路径
采纳方案:
长远方案 —— contextvars.ContextVar。
补充用例:
场景1(修复后):请求A设置 ctx_uid="user_a",请求B设置 ctx_uid="user_b",两者互不影响
场景2(修复后):asyncio.create_task() 产生的子协程继承父协程的 ctx_uid,行为正确
场景3(修复前):请求A的工具在执行中途,请求B修改了 CURRENT_USER_ID,导致脏读
3. 小结
通过 ContextVar 替代全局变量,实现每个 asyncio 协程拥有独立的用户 ID 上下文。请求入口(app.py 的 _confirm_request_preprocessor)调用 set_current_user(user_id) 设置上下文,工具执行时通过 get_user_id() 读取,始终与请求绑定,不再有任何跨请求污染风险。
涉及改动文件:
agent/runtime.py— 核心:引入ContextVartools.py— 移除全局变量,改用ContextVaragent/__init__.py— 导出更新agent/nodes/execute_tools.py— 读取方式更新agent/nodes/persistence.py— 读取方式更新