文章目录
-
- 前言
- [1. 先说说它到底是个啥](#1. 先说说它到底是个啥)
-
- [1.1 多用户](#1.1 多用户)
- [1.2 多智能体](#1.2 多智能体)
- [1.3 多渠道](#1.3 多渠道)
- [1.4 自托管](#1.4 自托管)
- [2. 四条藏在代码里的设计主线](#2. 四条藏在代码里的设计主线)
-
- [2.1 主线一:单进程,不搞外部队列](#2.1 主线一:单进程,不搞外部队列)
- [2.2 主线二:双数据库,谁都别嫌谁](#2.2 主线二:双数据库,谁都别嫌谁)
- [2.3 主线三:依赖只许向内流,不许往外拐](#2.3 主线三:依赖只许向内流,不许往外拐)
- [2.4 主线四:一张库,一个进程,一次重启](#2.4 主线四:一张库,一个进程,一次重启)
- [3. 运行时全景](#3. 运行时全景)
- [4. 一句话从 Web 到大脑,到底走了多远](#4. 一句话从 Web 到大脑,到底走了多远)
- [5. 黑话翻译表](#5. 黑话翻译表)
- [6. 技术装备清单](#6. 技术装备清单)
- [7. 你该从哪开始读](#7. 你该从哪开始读)
-
- [7.1 想看架构设计的](#7.1 想看架构设计的)
- [7.2 想搞集成和二次开发的](#7.2 想搞集成和二次开发的)
- [7.3 关心存储和运维的](#7.3 关心存储和运维的)
- [7.4 只关心安全的](#7.4 只关心安全的)
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
最近翻到一个挺有意思的东西,腾讯开源的,叫 Octop。
说是 AI 助手平台,其实就是一个 Python 包,pip 装完就能跑。
我第一次看到的时候还以为又是那种"装完要配五个环境变量、启动三个容器、最后跑不起来"的玩具。
结果跑起来一看------人家就一个进程,一个端口,数据全在你自己电脑的 ~/.octop/ 里。
行吧,那今天就带大家把这玩意儿的骨架扒一扒。这篇不写具体代码,先画个地图,省得你们跟我上次一样,一头扎进去三天没找着北。
1. 先说说它到底是个啥
一句话:自托管的多用户 AI 助手平台。
注意几个关键词,少一个味道就不对了。
1.1 多用户
不是那种"全家共用一个账号"的多用户,是真·多个用户,JWT 隔离,每个人有自己的会话、自己的专家,但是知识库可以共享。
怎么理解呢?就像公司茶水间------杯子是各人的,但茶叶是公共的。
1.2 多智能体
一个用户可以养好几个 Agent,行话叫"专家"。
不光能各干各的,还能组队,叫 AgentTeams。
你可以理解为:你手下有一个产品、一个前端、一个后端,它们自己开小会,不用你传话。
说到这个我就来气,我以前带团队的时候,产品和后端吵起来还得我居中传话。
人家 Octop 倒好,Agent 之间直接发消息,连我这个"项目经理"都省了。
1.3 多渠道
Web、命令行、飞书、钉钉、QQ、微信、Telegram、Discord、企业微信,再加一个定时任务。
你没看错,九个入口。
一般项目做一个 Web 界面就敢叫"全渠道触达"了,人家是真的把能发消息的地方全接了一遍。
1.4 自托管
划重点------除了要调大模型的 API,其他什么都不用装。
没有 Redis,没有 RabbitMQ,没有 Celery worker,没有那一堆"为了高可用"但你根本用不上的东西。
一个进程,一个端口,完事。
我上次给一个朋友介绍微服务架构,讲了两个小时,他最后问我:"那我本地怎么跑?"
我说:"先起 Redis,再起 MySQL,再起 Nacos,再起网关,再起订单服务,再起用户服务......"
他沉默了一会儿,说:"那我还是用 Excel 吧。"
Octop 就是那种"让你朋友不用回去用 Excel"的架构。
2. 四条藏在代码里的设计主线
读源码最怕什么?
最怕每个文件都写得挺漂亮,但你不知道它们为什么要这么写。
Octop 这边有四条主线,你抓住了,后面看什么都顺。
2.1 主线一:单进程,不搞外部队列
没有 Redis,没有 Celery,没有独立 worker。
所有乱七八糟的入口,都通过一个叫 HarnessProcessor 的东西在进程内汇流。
代价是什么?所有阻塞调用都得塞进 run_in_executor,不然事件循环就被你卡死了。
但好处也实打实------没有跨进程一致性问题,进程里的状态,理论上全可以从数据库重建出来。
这就像你把所有文档都存在一个文件夹里,每天下班拷一份到移动硬盘。
哪怕电脑炸了,换一台新的,把硬盘一插,什么都不耽误。
2.2 主线二:双数据库,谁都别嫌谁
控制面默认跑 SQLite(开了 WAL),也能切 PostgreSQL(用 psycopg3 连接池)。
同一套 Repo 代码,靠在边界把 ? 重写成 %s,再加一个 _CompatRow,把方言差异抹平了。
就像你做了一道菜,湖南人来了多放辣椒,广东人来了少放辣椒,但菜本身是同一盘。
存储还被明确分成三层:Octop 控制面、harness checkpoint、agent memory。
三层各过各的,不许混。
我以前见过一个项目,用户表、日志表、缓存、临时队列全塞在一个库里,最后一张表两千万行,查个列表要三秒。
后来 DBA 去他们公司楼下堵了他们三天。
2.3 主线三:依赖只许向内流,不许往外拐
dashboard/ ──HTTP──► api/ ──► infra/ ──► infra/utils/, octop.config
cli/ ──► launch.py ──► api/ + infra/
规矩是这样的:
infra/不许 importapi/、cli/、launch.py;api/不许 importcli/;infra/db/repos/只准依赖_base和utils。
这些不是建议,是 AGENTS.md 里写死的禁令。
唯一的例外是 launch.py,它能同时看到 infra.server 和 api.app------因为它就是那个把所有人串起来的启动入口。
我就想问一句,有多少项目的 import 是这种画风:
// 底层工具类 import 了顶层业务服务
// 业务服务又 import 了底层工具类
// 最后谁也离不开谁,重构?重构就是重写。
Octop 这套规矩,说白了就是一句话------底层不许知道上层是谁。
就像厨师不需要知道谁点的菜,他只负责做。
2.4 主线四:一张库,一个进程,一次重启
OctopServer.start() 每次都是按依赖顺序,把整棵运行时树重建一遍。
进程里没有那种"关不掉、清不掉、不知道什么时候被塞进来"的隐性状态。
所以它才能搞 replace_services(),把所有单例热重绑到新库上------
因为状态本来就是从库里来的,换个库就等于换了个存档点。
这让我想起我以前维护的一个系统,里面有个全局缓存,初始化的时候从数据库读了一波,之后就再也不刷新了。
每次改完配置,运维小哥都得在群里发一句"重启一下哦",然后盯着屏幕等三分钟。
Octop 这种设计,就相当于告诉你:别在内存里偷偷藏东西,有事儿就去查库。
虽然慢一点,但你睡得着觉。
3. 运行时全景
来,看全家福。
OctopServer
├─ DatabasePool SQLite (WAL) 或 PostgreSQL (psycopg 池)
├─ SharedServices DI 根:paths + config + RepoBundle
├─ ExpertCatalog 启动时扫描 agents/experts/library/
├─ UserManager
│ └─ AgentManager(全局注册表,一个进程一个)
│ └─ HarnessAgentRuntime(每个 agent 一个)
├─ HarnessAgent 智能体运行时(octop-harness / LangGraph)
├─ GlobalProcessor IM / Web / CLI / Cron 统一入口
├─ ChannelManager IM 连接(octop-gateway)
├─ BackendWorkspace 可插拔文件 I/O
├─ Gateway(ChannelManager + WebSocketHub + Cron 触发器)
├─ CronManager(进程级 APScheduler)
├─ TrajectoryService(轨迹投影 + 实时总线)
└─ FastAPI app(uvicorn)
怎么看这棵树?
最上面是数据库,最下面是 FastAPI。
中间一坨,全是干活的。
UserManager 管用户,AgentManager 管所有 Agent 的注册表------一个进程就一个。
每个 Agent 运行的时候有自己的 HarnessAgentRuntime,相当于每个专家有自己的工位。
GlobalProcessor 是所有消息的总入口,不管你是从 Web 发的、从飞书发的、还是定时任务触发的,最后都汇到它这儿。
CronManager 用的是 APScheduler,进程内调度。
TrajectoryService 负责把每次对话的轨迹投影成事件,一边落库一边实时推给前端。
我第一次看这张图的时候,第一反应是:这模块分的,比我衣柜还整齐。
我衣柜是什么样呢------外套和内裤挂在同一根杆上,找袜子得翻三个抽屉。
Octop 这个模块划分,简直就是程序员版的"断舍离"。
4. 一句话从 Web 到大脑,到底走了多远
来,我们把一次对话拆成玻璃盒,一步一步看。
假设你在网页上发了一句话。
用户输入
│
WebSocket /api/agents/{aid}/chat/ws ← 前端那层
▼
api/routers/chat/ws.py → gateway/ws/ws_channel.py
│ 组装 InboundMessage,解析 session_key
▼
GlobalProcessor.iter_turn_chunks(msg)
│ ① 解析用户/locale/session_key
│ ② 处理 slash 命令
│ ③ HITL 分支(需要人确认的那种)
│ ④ 构造 harness request
│ ⑤ 订阅轨迹
▼
AgentManager.stream() → HarnessAgent
│ LangGraph 执行,逐 chunk 产出
▼
TrajectoryService.observe_chunk()
│ 投影成 assistant / tool / user 事件
│ 聚合 token,落库 + 实时发布
▼
WebSocketHub.push_to_thread() → 浏览器渲染
看出门道没有?
从你按下回车,到浏览器打出第一个字,中间过了五个大节点。
最妙的是------如果你换成从飞书发这句话,ws_channel 换成对应的 ChannelManager,iter_turn_chunks 换成 __call__ → project_stream → MessageEvent,下游一模一样。
这就是"统一管线"的意义。
我见过太多项目,Web 一套接口、App 一套接口、小程序一套接口,三套接口背后是三套逻辑,最后改一个 bug 要改三个地方。
测试同学每天的工作就是:这三个端的表现怎么又不一样了?
Octop 这个设计,相当于不管你从哪个门进来,进了大厅之后走的是同一条走廊。
5. 黑话翻译表
读源码最怕一堆术语堆脸上,跟天书似的。
我给你们翻译一下。
| 术语 | 人话 | 主要源码 |
|---|---|---|
| surface | 消息入口:Web / CLI / IM / Cron | infra/gateway/* |
| turn | 一次用户→助手的完整往返 | history/trajectory/* |
| session_key | 全局会话寻址键,格式是 agent:channel:subject:chat_type |
gateway/threads.py:31 |
| thread | session_key 绑定的具体会话线程,带历史元数据 | gateway/threads.py |
| chunk | harness 吐出来的流式事件,可能是 token、可能是工具调用 | gateway/process/stream_project.py |
| trajectory | chunk 投影出来的、能存能回放的事件流 | history/trajectory/* |
| TaskRouter / Processor | 就是 GlobalProcessor,把消息路由到正确的 Agent |
gateway/process/processor.py:135 |
| expert | 面向用户的 Agent,一个 Agent = 一个专家 | infra/agents/* |
| workspace | Agent 的文件空间,后端可以插拔 | infra/backend/* |
特别说一下 session_key。
这玩意儿格式是 agent:channel:subject:chat_type,四个字段用冒号拼起来。
什么意思呢?
就是"哪个专家、从哪个渠道来的、哪个用户、在哪个会话类型里"------四个信息一拼,全局唯一。
我第一次见到这种设计的时候,心里想的是:这不就是把所有维度拼在一个字符串里当主键吗?
但仔细一想,确实好用。
你想找某个人在某个群里跟某个 Agent 的所有对话,直接按 session_key 一查,完事。
不用 join 三张表。
当然代价就是------如果你哪天想加第五个维度,这个 key 的格式就得变,所有存过的历史都得兼容。
这就像你家钥匙,一开始只有一把,开所有门。
等你加了门禁卡、指纹锁、人脸识别,回头看那把钥匙,它已经不代表什么了。
6. 技术装备清单
| 层 | 技术 |
|---|---|
| 语言 | Python 3.12+(asyncio,只有真要阻塞的时候才 run_in_executor) |
| Web | FastAPI + uvicorn;Scalar 文档在 /api/docs(默认关) |
| Agent 运行时 | octop-harness(基于 LangGraph) |
| 渠道桥接 | octop-gateway |
| 记忆 | octop-memory(分层召回 + FTS) |
| 浏览器 | octop-browser(CDP + 持久 profile) |
| 控制面 DB | SQLite(WAL,默认)或 PostgreSQL(psycopg3) |
| 前端 | React 18 + TypeScript + Vite + Ant Design |
| 调度 | APScheduler(AsyncIOScheduler) |
| 包管理 | uv(uv run pytest,禁止裸 pytest) |
| 质量门 | hatchling · ruff · mypy --strict · pytest(跨 Linux/Windows) |
注意几个细节。
Python 3.12+,mypy --strict。
做过 Python 项目的都知道,mypy --strict 是什么概念------
就是你写的每一个函数,参数类型、返回值类型,全都得标清楚,漏一个就给你报错。
很多团队的 mypy 是什么画风?
开了,但 --ignore-missing-imports、--no-strict-optional、--ignore-without-code,能关的全关了。
最后 mypy 跑一下,0 errors,皆大欢喜------其实是啥也没查。
Octop 是真·strict。
这说明什么?说明作者对类型系统是有信仰的。
还有一个,包管理用 uv,而且明令禁止裸 pytest,必须 uv run pytest。
这个细节我特别喜欢。
因为很多 Python 项目的"在我电脑上能跑",本质上是因为你用的是那个虚拟环境的 Python,但你切个终端,用全局 Python 跑,依赖全错。
Octop 直接把这条规则写死------不许用全局的,必须走 uv。
就像餐厅规定:所有菜必须从后厨出,不许服务员自己带食材进来。
7. 你该从哪开始读
最后说一下阅读路径。
不同的人,关心的东西不一样。
7.1 想看架构设计的
按这个顺序:01(组合根)→ 02(运行时)→ 03(管线)→ 04(投影)→ 05(历史)。
这是正序,从怎么组装的,到怎么跑的,到消息怎么流的,到轨迹怎么存的。
7.2 想搞集成和二次开发的
03(管线)→ 10(连接器 / MCP)→ 11(前端)。
你就关心一件事:我怎么把自己的东西接进来。
那从管线看起,看 MCP 怎么接,再看前端怎么调,差不多了。
7.3 关心存储和运维的
06(双库)→ 08(工作区后端)→ 12(定时)。
这三个看完,你就知道数据放哪了、文件怎么存的、定时任务怎么跑的。
7.4 只关心安全的
09,单读。
安全这东西,跟别的模块耦合不多,单独拎出来看就行。
行了,地图就画到这儿。
准备好了,就从下一篇开始,咱们聊聊"单进程架构"这个选择背后,到底是怎么权衡的。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。