Skip to content

Latest commit

 

History

History
376 lines (303 loc) · 23.6 KB

File metadata and controls

376 lines (303 loc) · 23.6 KB

BuildAgent — Python 编码 Agent MVP 框架设计与路线图

跟踪文档三件套(保持动态更新):

  • Plan.md(本文)— 策略路线图,仅在路线大调时变动
  • taskFinished.md — 已实现并通过测试的工作项(按时间倒序追加)
  • ToDo.md — 待办、已知问题、下一阶段步骤拆解

维护规则:每完成一项 Phase Step → 从 ToDo.md 移到 taskFinished.md。

Context

为什么做这件事:从零搭一个类 Claude Code 的通用编码 agent,已克隆样本 ../claudecode/(claw 项目,Rust 主实现 + Python 参考实现,约 5 万行)作为学习参考。目标是用 Python 走完一遍 agent loop 的完整生命周期,理解工具系统、权限模型、会话管理、子 agent 等关键设计。

输入条件

  • 语言:Python
  • 用途:通用编码 agent(需要 Read/Write/Edit/Bash/Grep/Glob 全套工具)
  • 颗粒度:完整 MVP 路线图(分阶段,每阶段都能跑)
  • LLM:DeepSeek API key,走 DeepSeek 的 Anthropic 兼容端点https://api.deepseek.com/anthropic)+ 官方 anthropic Python SDK;后续切回真正的 Anthropic API 几乎零改动。OpenAI Plus 是 ChatGPT 网页订阅,不附带 API quota,要用 OpenAI 模型还得单独充 API 余额

预期产出:分 5 个阶段(Phase 0 → 4)的可执行计划,每个阶段产出一个能 demo 的可运行版本,并标注从样本借鉴的具体文件 / 模式。


关键设计决策(决定后续整条路)

决策 1:内部消息协议用 Anthropic 风格

理由

  • Anthropic 的 content blocks(text / tool_use / tool_result 互相嵌套)是当前最干净的多轮工具协议;OpenAI 的 tool_calls + tool role 在多轮 + 多工具时容易拧巴
  • 样本(claw)也是这么做的:api/src/types.rs 把 OpenAI/xAI/DashScope 都翻译成 Anthropic 风格内部表示

落地:定义 ContentBlock = TextBlock | ToolUseBlock | ToolResultBlockMessage = { role, content: list[ContentBlock] }

决策 2:Phase 0 直接走 DeepSeek 的 Anthropic 兼容端点 + 官方 anthropic SDK

理由

  • 跟决策 1 的内部协议对齐:内部用 Anthropic 风格 ContentBlock,wire format 也是 Anthropic 原生 → 客户端翻译层退化为"几乎透传",比 OpenAI 反向翻译省一大半坑(无需自己拼 tool_callstool_use、无需处理 arguments 是 JSON 字符串的怪味)
  • 后续切回真正的 Anthropic API(claude-opus / claude-sonnet)只需改两个变量:base_url=None(走默认)+ model="claude-..."零代码改动
  • DeepSeek 的 Anthropic 代理早期有 tool name 字段丢失的 bug,视为已知风险在 Phase 1 验证一次;如果仍存在则在客户端层加 workaround(详见 Phase 1 坑警告)

落地AnthropicStyleClient 包装 anthropic.Anthropic(base_url=..., auth_token=...);内部 Message 与 SDK 的 MessageParam 几乎同构,直接 .model_dump() 即可。

配置约定(与 Claude Code 自带 DeepSeek 接入一致,便于一份 key 两边用):

ANTHROPIC_AUTH_TOKEN=sk-...                            # DeepSeek key 直接当 auth token
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
ANTHROPIC_MODEL=deepseek-v4-pro[1m]                    # 或 deepseek-v4-flash[1m] 走 haiku 替身

决策 3:工具用 Pydantic 模型定义 schema,自动生成 JSON Schema

理由:避免手写 JSON Schema 与执行函数签名的双重维护;样本的工具规范也是数据驱动(tools/src/lib.rs::mvp_tool_specs())。

决策 4:每个阶段单独可跑、单独可演示

