构建Claude Code的代理框架

akshay_pachaar 发布于 2026-07-16 阅读 39

文章详细介绍了如何构建一个编码Agent的 harness(即围绕模型的代码层),以 Claude Code 为例,使用 CrewAI 框架逐步实现核心循环、规划、子代理、沙箱、内存和检查点等功能。作者指出,代理的能力主要在于 harness 而非模型本身,并展示了如何通过 CrewAI 的配置实现这些功能。文章还讨论了 prompt、执行环境和工具选择等仍需开发者自行处理的部分,并指出了 harness 的成本和随着模型改进可能不再必要的一些限制。

Claude Code 的 harness 如何工作

Claude Code 的核心是一个简单的代理循环。你发送一条消息,模型决定下一步做什么,它要么直接回复,要么请求一个工具。如果它请求了工具,工具就会运行,结果会返回到对话中,然后模型再次做出决策。

这一过程会重复,直到模型返回一个最终答案,且不再调用任何工具为止。

在这个循环中,模型会读取文件、编辑代码、运行 Shell 命令并执行测试。这些并非独立的模式,它们只是同一个循环中的不同工具调用。

然而,仅凭循环本身并不足以构建一个可靠的编码 Agent。Claude Code 在此基础上添加了规划、文件工具、子代理、内存以及权限和沙箱系统。这些层并不取代循环,而是使其足够安全和可靠,能够用于实际工作。

图像

这就是我们将要重建的架构:先建立核心循环,再在其上叠加每一层,并将每一层对应到处理它的 CrewAI 功能。

核心代理循环

该循环按照相同的顺序运行,直到任务完成:

要求模型执行任务。

模型直接回复或请求一个或多个工具。

如果请求了工具,则运行这些工具并将结果返回给模型。

使用更新后的对话重复上述过程。

当模型回复且未请求任何工具时,任务完成。

图像

while True:
    reply = model(messages, tools)
    calls = [b for b in reply if b.type == "tool_use"]
    if not calls:            # 纯文本,无工具调用:任务完成
        return reply.text
    messages += [reply, run_all(calls)]

每次工具调用完成一个步骤,为模型提供新信息,并影响下一个决策。一个简单的问题可能一次迭代就完成,而修复一个复杂的 bug 或重构一个大型代码库可能需要数十次迭代,模型才能获得足够的信息来生成最终答案。

CrewAI 在你创建代理时就会自动提供此执行循环。你无需自己实现 while 循环,只需定义代理并为其分配任务即可。

构建第一个代理

让我们创建一个简单的 Bug 修复代理。

from crewai import LLM, Agent, Crew, Task

bug_fixer = Agent(
    role="Bug 修复者",
    goal="在代码库中找到并描述所报告 bug 的修复方法。",
    backstory="你通过读取目录和文件来构建对代码的准确理解。",
    llm="claude-sonnet-4-6",
)

task = Task(
    description="找到针对 {objective} 的修复方法。",
    expected_output="修复方法的简短描述以及它所属的文件。",
)

result = Crew(agents=[bug_fixer], tasks=[task]).kickoff(
    inputs={"objective": "account.py 中的透支 bug"}
)

这里有三个概念需要理解:

Agent 通过其角色、目标、LLM 和工具来定义谁来执行工作。

Task 描述任务内容。

Crew 将代理和任务组合在一起。调用 kickoff() 会运行前面描述的执行循环,无论底层模型是 Anthropic、OpenAI、Google 还是其他。

为代理提供工具

工具能够让一个只能生成文本的模型实际作用于代码库。它们可以读取文件、写入文件、运行 Shell 命令以及调用外部 API。

CrewAI 内置了文件系统工具:

FileReadTool 读取文件。

DirectoryReadTool 列出目录。

FileWriterTool 写入文件。

from crewai_tools import DirectoryReadTool, FileReadTool, FileWriterTool

read_file = FileReadTool()
write_file = FileWriterTool()
list_dir = DirectoryReadTool()

filesystem_tools = [read_file, write_file, list_dir]

这些工具还能充当外部内存。代理可以将大型搜索结果写入文件,只保留文件名,并在需要时重新读取,而不是将结果一直保存在模型的上下文窗口中。

