零基础复现Claude Code(七):上下文篇——让Agent看懂整个文件夹

0 阅读12分钟

零基础复现Claude Code(七):上下文篇——让Agent看懂整个文件夹

开篇:从"单文件"到"项目级"

第6篇的成就:我们组装了完整的Mini Claude Code——一个能通过命令行使用、能读写文件、能执行命令的Agent。

但现在有个明显的局限:

你:python agent.py "帮我修Bug"
Agent:Bug在哪个文件?
你:在main.py
Agent:好的,我去看看...

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

💡 回到"实习生"比喻:现在的Agent就像一个"只会按指示干活"的实习生。

你说:"帮我修Bug。" 他问:"Bug在哪个文件?" 你说:"你自己找啊!" 他愣住了:"我不知道怎么找...我只会看你告诉我的文件..."

真正有用的实习生应该能:

  • 自己搜索整个项目
  • 找到可能有Bug的文件
  • 理解项目结构(哪些是核心代码,哪些是测试)

这一篇,我们要给实习生"搜索能力"——让他能在整个项目中找到相关代码。

这一篇是**从"单文件Agent"到"项目级Agent"**的关键跃升。

本节目标

读完这篇文章,你将:

  • 理解上下文的真正含义:不是"把所有文件都读一遍",而是"找到相关的文件"
  • 掌握简陋版RAG原理:不用向量数据库,用关键词匹配实现代码搜索
  • 实现search_code工具:让Agent能在整个项目中搜索关键词
  • 看到Agent的自主搜索:用户只说"修Bug",Agent自己找到Bug在哪

原理深潜:上下文的本质问题

📍 回到第一篇的问题

还记得第一篇的场景吗?

"你接手了一个23个Python文件的项目,产品经理说有个Bug,但你不知道Bug在哪个文件..."

现在我们的Agent也面临同样的问题:当项目有几十个文件时,Agent怎么知道该看哪些?

问题定义:Token预算的困境

假设一个中型Python项目:

项目统计:
- 50个.py文件
- 每个文件平均200行
- 总共10,000行代码
- 约250,000个Token

GPT-4上下文窗口:8K Token

问题:你不可能把所有文件都塞进上下文窗口。

三种错误的解决方案:

方案A:把所有文件都读一遍

10,000行代码 ≈ 250K Token >> 8K Token
→ 超出上下文窗口,API报错

方案B:让用户指定文件

用户:帮我修Bug
Agent:Bug在哪个文件?
用户:我要是知道就不用你了!

方案C:随机读几个文件碰运气

Agent:我随机读了5个文件,没找到Bug...
用户:...

正确的解决方案:搜索相关文件

用户:帮我修登录失败的Bug
Agent:我搜索"login"关键词...
      找到3个相关文件:auth.py, user.py, api.py
      我先看auth.py...

简陋版RAG原理

RAG(Retrieval-Augmented Generation)= 检索增强生成。核心思想:

不是把所有信息都给模型,而是先检索相关信息,再生成回答

工业级RAG流程

1. 离线:把所有代码切片 → 向量化 → 存入向量数据库
2. 在线:用户问题 → 向量化 → 搜索相似向量 → 返回TopK文件

我们的简陋版RAG流程

1. 在线:用户问题 → 提取关键词 → 遍历所有文件 → 关键词匹配 → 返回匹配文件

公式化表达

相关文件 = TopK( 匹配分数(用户问题, 文件内容) )

其中:
- 匹配分数 = 关键词在文件中出现的次数
- TopK = 返回得分最高的K个文件(通常K=3-5)

对比教学

维度我们的简陋版工业级RAG
索引方式无索引,实时搜索向量数据库(FAISS、Pinecone)
匹配方式关键词匹配语义相似度
速度慢(遍历所有文件)快(向量检索)
准确度低(只能匹配字面)高(理解语义)
复杂度简单(50行代码)复杂(需要额外依赖)

为什么我们用简陋版?

因为我们的目标是"理解原理",而不是"做生产级工具"。简陋版能让你看清楚:

  • 什么是"检索"(遍历文件,匹配关键词)
  • 什么是"增强"(把搜索结果加入上下文)
  • 什么是"生成"(模型根据搜索结果回答)

真实Claude Code的代码索引

真实的Claude Code用的是更高级的方案:

代码图谱(Code Graph)

  • 用tree-sitter解析代码,构建AST(抽象语法树)
  • 提取函数、类、变量的定义和引用关系
  • 构建"谁调用了谁"的图谱

LSIF(Language Server Index Format)

  • 编译时生成代码索引
  • 支持"跳转到定义"、"查找引用"等IDE功能
  • 比简单的关键词搜索精确得多

语义搜索

  • 用代码专用的Embedding模型(如CodeBERT)
  • 理解代码的语义,而不只是字面匹配
  • 例如:搜索"登录"能找到authenticate()函数

我们的简陋版 vs 真实版的差距

用户:帮我找到处理用户登录的代码

简陋版:搜索"登录"关键词 → 找到包含"登录"字符串的文件
真实版:理解"用户登录"的语义 → 找到authenticate()、login()、verify_credentials()等函数

