零基础复现Claude Code(六):整合篇——组装Mini Claude Code
开篇:从零件到整机
第5篇的成就:我们给Agent装上了"终端"——它能执行命令、跑测试、验证自己的工作了。
现在,我们手里有了所有的零件:
- ✅ 大脑(LLMClient)—— 第2篇
- ✅ 推理循环(ReAct)—— 第3篇
- ✅ 双手(read_file、write_file)—— 第4篇
- ✅ 终端(run_cmd)—— 第5篇
但它们还是"散装"的——三个Python文件,没有统一的入口。
💡 回到"实习生"比喻:现在的Agent就像一个"组装到一半"的机器人。
大脑、双手、终端都有了,但还没有"启动按钮"。你每次想用它,都要手动导入模块、创建对象、调用方法...
这一篇,我们要给它装上"启动按钮"——一个命令行入口,让你能直接说:
python agent.py "帮我修Bug"然后Agent就开始工作了。
这一篇是从原型到工具的关键一步。
本节目标
读完这篇文章,你将:
- 看到完整的Agent运行:从用户输入到最终回答的全流程
- 理解对话历史管理:如何在Token预算内保留关键信息
- 掌握命令行入口设计:让Agent像真正的工具一样使用
- 获得成就感:你真的做出了一个能用的Agent!
原理深潜:完整数据流
📍 回到第一篇的架构图
还记得第一篇的三层架构吗?现在我们要把它们真正连接起来:
用户输入(命令行)
"python agent.py '帮我修Bug'"
↓
┌─────────────────────────────────────┐
│ main() 入口函数 │
│ - 解析命令行参数 │
│ - 创建Agent实例 │
│ - 调用agent.run() │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ ReAct循环(agent.run) │
│ while 未完成: │
│ 1. 调用LLM │
│ 2. 解析输出 │
│ 3. 执行工具 │
│ 4. 更新历史 │
└─────────────────────────────────────┘
↓
最终回答(打印到终端)
对话历史管理:滑动窗口策略
随着循环进行,messages列表会越来越长:
# 第1轮后:4条消息
messages = [system, user, assistant, observation]
# 第5轮后:12条消息
messages = [system, user, assistant, obs, assistant, obs, ...]
# 第10轮后:22条消息 → 可能超出Token限制!
问题:GPT-4的上下文窗口是8K Token,约6000个英文单词。如果对话历史太长,会:
- 超出上下文窗口,API报错
- 即使没超,也会消耗大量Token,成本高昂
解决方案:滑动窗口公式
保留消息 = [system] + [初始user] + 最近N轮对话
其中:
- 每轮对话 = 2条消息(assistant + observation)
- 保留轮数 N = 10
消息总数 = 2 + 2N = 2 + 20 = 22
可视化(messages列表结构):
[system] # 位置0(永远保留)
[初始user] # 位置1(永远保留)
[assistant_1, obs_1] # 第1轮 → 如果超过22条,这些会被丢弃
[assistant_2, obs_2] # 第2轮 → 如果超过22条,这些会被丢弃
...
[assistant_8, obs_8] # 第8轮 → 保留(最近10轮的一部分)
[assistant_9, obs_9] # 第9轮 → 保留
[assistant_10, obs_10] # 第10轮 → 保留(最新)
代码实现:
MAX_HISTORY = 10 # 只保留最近10轮对话
# 每次循环后
if len(messages) > MAX_HISTORY * 2 + 2: # system + user + 10轮 * 2条 = 22
# 保留system prompt和初始user消息 + 最后20条(最近10轮)
messages = [messages[0], messages[1]] + messages[-20:]
为什么是22和-20?
22= 2(system + 初始user)+ 10轮 × 2条/轮-20= 最近10轮 × 2条/轮
权衡:
- 保留太少:Agent会"忘记"之前做过什么
- 保留太多:Token预算爆炸
- 经验值:10-15轮是个好平衡点
循环终止的3种情况
while iteration < max_iterations:
# 情况1:模型输出Answer(任务完成)
if parsed['answer']:
return parsed['answer']
# 情况2:模型没有输出Action(卡住了)
if not parsed['action']:
return "任务未完成:模型没有输出下一步行动"
# 情况3:达到最大轮数(防止死循环)
if iteration >= max_iterations:
return "任务未完成:达到最大轮数限制"
动手实操:组装完整Agent
现在我们开始组装。目标是创建一个单文件、可运行的Agent。
第一步:创建统一入口(agent.py)
这是整合所有代码的主文件:
#!/usr/bin/env python3
"""
Mini Claude Code - 一个最小化的AI Agent实现
用法:python agent.py "你的任务"
"""
import sys
import os
# 导入前面篇章实现的模块
from llm_client import LLMClient
from tools import execute_tool
from react_agent import ReActAgent
def main():
"""命令行入口"""
# 🔑 解析命令行参数
if len(sys.argv) < 2:
print("用法: python agent.py '你的任务'")
print("示例: python agent.py '帮我写一个hello.py'")
sys.exit(1)
user_task = sys.argv[1]
# 🔑 创建Agent实例
print("🤖 Mini Claude Code 启动中...")
print(f"📝 任务: {user_task}\n")
agent = ReActAgent(max_iterations=15)
# 🔑 执行任务
try:
result = agent.run(user_task)
print("\n" + "=" * 60)
print("✅ 任务完成!")
print(f"📋 结果: {result}")
except KeyboardInterrupt:
print("\n\n⚠️ 用户中断")
except Exception as e:
print(f"\n\n❌ 错误: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
代码解读(约30行):
- 解析命令行参数
- 创建Agent实例
- 执行任务并打印结果
- 处理异常(用户中断、错误)
第二步:优化ReActAgent(核心循环)
在react_agent.py中,优化循环逻辑:
class ReActAgent:
def __init__(self, max_iterations=15):
self.client = LLMClient()
self.max_iterations = max_iterations
self.system_prompt = """你是一个Python工程师Agent。
可用工具:
- read_file(path): 读取文件
- write_file(path, content): 写入文件
- run_cmd(command): 执行命令(ls、cat、pytest等)
输出格式:
Thought: [思考]
Action: [工具调用]
任务完成时输出:
Thought: [总结]
Answer: [最终回答]
"""
def run(self, user_input):
"""运行ReAct循环"""
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_input}
]
print(f"用户:{user_input}\n")
print("=" * 60)
for iteration in range(self.max_iterations):
print(f"\n[第 {iteration + 1} 轮]")
# 🔑 步骤1:调用LLM
response = self.client.chat(messages)
parsed = self.parse_response(response)
# 🔑 步骤2:检查是否完成
if parsed['thought']:
print(f"💭 {parsed['thought']}")
if parsed['answer']:
print(f"✅ {parsed['answer']}")
return parsed['answer']
# 🔑 步骤3:执行Action
if parsed['action']:
print(f"🔧 {parsed['action']}")
observation = execute_tool(parsed['action'])
print(f"👀 {observation[:200]}...") # 截断显示
# 🔑 步骤4:更新历史
messages.append({"role": "assistant", "content": response})
messages.append({"role": "user", "content": f"Observation: {observation}"})
# 🔑 滑动窗口:保留最近10轮
if len(messages) > 22: # system + user + 10轮*2
messages = [messages[0], messages[1]] + messages[-20:]
else:
print("⚠️ 模型未输出Action")
break
return "任务未完成:达到最大轮数"
代码解读(约50行):
- 完整的ReAct循环
- 滑动窗口历史管理
- 友好的终端输出
第三步:文件组织结构
你的项目目录应该是这样的:
mini-claude-code/
├── agent.py # 主入口(本篇新增)
├── llm_client.py # LLM客户端(第2篇)
├── tools.py # 工具函数(第4、5篇)
├── react_agent.py # ReAct循环(第3篇,本篇优化)
├── test_workspace/ # 测试目录
└── README.md # 使用说明
完整代码:本篇展示的是核心逻辑(约80行),完整实现(包括parse_response、错误处理、日志)请查看GitHub仓库:github.com/your-repo/mini-claude-code
第四步:第一次完整运行!
现在,让我们测试完整的Agent:
# 设置API Key
export OPENAI_API_KEY="sk-..."
# 运行Agent
python agent.py "帮我写一个hello.py,输出Hello World,然后运行它"
你会看到完整的工作流:
🤖 Mini Claude Code 启动中...
📝 任务: 帮我写一个hello.py,输出Hello World,然后运行它
用户:帮我写一个hello.py,输出Hello World,然后运行它
============================================================
[第 1 轮]
💭 用户想让我创建一个hello.py文件并运行它
🔧 write_file('hello.py', 'print("Hello World")')
👀 成功:文件已保存到 hello.py
[第 2 轮]
💭 文件已创建,现在我应该运行它
🔧 run_cmd('python hello.py')
👀 Hello World
[第 3 轮]
💭 程序成功运行,输出了Hello World
✅ 任务完成!我创建了hello.py文件,内容是print("Hello World"),并成功运行,输出了Hello World。
============================================================
✅ 任务完成!
📋 结果: 任务完成!我创建了hello.py文件,内容是print("Hello World"),并成功运行,输出了Hello World。
恭喜!你的Mini Claude Code真的跑起来了!
> ⚠️ 使用建议
⚠️ 这是一个教学版Agent,不是生产级工具。
适合的使用场景:
- ✅ 学习Agent工作原理
- ✅ 在测试目录中做简单任务
- ✅ 作为更复杂Agent的起点
不适合的使用场景:
- ❌ 在生产环境运行
- ❌ 处理重要项目
- ❌ 执行复杂的多步骤任务
如果你想用于实际工作,建议:
- 增加错误重试机制
- 增加更多工具(搜索代码、Git操作等)
- 使用Function Calling API而不是字符串解析
- 增加日志和监控
与真实代码的对照
在真实的Claude Code实现中(rust版本),这部分对应的是:
| 我们的实现 | 真实代码位置 | 关键差异 |
|---|---|---|
agent.py 入口 | crates/claw-cli/src/main.rs | 真实版支持更多命令行选项 |
ReActAgent.run() | crates/runtime/src/conversation.rs 的 ConversationRuntime | 真实版有状态机、错误恢复 |
| 滑动窗口 | crates/runtime/src/compact.rs | 真实版用语义压缩而不是简单截断 |
想深入研究的读者:
- 打开
crates/claw-cli/src/main.rs,搜索fn main,你会看到完整的CLI入口 - 打开
crates/runtime/src/conversation.rs,可以看到状态机的实现
成就感时刻:你做到了什么?
让我们回顾一下这6篇文章的旅程:
第1篇:建立心智模型
- 理解了Agent = LLM + Tools + Loop
第2篇:让模型开口说话
- 实现了LLMClient,掌握了System Prompt
第3篇:实现ReAct循环
- 理解了Think→Act→Observe的循环
第4篇:赋予读写文件能力
- 实现了read_file和write_file
第5篇:赋予执行命令能力
- 实现了run_cmd,学会了安全过滤
第6篇:组装完整Agent
- 把所有零件组装成可用的工具
你现在拥有的能力:
- ✅ 一个约300行的可运行Agent
- ✅ 理解Agent的每个齿轮如何转动
- ✅ 能够扩展和定制你的Agent
- ✅ 看懂工业级Agent的实现原理
📝 自检清单(读完本篇请确认)
在进入下一篇之前,请确认你能回答以下问题:
- 为什么需要滑动窗口管理对话历史?
- 循环终止的3种情况是什么?
- 如何通过命令行使用你的Agent?
- 你能说出教学版和生产版的3个主要差异吗?
- 你能独立运行你的Mini Claude Code吗?
如果都能回答,恭喜你,你已经完成了Agent的核心实现!下一篇见!
⚠️ 新手容易踩的坑
-
坑1:忘记设置API Key
- 后果:程序启动就报错
- 正确做法:
export OPENAI_API_KEY="sk-..."
-
坑2:在项目根目录运行Agent
- 后果:可能误改重要文件
- 正确做法:先
cd test_workspace
-
坑3:滑动窗口设置太小
- 后果:Agent"失忆",重复做相同的事
- 正确做法:至少保留10轮历史
-
坑4:没有处理KeyboardInterrupt
- 后果:用户按Ctrl+C时程序崩溃
- 正确做法:用try-except捕获
下一步:让Agent看懂整个项目
现在你已经有了一个能用的Agent,但它有个明显的局限:
Agent只能处理你明确告诉它的文件。
比如,你说"帮我修Bug",它会问:"Bug在哪个文件?"你得告诉它"在main.py"。
但真实的Claude Code能做到:
- 你说"帮我修Bug"
- 它自己搜索整个项目,找到可能有Bug的文件
- 它理解项目结构,知道哪些文件是核心、哪些是测试
下一篇,我们将实现代码搜索和项目理解:
- 实现
search_code(query)工具 - 让Agent能遍历整个项目
- 用关键词匹配找到相关文件
- 理解"上下文"的真正含义
这就是从"单文件Agent"到"项目级Agent"的关键一步。
预告一个核心问题:当项目有1000个文件时,Agent怎么知道该看哪些?答案在下一篇揭晓。
系列进度
- ✅ 第1篇:总览与前置准备——Claude Code到底是什么?
- ✅ 第2篇:地基篇——让模型开口说话(System Prompt的艺术)
- ✅ 第3篇:灵魂篇——ReAct循环的骨架
- ✅ 第4篇:双手篇——赋予读写文件的能力
- ✅ 第5篇:终端篇——赋予执行命令的超能力
- ✅ 第6篇:整合篇——组装Mini Claude Code
- ⏭️ 第7篇:上下文篇——让Agent看懂整个文件夹
- 第8篇:反思与展望——我们得到了什么,还缺什么?