zcbot/DESIGN.md

21 KiB
Raw Blame History

设计文档

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


1. 边界

做什么

  • PPT:文本 / 会议纪要 → .pptx(用 python-pptx)
  • 科研申报:课题信息 → 分章节 .docx(用 python-docx)
  • 编码:文件编辑、shell 执行、迭代验证

不做什么

  • 子 agent / IM 渠道 / 多用户 / Web UI(初期 CLI 即可)/ 自定义 RAG / 锁定 Anthropic
  • Eval Suite:个人工具用 dogfooding 判断模型升级,造作 case 没区分度

关键约束

  • 模型自由:LiteLLM 接 OpenAI-compatible 任意 provider(默认 DeepSeek V4)
  • 任务持久化:任意时刻关机,下次能恢复
  • 演化性:模型升级时 agent 跟着升级,不需要大改架构

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/
│   └── tasks/<task_id>/
│       ├── state.json      # TaskState
│       ├── messages.json   # Session
│       ├── 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 不再用无锚通配规则盖住污染。

启动时拼装顺序

  1. config/agent.yaml 拿 default_model
  2. ModelCapabilities.load("deepseek_v4.flash", config/models/) 拿能力档案
  3. LLM(caps) 构造,从 env 读 API key
  4. 解析 task_dir(新建 or resume)
  5. 拼 system prompt:prompts/system/general_v1.md + SkillRegistry.discovery_block()(skill 列表)+ cwd + task_dir 绝对路径(产物根)
  6. 装配工具集(fs / shell / load_skill / run_python)
  7. 启动 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_calls
  • thinking_mode:对 declared=True 的模型试 reasoning_effort,看 API 是否接受 + 是否产 reasoning_content
  • long_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 + 落 messages.json

Task(core/task.py)= Session 的上层概念,含 mode / description / status (active/completed/abandoned) / model / reasoning_effort / cwd / created_at / updated_at / tokens_prompt / tokens_completion。落 state.json

存储:workspace/tasks/<task_id>/{state.json, messages.json}。每轮 agent.run 后调 sync_task_tokens 把 LLM 累计 tokens 写回。

懒创建 —— build_agent 新建分支不立刻 save,task_dir 在第一条 user 消息触发 Session.append → save() 时才物化(Session.save / TaskState.savemkdir(parents=True))。启动 REPL 后立刻 /exit 磁盘无痕,跨进程也安全(没有"另一个 REPL 刚 build_agent 还没说话就被这个进程当空 task 删掉"的窗口)。

REPL 内 task 切换 —— /new 开新 task,/resume [last|<id>] 切到已有 task(无参数列最近 10 个表格让用户选),/done /abandon 改状态,/desc 改描述。切走前 _cleanup_if_empty 守门:三条都满足才删 task_dir —— ① session 没 user 消息 ② 目录在磁盘上 ③ 目录里只剩 messages.json(state.json 存在 = /done /abandon /desc 留下的显式痕迹,要保)。

CLI:chat --mode coding --desc "..." [--resume last|<id>];tasks [--status active|completed|abandoned] 列任务。


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 当一个会持续变强的同事对待,告诉它目标,不告诉它步骤。

七条具体原则

  1. Prompt 用 WHY+WHAT,不用 HOW —— 详细教"应该怎么思考"会降智强模型
  2. Skill 渐进披露,不写完整流程 —— 对齐 Anthropic 标准
  3. 工具按原子操作切分,不做高级封装 —— 留出组合空间给模型
  4. Model Profile 化,不硬编码 —— 新模型 5 分钟接入
  5. Capability Probing —— yaml 是手填的,跑探测对账实际行为
  6. 版本化 Prompt —— prompts/system/active.md 软链接(尚未做,等真要切版本时再做)
  7. eval 评估 —— 设计阶段曾认为是关键,落地后判断:个人工具 dogfooding 更有效;已删

借鉴自(简版)

