zcbot/DESIGN.md

20 KiB

设计文档

本地运行的个人任务 agent,覆盖三类工作:汇报 PPT、科研申报书、代码。 模型自由(LiteLLM 接 OpenAI-compatible),代码可控(目标 1500-2000 行 Python)。


1. 边界

:PPT(python-pptx)/ 申报书(python-docx)/ 编码(读写文件 + shell + 迭代验证)。 不做:子 agent / IM 渠道 / 自定义 RAG / 锁定 Anthropic / Eval Suite(个人工具 dogfooding 替代)。多用户 / Web UI 归 §7。

关键约束:

  • 模型自由:LiteLLM + OpenAI-compatible(默认 DeepSeek V4)
  • 任务持久化:任意时刻关机,下次能恢复
  • 演化性:模型升级不需要大改架构
  • 形态兼容:本地 CLI 与 SaaS 共享同一份 core 和 storage(PG,无 SQLite / JSON 分支);CLI 长期保留(本地直跑 + --remote API client 双模式)

2. 架构

zcbot/
├── core/
│   ├── capabilities.py     # ModelCapabilities,从 yaml 加载
│   ├── llm.py              # LiteLLM 封装,按 capabilities 自动启 features
│   ├── loop.py             # ReAct 主循环
│   ├── probe.py            # 真实探测对账 yaml 声称的能力
│   ├── session.py          # 消息列表 + meta + 落盘
│   ├── skills.py           # SkillRegistry (Anthropic 渐进披露)
│   └── task.py             # TaskState
├── 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,ppt,proposal}/   # SKILL.md + references / scripts / assets
├── prompts/system/general_v1.md
├── config/{agent.yaml, models/deepseek_v4.yaml}
├── workspace/
│   ├── memory/{core.md, extended/*.md}   # 跨 task 共享记忆
│   └── tasks/<task_id>/                  # task_dir:仅 skill 产物,state/messages 在 PG
└── {main.py, cli.py}

task_dir = workspace/tasks/<task_id>/,所有 skill 产物写到这里,绝对路径在 system prompt 显式给 agent。写错位置(cwd / skills/ / repo 根)git status 立刻报红,不再用无锚 .gitignore 通配盖污染。

启动:读 agent.yaml → 加载 ModelCapabilitiesLLM(caps) → 解析 task_dir → 拼 system prompt(general_v1.md + skill discovery + cwd + task_dir 绝对路径)→ 装配工具 → REPL。新建路径懒创建,不预占文件(§3.6)。ZCBOT_DB_URL 指 PG(本地 docker compose / 远端 dev / 生产)。


3. 核心组件

3.1 主循环(core/loop.py)

ReAct:LLM → 若有 tool_calls 就执行 → 结果塞回消息 → 再调 LLM。无 tool_call 即返回。

  • 工具结果对模型截 16K 字符,用户预览 400 字符
  • 后台 daemon 线程每 100ms 刷 spinner:thinking... 1.3s ctx 12,345 tok
  • 每轮 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 分钟接入,不改代码。 字段: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 用真实调用对账:basic_chat / parallel_tools / thinking_mode / long_context(opt-in)。不改 yaml,只出 rich Table 报告。显式触发,不进启动路径(避免烧 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 重复即报错);工具按原子操作切分,不做 make_pptx() 这种高级封装。

3.5 Skill 系统(Anthropic 渐进披露)

对齐 Anthropic 2025-12 开放标准。三层加载:Discovery(name + description,几百 token)→ Activation(load_skill(name) 加载完整 SKILL.md,1-5K)→ Execution(SKILL.md 指 references/xxx 按需拉)。 原则:写 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 / model / reasoning_effort / task_dir / 时间戳 / tokens。直接 ORM 写 PG tasks。task_dir FS 目录只存 skill 产物,无 state.json / messages.json。本地 + SaaS 同一份 schema 和 ORM,差别只在 ZCBOT_DB_URL

懒创建 —— build_agent 不立刻 INSERT,Task / Session 在第一条 user 消息触发 append 时 INSERT;task_dir 目录在 skill 第一次落产物时 mkdir(parents=True)。启动 REPL 后立刻 /exit 不留 DB 行 + 不留目录。

REPL 内 task 切换 —— /new / /resume [last|<id>](无参列最近 10 个)/ /done /abandon / /desc。切走前 _cleanup_if_empty 守门:DB 无 messages FS task_dir 无产物 → DELETE + rmdir;任一痕迹存在则保留。

原子性 —— PG INSERT 天然原子;skill 产物走 core.session.atomic_write_text(tmp + fsync + replace)。

CLI:chat --mode coding --desc "..." [--resume last|<id>] [--remote <url>];tasks [--status ...]

3.7 双层记忆(core/memory.py)

跨 task 共享的事实(用户偏好 / 项目约定 / 模型 quirk)放 workspace/memory/:

文件 加载 适合
Core core.md 每次 build_agent 进 system prompt 跨任务高频精炼事实(几百 token)
Extended extended/*.md 索引(标题+绝对路径)进 prompt,内容靠 read 工具按需拉 大量低频专题

system prompt 每次 build_agent 重建,resume 也走 _build_system_prompt 并覆盖 messages[0] —— memory 演化即时生效。

memory 由人填(也允许 agent 用 write 写),系统不自动维护 —— 关键差异:事实由用户判断,不由 LLM 自动总结

memory 永远在 FS,不入 DB:本地 workspace/memory/,SaaS <storage_root>/users/<user_id>/memory/(bind mount 进容器)。理由:用户笔记语义,FS 读写 + 编辑器手编是产品的一部分;跨 task 共享靠"同一 user 同一目录"自动达成,无需 schema。


4. 模型路由

默认 default_model: deepseek_v4.flash。后续分模式路由思路:

模式 模型 理由
通用 / 编码 / PPT / 提案初稿 flash SWE-Bench 80.6,够用
复杂 bug / 提案终稿 pro + reasoning_effort=max 关键产出
fallback claude_4_7.opus V4 不行时手动切

成本量级(对比):

任务 flash pro-max 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 框架失败的核心:给 LLM 太多脚手架,模型升级后这些脚手架成枷锁。正确做法:把 LLM 当一个会持续变强的同事,告诉它目标,不告诉它步骤。

七条具体原则

  1. Prompt 用 WHY+WHAT 不用 HOW —— 教"怎么思考"会降智强模型
  2. Skill 渐进披露,不写完整流程
  3. 工具按原子操作切分,不做高级封装 —— 留组合空间
  4. Model Profile 化,不硬编码
  5. Capability Probing 对账实际行为
  6. 版本化 Prompt(等真要切版本时再做)
  7. eval 评估 —— 已删,dogfooding 更有效

借鉴

来源 借鉴
CoreCoder 主循环简洁实现 + Edit 唯一匹配
Anthropic Skills SKILL.md 渐进披露
nanobot Workspace + 任务隔离
smolagents LiteLLM + CodeAct 启发 run_python

6. 风险与取舍

风险 缓解
run_python sandbox 不够强(本地非真隔离) 工作目录限制 + 敏感 env 过滤;SaaS 走 docker exec(§7.5);本地依赖用户最终审阅
V4 某些复杂任务不如 Claude dogfooding 判断,fallback 手动切
Skill description 不准 → 触发不到 Pro 优化描述,实战观察
Long context 退化 probe --long-context 探测可靠 ceiling
本地 PG 离线 docker compose up -d 起本地 PG 兜底;也可连远端 dev / staging PG

Hybrid 范式而非纯 CodeAgent:V4 JSON tool call 已稳定;sandbox 成本只在需要时付;兼容 thinking。 Anthropic Skill 标准:行业标准已成,跨 SDK 兼容。 不做 subagent:状态管理爆炸;单 agent + skill 已覆盖 95% 场景。 不做 Eval Suite:个人单用户场景,dogfooding 信号比造作 case 强,probe 覆盖健康检查。


7. SaaS 化(草案,status=design,2026-05-12)

§1-§6 是本地 dogfood 形态;本节是SaaS 形态,把 core 包成多用户在线服务。 不引入 platform/core 切分 —— core 就是后端,直接对用户做 auth。两条形态共享同一份 core,差别只在 CLI 入口 vs HTTP 入口。本节落地前 §1-§6 路线照走,不阻塞 dogfood。

7.0 与本地形态的兼容性

SaaS 化不是"重写"也不是"取代 CLI",而是给同一份 core 加一个 HTTP 入口。落地过程中本地 CLI 始终可用。

共享:同一份 core/ / tools/ / SKILL.md / prompts。 差别:

维度 本地 SaaS
入口 cli.py chat 直调 core HTTP /v1/... + SSE
Storage PG(ZCBOT_DB_URL 指 docker compose / 远端 dev PG) PG(指生产 PG)
task_dir 根 workspace/tasks/<task_id>/(派生,私有) <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

CLI 长期双模式:本地直跑(默认,in-process,直连 PG,适合调内部状态)/ --remote https://...(HTTP 走 /v1,等价真实用户路径)。两模式共用 cli.py,差别只在 transport 层。

workspace/ 仅存 skill 产物,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 定义是项目代码,跟部署走,所有用户共享。

state / messages 两形态都在 PG,FS 只承担 skill 产物。多 task 共享同 folder 时由 §7.8 文件级悲观锁兜底。

7.2 资源模型(/v1)

POST/GET/PATCH/DELETE   /v1/folders[/{path}]            列树 / 创建 / 改名 / hard cascade
GET/POST/DELETE         /v1/folders/{path}/files[/{name}]   列 / 上传 / 下载 / 删
CRUD                    /v1/tasks[/{id}]                    {task_dir, mode, desc, model}
POST/GET                /v1/tasks/{id}/messages             发消息 / 历史(?search= 走 jsonb GIN / tsvector)
GET/POST                /v1/tasks/{id}/runs/{run_id}/{events,cancel}   SSE
GET                     /v1/{skills,models,usage}
POST                    /v1/probe                           (admin)

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。所有 storage/executor scoped by user_id无 tenant 层 —— 个人 SaaS 用不上,做企业版再加 org_id 等价隔离。

7.4 存储:Postgres + 本地文件系统

users(user_id uuid pk, email null, password_hash | oidc_subject null, plan null, created_at)
-- 本地形态固定 INSERT sentinel: user_id = '00000000-...',email/auth/plan 全 NULL

tasks(task_id uuid pk, user_id fk, task_dir text not null, mode, description,
      status, model_profile, tokens_prompt, tokens_completion, cost_usd,
      created_at, updated_at);
create index on tasks (user_id, task_dir);

messages(message_id uuid pk, task_id fk, idx int not null,
         payload jsonb not null, tokens_in, tokens_out, created_at,
         unique (task_id, idx));
create index on messages using gin (payload jsonb_path_ops);
-- 全文搜按需加 tsvector + GIN(中文 simple + pg_trgm 起步)

runs(run_id 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):查同 user 下是否存在 new LIKE existing/%existing LIKE new/%,中一则拒;同 task_dir 允许。

Folder rename(old → new,FS rename 成功后):UPDATE tasks SET task_dir = new || substring(task_dir from len(old)+1) WHERE user_id=? AND (task_dir = old OR task_dir LIKE old||'/%')old/% 而非 old%,避免 project_a 误中 project_a_other。running task 引用时禁 rename / delete。

Folder delete:hard cascade,前端 modal 列影响面 + 输入 folder 名二确认。先 DELETE messages → DELETE tasks → FS 递归删;DB 成功 FS 失败由后台 GC 兜底清孤儿目录。usage_events 不参与 cascade。

文件系统:

<storage_root>/users/<user_id>/
  memory/{core.md, extended/}      # per-user,不入 DB
  <user-given-paths>/...           # task_dir 散落其下

本地优先 S3(部署简化 / 低延迟),storage 抽象层留好后续可换。

Storage 实现:单一 PG ORM(本地 + SaaS 共用):一份 schema、一份 SQLAlchemy、一份查询,无 adapter,无 SQL 方言适配,无契约测试。alembic 管 migration;CLI 启动校验 schema 版本,落后报错让用户跑 cli db upgrade(本地)或部署管线自动 alembic upgrade head(SaaS)。cli migrate-from-fs --workspace ./workspace 一次性导旧 JSON。

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;同 user 多 task 不互隔(协作方便),跨 user 由独立实例隔离

资源限制: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) done
2 Storage 落 PG:Session / TaskState 改 SQLAlchemy 写 PG;alembic;cli migrate-from-fs;docker-compose.yml 起本地 PG 3 天
3 task_dir 双形态:TaskState.task_dir 可显式指定;tools/fs.py::_resolve 接 task_dir 注入;system prompt 注入两形态共用 1 天
4 Folder API:list / create / rename(cascade + 锁 running) / delete(hard cascade) / upload / download 2 天
5 No-subtask 校验:create_task 入口跑 §7.4 SQL 0.5 天
6 Executor + sandbox:run_python/shellExecutor.run(...);本地保留 subprocess executor,SaaS 走 docker;api_key_envKeyProvider 运行时注入 2-3 天
7 HTTP /v1:FastAPI + SSE + OIDC 4 天
8 CLI 双模式:transport 层抽象,默认 in-process;--remote 走 HTTP;本地直跑不删 1.5 天

代码量增量:+1000~1500 行(单一 PG 比双 adapter 省 500-800 行)。

7.7 分阶段落地

阶段 范围 工作量 验收
A #1 事件流化 done
B #2 #3 #4 #5(Storage 落 PG + task_dir 双形态 + Folder API + no-subtask) ~1 周 本地走 PG,messages 进 DB 全文搜可用;多 task + folder rename 单测;migrate-from-fs 跑通
C #6(Executor + sandbox) 3 天 两本地账号互不可见对方 folder,本地 subprocess executor 仍可用
D #7(HTTP /v1 + auth) 4 天 curl / Postman 跑通主流程
E #8(CLI transport 双模式) 1.5 天 默认本地直跑保留,--remote 走 HTTP 跑通
F 上线打磨(限流 / 监控 / 告警 / HA) 持续 SLO 99.5%

B 阶段一次性切换 —— 切到 PG 后本地与 SaaS 走相同代码路径,无回退、无双轨。dogfood 即生效(messages 进 DB → 全文搜、jsonb 查询立刻可用)。

7.8 已知风险

风险 缓解
过早抽象违背 §5 B 阶段单一 PG 无 adapter;C-E 各阶段独立 dogfood 价值
CLI 双模式分叉、本地直跑被忽略 transport 层抽象统一接口;CI 跑两路径同一组用例
/v1 冻死后演化慢 minor 半年兼容,major 6 个月 deprecation;/v1internal 实验
Rename 误中前缀 / 漏改子 task cascade SQL 用 old/% + 单测覆盖
Running task 被 rename / delete 后端校验 + UI 禁按钮
误删 folder 二确认 + 输入 folder 名;真要再加 trash bin
DB-then-FS 中断留孤儿目录 后台 GC 周期扫"FS 有但 DB 无引用"
同 folder 多 task 并发写同名 文件级悲观锁,冲突早失败
Sandbox 出站越权 egress allowlist 起步只放 LLM + PyPI
资源滥用 BYO key 默认;月度配额;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 等价。

skill 产物全落 cwd 不引入 artifacts 表:中间件是用户花 token 生成的资产,可下载可替换;artifacts 表是为不确定 UX 收益预付架构成本。真嫌乱 UI 加折叠视图。

hard cascade 而非 soft orphan:orphaned 让 list / resume / UI 都多一种特殊 case,"删 folder = 删项目"比"留对话残骸"自然。usage_events append-only 不 FK,task 硬删后月账仍存活。

本地也用 PG,不用 SQLite / JSON:① dogfood ≡ 真实用户路径,bug 在 dogfood 就能复现;② Docker 已是必然依赖(§7.5),docker compose up postgres 零增量门槛;③ 双 adapter 维护税远高于 PG 一次性配置成本;④ 本地 dev 也能连远端测试服。

CLI 不被 API 取代,而是双模式共存:本地直跑调 core 内部状态比 HTTP roundtrip 顺手;前端用户路径靠 --remote 打通。离线靠本地 docker compose PG 兜底,不靠"全栈零依赖"幻觉。

Memory 不入 DB:跨 task 共享靠"同一 user 同一 FS 目录"自动达成。md 用户直接编辑器改,DB 化反而要造 UI、违反 §3.7"事实由用户判断"。

Tasks/Messages 在 PG 但 skill 产物在 FS:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 —— DB 强项;skill 产物(*.pptx / *.docx / sections/*.md)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 —— 进 DB 要做"导出"多余操作。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
  • 价格:in ~$0.145/M,out ~$1.74/M(约 Claude Opus 1/6 ~ 1/7)
  • deepseek-chat / deepseek-reasoner 2026-07-24 下线 → 必须迁 deepseek-v4-flash / deepseek-v4-pro