这样可以让上下文窗口更小,模型更专注,这被 Anthropic 称为上下文工程

图像

内置工具只覆盖常见的工作流程。对于更特定的需求,你可以使用 @tool 装饰器将 Python 函数暴露为工具。

函数的文档字符串充当说明书,告诉模型该工具的作用、何时使用以及期望的输入。

from crewai.tools import tool
import subprocess

@tool("run_tests")
def run_tests(path: str = "tests/") -> str:
    """在指定路径运行 pytest 套件并返回结果。"""
    result = subprocess.run(
        ["pytest", path, "-q"], capture_output=True, text=True, timeout=120
    )
    output = result.stdout + result.stderr
    return output[-4000:] if len(output) > 4000 else output

规划长时间运行的任务

随着任务变得越来越复杂,单纯的执行循环开始丢失原始目标。经过足够多的工具调用、文件读取和中间结果后,上下文被填满,目标被后续的一切所淹没。

这种缓慢的退化被称为上下文腐烂(context rot)。

规划直接解决了这个问题。代理在执行任何工作之前会构建一个逐步的计划,并在整个执行过程中将该计划保留在上下文中。

计划本身并不执行工作。它是一张路线图,用于将模型与原始目标保持联系,这与 Claude Code 的待办事项列表功能相同。

图像

CrewAI 通过在 Crew 级别设置 planning=True 来添加此功能。它会在执行前生成一个计划,并在任务进行过程中保持其可用。

from crewai import Crew, LLM

crew = Crew(
    agents=self.agents,
    tasks=self.tasks,
    planning=True,
    planning_llm=LLM(model="gpt-4o-mini"),
)

注意:默认情况下,CrewAI 使用 gpt-4o-mini 进行规划,你可以将其替换为任何你喜欢的 LLM。

单个代理也可以通过设置 reasoning=True 来推理自己的任务:

from crewai import Agent

bug_fixer = Agent(
    role="Bug 修复者",
    goal="在代码库中找到并描述所报告 bug 的修复方法。",
    backstory="你通过读取目录和文件来构建对代码的准确理解。",
    tools=[FileReadTool()],
    reasoning=True,
    max_reasoning_attempts=3  # 可选:设置最大推理尝试次数
)

规划和推理解决不同的问题。规划为整体任务构建一个高级路线图,而推理则给单个代理在行动前思考其方法的时间。

当启用推理时,代理会:

反思任务并起草执行计划。

评估计划是否准备就绪。

如有必要,优化计划,直到满意或达到 max_reasoning_attempts

在执行前将最终确定的推理计划注入到任务中。

图像

它们共同使代理在长时间运行的任务中保持锚定,并减少偏离原始目标的情况。

通过子代理进行委派

规划能让代理保持专注,但并不能减少模型必须保存的信息量。在大型代码库中,即使是一个精心规划的任务也可能超出单个上下文窗口的限制。

要找到一个 bug 可能需要读取几十个文件,而主代理不需要将所有文件都保存在内存中。

子代理通过委派来解决这个问题。主代理将特定任务交给一个辅助代理,辅助代理在自己的上下文中工作并返回一个简短摘要。主代理只看到结论,而不是中间步骤。

图像

CrewAI 通过层级工作流程支持这一点,其中管理器代理将任务委派给专门代理,并组合它们的结果。

在我们之前的设置中,一个 Bug 修复者代理承担了所有繁重的工作。现在让我们将工作拆分为一个管理器和三个专门代理:

  • 代码库探索者:探索代码并映射仓库。
  • 软件工程师:实施请求的更改。
  • 测试运行者:在沙箱中运行测试并报告通过或失败。
  • 工程主管:监督这三个专门代理。

图像

from crewai import Crew, Agent, Task, Process

explorer = Agent(
    role="代码库探索者",
    goal="映射仓库并找出与任务相关的文件。",
    backstory="你读取目录和文件来构建对代码的理解。",
    tools=[read_file, list_dir],
    llm=llm,
)  # 其他两个专门代理同理

