Agent多用户并发下 CURRENT_USER_ID 竞态问题

## 1. 问题背景

by 于成季 ·
知与行 CloudBase

1. 问题背景

神秘人 Agent 部署在 CloudBase 云托管(容器模式),同一容器进程处理多用户并发请求。Agent 通过全局变量 CURRENT_USER_IDtools.py)和 USER_IDagent/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 — 核心:引入 ContextVar
  • tools.py — 移除全局变量,改用 ContextVar
  • agent/__init__.py — 导出更新
  • agent/nodes/execute_tools.py — 读取方式更新
  • agent/nodes/persistence.py — 读取方式更新
← 所有文章