自托管 AI Agent 平台的分层结构:无状态 API 进程与租约化的 ARQ Worker 共享同一个 zhiwu 核心库,文件与命令外置到独立沙箱,数据按事务、向量、图、对象、队列分工到五个存储。本页内容与 docs/01-architecture.md、docs/00-overview.md、docs/07-data-model.md、docs/adr/0001 同步。
五层结构,与 docs/diagrams/architecture.mmd 一致。CSS 图没有连线,节点之间的"谁调谁"写在每层底部的走向说明里。前置条件:PostgreSQL schema 已由独立的 storage-migrator 迁移到位,运行进程启动时只校验版本、不改表(utils/lifespan.py)。
webUser → spa(经浏览器)· cliUser → api(带 zhiwukey_ 前缀密钥)。
spa → api:开发期由 Vite、生产期由反向代理把 /api 转发到 :5050(web/vite.config.js)。
api · worker → core:两进程共享同一核心库;api 只 enqueue_job 入队,真正驱动 Agent 图的是 worker(services/run_worker.py)。
core → provisioner → sandboxBox:文件与命令经 CompositeBackend 交给 Provisioner,再由它 create/delete/proxy 到 :8080。
core → pg / redis / milvus / neo4j / minio 与出网调用 llm / langfuse;api →(入队) redis →(消费) worker。
MultiServerMCPClient 拉起,而不在沙箱容器里(agents/mcp/service.py)。用户可配置的远程 MCP 只走 sse / streamable_http。运行面由四个可执行进程加一个被共享的库组成。API 进程保持无状态,长任务全部下沉到 Worker;Provisioner 是独立 HTTP 服务;SPA 只调 /api。
| 进程 / 组件 | 职责 | 端口 | 入口 |
|---|---|---|---|
| API 进程 | REST/SSE、鉴权与两层登录限流、权限校验;写 agent_run_requests + agent_runs(pending) 后仅 enqueue_job,立即返回 201 + run_id;无状态可水平扩。 |
:5050 |
backend/server/src/server/main.py |
| ARQ Worker 进程 | 消费 process_agent_run / process_task,用 owner token 做租约(lease)CAS 抢占并心跳续租,驱动 LangGraph 执行与知识库后台任务。并发与超时:ARQ_MAX_JOBS 默认 10、job_timeout 默认 3600s、max_tries=2。 |
无监听端口(消费 Redis 队列) | backend/server/src/server/worker_main.py |
| zhiwu 核心库 | agents / knowledge / models / services / repositories / storage / config / permissions;被 API 与 Worker 双进程共享,非独立进程。 | — | backend/zhiwu/src/zhiwu/ |
| Sandbox Provisioner | 沙箱容器生命周期:create/delete/touch/quiesce 与 HTTP 反代;backend 可切换 memory / docker / kubernetes;访问 token 要求 ≥ 32 字符。 | :8002(供给的容器 :8080) |
docker/sandbox_provisioner/app.py |
| Web SPA | 工作台 / 管理 / 可视化,统一调 /api/*;开发期 Vite 代理 /api → :5050。 |
:5173 |
web/src/main.js |
CLI 客户端 zhiwu-cli/src/zhiwu_cli/main.py(命令 zhiwu)与 SPA 走同一套 /api,不是第五个常驻服务进程。
从用户消息到终态落库,跨 API 进程、Redis、Worker 与沙箱。Request 是意图,Run 是业务执行,Attempt 是一次占有事实,三张表把"提交—抢占—收敛"拆开。来源:services/run_queue_service.py、services/run_worker.py。
POST /api/agents/{slug}/threads/{thread_id}/runs → 鉴权中间件(Bearer JWT / API Key)→ 资源权限校验(share_config)→ 写 agent_run_requests(request_id 为幂等(idempotent)键)与 agent_runs(pending)。同线程已有活跃 Run 时按 queue_policy 排队 / 拒绝 / steer。
API 只做 enqueue_job(process_agent_run) 投到 Redis/ARQ,返回 201 + run_id,不在请求进程内跑重活(services/run_queue_service.py)。
Worker 用 owner token 做租约 CAS 抢占执行权,创建 agent_run_attempts 并置 runs:running;心跳续租,租约过期的 Attempt 记 lease_expired,Run 可被其他 Worker 接管(services/run_worker.py)。
ChatbotAgent.astream_events v3 跑模型推理 ↔ 工具调用循环:文件 / execute → CompositeBackend → Provisioner → 沙箱 :8080;知识检索 → Milvus hybrid / Neo4j 图谱;外部能力 → MultiServerMCPClient;Skill 激活 → 读投影目录 SKILL.md 放开依赖工具。敏感工具(write_file/edit_file/execute)触发 ToolApproval 中断,前端审批后以 run_type=resume 从 checkpoint 续跑。
ChunkedEventWriter 攒批 XADD 写入 Redis Stream;前端 GET /runs/{run_id}/events(SSE)由 API 进程读 Stream 转发,支持 Last-Event-ID / after_seq 断点续传。
终态以 DB 为准:正常结束 completed,异常 failed,取消以 PG 为准;checkpoint 持久化会话历史。Worker 启动与周期 reconcile 收敛过期租约、补发任务(services/run_worker.py)。
services/run_worker.py)。部分唯一索引 uq_agent_runs_one_active_per_thread 保证同一 (uid, agent_slug, thread) 只有一个非终态 Run,多 Worker 并发消费天然安全。核心库 backend/zhiwu/src/zhiwu/ 按包划分职责。依赖方向自上而下:server 进程壳(不属核心库)只装配路由/鉴权/lifespan,services 编排 agents 与 knowledge,二者向下用 models / storage / repositories,config 被各层读取。
| 包 | 职责 | 关键文件 |
|---|---|---|
| agents | LangGraph Agent 图、12 层中间件栈、工具、Skill / MCP 集成、沙箱后端 | agents/base.py · agents/buildin/chatbot/graph.py |
| knowledge | RAG:工厂 / 解析 / 分块 / 检索 / 图谱 / 评估;实现含 milvus / dify / notion | knowledge/factory.py · knowledge/runtime.py |
| models | chat / embed / rerank 模型适配与供应商管理 | models/(provider) |
| services | Run 队列与执行、对话、知识库后台任务、定时派发、审计、Skill / MCP 服务 | services/run_worker.py · services/run_queue_service.py |
| repositories | SQLAlchemy 仓储层,向上屏蔽表结构 | repositories/ |
| storage | PostgreSQL / Redis / MinIO / Neo4j 客户端与 schema 定义 | storage/postgres/ · storage/minio/client.py |
| config | options / settings:系统配置项入库与 Redis 缓存(TTL 300s,sensitive 不缓存) | config/options.py |
| permissions | RBAC 与资源 scope(share_config) | permissions/resource_permission.py |
扩展点:知识库类型经 KnowledgeBaseFactory 注册(knowledge/runtime.py)、Agent 后端图按 agents/buildin/ 目录 + backend_id 装配,Skill / MCP / 模型供应商均为数据驱动(PG 表),新增实例无需改代码。
PostgreSQL 共 36 张表(25 业务 + 11 知识),另有 LangGraph checkpoint 表用独立 autocommit 连接串。Redis 承担队列与事件,MinIO 存对象,Milvus 存向量,Neo4j 存图,文件系统存工作区与技能投影。来源:storage/postgres/models_business.py、models_knowledge.py、docs/07-data-model.md。
| 分组 | 代表表 | 说明 |
|---|---|---|
| 身份与组织 | users · departments · user_config · api_keys · cli_auth_sessions | argon2 密码、role(superadmin/admin/user)、uid;API Key 以 sha256 hash + HMAC 派生存储;user_config 存记忆与工具审批等 JSON 配置。 |
| Agent 与能力配置 | agents · agent_envs · model_providers · mcp_servers · skills · config_options | slug unique;模型 / 提示词 / 工具 / KB / MCP / Skills 绑定存于 agents 配置 JSON(无 junction 外键表)。 |
| 对话域 | projects · conversations · subagent_threads · messages · conversation_stats · tool_calls · message_feedbacks | thread_id UUID unique;messages 兼作审计表(message_type = model_audit|tool_audit),无独立 audit 表。 |
| 调度与任务 | scheduled_agent_jobs · scheduled_agent_runs · tasks · operation_logs | cron 5 段表达式 + IANA 时区;tasks 为 KB 解析/索引/图谱等后台任务,由 task_registry 派发。 |
| 知识库(11 表) | knowledge_bases · knowledge_files · knowledge_chunks · knowledge_graph_* · evaluation_* | 对外称 document、对内是 knowledge_files;图谱实体/三元组及其 mention 在 PG 有镜像表。 |
| 表 | 角色 | 关键字段 |
|---|---|---|
agent_run_requests | 意图 / 幂等入口 | request_id unique、queue_policy(enqueue/reject/steer)、status(queued/dispatched/cancelled/rejected/failed)、input_payload、dispatched_run_id |
agent_runs | 业务态 / 状态机 | status 7 态、run_type(chat/resume/subagent)、request_id unique、runtime_scope_id、租约字段(owner / expires)、langfuse_trace_id、token_usage JSON |
agent_run_attempts | 占有事实(不可变) | (run_id, attempt_no) unique、worker_id(owner token)、heartbeat / lease、outcome 6 值、error_type / message |
uq_agent_runs_one_active_per_thread:部分唯一索引,(uid, agent_slug, conversation_thread_id) WHERE status 非终态 → 单线程单写者。agent_runs.request_id unique → 幂等;ix_agent_runs_status_lease_expires → 租约回收扫描。uq_agent_run_attempts_run_attempt_no → attempt 事实不重号,终止后不得改写。skills.slug / agents.slug / mcp_servers.slug / conversations.thread_id / users.uid 均 unique。| 存储 | 用途 / 结构 | 来源 |
|---|---|---|
| Redis | ARQ 队列、Run 事件 Stream(XADD + MAXLEN 控制)、取消信号 key、config 缓存(TTL 300s,sensitive 项不缓存)、worker 健康 key。 | services/run_queue_service.py · config/options.py |
| MinIO | 三桶:knowledgebases(文档与解析产物)、kb-images(抽取图片)、public(可公开资源,如头像);对象键按 kb_id / 文档组织。 | storage/minio/client.py |
| Milvus | 每 KB 一个 collection;字段含 chunk 文本、稠密向量(维度默认 1024)、BM25 稀疏向量与元数据;支持 vector / BM25 / hybrid 加权融合检索。 | knowledge/implementations/milvus.py |
| Neo4j | 存知识图谱实体与关系;PG 侧以 knowledge_graph_entities / _triples / _entity_mentions / _triple_mentions 作镜像表。 | storage/neo4j/ · models_knowledge.py |
require_current_schema() 校验;迁移由 python -m zhiwu.storage_migration 执行,且只接受空库或基线 v1(BUSINESS_SCHEMA_VERSION = 1),版本不等于基线直接 RuntimeError —— 升级等于重建库,无历史升级阶梯(storage_migration.py、manager.py)。与 docs/diagrams/deployment.mmd 对应。关键事实:反向代理与完整平台编排不在本仓库——仓库只交付沙箱侧的镜像与 compose 清单,虚线框节点为部署侧提供。
/api 转发部署侧提供 · 仓库未包含浏览器 → nginx → api:TLS 终结与 nginx 配置均不属本仓。
ARQ_MAX_JOBSserver/worker_main.pymigrator 只建库时跑一次(migrator → pg),api / worker 长期运行。
api → pg · redis · minio(并 SSE 读 redis);worker → pg · redis · minio · milvus · neo4j。
PROVISIONER_BACKEND = docker | kubernetes | memorydocker/sandbox_provisionerworker →(创建/删除/touch/反代 :8002) prov → sbx;prov 用 docker backend 时经 docker.sock 起容器。
prov 与 sbx 对 user-data / skill-projections bind mount 同源;api 直读 user-data 提供工作区 / 工作台文件视图。
worker → ext_llm · ext_mcp · ext_langfuse;只有执行侧直连这三者。
docker-compose.yml(git ls-files docker 已确认),docker/nginx/ 与 k8s/ 为空目录且未被 git 跟踪,全仓无 nginx / caddy / traefik 与 443 / TLS 听口定义。因此 api / worker / web / storage-migrator / 反向代理均无仓内定义,下图服务名是从代码引用推断的。docker/sandbox_provisioner/docker-compose.yml(仅 1 个 service provisioner,zhiwu-sandbox-provisioner:local,宿主端口 8002)与 docker-compose.k8s.yml(同 service 换 kubernetes backend)。web/vite.config.js 默认 ^/api → http://api:5050、^/minio/public/ → http://minio:9000、REDIS_URL=redis://redis:6379/0。backend/zhiwu/src/zhiwu/storage_migration.py),缺的只是它的容器镜像与 Job 清单。docker/sandbox_provisioner/test/k8s-infra.yaml:Namespace zhiwu-know + 两个静态 PV/PVC(zhiwu-user-data、zhiwu-skills,storageClassName: ""),PVC 名须与 USER_DATA_PVC / SKILLS_PVC 对齐。摘自 docs/adr/0001-architecture-overview.md。该 ADR 由代码逆向归纳,非原始决策记录;下列"为什么 / 代价"部分动机需访谈补充。
决定:API(FastAPI)与执行(ARQ Worker)分开,共享 zhiwu 核心库。
为什么:Agent Run 是长任务(默认超时 3600s),不能阻塞请求进程;队列化后 API 无状态可水平扩,Worker 靠租约并发安全扩。
代价:事件回传跨进程,选 Redis Stream + SSE 轮询桥接而非进程内直推。server/main.py · server/worker_main.py
决定:requests(意图)/ runs(业务态)/ attempts(执行事实)三表。
为什么:幂等提交(request_id unique)、单线程单写者(部分唯一索引)、执行历史不可变审计;租约 CAS + 心跳实现崩溃接管。
代价:三表 + 租约字段 + 唯一索引的组合复杂度。models_business.py · services/run_worker.py
决定:Agent 框架用 LangGraph + langchain create_agent + deepagents 中间件。
为什么:需要 checkpoint 持久恢复(审批中断 resume)、成熟中间件生态、astream_events v3 细粒度流事件。
代价:12 层中间件栈顺序成为强约束,定制需理解拦截语义。agents/buildin/chatbot/graph.py
决定:沙箱以独立 HTTP 服务 Provisioner 供给。
为什么:Agent 生成的代码不可信;独立服务可切 backend(memory/docker/kubernetes)、集中管容器生命周期。
代价:docker backend 需挂 /var/run/docker.sock(高权限),K8s backend 为更强隔离路径。docker/sandbox_provisioner/app.py
决定:文件系统内容 + PG 索引 + 按 uid 只读投影。
为什么:技能正文大且被沙箱读取,不宜入 DB;投影隔离越权访问,禁符号链接 + 原子 rename + fail-closed。
代价:同步时机与版本哈希复杂;投影失败时技能降级不可用。agents/skills/service.py
决定:只信任内置 stdio,用户侧限定远程 transport。
为什么:stdio 等于任意命令执行;启动迁移强制禁用历史用户 stdio 配置。
代价:用户无法接入本地 stdio MCP 服务,攻击面收敛到 sse / streamable_http。agents/mcp/service.py
运行时的硬限制来自 docs/00-overview.md §5,交付面与运维面的边界来自各文档的"已确认结论"。以下是刻意不做或受限之处。
SANDBOX_PROVIDER=provisioner(agents/backends/sandbox/provider.py)。sse / streamable_http,stdio 只保留给内置服务(agents/mcp/service.py)。github.com、modelscope.cn(config/options.py remote_skill_source_policy)。delete(删除需审批与审计设计,暂不开放)(agents/backends/composite.py)。agents/skills/service.py)。docs/adr/0001 后果 · docs/07-data-model.md)。all-in-one-sandbox 镜像内预装运行时清单、以及 ADR 各决策的原始动机与当时对比过的备选项。