理由:样本的 PRD(prd.json)就是按 story 跟踪 passes: bool;按阶段交付能让你随时掉头调整方向,不会"走到一半发现路线错了"。


整体架构(最终形态,Phase 4 完成后)

┌─────────────────────────────────────────────────────┐
│  CLI (typer + rich)                                 │
│  - 入口、参数解析、TUI 渲染                           │
└──────────────────┬──────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────┐
│  Agent Loop (agent.py::run_turn)                    │
│  user msg → LLM → tool calls → tool exec →          │
│              ↑                            │         │
│              └────── 直到无 tool_use ────┘         │
└──┬──────────────┬─────────────────┬─────────────────┘
   │              │                 │
┌──▼──────┐  ┌────▼─────────┐  ┌────▼──────────┐
│ Session │  │ ToolRegistry │  │  LLMClient    │
│  - msgs │  │  - dispatch  │  │  - anthropic  │
│  - 持久化│  │  - schemas   │  │  - (openai*)  │
└─────────┘  └────┬─────────┘  └───────────────┘
                  │
        ┌─────────┼─────────┬──────────┬──────────┐
        ▼         ▼         ▼          ▼          ▼
      Read     Write/Edit  Bash      Grep      Glob
                            │
                ┌───────────▼──────────┐
                │ Permission Enforcer  │
                │ (read_only/ask/...)  │
                └──────────────────────┘

横切关注点:config.py(settings.json 优先级合并)、compact.py(自动压缩历史)、hooks.py(PreToolUse/PostToolUse)、render.py(流式输出)。


目录结构(一开始就摆好,每阶段往里填)

myAgent/                        # 当前目录(与 claudecode/ 平级)
├── Plan.md                     # ← 本文件
├── pyproject.toml              # 用 uv 或 poetry 管依赖
├── README.md
├── .env.example                # ANTHROPIC_AUTH_TOKEN=... + ANTHROPIC_BASE_URL=...
├── settings.example.json
├── src/my_agent/
│   ├── __init__.py
│   ├── cli.py                  # typer 入口
│   ├── agent.py                # ★ 核心 loop(Phase 0/1)
│   ├── session.py              # 消息历史 + 持久化(Phase 3)
│   ├── types.py                # ContentBlock / Message / ToolSpec
│   ├── config.py               # settings.json 加载(Phase 2)
│   ├── prompt.py               # system prompt + CLAUDE.md 加载
│   ├── compact.py              # 上下文压缩(Phase 3)
│   ├── hooks.py                # 钩子(Phase 3)
│   ├── permission.py           # 权限模式(Phase 2)
│   ├── llm/
│   │   ├── __init__.py
│   │   ├── base.py             # LLMClient 接口
│   │   ├── anthropic_style.py  # DeepSeek/Anthropic(Phase 0,主力)
│   │   └── openai_style.py     # OpenAI 兼容兜底(Phase 3+,可选)
│   ├── tools/
│   │   ├── __init__.py
│   │   ├── base.py             # Tool 基类 + Registry
│   │   ├── read.py             # Phase 1
│   │   ├── write.py            # Phase 1
│   │   ├── edit.py             # Phase 2
│   │   ├── bash.py             # Phase 1
│   │   ├── grep.py             # Phase 2
│   │   ├── glob.py             # Phase 1
│   │   └── task.py             # 子 agent(Phase 3)
│   └── ui/
│       ├── __init__.py
│       └── render.py           # rich 流式渲染(Phase 2)
└── tests/
    ├── test_agent_loop.py
    ├── test_tools.py
    └── fixtures/
        └── mock_llm.py         # Phase 0 就要有,方便不烧 token 跑测试