来源 借鉴
CoreCoder 主循环简洁实现 + Edit 唯一匹配约束
Anthropic Agent Skills SKILL.md + 渐进披露标准
nanobot Workspace + 任务隔离
smolagents LiteLLM 做模型层 + CodeAct 范式启发 run_python

6. 风险与取舍

已知风险

风险 缓解
run_python subprocess 沙盒不够强 限制工作目录 + 敏感 env 过滤;后续可升级 Docker
V4 在某些复杂任务不如 Claude dogfooding 判断,fallback 手动切
Skill description 不够好 → 触发不准 用 Pro 优化 description,实战观察
Long context 退化 probe --long-context 探测可靠 ceiling,不依赖宣称值
Session.save() 不原子,异常会留 0 字节文件 后续改 tmp + rename(已记 PROGRESS)

取舍说明

为什么用 Hybrid 范式而不是纯 CodeAgent:V4 JSON tool call 已稳定;沙盒成本只在需要时付;兼容 thinking 模式。

为什么用 Anthropic Skill 标准而不是自创:行业标准已成,跨 SDK 兼容;直接拿 Anthropic 现成 skills repo。

为什么不做 subagent:状态管理复杂度爆炸;单 agent + skill 已覆盖 95% 场景。

为什么不做 Eval Suite:DESIGN 旧版按团队/产品场景设计;个人单用户场景里,跑两个真实任务的 dogfooding 比造作 case 信号更强,probe 已覆盖健康检查。


7. Core / Platform 切分(草案,status=design,2026-05-09)

§1-§6 是 personal-tool track;本节是 platform track,目标把 core 包成多租户 SaaS。两者共享同一 core 代码,部署形态不同。本节落地前 §1-§6 路线照走,不阻塞 dogfood。

7.1 总原则