动手实操:实现代码搜索工具

现在我们开始写代码。目标是实现一个简陋但能用search_code工具。

第一步:实现search_code工具(核心逻辑)

tools.py中添加:

import os

def search_code(query: str, directory: str = ".") -> str:
    """
    在目录中搜索包含关键词的代码文件
    
    参数:
        query: 搜索关键词
        directory: 搜索目录(默认当前目录)
    
    返回:
        匹配结果(文件路径 + 匹配行)
    """
    results = []
    query_lower = query.lower()
    
    try:
        # 🔑 遍历目录中的所有.py文件
        for root, dirs, files in os.walk(directory):
            # 跳过常见的无关目录
            dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', 'node_modules', '.venv']]
            
            for file in files:
                # 只搜索Python文件
                if not file.endswith('.py'):
                    continue
                
                file_path = os.path.join(root, file)
                
                try:
                    with open(file_path, 'r', encoding='utf-8') as f:
                        lines = f.readlines()
                    
                    # 🔑 检查每一行是否包含关键词
                    matches = []
                    for i, line in enumerate(lines, 1):
                        if query_lower in line.lower():
                            matches.append(f"  行{i}: {line.strip()}")
                    
                    # 如果有匹配,记录文件
                    if matches:
                        results.append(f"\n📄 {file_path}")
                        results.extend(matches[:3])  # 每个文件最多显示3行
                        if len(matches) > 3:
                            results.append(f"  ... (还有{len(matches)-3}处匹配)")
                
                except Exception:
                    continue  # 跳过无法读取的文件
        
        # 🔑 限制返回结果数量
        if len(results) > 50:
            results = results[:50]
            results.append("\n... (结果太多,已截断)")
        
        if not results:
            return f"未找到包含'{query}'的文件"
        
        return "\n".join(results)
    
    except Exception as e:
        return f"搜索错误:{str(e)}"

代码解读(约60行,符合≤80行要求):

  • os.walk遍历目录
  • 跳过.git__pycache__等无关目录
  • 只搜索.py文件(可扩展到其他语言)
  • 关键词匹配(不区分大小写)
  • 每个文件最多显示3行匹配
  • 限制总结果数量,防止输出爆炸

第二步:集成到工具分发器

tools.pyexecute_tool函数中添加:

def execute_tool(action: str) -> str:
    tool_name, args = parse_action(action)
    
    # ... 已有的read_file、write_file、run_cmd ...
    
    elif tool_name == "search_code":
        if len(args) < 1:
            return "错误:search_code需要1-2个参数(关键词, [目录])"
        query = args[0]
        directory = args[1] if len(args) > 1 else "."
        return search_code(query, directory)
    
    else:
        return f"错误:未知工具 - {tool_name}"

第三步:更新Agent的System Prompt

react_agent.py中更新System Prompt:

self.system_prompt = """你是一个Python工程师Agent。

可用工具:
- read_file(path): 读取文件内容
- write_file(path, content): 写入文件
- run_cmd(command): 执行Shell命令
- search_code(query, [directory]): 搜索包含关键词的代码文件

工作流程建议:
1. 如果用户没有指定文件,先用search_code搜索相关代码
2. 找到相关文件后,用read_file读取详细内容
3. 修改代码后,用run_cmd运行测试验证

输出格式:
Thought: [思考]
Action: [工具调用]
"""

第四步:测试自主搜索

创建测试文件test_search.py

from react_agent import ReActAgent
import os

# 创建测试项目结构
os.makedirs("test_project/auth", exist_ok=True)
os.makedirs("test_project/api", exist_ok=True)

# 创建几个测试文件
with open("test_project/auth/login.py", "w") as f:
    f.write("""
def authenticate(username, password):
    # Bug: 密码验证逻辑有问题
    if username == "admin" and password == "123":
        return True
    return False
""")

with open("test_project/api/user.py", "w") as f:
    f.write("""
def get_user(user_id):
    return {"id": user_id, "name": "User"}

def login_user(username, password):
    from auth.login import authenticate
    return authenticate(username, password)
""")

# 创建Agent
agent = ReActAgent(max_iterations=15)

# 任务:让Agent自己找到登录相关的代码
result = agent.run("""
我们的登录功能有Bug,用户反馈密码验证不安全。
请帮我找到登录相关的代码,并分析问题。
""")

print("\n" + "=" * 60)
print(f"最终结果:{result}")

运行后你会看到Agent的自主搜索:

[第 1 轮]
💭 用户说登录功能有Bug,我应该先搜索登录相关的代码
🔧 search_code('login', 'test_project')
👀 📄 test_project/auth/login.py
  行2: def authenticate(username, password):
  行3:     # Bug: 密码验证逻辑有问题
  
  📄 test_project/api/user.py
  行5: def login_user(username, password):
  行7:     return authenticate(username, password)

