核心能力
这一页写的不是"支持哪些功能",而是每项能力的真实实现方式与边界:中间件按什么顺序执行、
检索参数默认是多少、技能文件如何投影到沙箱、MCP 会话什么时候建立、命令在哪台机器上跑。
内容全部来自 docs/ 下已对照代码核对过的文档,文末保留"当前做不到什么"一节。
Agent 运行:一条固定的十二层中间件栈
Agent 由 LangGraph create_agent(langchain 1.x)+ deepagents 中间件构建,主循环是
模型推理 → 产生 tool_calls → ToolNode 执行 → 结果回填 → 继续推理,直到无工具调用或触到
recursion_limit(DEFAULT_MAX_EXECUTION_STEPS = 300)。
docs/02-agent-execution-flow.md · agents/context.py#L24
中间件栈(按执行顺序)
顺序即洋葱层的包裹顺序,来自 chatbot/graph.py#L52-L122;"必选性"列写的是该层是否总在此 Agent 的栈里。docs/02-agent-execution-flow.md
| # | 中间件 | 职责 | 必选性 |
|---|---|---|---|
| 1 | SteerMiddleware | 在安全生命周期边界提前结束当前 Run,让位于用户 steer 消息接管(命中则返回 {"jump_to": "end"}) | 必选 |
| 2 | ZhiwuFilesystemMiddleware | 提供 ls / read_file / write_file / edit_file / glob / grep / execute 七个工具,没有 delete;基于 CompositeBackend 隔离 user / thread;工具结果超过 tool_token_limit(默认 3K tokens)触发驱逐到 large_tool_results/ | 必选 |
| 3 | SkillsMiddleware | 技能摘要注入 prompt、读取 SKILL.md 触发激活、按依赖动态放开 tools / mcp | 必选 |
| 4 | ZhiwuMemoryMiddleware | 注入记忆提示,并挂上 remember_memory / search_thread_messages / read_thread_messages | user_config.enable_memory |
| 5 | Subagent task | task 工具派生子智能体;subagent 禁用 present_artifacts / ask_user_question / install_skill,default 模式再禁用敏感工具 | 配置了子智能体时 |
| 6 | ZhiwuSummarizationMiddleware | 继承 deepagents SummarizationMiddleware,超过阈值压缩历史 | 必选 |
| 7 | TodoListMiddleware | 待办规划(write_todos 工具 + TODO_MID_PROMPT) | 必选 |
| 8 | PatchToolCallsMiddleware | deepagents 的工具调用参数纠错 | 必选 |
| 9 | ModelRetryMiddleware | 模型调用失败重试,max_retries 默认 2 | 必选 |
| 10 | ImageInputCompatibilityMiddleware | OpenAI 工具调用里的图片路径转 OCR 解析 | 必选 |
| 11 | TokenUsageMiddleware | 每次主模型调用后写用量快照,按模型分桶 | 必选 |
| 12 | HumanInTheLoopMiddleware | 敏感工具审批,白名单固定为 write_file / edit_file / execute(SENSITIVE_BACKEND_TOOLS);当前 Project workdir 内的写入豁免 | tool_approval_mode |
审批白名单与豁免条件见 agents/tool_approval.py#L13,L36-L57,L116-L121;文件工具七件套无 delete 见 agents/backends/composite.py#L21-L31。
四类记忆:谁负责什么、活多久
◷短期 · Run 内
就是 LangGraph state(ChatBotState,含 subagent_runs 的 merge reducer)加 checkpoint。Run 结束不残留,恢复靠同一 thread_id 的 checkpoint 而不是内存对象。docs/02-agent-execution-flow.md
≡会话摘要
阈值 DEFAULT_SUMMARY_THRESHOLD_K = 100(100K tokens)触发自动摘要,保留最近 DEFAULT_SUMMARY_KEEP_MESSAGES = 10 条,工具结果截断至 DEFAULT_SUMMARY_TOOL_RESULT_TOKEN_LIMIT = 300 tokens;被驱逐的超大会话历史/工具结果写成文件(conversation_history/、large_tool_results/)。常量定义在 agents/context.py#L18-L26。
✦长期 · 用户级
默认关闭。user_config.enable_memory 打开后,第 4 层 Memory 中间件才挂上 remember_memory 写入工具与跨线程检索工具(agents/middlewares/memory.py)。
▤工作区注入
项目级 AGENTS.md 与用户级 USER.md 每次进 system prompt,工作区上下文整体上限 64KB;解析发生在 Worker 构建 ChatBotContext 时,不在模型调用里。agents/context.py
控制能力:Steer、取消、中断、重试
Steer(转向)
新消息以 queue_policy=steer 入队,SteerMiddleware 在安全边界结束当前 Run 让位。边界被精确定义为两个钩子点:
abefore_model:每次模型调用前检查aafter_model:仅当最后一条消息不含 tool_calls 才检查(steer.py#L14-L19)- 因此绝不跳过待执行的工具批次;middleware 全文 40 行,只读 state 判定 + 跳转,没有任何消息截断逻辑,已生成输出靠 checkpointer 保留
取消
双轨下发:Redis 取消信号负责快速通知,PG 的 cancel_requested 状态是权威。Worker 在检查点后收敛终态,取消以 PG 为准(run_worker 三条约定之一)。run_worker.py#L863-L875
中断与审批
HumanInTheLoop 命中敏感工具时把 Run 收敛为 interrupted 终态快照;用户 approve / edit / reject 之后由新的 run_type=resume Run 从 checkpoint 续跑,而不是原地唤醒。docs/02-agent-execution-flow.md
重试的三层落点
- 模型层:
ModelRetryMiddleware,max_retries默认 2 - 任务层:ARQ
max_tries=2、job_timeout = ZHIWU_JOB_TIMEOUT_SECONDS(默认 3600s),Attempt 记retry_released - 启动层:
_worker_startup收敛过期租约、补发任务、恢复定时派发,外加周期 reconcile 循环(run_worker.py#L1642-L1705)
task 工具派生 subagent(run_type=subagent),子智能体禁用集固定为
present_artifacts / ask_user_question / install_skill
(_SUBAGENT_DISABLED_TOOLS,buildin/subagent/graph.py#L30-L48)。
Worker 只开一条 astream_events(version="v3") 流:后台 task 收集"namespace → 子线程路由",
主流逐事件反查后把 namespace 与 thread_id 注入 metadata,
所有子智能体事件都走同一个父 Run 的 Redis Stream / SSE,子 Run 不单独发流;
values 只在根 namespace 才 yield,所以子图快照不污染父线程终态。
agents/base.py#L229-L293thread_id 的子事件会被丢弃;
model_audit / tool_audit 只在根线程记录,子线程不落审计
(services/chat_service.py#L1376-L1410)。前端也不靠 namespace 建树,而是自己复算
child_thread_id = hash_id("subagent_", "{parent_thread_id}:{slug}:{tool_call_id}", 64),
两侧哈希约定必须同步维护。知识库 RAG:离线摄取与在线检索是两条链路
工厂注册三种 KB 实现:MilvusKB(本地全链路)、DifyKB、NotionKB(外部只读连接器)
(knowledge/runtime.py#L7-L14)。入库是异步批处理,所以不适合强实时数据;
每 KB 一个 Milvus collection,KB 数量极大时需要单独评估(implementations/milvus.py#L322-L375)。
docs/03-rag-pipeline.md
离线摄取链路
文件状态机 uploaded → parsing → parsed → indexing → indexed,失败态 error_parsing / error_indexing(knowledge/base.py#L21-L28);parse_file 用 owner fence(processing_task_id / processing_owner 条件更新)防止双 Worker 抢同一文件(base.py#L246-L300)。
Web 上传 / API 上传 / 会话附件;元数据入 PG knowledge_* 表,文件入 MinIO(buckets:knowledgebases / kb-images / public,storage/minio/client.py#L67-L76)。
Worker process_task 驱动 parse_file,Office 走 docling、PDF 走 pypdf、图片与扫描件走 OCR 引擎工厂。
所有格式先转成统一中间表示(knowledge/parser/unified.py),内嵌图片抽取到 kb-images。
引擎版本 ragflow_like_v1(chunking/ragflow/presets.py#L39),6 套 preset 可选,支持分隔符与长度切分、章节层级合并、语义聚类。
模型来自 system_options embed_model,默认 siliconflow-cn:Pro/BAAI/bge-m3(config/options.py#L115),向量维度默认 1024。
每 KB 独立 collection,含稠密向量 + BM25 稀疏字段;chunk 正文与元数据镜像在 PG knowledge_chunks。
LLM 抽取实体与三元组写 Neo4j + 向量索引 + PG 镜像表;这是长任务,ARQ job_timeout 需按量调大(run_worker.py#L1741-L1743 注释)。
⌗解析与 OCR
docling 处理 docx / pptx / xlsx(parser/unified.py#L160-L241),pypdf 处理 pdf;OCR 引擎工厂注册 6 个(parser/factory.py#L49-L55),默认引擎由 system_options default_ocr_engine 控制。
rapid_ocr(默认)mineru_ocrpp_structure_v3_ocrdeepseek_ocrpaddleocr_vl_1_6paddleocr_pp_ocrv6
▦分块 preset
引擎 ragflow_like_v1 提供 6 个 preset(presets.py#L7-L36),入库时按文档类型选择:
general(默认)qa·book·lawssemantic(语义聚类)·separator(按分隔符)
重解析/重索引是文件级动作:POST /api/knowledge/databases/{kb_id}/documents/{parse|parse-pending|index|index-pending} 四个异步端点;collection 级"推倒重建"没有入口,只能靠图谱 graph-build/reset 或删 KB 重灌。knowledge_router.py#L916-L1000
在线检索参数
Agent 工具 query_kb / search(或外部 API)触发 MilvusKB.aquery,参数为 dataclass 定义(implementations/milvus.py#L41-L215,默认合并 #L839-L844)。
| 参数 | 默认 | 说明 |
|---|---|---|
search_mode | vector | vector(ANN 相似度)/ keyword(BM25)/ hybrid(WeightedRanker) |
final_top_k | 10 | 最终返回的 chunk 数,链路末端截断 |
similarity_threshold | 0.2 | 相似度过滤,与 KB / 文档范围过滤叠加 |
vector_weight / bm25_weight | 0.7 / 0.3 | hybrid 模式的加权融合 |
bm25_top_k / bm25_drop_ratio_search | — | BM25 召回量与 term 丢弃率 |
use_reranker + reranker_model | 关;默认 siliconflow-cn:Pro/BAAI/bge-reranker-v2-m3 | 开启后先召回 recall_top_k = 50 再精排(config/options.py#L121) |
use_graph_retrieval | 关 | 配 graph_entity_top_k / graph_triple_top_k / graph_top_k,从 Neo4j 召回实体与三元组并把图上下文并入结果 |
◈GraphRAG
LLM 抽取实体与三元组 → Neo4j 图存储 + MilvusGraphVectorStore 向量索引,PG 侧镜像 knowledge_graph_entities / knowledge_graph_triples / knowledge_graph_mentions 三张表(knowledge/graphs/、storage/postgres/models_knowledge.py)。图谱构建与文档索引互不影响,另有 /api/knowledge/databases/{kb_id}/graph-build/* 六个入口(knowledge_router.py#L461-L600),其中 reset / reconcile 属高危运维动作。
◎评估指标口径
数据集与运行记录在 evaluation_datasets / evaluation_runs 表,接口在 routers/knowledge_eval_router.py。指标计算有几处反直觉但确定的取舍:
- 检索侧实现了 precision / recall / f1,但
calculate_retrieval_metrics(k_values=[1,3,5,10])只输出 recall@k 与 f1@k——precision 算了不上报 - 答案侧
judge_correctness是 LLM judge,二值制 1.0 / 0.0,无部分得分 calculate_overall_score:有标注答案取准确率均值,否则退化为 recall@10 均值- 条目缺
gold_chunk_ids不算检索指标、缺gold_answer不算答案指标(evaluator.py#L88-L95)
knowledge/ 下无 rewrite 模块,
query_kb(kb_id, query_text, file_name) 是单查询直通 retrieve(),不拆分、不并行扩展,
关键词提炼完全交给调用方的模型(grep rewrite|query_expansion|multi_query 在
knowledge/ 与 agents/toolkits/kbs/ 均无命中)。
第二,DifyKB / NotionKB 是只读检索连接器:基类三个类属性
requires_embedding_model = False、supports_documents = False、apply_chunk_defaults = False,
所有写/解析/索引/预览/下载方法直接抛 ValueError(implementations/read_only_connectors.py#L6-L13);
DifyKB 只实现了 aquery,NotionKB 额外实现了 open_file_content / find_file_content,
但后者在 Agent 工具链路上不可达——manager 层 open_document / find_in_document
开头就 _require_kb_supports_documents,必然抛错(manager.py#L1188,L1207,L1219-L1231)。Skills:以 SKILL.md 为载体的指令包
一个 Skill 不是函数,没有 JSON Schema 接口:frontmatter 声明 name / slug / description,
正文是给 Agent 的操作手册,可附 scripts/ 脚本。索引存 PG skills 表,内容存共享文件系统目录
(models_business.py#L363-L365)。docs/04-skills.md
skills 表关键字段:slug(unique,等于目录名)、source_type(builtin / upload / remote)、三类依赖声明 tool_dependencies / mcp_dependencies / skill_dependencies(JSON 列表,由管理侧 API/服务设置,不是解析 frontmatter 得来的,service.py#L737-L790)、dir_path(相对 Skill 数据根目录)、version + content_hash、share_config + enabled。
三级目录:真源 → 共享源根 → 按 uid 投影
buildin/
__init__.py # BUILTIN_SKILLS: list[BuiltinSkillSpec](#L20-L67)
deep-research/SKILL.md
html-preview/SKILL.md
image-gen/SKILL.md
knowledge-base/SKILL.md
mysql-reporter/
SKILL.md
scripts/{list_tables,describe_table,query}.py + _mysql_common.py
ZHIWU_SKILL_DATA_DIR
上传与远程技能的源文件根目录。仓库工作区里对应的 skill-sources/shared/ 不是入库内容,而是启动时由内置包拷出的运行期产物:init_builtin_skills → _replace_skill_target(get_skills_root_dir()/<slug>, spec["source_dir"])(skills/service.py#L1813-L1828),且已被根 .gitignore#L18 排除。
ZHIWU_SKILL_PROJECTION_DIR/<uid>/
按用户授权生成的只读投影,本地对应 skill-projections/(同样不入库),并发控制用 .locks/<uid>.lock advisory lock。沙箱里看到的虚拟路径是 /home/gem/skills/<slug>/SKILL.md,个人技能则是 /home/gem/user-data/agents/skills/<slug>/SKILL.md(backends/paths.py#L10-L28)。
skill-sources/ 与 skill-projections/ 都是运行期产物,
在 git 里看不到它们是正常的;改技能内容要么改内置真源(会随启动同步),要么走管理侧上传/远程安装。投影同步的五步与它的时机
Agent 配置绑定的技能 + 用户可访问技能(按 share_config read scope 过滤)。
拷到临时目录,刻意不使用 symlink——沙箱不应信任宿主侧符号链接。
<uid>.lock 防同一用户并发重刷。
临时目录整体切换到正式投影目录,读者看不到半成品。
任一步失败就清理临时目录并让投影不可用,宁可技能用不了也不降级直读源目录(skills/service.py#L384-L408)。
时机 = Run 构图阶段
sync_agent_context_skills(context) 在 get_graph() 内被调用(chatbot/graph.py#L158、subagent/graph.py#L121),其内部就是 refresh_user_skill_projection_async(uid)(backends/composite.py#L115-L118)。不是每轮模型调用都刷,因此:
- 正在跑的 Run 不会在后续回合重新拉投影,技能更新从下一个 Run 才生效;
- 例外:授权变更时服务端会立即重刷已存在投影的 uid(
apply_skill_projection_policy_change,skills/service.py#L392,L408),但已开跑的 Run 仍按自己那份 CompositeBackend 视图工作。
内置技能 version + content_hash 变更才会重投影;上传/远程技能以 slug 唯一覆盖式管理。
懒激活与工具门控
- prompt 里只放摘要:名称 + 描述注入 system prompt,全文不进上下文(
middlewares/skills.py#L65-L140)。 - 读文件即激活:模型
read_file读取/home/gem/skills/<slug>/SKILL.md(或投影内的个人技能路径)时,slug 写入activated_skills(state reducer 合并)。 - 依赖闭包展开:激活后才把该技能声明的
tools/mcps/skills依赖放开,技能间依赖递归展开走 DFSexpand_skill_closure,并禁止自依赖(service.py#L763)。 - 门控只管可见性:未激活技能的工具对模型不可见,但仍已在 ToolNode 注册(可执行兜底),所以"看不见"不等于"调不动"。
- 预加载:
_preloaded_skill_contents可对小技能直接注入全文,跳过一轮read_file。
内置 5 个技能
| slug | 用途 | 声明依赖(代码真值) | version |
|---|---|---|---|
| deep-research | 深度研究编排:澄清 → 拆解 → 并行子智能体调研 → 对抗核验 → 带引用报告 | tool_dependencies=("web_search",)、skill_dependencies=("html-preview",) | 2026.07.29 |
| html-preview | 以 Markdown html:preview 围栏输出静态 HTML/CSS 可视化 | 无(三类依赖全空) | 2026.07.23 |
| image-gen | 沙箱内文生图(Qwen-Image 兼容接口)并保存到 outputs | tool_dependencies=("present_artifacts",) | 2026.06.02 |
| knowledge-base | 知识库检索 / 打开文档 / 文档内定位 / 思维导图 | tool_dependencies=("list_kbs","query_kb","find_kb_document","open_kb_document","get_mindmap","search_file","download_kb_file") | 2026.06.24 |
| mysql-reporter | MySQL 查询报表 + Charts MCP 可视化,入口含 scripts/query.py --sql --timeout 60 | mcp_dependencies=("mcp-server-chart",) | 2026.06.05 |
来源:agents/skills/buildin/__init__.py#L20-L67(BUILTIN_SKILLS)与各 SKILL.md frontmatter 实测。
init_builtin_skills() 每次启动都会比对 DB 现有值,
不一致就用 repo.update_dependencies(...) / update_builtin_install(...)
回写成代码声明值(skills/service.py#L1839-L1858)——管理界面手改内置技能的依赖或版本,
重启后会被覆盖。只有非内置(上传 / 远程)技能的依赖字段以 DB 为准。远程安装
⌁一次性沙箱里取清单
执行 npx -y skills add <source> --list,thread_id = remote-skill-<uuid>、inherit_env=False(不泄漏宿主环境变量),CLI_TIMEOUT = 300 秒(agents/skills/remote_install.py#L26-L101)。源地址经 remote_skill_source_policy 白名单校验,默认只允许 github.com、modelscope.cn(HTTPS,config/options.py)。
⇄draft prepare → confirm
两段式:#L256-L290
- prepare 只取清单返回给用户确认,不落盘
- confirm 才下载到共享目录并入库
- draft 可以直接放弃,不产生任何写入
install_skill 工具不是 superadmin 专属:它没有角色参数,唯一硬性限制是子智能体运行时直接报错,作用范围限于当前 uid 的个人技能空间(toolkits/buildin/install_skill.py#L81-L92,L201-L214)。门槛由"Agent 是否绑定该工具 + 个人空间隔离"控制。
MCP:只做 Client,且只做 Tools
ZhiWu 在后端 Worker 进程内跑一个 MultiServerMCPClient(langchain-mcp-adapters 0.3.2,agents/mcp/service.py#L16),
把 MCP Server 的 tools 包装成 LangChain 工具并入 Agent ToolNode。它不作为 MCP Server 对外提供服务。
docs/05-mcp.md
⇄Transport:三种支持,两种可配
协议层支持 stdio / sse / streamable_http(models_business.py#L724),但用户创建/更新时强制校验 transport ∈ _USER_CONFIGURABLE_TRANSPORTS = ("sse", "streamable_http"),否则 ValueError(service.py#L36,L428-L429,L482-L483)。
stdio只留给内置服务——启动时自动禁用历史遗留的用户 stdio 配置(service.py#L110-L118),杜绝任意命令执行面- 内置
mcp-server-chart(npx -y @antv/mcp-server-chart);退役的sequentialthinking会在启动时自动删除(service.py#L52) - 内置服务连接字段以代码定义为准,DB 只同步展示字段与
disabled_tools(_SYNCED_MCP_FIELDS,service.py#L54-L96)
◇能力支持矩阵
| 能力 | 状态 | 说明 |
|---|---|---|
| Tools | ✅ | get_mcp_tools 拉取并缓存,模型直接调用 |
| Resources | ❌ | 全仓无 get_resources / read_resource 调用;adapters 0.3.2 库本身提供该 API |
| Prompts | ❌ | 无 get_prompts() / load_mcp_prompt() 调用;同样是"库支持、应用未用" |
工具装载路径只调 client.get_tools()(service.py#L301,L326,L377),能力发现响应也不含 resources/prompts 字段。
工具发现、缓存与会话
- 配置每次现查 DB:
get_mcp_tools(server_slugs, disabled_tools)按 slug 从mcp_servers表读配置,配置以数据库为准(service.py#L250-L355)。 - 工具对象进程内缓存:key =
slug:config_hash,配置变了 hash 就变,缓存自然失效;_mcp_tools_stats统计启用/禁用数供管理端报告。 - session 不是常驻的:缓存的只是
StructuredTool对象(内含连接参数),真正的 MCP session 在每次call_tool时按需创建、调用完立即关闭(adapters 0.3.2tools.py#L461,L466,L586,L590)。因此 stdio 内置 chart 服务每次工具调用都会重新npx拉一个子进程,initialize / tools/list 握手按次发生,跨调用不复用连接、没有连接池。 - 工具名不重命名:只在
tool.metadata["id"]写mcp__{camelCase(slug)}__{camelCase(tool)}作展示用唯一 ID(service.py#L303-L311)。 - 与技能联动:Skill 激活时把其
mcp_dependencies声明的服务工具动态并入可见工具集(middlewares/skills.py)。
错误语义:没有重试、没有熔断
✕发现阶段:吞掉异常返回空列表
get_mcp_tools 分别捕获 ExceptionGroup 与 Exception,只写日志并返回空列表;get_mcp_client 构造失败返回 None,调用方同样拿到空列表(service.py#L188-L190,L338-L343)。失败的外部表现就是"这个 server 没有工具",不阻断 Run。库层与应用层都没有 backoff / retry / 熔断 / 失败计数,所以不可用的 server 每轮组工具都会重新尝试建连。
↺调用阶段:只吞 server 侧 isError
ZhiWu 显式设 tool.handle_tool_error = True(service.py#L312-L313),MCP 执行错误(isError=True)被转成 ToolMessage 错误内容回填给模型,由模型决定重试或绕行。注意 adapters 的语义边界:transport / session 失败与内容转换错误不受该开关控制、始终抛出。超时靠 mcp_servers 表的 timeout / sse_read_timeout 字段透传,Run 级兜底是 ARQ job_timeout。
env(stdio 密钥)与 headers(远程 Bearer Token)在 DB 中明文存储,
MCPServer.to_dict() 原样输出、管理端接口返回明文不脱敏、无尾号打码。
可见范围只靠角色守卫收敛:GET /api/system/mcp-servers 对 admin / superadmin 给全量明文,
普通用户改走手写的 5 字段白名单(name/description/icon/enabled/tags);
单条详情 GET /api/system/mcp-servers/{slug} 是 get_admin_user,普通用户拿不到这两个字段
(mcp_router.py#L79-L125、models_business.py#L750-L772)。
也就是说:任何管理员都能读到明文密钥。同类问题也存在于模型供应商 api_key,改进建议见 docs/11-security.md 第 6 节。安全沙箱:把"执行"这件事搬出宿主进程
Agent 的 execute 与文件工具如果直接在 Worker 进程里跑,等于把任意代码执行、宿主文件读写和环境变量泄露
都放给了一个由模型决定的命令串。ZhiWu 的做法是:Worker 只持有路径守卫与一个 HTTP 客户端,
真正的执行落在每个 runtime scope 一个的独立容器里。
CompositeBackend 与路径守卫
- 文件工具七件套
ls / read_file / write_file / edit_file / glob / grep / execute,没有 delete(agents/backends/composite.py#L21-L31)。 - 读限定 readable roots、写限定 writable roots,且按 user / thread 做隔离;每 Run 独享一个 CompositeBackend 与图实例。
- 模型看到的是虚拟路径前缀
/home/gem/user-data与/home/gem/skills,由 backend 映射到宿主/容器真实路径(agents/backends/paths.py#L10-L28);投影同步刻意不使用符号链接,因为沙箱不应信任宿主侧 symlink。 present_artifacts只接受workdir_path/、/home/gem/user-data/…、/home/gem/skills/…白名单前缀下的沙箱内普通文件(目录与链接拒绝),临时(ephemeral,workdir_path为空)会话直接拒绝(toolkits/buildin/tools.py#L221-L291)。
Provisioner:一个独立的 FastAPI 服务
端口与 backend
Provisioner 自身监听 :8002(PROVISIONER_PUBLIC_URL 默认 http://sandbox-provisioner:8002,app.py#L420),沙箱容器内 HTTP 服务监听 :8080(SANDBOX_CONTAINER_PORT,app.py#L747)。后端由 PROVISIONER_BACKEND 选择,三种:docker / kubernetes / memory(app.py#L2125)。
每个 runtime scope(runtime_scope_id,来自 Run 快照)对应一个沙箱容器;鉴权统一走 require_provisioner_auth Bearer token,且 SANDBOX_PROVISIONER_TOKEN 与其他两项密钥一样必须 ≥ 32 字符、互不相等。
enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest
(app.py#L48-L50),仓库里只有供给它的 Provisioner 代码,
没有该镜像的 Dockerfile——镜像内有什么服务、监听哪些端口,属于上游镜像的事实。本机 Docker
通过 Docker SDK 按 scope 建容器,挂载宿主目录、分配 per-sandbox 网络(DOCKER_NETWORK_PREFIX / DOCKER_ADDRESS_POOL / DOCKER_SUBNET_PREFIX 默认 28,app.py#L79)。删除走并发池 SANDBOX_DELETE_CONCURRENCY 默认 16、容器停止 SANDBOX_CONTAINER_STOP_TIMEOUT_SECONDS 默认 2s(app.py#L368,L384)。前提是 provisioner 自己能访问 docker.sock(见下方风险)。
Kubernetes
每 scope 一个 Pod,用 PVC 承载持久目录:USER_DATA_PVC 默认 zhiwu-user-data(app.py#L1479),volume 挂载点 /home/gem/user-data 对应 subPath shared/<uid>/workspace、/home/gem/skills 对应 skill-projections/<uid>(app.py#L1708-L1709),另有 init container 初始化目录(app.py#L1567)。健康等待窗口 SANDBOX_HEALTH_TIMEOUT_SECONDS 默认 60s(app.py#L1838)。文档把 K8s backend 列为比 docker.sock 更强的隔离选项(docs/11-security.md 第 7 节)。
memory(默认值)
不创建任何容器,只返回已存在沙箱的 URL,用于本机联调。可选 MEMORY_SANDBOX_UID_URLS=uid=http://host:port,… 逐个显式登记每用户专属地址;未配置时所有人共用模板 URL,且 uid 只能含字母数字与 - / _(app.py#L167-L200)。生产不应使用这个 backend。
生命周期端点
详细请求/响应见 API 与 CLI 页;这里只列能力面。全部端点在 docker/sandbox_provisioner/app.py,均需 Bearer token。
| 动作 | 端点 | 行为要点 |
|---|---|---|
| 创建 | POST /api/sandboxes | backend 内部先发现已运行容器再创建,幂等;容量不足 503、参数非法 400(app.py#L2196-L2235) |
| 查询 | GET /api/sandboxes/{id} | discover() 拿状态,未找到返 404;顺带更新 idle 记录 |
| 保活 | POST /api/sandboxes/{id}/touch | Worker 侧按 SANDBOX_KEEPALIVE_INTERVAL_SECONDS(默认 30s,provider.py#L134)周期调用;未被 touch 的沙箱由 idle reaper 回收(SANDBOX_IDLE_TIMEOUT_SECONDS 默认 600,app.py#L2010-L2015) |
| 删除 | DELETE /api/sandboxes/{id} | 对 404 视作"已删除",因此重复删除安全(幂等) |
| 反代 | ANY /api/sandboxes/{id}/proxy/{path} | Agent 的所有文件/命令调用实际走这条通道到容器 :8080;上游超时可配(connect 10s / read 600s / write 600s / pool 10s,app.py#L130-L140),跳带头会被剥离 |
| 运维 | GET /api/sandboxes、POST /api/sandboxes/quiesce、GET /health | 列举、静默(等待在途操作排空)与健康探测 |
create 20s × 最多 4 次、delete 20s × 最多 3 次、退避基数 1s
(provisioner_client.py#L26-L29,env 入口 SANDBOX_PROVISIONER_CREATE_TIMEOUT_SECONDS / _ATTEMPTS 等,
provider.py#L125-L127)。之所以敢重试,是因为 create/delete 在 provisioner 侧都是幂等的;
单次读超时绝不允许设成 None,否则会在 socket recv 上永久挂起并拖死 idle reaper 的排空等待。运行时裁剪与挂载
⌥SANDBOX_RUNTIME_PROFILE
三档 profile 通过往容器注入 DISABLE_* 开关来裁剪镜像内的运行时(app.py#L52-L77,默认 core,app.py#L320):
core:关掉 browser / MCP browser / VNC / Jupyter / Code Server / Node REPL 全部六项browser:只放开 browser、MCP browser 与 VNC,仍关 Jupyter / Code Server / Node REPLfull:六项全开
无效值在启动期直接 RuntimeError,不会静默降级。
⚑bind mount 与 docker.sock
持久挂载根只有 /home/gem/skills、/home/gem/user-data、/home/gem/projects(app.py#L101-L105),docker backend 实际允许的只有前两项(app.py#L916):宿主 UserWorkspace 的 user-data/ 与技能投影 skill-projections/<uid> 被 bind 进容器(app.py#L904,L953-L954,L1290)。
高危权限:docker backend 要求把 /var/run/docker.sock 挂给 provisioner,这等于给该容器宿主 root 级权限——仅限受控主机使用,生产更应选 Kubernetes backend(docs/11-security.md 第 7 节、docker-compose.yml 注释)。远程技能安装用的临时沙箱额外设 inherit_env=False,不继承宿主环境变量。
SANDBOX_EXEC_TIMEOUT_SECONDSSANDBOX_MAX_OUTPUT_BYTESSANDBOX_KEEPALIVE_INTERVAL_SECONDSSANDBOX_IDLE_TIMEOUT_SECONDS超时与输出上限定义在 agents/backends/sandbox/backend.py#L224-L225(超限即按字节截断,#L673-L674),汇总见 docs/08-configuration.md。
已知边界与限制
这一节把本页所有"当前做不到 / 需要自己补 / 容易踩"的条目集中起来。全部是已对照代码确认的事实,不是推测;措辞保持中性,但对运维者有实际影响。
- 审计没有独立表——生命周期审计写进
messages(message_type = model_audit | tool_audit),Tool 审计额外向tool_calls写兼容投影行;36 张表清单里确实没有 audit 表(docs/07-data-model.md已确认结论、docs/10-observability.md)。要接外部审计系统,只能消费messages。 - 日志级别硬编码 DEBUG、不可配置:
setup_logger(name, level="DEBUG", console=True)的默认值就是最终值,全仓无LOG_LEVEL/ZHIWU_LOG_*环境变量,生产与开发一样 DEBUG 落盘、无法降噪;只有httpx/openai/neo4j/urllib3被桥接为 WARNING(docs/10-observability.md、utils/logging_config.py#L59)。 - 登录锁定阈值 5 次 / 300 秒是代码常量,没有环境变量入口(
MAX_LOGIN_FAILED_ATTEMPTS、LOGIN_LOCK_DURATION_SECONDS,models_business.py#L34-L35);仓内也没有管理端解锁端点,只能等过期或改 DB。副作用:锁定期内 JWT 全站返 423,而 API Key 分支提前 return、不受锁定影响(auth_middleware.py#L72-L102)。 /api/agent-invocation/*通道没有限流与配额:与/api/agent/runs一样只挂get_required_user,全仓唯一的节流逻辑在登录链路;非登录路径无任何 429。大规模并发评测不会被"供应商式限流"拦下,只会被ARQ_MAX_JOBS(默认 10)的背压排队(docs/06-api.md已确认结论、docs/14-cli.md)。
按能力线汇总
Agent
- Steer 只在两个钩子点生效,不会打断待执行的工具批次,长命令期间 steer 不生效
- Steer 入队侧限制:只允许
source in {chat, channel}(否则 422),同线程只允许一条 pending steer - 审批只在
write_file / edit_file / execute上,当前 Project workdir 内写入豁免;读类工具与 MCP 工具不进审批 - 子线程不落审计;
run_type=subagent禁用 present/ask/install 三工具 - 步数上限
DEFAULT_MAX_EXECUTION_STEPS = 300、Job 超时默认 3600s,二者都是配置常量而非按 Agent 动态 - 降级不对称:内置 MCP 启动失败不阻断,内置 Skills 同步失败中止启动(
lifespan.py#L61-L212)
RAG
- 无服务端 Query 改写 / 多查询扩展,召回质量完全依赖调用方模型的关键词提炼
- collection 级重建无入口,整体重建只能删 KB 重灌;重解析/重索引只到文件级
- 检索无结果级缓存(未发现实现);每 KB 一个 collection,KB 数量极大时需评估
- 评估只上报 recall@k 与 f1@k(precision 计算但不输出),LLM judge 二值制
- 图谱/评估以本地
knowledge_chunks为输入,只读连接器不产生 chunk,实际"无料可算"(代码层未按 kb_type 拦截) - 入库是异步批处理,不适合强实时数据
Skills
- 技能更新从下一个 Run 才生效(投影同步只在构图阶段)
- 投影同步失败 fail-closed:表现为技能整体不可用,而不是回退读源目录
- 内置技能的依赖与版本每次启动被代码回写,管理界面手改会被覆盖
- 投影五步(拷贝/锁/原子 rename/fail-closed)没有专项单测:全仓
test/下grep refresh_user_skill_projection0 命中 - 远程安装受域名白名单(默认
github.com/modelscope.cn)与 300s CLI 超时约束 - Skill 没有独立重试器,脚本失败只作为工具错误回填给模型
MCP 与沙箱
- Resources / Prompts 未接入;Tools 是唯一使用的 MCP 能力
- 无 backoff / 重试 / 熔断,不可用 server 每轮重建连,失败静默成空工具列表
- stdio 每次工具调用重新
npx拉子进程,冷启动开销按次发生 env/headers(以及模型供应商api_key)DB 明文存储、管理端明文返回docker.sock挂载给 provisioner ≈ 宿主 root 权限,仅限受控主机- 沙箱镜像是第三方
all-in-one-sandbox,不由本仓构建,镜像内容不受本仓版本控制 - 单条命令 180s / 输出 262144 字节截断,长时间任务需自行后台化
docs/11-security.md 已确认结论);
仓库内只有 docker/sandbox_provisioner/ 两套 compose 清单,不存在一条起全栈的 docker compose up,
详见 部署与配置。