形态 数据归属 接口
Core(自己做) 独立 HTTP/SSE service + sandbox 进程组 对话 / 文件 / 产物 / tokens / 用量 /v1/* REST + SSE
Platform(团队做) BFF + Web/Mobile UI + Auth + Billing 终端用户 / 订阅 / 发票 调用 core /v1/*

CLI 也是 core 的一个客户端 —— 走同一 /v1,本地起 core 跑 localhost。dogfood 和平台走同一份代码路径,bug 先在自己身上发现。

参考蓝本:OpenAI Assistants API 形态(stateful agent service:/threads /messages /files /runs)。

7.2 资源模型与接口(/v1)

POST   /v1/tasks                          创建 task(mode/desc/model)
GET    /v1/tasks                          列表
GET    /v1/tasks/{id}                     详情
PATCH  /v1/tasks/{id}                     改 mode/desc/status
DELETE /v1/tasks/{id}                     删 task(连带 messages/files/产物)

POST   /v1/tasks/{id}/files               上传(multipart,入 task_dir/source/)
GET    /v1/tasks/{id}/files               列表
GET    /v1/tasks/{id}/files/{name}        下载(产物也走这里)
DELETE /v1/tasks/{id}/files/{name}

POST   /v1/tasks/{id}/messages            发消息,返回 {run_id}
GET    /v1/tasks/{id}/messages            历史
GET    /v1/tasks/{id}/runs/{run_id}/events   SSE 事件流
POST   /v1/tasks/{id}/runs/{run_id}/cancel

GET    /v1/skills                         可用 skill 列表
GET    /v1/models                         可用 model profile
POST   /v1/probe                          (admin) 跑 capability probe

GET    /v1/usage                          tokens/cost/quota 状态(by tenant)

SSE 事件格式:

{"type":"tool_call","run_id":"...","name":"read","args":{...},"ts":"..."}
{"type":"tool_result","run_id":"...","name":"read","preview":"...","truncated":false}
{"type":"text","run_id":"...","delta":"..."}               LLM 流式 token
{"type":"usage","run_id":"...","prompt":1234,"completion":567,"cost_usd":0.012}
{"type":"done","run_id":"..."}

版本化:/v1 半年内 minor 向后兼容,major 6 个月 deprecation 窗口。

7.3 认证模型

Core 只信平台,不直接对终端用户

Authorization: Bearer <platform_api_key>     ← 绑死平台租户
X-Tenant-Id:   <tenant_uuid>                  ← 平台 sign 的 JWT claim
X-User-Id:     <user_uuid>                    ← 同上
X-Request-Id:  <uuid>                         ← 跨服务 trace

平台对终端用户做 OIDC/Clerk auth,把租户/用户 ID 签进 JWT 给 core 验签 —— 平台无法伪造租户 ID。多租户隔离在 core 这一层强制,平台 bug 不会泄露跨租户数据。

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

结构化数据走 Postgres(service 形态多 worker 必须):

tenants(id, name, api_key_hash, plan, created_at, status)
tasks(id, tenant_id, user_id, mode, description, status, model_profile,
      tokens_prompt, tokens_completion, cost_usd, created_at, updated_at)
messages(id, task_id, role, content, tool_calls, tool_call_id,
         reasoning_content, created_at)
runs(id, task_id, status, started_at, finished_at, error, tokens_p, tokens_c, cost_usd)
files(id, task_id, name, path, size, content_type, uploaded_at, kind)
                                                  ← kind: source / intermediate / artifact
usage_events(id, tenant_id, task_id, run_id, kind, value, ts)
                                                  ← 计费/审计明细,append-only
quotas(tenant_id, max_concurrent_runs, max_tokens_month, max_storage_bytes, ...)

文件走本地磁盘:

<storage_root>/
  tenants/{tenant_id}/
    tasks/{task_id}/
      source/      ← 用户上传(PDF / 团队介绍等)
      intermediate/← 中间产物(spec_lock.md / sections / slides)
      artifact/    ← 最终产物(.docx / .pptx)

files.path 存相对 <storage_root> 的路径。<storage_root> 由部署配置决定(单机 = 一个目录,多 worker = 共享挂载点)。

为什么本地而不 S3:简化首版部署,运维门槛低;访问延迟低;tasks 文件总量 100GB 级别本地盘够用。规模真起来后,files 表 + storage 抽象层只需换 backend,接口不动

7.5 沙盒:Per-task 容器 + Per-run exec

选择 理由
每 task 一个长驻容器 起容器 ~300ms 太慢;一个 task 多轮 tool call 共享容器才划算
每 run 一次 docker exec 进容器跑 run_python/shell;exec 级 timeout/资源限制
容器空闲 N 分钟回收 不浪费;下次 resume 重新拉起
task_dir 直接 bind mount 进容器 宿主 <storage_root>/.../tasks/{id}/ 挂到容器 /workspace,无同步开销

资源限制:cgroup CPU/mem 上限、磁盘配额、网络 egress allowlist(只能出 LLM API 和 PyPI 镜像)、root 文件系统 read-only、no-new-privileges、drop ALL caps。

选型:Phase 起步用 Docker(运维门槛低);流量起来后视情况换 gVisor / Firecracker / e2b。Executor Protocol 抽象后切换成本低。

7.6 Core 代码改造(按依赖顺序)

# 改造 影响文件 估时
1 事件流化 loop.py —— console.print 改成 yield Event;CLI 接 console sink,API 接 SSE sink core/loop.py cli.py 半天
2 Storage 层 —— Session/TaskState 落 Postgres;files<storage_root> core/session.py core/task.py main.py 新增 core/storage/ 2 天
3 Executor 抽象 —— run_python/shellExecutor.run(code, timeout, env);实现 = docker exec 到 per-task 容器 tools/run_python.py tools/shell.py 新增 core/executor/ 2 天
4 Config + 多租户上下文 —— 每次请求带 (tenant_id, user_id),所有 storage/executor 调用都 scoped main.py cli.py 全链路 1 天
5 api_key_env 退役 —— 改 KeyProvider,运行时从 vault 取(平台 BYO 模式则逐请求注入) core/capabilities.py core/llm.py 半天
6 HTTP 外壳 —— FastAPI app,把上面五层包成 /v1/* + SSE 新增 core/api/ 3-4 天

代码量增量预估:+800~1200 行(API 层 + storage 层 + executor 层 + 配套测试)。

7.7 职责矩阵

事项 Core Platform 备注
终端用户 auth OIDC/Clerk
平台↔core 鉴权 验签 签发 Bearer + JWT
多租户数据隔离 enforce platform bug 不能跨租户
Prompt injection 防护 只有 core 看得见 LLM I/O
敏感数据出站审计 产事件 消费转 SIEM 双层
配额超限拒绝 计数 + 拒绝 展示给用户 core 不能信平台传值
文件病毒扫描 入库再扫一次 pre-upload 扫 双层
GDPR 删除 提供 DELETE 触发 数据在 core 这边
LLM API key(BYO) 运行时注入,不持久化明文 加密存,逐请求传 降攻击面
计费 产 usage 事件 汇总 + Stripe core 不碰钱
监控 / SLO 自己负责 独立服务

7.8 分阶段落地

阶段 目标 工作量 验收
A 改造 §7.6 #1(事件流化) CLI 不变,但 loop yield event;为后续铺路 半天 CLI 走 event sink 跑通 dogfood
B 改造 #2-#5(storage / executor / DI / key) 单进程仍可跑;接口齐备 1 周 CLI 接 Postgres 跑通本地多 task
C 改造 #6(HTTP 外壳) /v1/* 跑通,docker compose 起完整栈(core api + sandbox + PG) 4 天 curl / Postman 跑通主流程
D 多租户 + Auth + Quota + Audit 平台接得上 1 周 平台 demo 跑通
E 上线打磨(限流/监控/告警/HA) 可承接平台真实流量 持续 SLO 99.5%

A 立刻可做,独立有价值。B-D 等平台团队 kickoff 时间锁定后开。E 上线后持续投入。

7.9 已知风险

风险 缓解
过早抽象违背 §5 哲学 A 阶段单独有价值(支持 Web UI);B-E 等平台到位再做
/v1 冻死后核心演化变慢 minor 向后兼容窗口,major 6 个月 deprecation;内部加 /v1internal 用于实验
Sandbox 网络出站限制不当 Egress allowlist 起步只放 LLM endpoint + PyPI 镜像;skill 需要新出站靠申请
Per-task 容器 ID 泄漏到对话 tool 结果 sanitize,容器内 hostname/IP 不暴露
平台 bug 把 core 冲垮 平台维度限流(rate limit by Bearer key)+ 租户维度并发上限
文件计费/存储滥用 上传大小上限、月度配额、保留期(过期自动清)
LLM 成本失控 BYO key 默认;平台代付要 per-tenant 月预算硬上限
本地盘容量瓶颈 files 表 + storage 抽象层,换 backend 接口不动;LRU 清理冷 task
多 worker 共享本地盘 起步单机部署即可;需要扩 worker 时上 NFS 或换对象存储

7.10 取舍说明

为什么 Docker 起步而不直接上 Firecracker/gVisor:Docker 运维门槛最低,Executor Protocol 抽象后可平滑切换。提前上 microVM 是过度工程。

为什么不复用 K8s Job per run:Job 启停成本高(秒级),agent loop 一轮 tool call 才几百毫秒。Per-task 长驻容器 + per-run exec 是性能/隔离的最佳折中。

为什么本地文件而不 S3:首版部署/运维门槛低,访问延迟低,100GB 级别单机够用。storage 抽象层留好,真要切对象存储改 backend 不改接口。

为什么 Postgres 而不 SQLite:service 形态下 API + sandbox + 后续 worker 都要读写状态,SQLite 单写锁会成为瓶颈。Postgres 起步成本可接受。


附录: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