[第 2 轮]
💭 找到了两个相关文件,我先看auth/login.py的详细内容
🔧 read_file('test_project/auth/login.py')
👀 def authenticate(username, password):
    # Bug: 密码验证逻辑有问题
    if username == "admin" and password == "123":
        return True
    return False

[第 3 轮]
💭 我看到问题了!密码硬编码为"123",这非常不安全
✅ 找到了Bug:在test_project/auth/login.py的authenticate函数中,密码被硬编码为"123",这是严重的安全问题。应该使用密码哈希和数据库验证。

恭喜!你的Agent现在能自主搜索整个项目了!

> ⚠️ 性能与局限性

⚠️ 我们的简陋版搜索有明显的性能问题。

性能瓶颈:

  • 每次搜索都遍历所有文件(O(n)复杂度)
  • 大项目(1000+文件)可能需要几秒钟
  • 没有缓存,重复搜索浪费时间

改进方向:

  1. 建立索引:第一次遍历时建立文件索引,后续搜索直接查索引
  2. 并行搜索:用多线程同时搜索多个文件
  3. 智能过滤:根据文件大小、修改时间等启发式规则优先搜索

真实Claude Code的方案:

  • 用tree-sitter构建代码AST
  • 用Language Server Protocol(LSP)提供实时索引
  • 用向量数据库支持语义搜索

与真实代码的对照

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

我们的实现真实代码位置关键差异
search_code()crates/runtime/src/file_ops.rsgrep_search()真实版用ripgrep,支持正则、多线程
关键词匹配crates/lsp/ 模块真实版用LSP,支持语义搜索
文件遍历glob_search()真实版支持glob模式、gitignore过滤

想深入研究的读者

  • 打开crates/runtime/src/file_ops.rs,搜索grep_search,你会看到基于ripgrep的实现
  • 打开crates/lsp/src/manager.rs,可以看到LSP集成的代码

完整代码:本篇展示的是核心逻辑(约70行),完整实现(包括索引缓存、并行搜索)请查看GitHub仓库。

代码搜索的3个设计原则

通过上面的实现,我们总结出设计代码搜索工具的3个原则:

原则1:先搜索,再读取

❌ 低效的方式:

Agent:我读取所有文件,看看哪个有Bug...
→ 浪费Token,可能超出上下文窗口

✅ 高效的方式:

Agent:我先搜索"login"关键词,找到3个相关文件
      然后只读取这3个文件的详细内容
→ 节省Token,精准定位

原则2:限制搜索范围

❌ 危险的搜索:

search_code("password", "/")  # 搜索整个文件系统
→ 可能搜索到系统文件、敏感文件

✅ 安全的搜索:

search_code("password", "./project")  # 只搜索项目目录
→ 限制在安全范围内

原则3:截断搜索结果

❌ 不截断:

搜索"import"关键词 → 返回1000个匹配 → Token爆炸

✅ 截断:

搜索"import"关键词 → 返回前50个匹配 → 可控

📝 自检清单(读完本篇请确认)

在进入下一篇之前,请确认你能回答以下问题:

  • 为什么不能把所有文件都读进上下文窗口?
  • 什么是RAG?我们的简陋版和工业级的区别是什么?
  • search_code工具的核心逻辑是什么?
  • 为什么要跳过.git__pycache__等目录?
  • 你能说出简陋版搜索的3个主要局限吗?

如果都能回答,恭喜你,Agent的"搜索能力"部分你已经掌握了。下一篇见!

⚠️ 新手容易踩的坑

  1. 坑1:搜索整个文件系统

    • 后果:搜索到系统文件,耗时长,可能泄露敏感信息
    • 正确做法:限制搜索目录为项目根目录
  2. 坑2:不跳过无关目录

    • 后果:搜索.gitnode_modules等目录,浪费时间
    • 正确做法:用黑名单过滤无关目录
  3. 坑3:不限制搜索结果数量

    • 后果:搜索常见关键词(如"import")返回几千个结果
    • 正确做法:限制每个文件显示3行,总结果显示50个
  4. 坑4:用正则表达式搜索

    • 问题:正则表达式复杂,容易出错
    • 正确做法:简陋版用简单的字符串匹配就够了

下一步:回顾与展望

现在你已经完成了Mini Claude Code的所有核心功能:

  • ✅ 大脑(LLM + System Prompt)
  • ✅ 推理循环(ReAct)
  • ✅ 双手(read_file、write_file)
  • ✅ 终端(run_cmd)
  • ✅ 搜索(search_code)

你的Agent现在能做什么?

  • 自主搜索整个项目
  • 找到相关代码文件
  • 读取、修改、测试代码
  • 完成"修Bug"、"加功能"等任务

但它还缺什么?

下一篇(最后一篇),我们将进行反思与展望

  1. 回顾这8篇文章的旅程
  2. 诚实地对比:我们的Mini版 vs 真实Claude Code
  3. 列出我们没有实现的关键功能
  4. 给出"下一步学习路线图"

这就是从"教学原型"到"理解工业级实现"的最后一步。

预告一个核心问题:我们的Agent成功率有多高?如果它犯错了,怎么自动恢复?答案在下一篇揭晓。


系列进度

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