31 KiB
设计文档
一个本地运行的个人任务 agent。覆盖三类工作:写汇报 PPT、写科研申报书、写代码。 模型自由(LiteLLM 接 OpenAI-compatible),代码可控(目标 1500-2000 行 Python,自己读得懂)。
1. 边界
做什么
- PPT:文本 / 会议纪要 →
.pptx(用python-pptx) - 科研申报:课题信息 → 分章节
.docx(用python-docx) - 编码:文件编辑、shell 执行、迭代验证
不做什么
- 子 agent / IM 渠道 / 自定义 RAG / 锁定 Anthropic(注:多用户 / Web UI 是 §7 SaaS 化路线,personal-tool 阶段不做)
- Eval Suite:个人工具用 dogfooding 判断模型升级,造作 case 没区分度
关键约束
- 模型自由:LiteLLM 接 OpenAI-compatible 任意 provider(默认 DeepSeek V4)
- 任务持久化:任意时刻关机,下次能恢复
- 演化性:模型升级时 agent 跟着升级,不需要大改架构
- 形态兼容:本地 CLI 与 SaaS 共享同一份 core 和同一种 storage(PG,无 SQLite / JSON 分支);CLI 长期保留(本地直跑 +
--remoteAPI client 双模式),不会被 HTTP API 取代(详 §7.0)
2. 架构
目录树(实际)
zcbot/
├── core/
│ ├── capabilities.py # ModelCapabilities,从 yaml 加载
│ ├── llm.py # LiteLLM 封装,按 capabilities 自动启用 features
│ ├── loop.py # ReAct 主循环
│ ├── probe.py # 真实探测对账 yaml 声称的能力
│ ├── session.py # 消息列表 + meta + 落盘 messages.json
│ ├── skills.py # SkillRegistry (Anthropic 渐进披露格式)
│ └── task.py # TaskState (mode/desc/status/tokens/timestamps)
├── tools/
│ ├── base.py # Tool 基类 + _resolve 路径
│ ├── fs.py # read / write / edit (唯一匹配) / glob / grep
│ ├── shell.py # subprocess + 黑名单
│ ├── run_python.py # tmp .py + subprocess,过滤敏感 env
│ └── skill_tool.py # load_skill
├── skills/
│ ├── coding/ # SKILL.md
│ ├── ppt/ # SKILL.md + references/ + scripts/ + assets/
│ └── proposal/ # SKILL.md
├── prompts/system/
│ └── general_v1.md
├── config/
│ ├── agent.yaml
│ └── models/
│ └── deepseek_v4.yaml # flash + pro 两档
├── workspace/
│ ├── memory/ # 双层记忆 (workspace 级,跨 task 共享)
│ │ ├── core.md # 注 system prompt,常驻
│ │ └── extended/ # 索引(标题+绝对路径)注 prompt,内容靠 read 工具按需拉
│ │ └── *.md
│ └── tasks/<task_id>/ # task_dir:仅 skill 产物,state/messages 在 PG
│ ├── spec_lock.md # skill 阶段一产物 (proposal/ppt)
│ ├── source/ # proposal 用户素材 (PDF / 团队介绍)
│ ├── source.md # ppt 转过的素材
│ ├── sections/ # proposal 逐章 md (01_summary.md ... 12_appendix.md)
│ ├── slides/ # ppt 中间素材 (chart_p?.png)
│ └── <topic>.docx / .pptx # 最终产物
├── main.py # 装配 (build_agent)
└── cli.py # CLI: chat / tasks / probe
task_dir = workspace/tasks/<task_id>/,所有 skill 产物都写到这里。task_dir 绝对路径在 system prompt 里显式给 agent,SKILL.md 的 <task_dir> 占位符指向它。如果 agent 写错位置(写到 cwd / skills/ / repo 根),git status 会立刻报红 —— .gitignore 不再用无锚通配规则盖住污染。
启动时拼装顺序
- 读
config/agent.yaml拿 default_model;ZCBOT_DB_URL环境变量指向 PG(本地 dev 连远端测试 PG 或 docker compose 起的本地 PG;两形态同一种 schema) ModelCapabilities.load("deepseek_v4.flash", config/models/)拿能力档案LLM(caps)构造,从 env 读 API key- 解析 task_dir(新建 or resume)
- 拼 system prompt:
prompts/system/general_v1.md+SkillRegistry.discovery_block()(skill 列表)+ cwd + task_dir 绝对路径(产物根) - 装配工具集(fs / shell / load_skill / run_python)
- 启动 REPL —— 新建路径不预占文件(懒创建,见 §3.6)
3. 核心组件
3.1 主循环(core/loop.py)
ReAct 风格:LLM → 若有 tool_calls 就执行 → 把结果塞回消息列表 → 再调 LLM。无 tool_call 即返回。
- 工具结果对模型截断到 16K 字符,对用户预览 400 字符
- thinking spinner 由后台 daemon 线程每 100ms 刷新文本:
thinking... 1.3s ctx 12,345 tok(累计 token 反映上下文大小) - 每轮 LLM 返回追加 dim 一行
[in N out N t Xs]—— 留痕本轮成本 - assistant 文字走
rich.markdown.Markdown渲染,粗体/列表/表格/代码块正常展示(非流式,整段渲染) max_iterations从 capabilities 读,不同模型不同
3.2 Model Profile(core/capabilities.py + config/models/*.yaml)
核心思想:每个模型一份 yaml 档案,agent 行为按档案动态调整。新模型 5 分钟接入,不改代码。
ModelCapabilities 字段:max/reliable_context、max_output、parallel_tools、tool_calling_quality、thinking_mode、reasoning_effort_levels、code_quality、enable_run_python、max_iterations、optimal_temperature、prompt_caching、extended_thinking、api_base、api_key_env。
LLM.chat 按 capabilities 自动启 parallel_tool_calls / reasoning_effort / Anthropic prompt-caching header。
3.3 Capability Probing(core/probe.py + cli.py probe)
yaml 是手填的,可能错。probe 用真实 LLM 调用对账:
basic_chat:连通性parallel_tools:给两个独立工具,看 single response 是否 ≥2 个 tool_callsthinking_mode:对 declared=True 的模型试 reasoning_effort,看 API 是否接受 + 是否产 reasoning_contentlong_context:needle-in-haystack 简化版(opt-in,默认关)
不修改 yaml,只输出 rich Table 报告。退出码 0/2/3 区分 ok / mismatch / error。显式触发,不进启动路径(每次启动跑会烧 API)。
3.4 工具系统(Hybrid 范式)
两类工具并存:
- JSON tool call(
tools/):read / write / edit / glob / grep / shell / run_python / load_skill —— 处理离散操作 - Code execution(
run_python):tmp.py+ subprocess + 工作目录限制 + 敏感 env 过滤(*API_KEY *TOKEN *SECRET *PASSWORD *PRIVATE_KEY)—— 处理批处理 / 算数据 / 生成文档
关键设计:
edit用 唯一匹配约束(CoreCoder 风格):old_str 必须只出现一次,否则报错。防 LLM 改错地方。- 工具按原子操作切分,不做高级封装。
make_pptx()❌,run_python(code)调python-pptx✅。粒度太粗会接收不到模型升级红利。
3.5 Skill 系统(Anthropic 渐进披露标准)
对齐 Anthropic 2025-12 开放标准,跨平台兼容(Claude Code / Codex CLI / Gemini CLI 都用)。
三层加载:
| 层 | 时机 | 内容 | Token |
|---|---|---|---|
| Discovery | agent 启动 | 仅 name + description,所有 skill 都读 |
几百 |
| Activation | load_skill(name) |
完整 SKILL.md | 1000-5000 |
| Execution | SKILL.md 指 references/xxx |
单个 reference 文件 | 视情况 |
Skill 设计原则:写 WHY+WHAT,不写 Step 1/2/3。让模型自己规划。description 要明确具体——决定模型能否触发。
3.6 Session 与 Task
Session(core/session.py)= 消息列表 + meta,直接 ORM 写 PG messages 表(append-only,jsonb 存 LiteLLM 原样 payload)。
Task(core/task.py)= Session 的上层概念,含 mode / description / status (active/completed/abandoned) / model / reasoning_effort / task_dir / created_at / updated_at / tokens_prompt / tokens_completion。直接 ORM 写 PG tasks 表。
存储:Session / Task → PG;task_dir FS 目录只存 skill 产物(spec_lock / sections / *.docx / *.pptx 等),不再有 state.json / messages.json。每轮 agent.run 后 sync_task_tokens UPDATE 累计 tokens。本地 + SaaS 同一份 schema 和 ORM 实现,无 adapter 抽象层,差别只在 ZCBOT_DB_URL(本地连 docker compose 起的 PG / 远端 dev PG,SaaS 连生产 PG)。
懒创建 —— build_agent 新建分支不立刻 INSERT,Task / Session 在第一条 user 消息触发 Session.append 时才 INSERT;task_dir FS 目录在 skill 第一次落产物时 mkdir(parents=True)。启动 REPL 后立刻 /exit 不留 DB 行 + 不留 FS 目录,跨进程安全。
REPL 内 task 切换 —— /new 开新 task,/resume [last|<id>] 切到已有 task(无参数列最近 10 个表格让用户选),/done /abandon 改状态,/desc 改描述。切走前 _cleanup_if_empty 守门:DB 里该 task 没 messages 行 且 FS task_dir 没产物 → DELETE tasks 行 + rmdir task_dir;任一痕迹存在则保留。
原子性 —— PG INSERT 天然原子,messages / tasks 写入无 0 字节风险。skill 产物(spec_lock.md / sections/*.md 等)仍走 core.session.atomic_write_text(tmp + fsync + replace),避免大文件写一半留半文件。
CLI:chat --mode coding --desc "..." [--resume last|<id>] [--remote <url>];tasks [--status active|completed|abandoned] 列任务。
3.7 双层记忆(core/memory.py)
跨 task 共享的事实(用户偏好 / 项目约定 / 模型 quirk 备忘)放 workspace/memory/,两层切法:
| 层 | 文件 | 加载时机 | 适合内容 |
|---|---|---|---|
| Core | workspace/memory/core.md |
每次 build_agent 拼进 system prompt | 跨任务高频用的精炼事实(几百 token 内) |
| Extended | workspace/memory/extended/*.md |
索引(标题+绝对路径)进 prompt,内容靠 read 工具按需拉 |
大量低频专题(API 速查 / 历史事件) |
system prompt 每次 build_agent 重建,resume 也走 _build_system_prompt 并覆盖 messages[0] —— memory 演化即时生效。代价:resume 时上下文里的 system 段可能和上一轮不一样,但跨轮强一致性不是个人 agent 的痛点,memory 时效性更重要。
memory 文件由人填(也允许 agent 用 write 写)。系统不自动维护 —— 这是和"auto memory"框架的关键差异:事实由用户判断,不由 LLM 自动总结(后者噪音和误判风险高)。
形态兼容 —— memory 永远在 FS,不入 DB:
- 本地形态:
workspace/memory/{core.md, extended/} - SaaS 形态:
<storage_root>/users/<user_id>/memory/{core.md, extended/}(bind mount 进容器)
理由:① memory 本质是"用户笔记",FS 读写 + 编辑器手编是产品语义的一部分,DB 化反而要造一层 UI 让用户改 md;② 跨 task 共享靠"同一 user 看同一份目录"语义自动达成,不需要 schema 设计;③ 不参与 §7.4 表结构,task 删/folder 删都不连带 memory。memory 不分 folder,是 per-user 单一命名空间。
4. 模型路由
默认配置(config/agent.yaml)
default_model: deepseek_v4.flash
设计上的分模式路由(后续要做)思路:
| 模式 | 模型 | 理由 |
|---|---|---|
| 通用 / 编码 / PPT / 提案初稿 | flash | flash SWE-Bench 80.6,够用 |
| 复杂 bug / 提案终稿 | pro + reasoning_effort=max | 关键产出值得花 |
| fallback | claude_4_7.opus | V4 不行时手动切 |
成本量级
| 任务 | flash | pro-max | Claude Opus 4.7 |
|---|---|---|---|
| 修一个 bug(~10 轮) | $0.01 | $0.05 | $0.30 |
| 5 页汇报 PPT | $0.05 | $0.20 | $1.50 |
| 完整申报书 | $0.30 | $1.50 | $10-15 |
99% 任务 flash 够用,关键终稿升级 Pro。
5. 设计哲学
核心原则:Less Scaffolding, More Trust
老 agent 框架(早期 LangChain、AutoGPT)失败的核心:给 LLM 太多脚手架,模型升级后这些脚手架成枷锁。
正确做法:把 LLM 当一个会持续变强的同事对待,告诉它目标,不告诉它步骤。
七条具体原则
- Prompt 用 WHY+WHAT,不用 HOW —— 详细教"应该怎么思考"会降智强模型
- Skill 渐进披露,不写完整流程 —— 对齐 Anthropic 标准
- 工具按原子操作切分,不做高级封装 —— 留出组合空间给模型
- Model Profile 化,不硬编码 —— 新模型 5 分钟接入
- Capability Probing —— yaml 是手填的,跑探测对账实际行为
- 版本化 Prompt ——
prompts/system/active.md软链接(尚未做,等真要切版本时再做) - eval 评估 —— 设计阶段曾认为是关键,落地后判断:个人工具 dogfooding 更有效;已删
借鉴自(简版)
| 来源 | 借鉴 |
|---|---|
| CoreCoder | 主循环简洁实现 + Edit 唯一匹配约束 |
| Anthropic Agent Skills | SKILL.md + 渐进披露标准 |
| nanobot | Workspace + 任务隔离 |
| smolagents | LiteLLM 做模型层 + CodeAct 范式启发 run_python |
6. 风险与取舍
已知风险
| 风险 | 缓解 |
|---|---|
| run_python subprocess 沙盒不够强(本地形态非真隔离) | 限制工作目录 + 敏感 env 过滤;SaaS 形态走 docker exec(§7.6 #6),本地依赖用户对模型生成代码的最终审阅 |
| V4 在某些复杂任务不如 Claude | dogfooding 判断,fallback 手动切 |
| Skill description 不够好 → 触发不准 | 用 Pro 优化 description,实战观察 |
| Long context 退化 | probe --long-context 探测可靠 ceiling,不依赖宣称值 |
| 本地 PG 连接不稳定 / 离线 dogfood | docker compose up -d 一行起本地 PG 兜底;也可连远端 dev / staging PG;CI 用 ephemeral PG container |
取舍说明
为什么用 Hybrid 范式而不是纯 CodeAgent:V4 JSON tool call 已稳定;沙盒成本只在需要时付;兼容 thinking 模式。
为什么用 Anthropic Skill 标准而不是自创:行业标准已成,跨 SDK 兼容;直接拿 Anthropic 现成 skills repo。
为什么不做 subagent:状态管理复杂度爆炸;单 agent + skill 已覆盖 95% 场景。
为什么不做 Eval Suite:DESIGN 旧版按团队/产品场景设计;个人单用户场景里,跑两个真实任务的 dogfooding 比造作 case 信号更强,probe 已覆盖健康检查。
7. SaaS 化(草案,status=design,2026-05-12)
§1-§6 是 本地 dogfood 形态;本节是 SaaS 形态,把 core 包成多用户在线服务。 不引入 platform/core 切分 —— core 就是后端,直接对用户做 auth(原"平台签 JWT、core 验签"多租户方案废弃)。两条形态共享同一份 core,差别只在 CLI 入口 vs HTTP 入口。本节落地前 §1-§6 路线照走,不阻塞 dogfood。
7.0 与本地形态的兼容性
SaaS 化不是"重写"也不是"取代 CLI",而是给同一份 core 加一个 HTTP 入口。落地过程中本地 CLI 必须始终可用。
两条形态共享:
- 同一份
core/(loop / capabilities / skills / memory / storage 接口) - 同一份
tools/(底层 executor 从 subprocess 换 docker exec,接口不变) - 同一份 SKILL.md 和 prompts
两条形态差别:
| 维度 | 本地形态 | SaaS 形态 |
|---|---|---|
| 入口 | cli.py chat ... 直调 core |
HTTP /v1/... + SSE |
| Storage | PG(ZCBOT_DB_URL 指 docker compose / 远端 dev PG) |
PG(ZCBOT_DB_URL 指生产 PG) |
| task_dir 根 | workspace/tasks/<task_id>/(派生,task 私有) |
<storage_root>/users/<user_id>/<task_dir>/(用户给,可共享) |
| Memory | workspace/memory/(FS) |
<storage_root>/users/<user_id>/memory/(仍是 FS) |
| Sandbox | subprocess + env 过滤(非真隔离) | per-task docker exec |
| Auth | 无(单用户 user_id='local') |
OIDC + JWT(user_id) |
CLI 长期双模式:
- 本地直跑:
cli.py chat(默认),直接调 core in-process,直连 PG。适合 dogfood / 调 core 内部状态 - API client:
cli.py chat --remote https://...,走 HTTP /v1,跟前端用户路径一致
两模式共用 cli.py 入口,差别只在 transport 层(in-process call vs HTTP)。dogfood ≡ 真实用户路径只在 --remote 模式下成立;本地直跑模式永久保留(调试 core 内部状态比 HTTP roundtrip 顺手)。
本地 PG 连接 —— ZCBOT_DB_URL 指向 docker compose 起的本地 PG(docker compose up -d 一行起,repo 自带 docker-compose.yml)或远端 dev / staging PG。离线场景靠本地 docker compose 兜底,不靠"零依赖"幻觉。
workspace/ 目录:仅存 skill 产物(spec_lock / sections / *.docx / *.pptx),state / messages 全在 PG。本地 vs SaaS 差别只在 task_dir 根路径,不在 storage 形态。
7.1 心智模型:Folder-centric,task-as-DB-record
参考 Claude Code(cwd 是 anchor,状态存别处)+ OpenAI Assistants(stateful agent service)。
- Folder = 用户的"硬盘",路径
users/<user_id>/<user-defined>/...。能浏览、新建、改名、上传、下载,和本地文件管理器体感一致。folder 没 ID,path 就是标识;改名走 prefix cascade。 - Task = DB 一行,带
task_dir指向 folder(相对 user root)。同 folder 允许多 task,但 task 之间不允许嵌套(no-subtask)。 - Messages = DB 表,append-only,
jsonb存 LiteLLM 原样 payload。 - Skill 运行产物 全落 cwd,不引入 artifacts 表;终稿后 SKILL.md 指示 agent 清中间件。
- Skill 定义 是项目代码,跟部署走,所有用户共享,不入用户 folder。
task_dir 在两形态的对应(§7.0 总览的展开):
- 本地形态:
task_dir = workspace/tasks/<task_id>/(派生,task 私有,无并发写冲突) - SaaS 形态:
task_dir = <storage_root>/users/<user_id>/<user-given-path>/(用户给,可被同 user 多 task 共享)
state / messages 两形态都在 PG,FS 只承担 skill 产物(sections / *.docx / 中间件)。多 task 共享同 folder 时由 §7.8 文件级悲观锁兜底(并发写同名文件冲突早失败,推到模型自纠)。
7.2 资源模型与接口(/v1)
POST /v1/folders 创建
GET /v1/folders 列树
GET /v1/folders/{path} 详情(task 列表 + 文件列表)
PATCH /v1/folders/{path} 改名/移动(prefix cascade)
DELETE /v1/folders/{path} hard cascade(连带 task+messages,前端二确认)
POST /v1/folders/{path}/files 上传(multipart)
GET /v1/folders/{path}/files[/{name}] 列 / 下载
DELETE /v1/folders/{path}/files/{name}
POST /v1/tasks 创建({task_dir, mode, desc, model})
GET /v1/tasks 列(?task_dir= ?status= 过滤)
GET /v1/tasks/{id} 详情
PATCH /v1/tasks/{id} 改 mode/desc/status
DELETE /v1/tasks/{id} 删 task(messages 一起删,不动 cwd 文件)
POST /v1/tasks/{id}/messages 发消息,返回 {run_id}
GET /v1/tasks/{id}/messages 历史(?search= 走 jsonb GIN / tsvector)
GET /v1/tasks/{id}/runs/{run_id}/events SSE 事件流
POST /v1/tasks/{id}/runs/{run_id}/cancel
GET /v1/skills | /v1/models | /v1/usage
POST /v1/probe (admin) 跑 capability probe
SSE 事件:tool_call / tool_result / text (delta) / usage / done,带 run_id。
版本化:/v1 minor 半年内向后兼容,major 6 个月 deprecation。
7.3 认证模型
OIDC / Clerk / 自建邮箱登录,JWT 只带 user_id claim:
Authorization: Bearer <user_jwt>
X-Request-Id: <uuid>
所有 storage/executor 调用 scoped by user_id。无 tenant 层 —— 个人 SaaS 用不上,日后做企业版加 org_id claim 等价隔离。
7.4 存储:Postgres + 本地文件系统
users(user_id uuid pk, email null, password_hash | oidc_subject null, plan null, created_at)
-- 本地形态固定 INSERT 一行 sentinel: user_id = '00000000-0000-0000-0000-000000000000',
-- email / auth / plan 全 NULL;CLI 启动时若不存在则建,tasks 全部 FK 到它
tasks(
task_id uuid pk,
user_id uuid fk,
task_dir text not null, -- 相对 user root,如 "project_a/sub"
mode text, -- coding / proposal / ppt / chat
description text,
status text, -- pending / running / paused / done
model_profile text,
tokens_prompt int default 0,
tokens_completion int default 0,
cost_usd numeric default 0,
created_at timestamptz,
updated_at timestamptz
);
create index on tasks (user_id, task_dir);
messages(
message_id uuid pk,
task_id uuid fk,
idx int not null,
payload jsonb not null, -- LiteLLM dict 原样
tokens_in int, tokens_out int,
created_at timestamptz,
unique (task_id, idx)
);
create index on messages using gin (payload jsonb_path_ops);
-- 对话全文搜按需加 tsvector + GIN(中文起步 simple + pg_trgm)
runs(run_id uuid pk, task_id fk, status, started_at, finished_at, error, tokens_p, tokens_c)
usage_events(id, user_id, task_id uuid, run_id uuid, kind, value, ts)
-- append-only。task_id/run_id 不 FK,task 硬删后审计记录仍存活
No-subtask 校验(create_task 入口):
SELECT 1 FROM tasks
WHERE user_id = ?
AND ( ? LIKE task_dir || '/%' -- new 在已有之下 → 拒
OR task_dir LIKE ? || '/%' ); -- 已有在 new 之下 → 拒
-- 同 task_dir 允许(同 folder 多 task)
Folder rename(改名 old → new,FS rename 成功后跑):
UPDATE tasks
SET task_dir = ? || substring(task_dir from char_length(?) + 1) -- new, old
WHERE user_id = ? AND (task_dir = ? OR task_dir LIKE ? || '/%'); -- old, old
LIKE 用 old/% 而非 old%,避免 project_a 误中 project_a_other。running task 引用该 folder 时禁 rename / delete(后端校验 + UI 禁按钮)。
Folder delete:hard cascade,前端 modal 列影响面("将删 N 个对话、M 条消息、K 个文件")+ 输入 folder 名二确认。
-- 先 DB 后 FS;DB 失败 FS 不动一致;DB 成功 FS 失败由后台 GC 兜底清孤儿目录
DELETE FROM messages
WHERE task_id IN (SELECT task_id FROM tasks
WHERE user_id=? AND (task_dir=? OR task_dir LIKE ?||'/%'));
DELETE FROM tasks
WHERE user_id=? AND (task_dir=? OR task_dir LIKE ?||'/%');
-- 然后 FS 递归删 folder
usage_events 不参与 cascade(审计 append-only)。
文件系统:
<storage_root>/users/<user_id>/
memory/{core.md, extended/} # 跨 task 的 per-user 记忆,不入 DB
project_a/source/ sections/ proposal.docx
project_b/...
本地优先 S3(简化部署 / 低延迟),storage 抽象层留好后续可换 backend。
Storage 实现:单一 PG ORM(本地 + SaaS 共用):
- 一份 schema、一份 ORM(SQLAlchemy)、一份查询代码,无 adapter 抽象层,无 SQL 方言适配,无契约测试
- 本地 dev 连接:
ZCBOT_DB_URL=postgresql://...环境变量;repo 自带docker-compose.yml起本地 PG(零配置)或连远端 dev / staging PG - Schema 演化:alembic 管理 migration,
db/migrations/*.py与代码一同版本化;CLI 启动校验当前 schema 版本,落后报错让用户跑cli db upgrade(本地)或部署管线自动alembic upgrade head(SaaS) - 旧 workspace JSON 一次性迁移:
cli migrate-from-fs --workspace ./workspace把state.json/messages.json导入 PG,完成后 workspace 进只读 archive 模式 - 本地单用户 sentinel:DB init 时若 users 表无 sentinel 行则 INSERT;本地 CLI 所有 tasks 全 FK 到这一行,无 auth 流程,但 schema 与 SaaS 完全一致
- memory 不参与:per-user FS,两形态都不入 DB
7.5 沙盒:Per-task 容器 + Per-run exec
| 选择 | 理由 |
|---|---|
| 每 task 长驻容器 | 起容器 ~300ms 太慢;多轮 tool call 共享划算 |
每 run 一次 docker exec |
exec 级 timeout/资源限制 |
| 空闲 N 分钟回收 | 不浪费,resume 时拉起 |
| bind mount = user root | <storage_root>/users/<user_id>/ → 容器 /workspace;同用户多 task 不互隔(协作方便),跨用户由独立容器实例隔离 |
资源限制:cgroup CPU/mem、磁盘配额、egress allowlist(只放 LLM + PyPI 镜像)、root fs read-only、no-new-privileges、drop ALL caps。
选型:起步 Docker(运维门槛低);流量起来后视情况换 gVisor / Firecracker / e2b。Executor Protocol 抽象后切换成本低。
7.6 Core 代码改造(按依赖顺序)
| # | 项 | 影响文件 | 估时 |
|---|---|---|---|
| 1 | loop.py |
已完成(commit 375bb29) |
— |
| 2 | Storage 落 PG:Session / TaskState 改 SQLAlchemy ORM 写 PG messages / tasks 表(单一实现,无 adapter 抽象);alembic 管 schema migration;cli migrate-from-fs 一次性把现有 workspace JSON 导入;repo 加 docker-compose.yml 起本地 PG 用于 dev |
core/session.py core/task.py 新增 core/storage/ db/migrations/ cli.py::migrate_from_fs cli.py::db_upgrade docker-compose.yml requirements.txt |
3 天 |
| 3 | task_dir 双形态共存:TaskState.task_dir 可显式指定(本地默认 workspace/tasks/<task_id>/,SaaS = 用户给路径);tools/fs.py::_resolve 接受 task_dir 注入;system prompt 注入逻辑两形态共用 |
core/task.py tools/fs.py main.py prompts/system/general_v1.md |
1 天 |
| 4 | Folder API:list / create / rename(cascade + 锁 running task) / delete(hard cascade,前端二确认强校验) / upload / download | 新增 core/folders/ |
2 天 |
| 5 | No-subtask 校验:create_task 入口跑 §7.4 的 SQL |
core/task.py |
0.5 天 |
| 6 | Executor + 沙箱:run_python/shell → Executor.run(...),docker exec 到 per-user/per-task 容器;api_key_env → KeyProvider(运行时注入);本地形态保留 subprocess executor,SaaS 形态走 docker executor |
tools/run_python.py tools/shell.py core/capabilities.py core/llm.py 新增 core/executor/ |
2-3 天 |
| 7 | HTTP /v1:FastAPI + SSE + OIDC | 新增 core/api/ core/auth/ |
4 天 |
| 8 | CLI 双模式:加 transport 层抽象 —— 无 --remote 时走 in-process 直调 core(本地形态);--remote <url> 走 HTTP API client(dogfood ≡ 真实用户路径);不删除本地直跑 |
cli.py 加 core/transport/ |
1.5 天 |
代码量增量:+1000~1500 行(单一 PG 实现比双 adapter 方案省 500-800 行;无契约测试集 / 无方言适配层)。
7.7 分阶段落地
| 阶段 | 范围 | 工作量 | 验收 |
|---|---|---|---|
| A | §7.6 #1 | done | ✅ |
| B | §7.6 #2 #3 #4 #5(Storage 落 PG + task_dir 双形态 + Folder API + no-subtask) | ~1 周 | 本地 CLI 走 PG,messages 进 DB 可全文搜;多 task + folder rename 单测过;migrate-from-fs 跑通 |
| C | §7.6 #6(Executor + sandbox) | 3 天 | 两本地账号互不可见对方 folder,本地 subprocess executor 仍可用 |
| D | §7.6 #7(HTTP /v1 + auth) | 4 天 | curl/Postman 跑通主流程 |
| E | §7.6 #8(CLI transport 双模式) | 1.5 天 | CLI 默认本地直跑保留,--remote 走 HTTP 也跑通 |
| F | 上线打磨(限流 / 监控 / 告警 / HA) | 持续 | SLO 99.5% |
B 阶段一次性切换 —— 切到 PG 后本地与 SaaS 走相同代码路径,无回退、无双轨。dogfood 即生效(messages 进 DB → 全文搜、jsonb 查询立刻可用)。前置:repo 提供 docker-compose.yml,作者本机 docker compose up -d postgres 一行准备好 dev DB。
7.8 已知风险
| 风险 | 缓解 |
|---|---|
| 过早抽象违背 §5 哲学 | B 阶段单一 PG 实现无 adapter 抽象层;C-E 各阶段独立 dogfood 价值,"先有场景再加" |
| 本地 PG 连接 / 离线 dogfood | docker compose up -d 本地起 PG 兜底;也支持连远端 dev / staging PG;CI 用 ephemeral PG container |
| CLI 双模式分叉、本地直跑被忽略 | transport 层抽象统一接口;CI 跑 in-process 和 HTTP 两路径同一组用例 |
/v1 冻死后演化慢 |
minor 半年兼容,major 6 个月 deprecation;/v1internal 实验 |
| Rename 误命中前缀 / 漏改子 task | cascade SQL + 单测覆盖 project_a 不中 project_a_other |
| 运行中 task 被 rename / delete | 后端校验 + UI 禁按钮 |
| 误删 folder 丢对话 | 前端二确认 + 输入 folder 名;真要再加 trash bin(延迟 cascade) |
| DB-then-FS 中断留孤儿目录 | 后台 GC 周期扫 "FS 有但 DB 无引用" 的目录 |
| 同 folder 多 task 并发写同名文件 | 文件级悲观锁,冲突早失败 |
| Sandbox 出站越权 | egress allowlist 起步只放 LLM + PyPI 镜像 |
| 资源滥用(LLM / 存储) | BYO key 默认;月度 token & 存储配额;cold task LRU 清 |
7.9 取舍说明
path-as-identity 而非 folder_id:folder 真实存在于 FS,folder_id 等于造两份 source of truth(易不一致)。rename 是 UI 主动动作,cascade 单事务搞定。
user auth 而非 tenant 层:个人 SaaS 用不上。日后做企业版加 org_id claim,数据隔离规则等价。提前抽象 MVP 多 NULL 一层。
skill 中间件全落 cwd 不引入 artifacts 表:中间件是用户花 token 生成的资产,可下载可替换;artifacts 表 + 分类是为不确定的 UX 收益预付架构成本。真嫌乱 UI 加折叠视图。
hard cascade 而非 soft orphan:orphaned 让 list/resume/UI 都多一种特殊 case,代码长尾;"删 folder = 删项目" 比 "留对话残骸" 自然。usage_events append-only 不 FK,task 硬删后月账仍存活。
Docker + Postgres 起步:运维门槛最低,Executor 抽象层留好,切 microVM / S3 都是 backend 替换不动接口。
本地也用 PG,不用 SQLite / JSON:
- dogfood ≡ 真实用户路径 —— 本地与 SaaS 走相同 SQL 方言、相同事务语义、相同 ORM,bug 在 dogfood 阶段就能复现,不会等到生产
- Docker 已经是必然依赖 —— §7.6 #6 沙盒走 docker exec;装 Docker 是前提,顺手
docker compose up postgres是零增量门槛 - 双 adapter 维护税远高于 PG 一次性配置成本 —— 一份 schema、一份 ORM、一份查询;SaaS 起步即终态,切换成本归零
- 本地 dev 也能连测试服 —— 不强迫本机起 PG,作者可直接连远端 dev / staging PG 跑 dogfood,体感跟连 SaaS 几乎一致
CLI 不被 API 取代,而是双模式共存:本地直跑模式调 core 内部状态比 HTTP roundtrip 顺手;前端用户路径靠 --remote 模式打通。transport 层抽象代价小、长期价值高 —— 删本地直跑省不下多少代码,反而失去最便利的调试入口。离线靠本地 docker compose PG 兜底,不靠"全栈零依赖"幻觉。
Memory 不入 DB:跨 task 共享靠"同一 user 看同一份 FS 目录"的语义自动达成,不需要 schema。md 文件用户直接编辑器改,DB 化反而要造 UI、违反 §3.7 "事实由用户判断" 原则。两形态 memory 行为一致(只是根目录不同),迁移零成本。
为什么 Tasks/Messages 在 PG 但 skill 产物在 FS:tasks / messages 是元数据 + 对话流,需要查询、过滤、全文搜、跨 task 统计 —— 都是 DB 强项,jsonb GIN / pg_trgm 让查询代码不爆炸。skill 产物(*.pptx / *.docx / sections/*.md)是终用户拿走的文件,期望直接在文件管理器看到、用 Office 打开、邮件附件发出去 —— 进 DB 就要做"导出"这一步多余操作,且二进制 BLOB 在 PG 里没 GIN 索引价值。FS 是产物的天然存储,DB 是元数据 / 状态 / 查询索引的天然存储,各司其职。同理 §7.5 沙盒 bind mount = user root,容器里看到的就是用户在 Web UI 里看到的目录,无中间层翻译。
附录:DeepSeek V4 关键事实(2026-04-24)
- V4-Pro:1.6T 总 / 49B 激活,1M context,SWE-Bench 80.6 / Terminal-Bench 67.9 / MCPAtlas 73.6
- V4-Flash:284B 总 / 13B 激活,1M context
- 三种推理模式:non-thinking / thinking / thinking-max
- 价格:输入 ~$0.145/M,输出 ~$1.74/M(约 Claude Opus 1/6 ~ 1/7)
deepseek-chat/deepseek-reasoner在 2026-07-24 后下线 → 必须迁deepseek-v4-flash/deepseek-v4-pro