跟踪文档三件套(保持动态更新):
- Plan.md(本文)— 策略路线图,仅在路线大调时变动
- taskFinished.md — 已实现并通过测试的工作项(按时间倒序追加)
- ToDo.md — 待办、已知问题、下一阶段步骤拆解
维护规则:每完成一项 Phase Step → 从 ToDo.md 移到 taskFinished.md。
为什么做这件事:从零搭一个类 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)+ 官方anthropicPython SDK;后续切回真正的 Anthropic API 几乎零改动。OpenAI Plus 是 ChatGPT 网页订阅,不附带 API quota,要用 OpenAI 模型还得单独充 API 余额
预期产出:分 5 个阶段(Phase 0 → 4)的可执行计划,每个阶段产出一个能 demo 的可运行版本,并标注从样本借鉴的具体文件 / 模式。
理由:
- Anthropic 的
content blocks(text / tool_use / tool_result 互相嵌套)是当前最干净的多轮工具协议;OpenAI 的tool_calls+toolrole 在多轮 + 多工具时容易拧巴 - 样本(claw)也是这么做的:
api/src/types.rs把 OpenAI/xAI/DashScope 都翻译成 Anthropic 风格内部表示
落地:定义 ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;Message = { role, content: list[ContentBlock] }。
理由:
- 跟决策 1 的内部协议对齐:内部用 Anthropic 风格 ContentBlock,wire format 也是 Anthropic 原生 → 客户端翻译层退化为"几乎透传",比 OpenAI 反向翻译省一大半坑(无需自己拼
tool_calls↔tool_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 替身理由:避免手写 JSON Schema 与执行函数签名的双重维护;样本的工具规范也是数据驱动(tools/src/lib.rs::mvp_tool_specs())。
理由:样本的 PRD(prd.json)就是按 story 跟踪 passes: bool;按阶段交付能让你随时掉头调整方向,不会"走到一半发现路线错了"。
┌─────────────────────────────────────────────────────┐
│ 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 已按 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.py:ContentBlock、Message数据类(用pydantic或dataclasses)—— ✅ 已完成src/my_agent/llm/base.py:LLMClient抽象类,签名chat(messages, tools, system) -> AssistantMessage—— ✅ 已完成src/my_agent/llm/anthropic_style.py:AnthropicStyleClient(auth_token, base_url, model);用anthropicSDK,base_url=https://api.deepseek.com/anthropic,model="deepseek-v4-pro[1m]"(或留空让用户自配)src/my_agent/agent.py:Agent.run(user_input):单轮,调 client,打印 text —— ✅ 已完成src/my_agent/cli.py:typer单命令chat;env 默认从ANTHROPIC_AUTH_TOKEN+ANTHROPIC_BASE_URL读tests/fixtures/mock_llm.py:可注入的 mock client(这是 0 阶段就要做的,否则后面每轮调试都要烧 token)—— ✅ 已完成
依赖:anthropic、pydantic、typer、rich、python-dotenv;openai 暂时移到 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 返回的contentblocks 直接映射到内部ContentBlock -
cli.py默认读ANTHROPIC_AUTH_TOKEN、ANTHROPIC_BASE_URL、ANTHROPIC_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 测试
能干什么:agent 能用 4 个核心工具自主完成"读这个目录、找包含 X 的文件、写一个总结到 out.md"这种任务。
关键文件:
src/my_agent/tools/base.py:Tool抽象基类:name: str、description: str、input_schema: dict、def 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_useblocks 直接落到内部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防死循环
能干什么: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 到 Pythonre)src/my_agent/permission.py:PermissionMode = Enum("read_only" | "ask" | "allow_all" | "danger")class PermissionPolicy:check(tool_name, input) -> Decision(allow/deny/ask)- 默认规则:Bash/Write/Edit 在
ask模式下要确认;read_only直接拒绝写入类工具 - 内置 deny 规则(如
rm -rf /、git push --force警告)
src/my_agent/config.py:load_settings()按优先级合并:CWD.my-agent/settings.json→~/.my-agent/settings.json→ 默认值;环境变量覆盖(ANTHROPIC_AUTH_TOKEN、ANTHROPIC_BASE_URL、ANTHROPIC_MODEL、MY_AGENT_PERMISSION_MODE)src/my_agent/prompt.py:扩展加载 CWD 的CLAUDE.md(或自定义AGENTS.md)拼到 system promptsrc/my_agent/llm/anthropic_style.py:改用流式(stream=True或messages.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)
能干什么:会话能保存能续接;上下文长了会自动压缩;agent 可以派发子任务给 sub-agent;可挂钩子;多 provider 可切换。
新增:
src/my_agent/session.py:Session类:messages、session_id、created_at、model、permission_modesave()/load(session_id):JSON 持久化到~/.my-agent/sessions/<id>.jsonSession.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_calls↔ToolUseBlock双向翻译;只在用户显式传--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(持久化字段)
只在前面都稳了再做:
- 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.sh和claw doctor)
[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"]每个阶段交付时都跑这套:
- 单元测试:
pytest tests/—— 每个工具独立测试,agent loop 用MockLLMClient测 - 冒烟测试脚本:
scripts/smoke.sh,每阶段一组真实任务(写到 README) - token 预算:用 mock client 跑功能验证;用真 client 时打印每轮 input/output token 数(DeepSeek 控制台也能看)
- 参考样本对照:每个 Phase 完成时,挑 2-3 个对应的样本文件做对比阅读,确认你的实现没漏关键细节
- DeepSeek Anthropic 代理的 tool 字段稳定性:早期版本有
tool_use.name丢失 / 错位的 case;Phase 1 跑通第一个 tool 调用时强制打整条响应做一次校验,必要时在 client 加 fallback(按 input_schema 反查工具名) tool_choice在 DeepSeek 上的行为:先固定tool_choice="auto",遇到模型不主动调用工具的情况再考虑显式指定 —— 但 Anthropic 协议下tool_choice的对象格式与 OpenAI 不同,注意按 SDK 文档传- 模型名带
[1m]后缀:这是 DeepSeek Anthropic 代理的别名(1M 上下文版本),SDK 调用时直接当model字符串传即可;不能省略中括号 - 不要过早做 Rust 重写:你已经有样本的 Rust 代码可以读;Python 版的目的是吃透 agent loop 设计,性能远不是瓶颈
- 不要直接抄样本代码:claw 是 Rust + 高度工程化,照抄会陷入 boilerplate;只看设计、自己用 Python idiomatic 的方式写
- DeepSeek 的 thinking mode:deepseek-v4-pro 默认就是 reasoning 模型(响应里会带
thinkingblock),Phase 0/1 先 忽略 / 透传 thinking 内容,不要喂回下一轮 messages;Phase 3 做会话持久化时再决定要不要保留
| 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 重命名。