一、先说我遇到的问题
去年到今年,我陆陆续续写过几个 Agent 小工具。每次的体验都惊人地一致:
demo 阶段如丝般顺滑,上线阶段如鲠在喉。
跟着任何一个主流框架的 quickstart,二十行代码,一个能调工具、能多轮对话的 Agent 就跑起来了,演示效果很唬人。可一旦我想把它变成一个多人能同时用的服务,问题就接踵而至:
- 两个用户同时发消息,会话状态开始串台------A 的上下文漏进了 B 的回复。
- 我到底该给每个用户一个 Agent 实例,还是共享一个?共享会不会有并发安全问题?独享内存会不会爆?
- 实例什么时候回收?空闲多久算空闲?并发上限怎么设?
- 会话要持久化、进程重启要能恢复,这套又得自己从头搭。
我翻遍了手上几个框架的文档,发现这部分几乎都被当成"留给读者的练习":要么让你去用它收费的托管平台,要么一句"生产部署请参考最佳实践"就把你打发了。
后来我查了一圈资料才确认,这不是我一个人的错觉------Salesforce 专门写过工程博客讲他们的多租户 Agent 平台怎么扛 7000+ 并发会话不串台;2026 年初 Medium / dev.to 上一连串「Multi-Tenant AI Agent Infrastructure」实战文,焦点全是同一件事:会话状态串扰、per-user 隔离、资源池化、向量库的租户边界。
「多用户会话隔离」是真实的生产难题,而大多数框架把它甩给了你。
milu 就是我为了系统性地解决这个问题,顺手把另外几个老问题(国产模型接入、工具安全、全家桶)一起做掉的产物。这篇文章主要讲讲 milu 是怎么填「多用户生产化」这道坑的,以及它的整体设计取舍。
项目地址:github.com/stephonGAO/... (MIT,
pip install -U milu即用,欢迎 star)
二、核心:AgentPool 多用户并发资源池
先看一个事实------Agent 实例是带状态的 。它持有对话历史、会话对象、MCP 连接、工具注册表、甚至"是否已经开始干活"这种运行期标记。这意味着多个用户绝对不能共享同一个 Agent 实例。
那唯一安全的方案就是 per-user Agent 。但"每人一个实例"立刻带来新问题:实例怎么管理、怎么限量、怎么回收。这正是 AgentPool 干的事:
python
from milu import AgentPool, ModelRegistry
# LLM 实例本身协程安全,可以在所有用户间共享(底层是 AsyncOpenAI)
llm = ModelRegistry.create("qwen", model="qwen3.6-plus")
pool = AgentPool.from_llm(llm)
await pool.start()
# 按 (user_id, session_id) 拿到这个用户专属的 Agent,用完自动归还
async with pool.acquire("user-1", "session-A") as h:
async for evt in h.agent.run("你好"):
...
await pool.stop()
AgentPool 守着四个硬不变量,这也是它和"自己写个 dict 缓存 Agent"的本质区别:
- 每个
(user_id, session_id)最多一个实例(隔离的根基); - 实例总数 ≤
max_agents(LRU 淘汰,防内存爆); - 并发执行的 run ≤
max_concurrent_runs(全局信号量限流); - 空闲超过
idle_ttl_seconds的实例被后台清理。
会话、记忆、知识库都按 user_id 自动派生隔离,你不用操心。
真正的难点不在池子,在"无状态化"
这里有个容易被忽略的坑。就算你给每个用户独立 Agent,如果工具内部用了模块级全局变量来存状态,照样串台。比如一个 todo 工具用模块级单例存当前计划,两个用户的 Agent 跑在同一个进程里,计划就会互相覆盖。
milu 的做法是全链路 ContextVar 无状态化 :todo 的计划存储、子代理的事件收集、父级安全模式的继承,全部通过 ContextVar 在 Agent.run() 入口按调用注入,实现 asyncio 任务级隔离。源码里这条约束被反复强调:
新增任何"跨调用共享"的工具状态,一律用 ContextVar,切勿用模块级全局变量。
这块我专门写了并发隔离的测试(test_concurrency_with_pool.py、test_todo_concurrent_isolation.py、test_subagent_concurrent_isolation.py 等),整个项目 1100+ 测试里,这部分是核心保证。
顺带一提,我之所以对"并发崩溃"这么敏感,是因为调研时看到国产框架里有公开的并发翻车案例------某框架的群聊模块多用户同时发消息时 IndexError、Code Interpreter 多用户调用有线程安全瓶颈。这些都是真实发生过的、能在 CSDN 搜到的事故。无状态化不是过度设计,是被现实教育出来的。
三、第二个动机:国产模型不该是二等公民
我日常用的是 DeepSeek 和通义,但很多框架对国产模型的支持是"靠社区零散包"或"自己配 base_url + LiteLLM 转一道"。每接一家,我都要:查它的 base_url、适配它的 thinking 模式开关、处理它内置联网搜索的参数、规避它不支持的字段......然后自己维护一张多厂商路由表。
milu 把 9 家提供商收敛到一个接口,6 家国产模型(通义、DeepSeek、Kimi、GLM、MiniMax、豆包)+ OpenAI / Gemini / Claude,切换就一行:
python
from milu import ModelRegistry, Message, MessageRole
llm = ModelRegistry.create("deepseek") # 换 "kimi" / "glm" / "qwen" ... 同一接口
async for chunk in llm.chat([Message(role=MessageRole.USER, content="你好")]):
print(chunk.content or "", end="", flush=True)
API Key 从 {PROVIDER}_API_KEY 环境变量读,各家的参数差异(thinking、内置搜索、参数过滤)已经逐家适配好。
实现上一个关键决定:所有 provider 统一用 openai.AsyncOpenAI 作为 HTTP 客户端 ,靠不同 base_url + extra_body 适配各家。好处是 AsyncOpenAI 协程安全,所以一个 LLM 实例能在多用户间安全共享------这正好和上面 AgentPool 的设计咬合:共享 LLM、隔离 Agent。
还有个国内特别实在的点:内置 web_search 工具是可插拔后端 ,DDG 国内连不上,配 WEB_SEARCH_PROVIDER=bocha 就能国内直连。这种细节,没在国内踩过坑的框架不会替你想。
四、第三件事:工具执行得有真正的安全模型
Agent 一旦能调 shell_command、file_write、python_repl,"它会不会把我电脑搞坏"就成了真问题。milu 给了四种操作模式:
python
agent = Agent(llm, mode="manual") # 不安全工具等人工审批
agent.set_mode("talk") # 只读:不安全工具一律拦截
| 模式 | 行为 |
|---|---|
talk |
只读,所有不安全工具调用被拦 |
manual |
安全工具直接跑;不安全工具产出确认事件、等审批 |
auto(默认) |
自主决策;不安全调用交 AI 安全判定器裁决(放行 / 转确认 / 拒绝) |
superwork |
全权限,跳过一切检查 |
auto 模式那个 AI 安全判定器 是对齐 Claude Code auto 模式分类器做的:安全工具走快路径零开销,其余不安全调用批量交给一个判定 LLM 三态裁决。而且------子代理会通过 ContextVar 继承父级的模式和确认回调,也就是说你没法靠"委派给子代理"来绕过审批。委派不构成安全旁路,这点我觉得挺重要。
五、剩下的就是"全家桶开箱即用"
到这里 milu 的三个核心动机讲完了:多用户生产化、国产模型一等公民、工具安全。剩下的能力是"既然都做了 Agent,该有的都给齐":
一个 Agent(llm) 默认就是完整体------内置系统提示词、20+ 工具、技能、三个子代理、会话持久化、上下文自动压缩,全都有,传显式参数才覆盖:
python
from milu import Agent, ModelRegistry, TextDelta, AgentDone
agent = Agent(ModelRegistry.create("deepseek"))
async for evt in agent.run("读一下 ./report.pdf 然后总结"):
if isinstance(evt, TextDelta):
print(evt.text, end="", flush=True)
elif isinstance(evt, AgentDone):
print(f"\n[共 {evt.turn_count} 轮]")
清单(都是库内置,不用再装一堆集成包):
- 20+ 内置工具 :文件读写、shell、Python REPL、HTTP、网页正文提取、可插拔搜索、Office/PDF 文档解析、图片视觉输入、日期、结构化输出、todo、长期记忆。
- MCP 协议:stdio / streamable HTTP / SSE 三种传输,而且 MCP 工具用"休眠池"设计------schema 不污染上下文,Agent 按需发现激活。
- RAG 知识库 :分块向量化、余弦检索、来源路由常驻 prompt、可选每轮自动检索,
kb_search/kb_ingest/kb_manage三工具。 - 子代理:内置调研员 / 阅读员 / 编码员三件套,上下文隔离、权限收窄、可并行。
- 技能系统:9 个内置技能,元数据常驻、正文按需加载。
- 定时任务 :多用户 cron 调度,能嵌在
milu chat/milu serve里跑。 - 内置 Web 服务 :
milu serve一行起多用户 SSE 流式对话 + 全功能演示前端(纯 vanilla 无构建链)。
而这一切,一个 pip install -U milu 全都装好,CLI、Web 服务、RAG、MCP 都在核心依赖里。
bash
pip install milu
milu # 首次运行引导你选厂商、填 Key,然后直接聊
milu serve # 一行起多用户 Web 服务,浏览器开 http://127.0.0.1:8000
六、五分钟上手
bash
pip install milu
milu # 跟着引导走,零配置开聊
或者嵌进你自己的代码,三行:
python
from milu import Agent, ModelRegistry
agent = Agent(ModelRegistry.create("deepseek"))
async for event in agent.run("现在几点?用工具查一下"):
...
项目还很新(v0.1.0,star 也刚起步),但 1100+ 测试、中英双语文档、Docker 部署文档都在位。如果这篇文章里有哪个点戳到你了,欢迎去 GitHub 点个 star,或者直接提 issue 拍砖------冷启动阶段,你的每一条反馈我都会认真看。
🦌 GitHub:github.com/stephonGAO/... 📦 PyPI:
pip install -U milu