manager = Agent(
    role="工程主管",
    goal="将请求分解为步骤,并将每个步骤委派给合适的专门代理。",
    backstory="你决定谁做什么,审查测试,在变更完成后结束任务。",
    llm=llm,
    allow_delegation=True,
)

crew = Crew(
    agents=[explorer, coder, tester],
    tasks=[task],
    manager_agent=manager,
    process=Process.hierarchical,
)

需要注意的是,allow_delegation 默认是禁用的,因此必须在管理器上显式启用。

沙箱化:保护代理执行

拥有 Shell 访问权限的代理可能会运行破坏性命令,而告诉模型不要这样做并不是一种安全措施。

真正的保护来自两个层面:

  • 权限系统:要求对敏感操作进行审批。
  • 沙箱:隔离执行环境,使得即使批准的命令也无法触及宿主机。

Anthropic 采用了相同的方法。将代码执行移到沙箱中可以减少用户需要审批操作的频率,同时仍然保护宿主机系统。

图像

CrewAI 中的沙箱化

在沙箱内而非宿主机上执行代码,就应用了第二个层面。在此设置中,代码在 E2B 内部运行,E2B 会为每个会话启动一个新的虚拟机,并在会话结束后销毁它。

Shell 命令和 Python 代码完全在该隔离环境中运行。

图像

from crewai_tools import E2BExecTool, E2BPythonTool
sandbox_tools = [E2BExecTool(), E2BPythonTool()]  # 运行测试 / 运行代码

人在回路中的审批

在 Task 上设置 human_input=True 会使 Crew 在生成答案后暂停。你审查输出,然后批准它或将其发回进行另一次迭代。

当执行到达该任务时,CrewAI 会通过标准输入等待你的反馈。

from crewai import Task

task = Task(
    description=(
        "在工作目录 ./workspace 中,{objective}。"
        "先探索代码,进行更改,然后运行测试并报告。"
    ),
    expected_output="更改的文件摘要和最终测试输出。",
    human_input=True,
)

如果你的 Crew 运行在 Web 应用或聊天界面(而非终端)后面,CrewAI 基于 Webhook 的人在回路系统会处理相同的审查步骤。

内存与检查点

默认情况下,代理在一次运行结束后会忘记所有内容。第二天回来修复同一个项目中的另一个 bug,它会从头开始。

有两种机制可以让代理跨运行携带信息,各自服务于不同的目的:

  • 检查点:在运行期间保存代理状态,以便在中断后恢复,或从同一点沿不同路径继续。
  • 持久内存:跨不同对话存储事实,包括项目偏好,例如“在完成前始终格式化最终代码”。

图像

CrewAI 中的内存

CrewAI 提供了一个统一的内存接口,而非独立的短期、长期、实体和外部内存类型。保存时,它使用 LLM 识别重要细节,组织它们,并使它们以后可检索。

在 Crew 上设置 memory=True 可使其跨运行具有记忆能力。每次任务完成后,CrewAI 从输出中提取有用的事实并存储它们;在未来的运行中,它会检索相关记忆并将其添加到任务提示中。

图像

from crewai import Crew

crew = Crew(
    agents=[explorer, coder, tester],
    tasks=[task],
    memory=True,
)

Crew 中的所有代理共享其内存,除非某个代理被赋予了它自己的内存。

CrewAI 中的检查点

检查点是代理进度的快照,包括其配置、任务状态、内存、中间结果、输入和执行历史。

默认情况下,CrewAI 会在任务完成时创建一个检查点,从而允许工作流在中断后从该点恢复。

检查点可以存在于两个内置存储之一:

  • JsonProvider:将每个检查点保存为独立的 JSON 文件,易于阅读和手动检查。
  • SqliteProvider:将所有检查点保存在单个 SQLite 数据库中,在频繁检查点创建和较大工作负载下表现更好。

图像

from crewai import Crew

crew = Crew(
    agents=[explorer, coder, tester],
    tasks=[task],
    checkpoint=True,
)

Crew、Flow 和 Agent 都接受 checkpoint 参数,子级会继承父级的值,除非它们设置了自身的值。

整合在一起