Phase 0 — Hello Loop(目标:1-2 天,~250 行

⚠️ Phase 0 已按 OpenAI 兼容路径完成(见 taskFinished.md)。改决策 2 后需要做一次 Phase 0.5 重构:把 llm/openai_style.py 替换为 llm/anthropic_style.py,env 变量与 CLI 默认值同步更新。types.py / agent.py / cli.py 的命令骨架保持不动 —— 改动集中在 LLM 客户端这一层。

能干什么:CLI 收到一句话 → 调 DeepSeek 的 Anthropic 端点 → 把回复打到屏幕上。没有工具

关键文件

  • src/my_agent/types.pyContentBlockMessage 数据类(用 pydanticdataclasses)—— ✅ 已完成
  • src/my_agent/llm/base.pyLLMClient 抽象类,签名 chat(messages, tools, system) -> AssistantMessage —— ✅ 已完成
  • src/my_agent/llm/anthropic_style.pyAnthropicStyleClient(auth_token, base_url, model);用 anthropic SDK,base_url=https://api.deepseek.com/anthropicmodel="deepseek-v4-pro[1m]"(或留空让用户自配)
  • src/my_agent/agent.pyAgent.run(user_input):单轮,调 client,打印 text —— ✅ 已完成
  • src/my_agent/cli.pytyper 单命令 chat;env 默认从 ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL
  • tests/fixtures/mock_llm.py:可注入的 mock client(这是 0 阶段就要做的,否则后面每轮调试都要烧 token)—— ✅ 已完成

依赖anthropicpydantictyperrichpython-dotenvopenai 暂时移到 optional(Phase 3+ 才考虑做兜底 provider)。

验证

echo "你好" | python -m my_agent chat
# 期望输出:DeepSeek 的中文问候

从样本学什么

  • ../claudecode/rust/crates/api/src/anthropic.rs(如有)/ api/src/types.rs —— 看 Anthropic 原生协议字段
  • ../claudecode/rust/crates/runtime/src/conversation.rs::run_turn —— 暂时只看不带 tool 的部分(前 ~50 行)

Phase 0.5 重构清单(移到 ToDo.md 跟踪):

  • 删 / 弃用 llm/openai_style.py(暂保留文件留为 Phase 3+ 兜底参考)
  • 新增 llm/anthropic_style.py,封装 anthropic.Anthropic(base_url=..., auth_token=...).messages.create(...),把 SDK 返回的 content blocks 直接映射到内部 ContentBlock
  • cli.py 默认读 ANTHROPIC_AUTH_TOKENANTHROPIC_BASE_URLANTHROPIC_MODEL;保留 --api-key-env/--base-url/--model 三个 override
  • .env.example 字段更新;同时把 D-1/D-2 这两个原本针对 OpenAI 工具翻译的债务条目重写为"Anthropic 兼容代理 tool 字段验证"(详见 Phase 1 坑警告)
  • 单测保持 mock 路径,新增一个针对 AnthropicStyleClient._to_anthropic() 的 round-trip 测试

Phase 1 — Tool Loop(目标:3-5 天,~600 行新增

能干什么:agent 能用 4 个核心工具自主完成"读这个目录、找包含 X 的文件、写一个总结到 out.md"这种任务。

关键文件

  • src/my_agent/tools/base.py
    • Tool 抽象基类:name: strdescription: strinput_schema: dictdef run(input: dict) -> str
    • 用 Pydantic v2 的 BaseModel 定义 input model,.model_json_schema() 自动生成 schema
    • ToolRegistry:装饰器 @register 注册;get_specs() -> list[dict]execute(name, input) -> str
  • src/my_agent/tools/read.py:Read(文件路径 → 文件内容,限制 2000 行)
  • src/my_agent/tools/write.py:Write(path + content)
  • src/my_agent/tools/bash.py:Bash(command + timeout,用 subprocess.run,捕获 stdout/stderr/exit code)
  • src/my_agent/tools/glob.py:Glob(pattern → 文件列表,用 pathlib.Path().glob()
  • src/my_agent/agent.py:升级 run_turn 为循环:
    while True:
        assistant_msg = client.chat(messages, tool_specs, system)
        messages.append(assistant_msg)
        tool_uses = [b for b in assistant_msg.content if b.type == "tool_use"]
        if not tool_uses: break
        for tu in tool_uses:
            result = registry.execute(tu.name, tu.input)
            messages.append(ToolResultMessage(tool_use_id=tu.id, content=result))
    
  • src/my_agent/llm/anthropic_style.py:扩展 chat()tools=[ToolSpec...] 转为 Anthropic SDK 的 tools=[{name, description, input_schema}];解析响应里的 tool_use blocks 直接落到内部 ToolUseBlock(无需翻译,名称/字段几乎一一对应)
  • src/my_agent/prompt.py:硬编码一份基础 system prompt(参考 ../claudecode/rust/crates/runtime/src/prompt.rs

验证

my-agent chat "找出 src/ 下所有 .py 文件,统计每个的行数,写入 stats.md"

从样本学什么

  • ../claudecode/rust/crates/runtime/src/conversation.rs::run_turn 完整逻辑(tool 循环、迭代次数限制、错误处理)
  • ../claudecode/src/tools.py + ../claudecode/src/reference_data/tools_snapshot.json —— 工具规范的数据驱动设计
  • ../claudecode/rust/crates/runtime/src/file_ops.rs —— Read/Write 的边界检查、二进制检测、size 限制
  • ../claudecode/rust/crates/runtime/src/bash.rs —— Bash 的超时和后台执行模式

坑警告(DeepSeek Anthropic 端点专属):

  • 第一轮跑 tool_use 时一定要打印整条响应:DeepSeek 的 Anthropic 代理早期有 tool_use.name 字段被吞 / 改名的 case;如果出现,在 client 里加 fallback:当 name 为空时回填注册表里第一个匹配 input_schema 的工具,并打 warning
  • tool_use.input 在 Anthropic 协议里已经是对象,不是 JSON 字符串 —— 这是相比 OpenAI 兼容路径少踩的一个坑
  • 多轮工具调用时,user 消息里 tool_result 块的 tool_use_id 必须严格对应 assistant 上一轮的 tool_use.id;并且一个 user message 里要把这一批 tool_use 的所有结果一次性回填(不能拆成多条)
  • max_iterations=20 防死循环

Phase 2 — 生产级核心(目标:5-7 天,~1500 行新增

能干什么:agent 跑得"安全"——不该写的不写、不该跑的命令会问你;用户能看到流式输出;配置可以从 settings.json 读。

新增/扩展

  • src/my_agent/tools/edit.py:Edit(file + old_string + new_string,强制要求 read 过该文件——参考 Claude Code 的 read-before-edit 不变量)
  • src/my_agent/tools/grep.py:Grep(pattern + path,调 ripgrep 子进程,没装则 fallback 到 Python re
  • src/my_agent/permission.py
    • PermissionMode = Enum("read_only" | "ask" | "allow_all" | "danger")
    • class PermissionPolicycheck(tool_name, input) -> Decision(allow/deny/ask)
    • 默认规则:Bash/Write/Edit 在 ask 模式下要确认;read_only 直接拒绝写入类工具
    • 内置 deny 规则(如 rm -rf /git push --force 警告)
  • src/my_agent/config.pyload_settings() 按优先级合并:CWD .my-agent/settings.json~/.my-agent/settings.json → 默认值;环境变量覆盖(ANTHROPIC_AUTH_TOKENANTHROPIC_BASE_URLANTHROPIC_MODELMY_AGENT_PERMISSION_MODE
  • src/my_agent/prompt.py:扩展加载 CWD 的 CLAUDE.md(或自定义 AGENTS.md)拼到 system prompt
  • src/my_agent/llm/anthropic_style.py:改用流式(stream=Truemessages.stream() 上下文管理器),把 text / tool_use 增量回调出来
  • src/my_agent/ui/render.py:用 rich.live.Live + spinner 渲染流式输出 + tool call 块
  • src/my_agent/cli.py:加 --permission-mode--model--system 等参数;REPL 模式(多轮对话)

验证

my-agent chat --permission-mode ask "把 README.md 第一节改成英文"
# 应该弹确认提示
my-agent chat --permission-mode read_only "运行 ls"
# 应该被拒绝

从样本学什么

  • ../claudecode/rust/crates/runtime/src/permissions.rs(PermissionMode 枚举)+ permission_enforcer.rs::check()
  • ../claudecode/rust/crates/runtime/src/config.rs::ConfigLoader(优先级链)
  • ../claudecode/rust/crates/rusty-claude-cli/src/render.rs(spinner、ColorTheme,用 crossterm;Python 等价物是 rich)

Phase 3 — 进阶能力(目标:7-10 天,~2500 行新增

能干什么:会话能保存能续接;上下文长了会自动压缩;agent 可以派发子任务给 sub-agent;可挂钩子;多 provider 可切换。

新增

  • src/my_agent/session.py
    • Session 类:messagessession_idcreated_atmodelpermission_mode
    • save() / load(session_id):JSON 持久化到 ~/.my-agent/sessions/<id>.json
    • Session.list_recent()resume_latest()
    • CLI:my-agent --resume <id>my-agent sessions list
  • src/my_agent/compact.py
    • should_compact(session, threshold) -> bool(按 token 估算)
    • compact(session) -> Session:旧消息让 LLM 总结,保留近 N 条原文;注意 tool_use/tool_result 配对不能拆开
    • 触发点:每轮 run_turn 开始时检查
  • src/my_agent/tools/task.py:Task 工具(spawn 子 agent)
    • 子 agent 拿到独立的 messages 列表(context 隔离)
    • 共享同一个 ToolRegistry 和 PermissionPolicy
    • 返回最终 text 给父 agent
  • src/my_agent/hooks.py
    • HookEvent = Literal["pre_tool_use", "post_tool_use", "stop"]
    • 钩子是 settings 里配的 shell 命令;run_hook(event, context_dict) -> HookResult(allow/deny/modify)
    • 在 agent loop 里调用:tool 执行前后
  • src/my_agent/llm/anthropic_style.py:扩展 provider 切换 —— base_url=None 时走官方 Claude API(用于线上 / 高质量任务),base_url=https://api.deepseek.com/anthropic 时走 DeepSeek(用于日常 / 省钱)
  • src/my_agent/llm/openai_style.py重新启用):作为 OpenAI 兼容兜底(DashScope、xAI、本地 vLLM 等),按需补全 tool_callsToolUseBlock 双向翻译;只在用户显式传 --provider openai 时启用
  • src/my_agent/llm/__init__.py::build_client(provider, model):根据配置返回对应 client

验证

my-agent chat "重构 src/,把 utils 拆成 io/text/math 三个子模块"
# 中途 ctrl+c
my-agent --resume latest
# 能继续

my-agent chat "用 5 个并行 sub-agent 分析 src/ 下每个目录的复杂度"
# Task 工具被调起

从样本学什么

  • ../claudecode/rust/crates/runtime/src/compact.rs::compact_session(消息对配对处理是关键,别拆 tool_use/tool_result 对)
  • ../claudecode/rust/crates/runtime/src/task_registry.rs::Task(子 agent 数据结构)
  • ../claudecode/rust/crates/runtime/src/hooks.rs(HookEvent + 配置驱动)
  • ../claudecode/rust/crates/runtime/src/session.rs(持久化字段)

Phase 4(可选)— 生态扩展

只在前面都稳了再做:

  • MCP 客户端:通过 stdio 跑外部 server(参考 ../claudecode/rust/crates/runtime/src/mcp_lifecycle_hardened.rs 的 JSON-RPC 协议处理)
  • Slash commands:用户输入 /clear/compact/agents 拦截在 LLM 前
  • Plugin 系统:用户可以扔 Python 脚本进 ~/.my-agent/plugins/ 自动加载新工具
  • Prompt caching:Anthropic 原生支持,OpenAI 兼容 provider 多数也支持,加 cache_control 标记
  • 健康检查命令my-agent doctor,验证 API key、工具可用性、配置正确性(参考 ../claudecode/install.shclaw doctor

推荐依赖(pyproject.toml)

[project]
dependencies = [
  "anthropic>=0.40",       # Phase 0 接 DeepSeek 的 Anthropic 端点;Phase 3 同一份代码切到官方 Claude
  "pydantic>=2.0",         # 工具 schema
  "typer>=0.12",           # CLI
  "rich>=13",              # TUI
  "python-dotenv>=1.0",
  "httpx>=0.27",           # 可能要直调
]
[project.optional-dependencies]
openai = ["openai>=1.50"]  # Phase 3+ 兜底 provider,需要时再装
dev = ["pytest>=8", "pytest-asyncio", "ruff", "mypy"]

验证(端到端)

每个阶段交付时都跑这套:

  1. 单元测试pytest tests/ —— 每个工具独立测试,agent loop 用 MockLLMClient
  2. 冒烟测试脚本scripts/smoke.sh,每阶段一组真实任务(写到 README)
  3. token 预算:用 mock client 跑功能验证;用真 client 时打印每轮 input/output token 数(DeepSeek 控制台也能看)
  4. 参考样本对照:每个 Phase 完成时,挑 2-3 个对应的样本文件做对比阅读,确认你的实现没漏关键细节

风险与注意事项

  1. DeepSeek Anthropic 代理的 tool 字段稳定性:早期版本有 tool_use.name 丢失 / 错位的 case;Phase 1 跑通第一个 tool 调用时强制打整条响应做一次校验,必要时在 client 加 fallback(按 input_schema 反查工具名)
  2. tool_choice 在 DeepSeek 上的行为:先固定 tool_choice="auto",遇到模型不主动调用工具的情况再考虑显式指定 —— 但 Anthropic 协议下 tool_choice 的对象格式与 OpenAI 不同,注意按 SDK 文档传
  3. 模型名带 [1m] 后缀:这是 DeepSeek Anthropic 代理的别名(1M 上下文版本),SDK 调用时直接当 model 字符串传即可;不能省略中括号
  4. 不要过早做 Rust 重写:你已经有样本的 Rust 代码可以读;Python 版的目的是吃透 agent loop 设计,性能远不是瓶颈
  5. 不要直接抄样本代码:claw 是 Rust + 高度工程化,照抄会陷入 boilerplate;只看设计、自己用 Python idiomatic 的方式写
  6. DeepSeek 的 thinking mode:deepseek-v4-pro 默认就是 reasoning 模型(响应里会带 thinking block),Phase 0/1 先 忽略 / 透传 thinking 内容,不要喂回下一轮 messages;Phase 3 做会话持久化时再决定要不要保留

关键参考文件清单(按 Phase 速查)

Phase 你要写 样本对照(先看这个)
0 agent.py, llm/anthropic_style.py runtime/src/conversation.rs::run_turn(前 50 行), api/src/types.rs(Anthropic message 部分)
1 tools/*.py, agent.py 升级 tools/src/lib.rs, runtime/src/file_ops.rs, runtime/src/bash.rs
2 permission.py, config.py, ui/render.py runtime/src/permissions.rs, runtime/src/config.rs, rusty-claude-cli/src/render.rs
3 session.py, compact.py, tools/task.py, hooks.py, llm/openai_style.py(兜底) runtime/src/session.rs, runtime/src/compact.rs, runtime/src/task_registry.rs, runtime/src/hooks.rs
4 mcp.py, plugins.py, doctor.py runtime/src/mcp_lifecycle_hardened.rs, plugins/src/lib.rs, install.sh

时间预算(粗估,单人 + 边学边写)

  • Phase 0:1-2 天
  • Phase 1:3-5 天(核心难点:tool 协议翻译)
  • Phase 2:5-7 天
  • Phase 3:7-10 天(核心难点:compact 的 tool_use/result 配对)
  • Phase 4:按需

累计到能用(Phase 0-2 完成):约 2 周 累计到对标 Claude Code 主要能力(Phase 0-3):约 3-4 周


下一步

Phase 0 已按 OpenAI 路径完成(见 taskFinished.md)。即将做的是 Phase 0.5 重构(详见上文 Phase 0 节末尾清单):把 llm/openai_style.py 替换为 llm/anthropic_style.py,env 变量切到 ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL / ANTHROPIC_MODEL,跑一次真实端到端,再进 Phase 1。types/agent/cli 的骨架不动,改动集中在 LLM 客户端这一层 + 一处 env 重命名。