零基础复现Claude Code(六):整合篇——组装Mini Claude Code

36 阅读10分钟

零基础复现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个英文单词。如果对话历史太长,会:

  1. 超出上下文窗口,API报错
  2. 即使没超,也会消耗大量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的起点

不适合的使用场景:

  • ❌ 在生产环境运行
  • ❌ 处理重要项目
  • ❌ 执行复杂的多步骤任务

如果你想用于实际工作,建议:

  1. 增加错误重试机制
  2. 增加更多工具(搜索代码、Git操作等)
  3. 使用Function Calling API而不是字符串解析
  4. 增加日志和监控

与真实代码的对照

在真实的Claude Code实现中(rust版本),这部分对应的是:

我们的实现真实代码位置关键差异
agent.py 入口crates/claw-cli/src/main.rs真实版支持更多命令行选项
ReActAgent.run()crates/runtime/src/conversation.rsConversationRuntime真实版有状态机、错误恢复
滑动窗口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. 坑1:忘记设置API Key

    • 后果:程序启动就报错
    • 正确做法:export OPENAI_API_KEY="sk-..."
  2. 坑2:在项目根目录运行Agent

    • 后果:可能误改重要文件
    • 正确做法:先cd test_workspace
  3. 坑3:滑动窗口设置太小

    • 后果:Agent"失忆",重复做相同的事
    • 正确做法:至少保留10轮历史
  4. 坑4:没有处理KeyboardInterrupt

    • 后果:用户按Ctrl+C时程序崩溃
    • 正确做法:用try-except捕获

下一步:让Agent看懂整个项目

现在你已经有了一个能用的Agent,但它有个明显的局限:

Agent只能处理你明确告诉它的文件。

比如,你说"帮我修Bug",它会问:"Bug在哪个文件?"你得告诉它"在main.py"。

但真实的Claude Code能做到:

  • 你说"帮我修Bug"
  • 它自己搜索整个项目,找到可能有Bug的文件
  • 它理解项目结构,知道哪些文件是核心、哪些是测试

下一篇,我们将实现代码搜索和项目理解

  1. 实现search_code(query)工具
  2. 让Agent能遍历整个项目
  3. 用关键词匹配找到相关文件
  4. 理解"上下文"的真正含义

这就是从"单文件Agent"到"项目级Agent"的关键一步。

预告一个核心问题:当项目有1000个文件时,Agent怎么知道该看哪些?答案在下一篇揭晓。


系列进度

  • ✅ 第1篇:总览与前置准备——Claude Code到底是什么?
  • ✅ 第2篇:地基篇——让模型开口说话(System Prompt的艺术)
  • ✅ 第3篇:灵魂篇——ReAct循环的骨架
  • ✅ 第4篇:双手篇——赋予读写文件的能力
  • ✅ 第5篇:终端篇——赋予执行命令的超能力
  • ✅ 第6篇:整合篇——组装Mini Claude Code
  • ⏭️ 第7篇:上下文篇——让Agent看懂整个文件夹
  • 第8篇:反思与展望——我们得到了什么,还缺什么?