以下是一个任务上使用完整 harness 的示例,包含执行循环、工具、规划、子代理、沙箱化和内存协同工作:

from crewai import Agent, Crew, LLM, Process, Task
from crewai.tools import tool
from crewai_tools import (DirectoryReadTool, FileReadTool, FileWriterTool,
E2BExecTool, E2BPythonTool)

llm = LLM(model="anthropic/claude-sonnet-4.6")

list_dir = DirectoryReadTool(directory="./workspace")
filesystem_tools = [FileReadTool(), FileWriterTool(), list_dir]
sandbox_tools = [exec_tool, E2BPythonTool()]

@tool("run_tests")
def run_tests(path: str = "tests/") -> str:
    """将 ./workspace 同步到沙箱,然后在那里运行 pytest。"""
    return E2BExecTool().run(command=sync_and_test_command(path))

explorer = Agent(role="代码库探索者", goal="映射仓库,找出相关文件。",
    tools=[read_file, list_dir], llm=llm)
coder = Agent(role="软件工程师", goal="实施请求的更改。",
    tools=filesystem_tools, reasoning=True, llm=llm)
tester = Agent(role="测试运行者", goal="在沙箱中运行测试,报告通过/失败。",
    tools=sandbox_tools + [read_file] + [run_tests], llm=llm)
manager = Agent(role="工程主管", goal="委派步骤,测试通过后完成。",
    allow_delegation=True, llm=llm)

task = Task(
    description="在 ./workspace 中,{objective}。探索、编辑、测试、报告。",
    expected_output="更改摘要和测试输出。", human_input=True,
)
crew = Crew(
    agents=[explorer, coder, tester], tasks=[task],
    manager_agent=manager, process=Process.hierarchical,
    planning=True, memory=True, checkpoint=True,
)
result = crew.kickoff(inputs={"objective": "修复 account.py 中失败的测试"})

当成功可以自动检查时,代理 harness 最容易评估。测试套件为代理提供了具体的目标,因此它可以规划、编辑、测试并重复,直到所有测试通过。

为此,我们在一个小型代码库上进行了测试:一个包含两个真实 bug 和五个测试(其中三个失败)的 BankAccount 类。规则是只修复实现,不修改测试。

这反映了 Anthropic 在内部评估编码 Agent 的方式。一个已发布的示例中,Claude 针对一大套失败测试重建了 claude.ai 界面的克隆版本。

在这里,harness 将项目从 3 个失败 2 个通过变成了全部 5 个通过,而“只改实现”的规则堵住了编辑或删除失败测试的捷径。

图像

仍然属于你的职责

系统的某些部分并不是框架为你构建的:

  • 提示词。每个代理的行为来自其角色、目标和背景故事。要正确设置这些,需要测试和迭代,没有任何配置标志可以替代。
  • 执行环境。沙箱(无论是 E2B 还是自管理的 VM)必须由你来设置和连接。
  • 工具选择。每个代理获得哪些工具,以及哪个代理应该拥有什么权限,都是框架不会替你做出的设计决策。

此外,harness 本身也带来成本。规划、子代理和循环都会增加 API 调用,因此一个复杂的代理设置可能比一个单次模型调用直接解决的任务更昂贵。

还有一个值得关注的长期局限性。随着模型的改进,一些脚手架将变得不再必要,因为今天构建到 harness 中的某些东西是对当前模型限制的变通,而非永久需求。

Anthropic 最初使用上下文重置来防止 Claude Sonnet 4.5 过早结束任务,而在功能更强的 Claude Opus 4.5 上就不再需要了。

图像

总结

以上就是整个发现。编码 Agent 的能力主要存在于 harness 中,而一个编排框架为你提供的 harness 组件比你想象的要多。

循环、规划、委派、沙箱化和内存都可以通过配置实现,而提示词、执行环境和工具选择则仍属于你的职责。

如果你想在自己的代码库上运行这个,CrewAI 文档涵盖了这里使用的每一个功能,并且该框架是完全开源的。查看 CrewAI 文档 → 在此处查找所有代码 →

感谢阅读!

祝好!:)

  • 原文链接: x.com/akshay_pachaar/sta...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论