一步步构建 Claude Code 的 Harness

Let's build Claude Code's harness (step-by-step)

中文译文 · 21k 字

一句话摘要

逐步搭建 Claude Code 的 Harness 结构

让我们一步步构建 Claude Code 的 harness 2026 年 7 月 15 日 · 16 分钟阅读 · 查看原文 ↗ Claude AI 如果你曾经试过构建自己的编码 agent,你就知道这是怎么回事。你把一个模型接到文件工具和 shell 上,把它指向一个真实的代码库,然后它在十几个工具调用之内就崩了。 它读错文件,做一半就丢掉了目标,还把不再需要的输出塞满了上下文。 然后同样的任务交给 Claude Code,它干净利落地完成了。容易得出的结论是 Anthropic 就是有更好的模型,而这个结论恰恰错过了真正工作发生的地方。 差别在于 harness。harness 就是包裹在模型周围的那些普通代码,它处理规划、工具执行、记忆和安全,而模型只决定下一步。 一个完整的 harness agent 画出来是这样的: 这张图看起来很乱,但它可以分成四组: 记忆把模型的工作上下文以及它跨会话学到的知识喂给它。 技能编码了 agent 应该如何运作,也就是它遵循的流程、约束和启发式方法。 协议把 agent 与用户、工具和其他 agent 连接起来。 harness 核心通过子 agent 编排、沙箱、评估器、审批循环、可观测性和上下文压缩,把这一切绑在一起。 Anthropic 把这种拆分描述为大脑和手。模型是挑选每一个动作的大脑,harness 是执行动作并让运行保持在轨道上的手。 所以你的 agent 和 Claude Code 之间的差距不是模型,而是围绕模型的那些机器。 Claude Code 是今天生产中能力最强的 harness 之一,而它却是由那张插图中数量少得惊人的一组层构建而成的。为了看看有多少机器你得自己构建,我在 CrewAI(一个用于编排 agent 的开源框架)里重建了它。 它映射到内置功能的部分比我想象的要多,而没有映射到的部分,才是真正工程所在的地方。 让我们一层一层地构建它,从核心循环开始,再往上叠加规划、子 agent、沙箱和记忆。在每一步,我们都会标记出框架在哪里到头了,你的工作又从哪里开始。 Claude Code 是一个简单的循环,周围包着若干层 Claude Code 的中心是一个普通的 agent 循环。你给它发一条消息,模型决定下一步做什么,它要么直接回应,要么请求一个工具。如果它请求工具,工具运行,结果回到对话里,模型再做决定。 如此反复,直到模型返回一个不再有进一步工具调用的最终答案。 在那个循环里,模型读文件、改代码、运行 shell 命令、执行测试。这些不是独立的模式。它们只是同一个循环里不同的工具调用。 然而,单单一个循环不足以构成一个可靠的编码 agent。Claude Code 在它周围加了规划、文件工具、子 agent、记忆,以及一套权限和沙箱系统。这些层并不取代循环,它们让循环安全可靠到足以应对真实工作。 这就是我们要重建的架构,先核心循环,然后往上每一层,并把每一层映射到处理它的 CrewAI 功能上。 整个循环就是:决定、行动、反馈、重复 这个循环重复同一个序列,直到任务完成: 要求模型执行任务。 模型直接回应,或请求一个或多个工具。 如果请求了工具,运行它们并把结果返回给模型。 用更新后的对话重复。 当模型回应而不请求任何工具时,任务完成。 每一次工具调用完成一步,给模型新的信息,并汇入下一个决定。一个简单的问题可能一次迭代就结束,而修复一个复杂的 bug 或重构一个大型代码库,可能要迭代几十次,模型才有足够的信息给出最终答案。 一旦你创建了一个 agent,CrewAI 就自动提供这个执行循环。你不用自己实现那个 while 循环,你定义 agent 并给它分配一个任务。 循环是免费得到的 让我们创建一个简单的 Bug Fixer agent。 这里要理解三个概念: Agent 通过它的角色、目标、LLM 和工具来定义谁来做这件事。 Task 描述这项任务。 Crew 把 agent 和任务放在一起。调用 kickoff() 就会运行上面描述的同一个执行循环,无论底层模型是 Anthropic、OpenAI、Google 还是别的什么。 工具:把一个文本生成器变成一个能做事的东西 工具是让一个只会生成文本的模型真正能在代码库上工作的东西。它们读文件、写文件、运行 shell 命令、调用外部 API。 CrewAI 开箱即用地提供了文件系统工具: FileReadTool 读取文件。 DirectoryReadTool 列出目录。 FileWriterTool 写入文件。 这些也兼作外部记忆。与其把一个大的搜索结果塞进模型的上下文窗口,agent 可以把它写进一个文件,只保留文件名,需要时再读回来。 这能让上下文窗口更小,模型更专注,这正是 Anthropic 所说的上下文工程。 内置工具只覆盖常见工作流。对于任何更具体的事情,你用 @tool 装饰器把一个 Python 函数暴露成工具。 docstring 就是使用说明书,告诉模型这个工具做什么、何时用它、以及它期望什么输入。 规划让目标不被埋没 随着任务变得更复杂,一个普通的执行循环开始丢掉最初的目标。在足够多的工具调用、文件读取和中间结果之后,上下文被塞满,目标被它之后出现的一切挤了出去。 这种缓慢的退化,就是人们所说的上下文腐烂。 规划直接解决这个问题。agent 在做任何工作之前构建一个逐步计划,并在整个执行过程中把那个计划保留在上下文里。 计划不干活。它是一张路线图,让模型始终与最初的目标保持连接,这正是 Claude Code 的待办清单所做的工作。 CrewAI 在 crew 层面用 planning=True 来加入这一点。它在执行前生成一个计划,并在任务推进过程中让它保持可用。 注意:默认情况下,CrewAI 用 gpt-4o-mini 做规划,你可以换成你偏好的任何 LLM 来做这一步。 单个 agent 也可以用 reasoning=True 来推理自己的工作: 规划和推理解决的是不同的问题。规划为整个任务构建一张高层路线图,而推理给单个 agent 时间,在行动前思考它自己的方法。 当推理启用时,agent 会: 反思任务并起草一份执行计划。 评估这份计划是否就绪。 如有必要就完善计划,直到它满意或达到 max_reasoning_attempts。 在执行前把最终定稿的推理计划注入任务。 两者一起,让 agent 在长时间运行的任务上保持锚定,减少偏离最初目标。 子 agent 让主上下文保持小 规划让 agent 保持专注,但它并没有减少模型必须持有的信息量。在一个大型代码库上,即便是规划得很好的任务,也可能超出单个上下文窗口。 找到一个 bug 可能需要读几十个文件,而主 agent 不需要把它们都留在内存里。 子 agent 通过委派来解决这一点。主 agent 把一个具体任务交给一个助手 agent,助手在自己的上下文里工作,并返回一段简短的摘要。主 agent 看到的是结论,而不是中间步骤。 CrewAI 通过层级工作流支持这一点,一个管理者 agent 把任务委派给专家 agent 并合并它们的结果。 在我们前面的设置里,一个 Bug Fixer agent 包揽了所有重活。让我们把工作拆分给一个管理者和三个专家: Codebase Explorer 探索代码并绘制仓库地图。 Software Engineer 实现所请求的改动。 Test Runner 在沙箱里运行测试并报告通过或失败。 Engineering Lead 监督这三个专家。 有一点要注意,allow_delegation 默认是禁用的,所以必须在管理者身上显式开启。 安全来自沙箱和审批,而不是提示词 一个有 shell 权限的 agent 可以运行破坏性命令,而告诉模型别做某事并不是一种防护。 真正的保护来自两层: 一个权限系统,对敏感操作要求审批。 一个隔离执行的沙箱,这样即便是被批准的指令也碰不到宿主机。 Anthropic 用的是同样的方法。把代码执行移进沙箱,能减少用户需要审批操作的频率,同时仍然保护宿主系统。 CrewAI 中的沙箱 在沙箱里而不是在宿主机上执行代码,就是加上了第二层。在这个设置里,代码在 E2B 里运行,它每次会话都会拉起一台全新的 VM,用完就销毁。 Shell 命令和 Python 完全在那个隔离环境里运行。 一个标志就能让 crew 停下来等你审查 在 Task 上设置 human_input=True,会在它生成答案之后暂停 crew。你审查输出,然后批准它,或者把它打回去再迭代一次。 当执行到达那个任务时,CrewAI 通过标准输入等待你的反馈。 from crewai import Task task = Task( description=( "In the working directory ./workspace, {objective}. " "Explore the code first, make the change, then run the tests and report." ), expected_output="A summary of the files changed and the final test output.", human_input=True, ) 如果你的 crew 跑在一个 web 应用或聊天界面后面,而不是终端后面,CrewAI 基于 webhook 的人在环(human-in-the-loop)系统会处理同样的审查步骤。 记忆承载事实,检查点承载进度 默认情况下,一次运行结束后,agent 会忘掉一切。明天回来修同一个项目里的另一个 bug,它从零开始。 有两个机制让 agent 能跨运行携带信息,而且各自服务于不同的目的: 检查点(Checkpointing)在运行期间保存 agent 的状态,这样它可以在中断后恢复,或者从同一点沿着不同的路径继续。 持久记忆(Persistent memory)跨不同的对话存储事实,包括像"完成前总是格式化最终代码"这样的项目偏好。 [图片:并排对比检查点保存运行状态 vs 记忆跨会话保存事实] 记忆只需要在 crew 上设一个标志 CrewAI 提供了一个统一的 Memory 接口,而不是分开的短期、长期、实体和外部记忆类型。保存时,它用一个 LLM 来识别重要细节、组织它们,并让它们以后可以检索。 在 crew 上设置 memory=True 就给了它跨运行的记忆。每个任务之后,CrewAI 从输出中提取有用的事实并存储它们,在未来的运行中,它检索相关的记忆并把它们加进任务提示词里。 from crewai import Crew crew = Crew( agents=[explorer, coder, tester], tasks=[task], memory=True, ) 一个 crew 里的所有 agent 共享它的记忆,除非某个 agent 被给了自己的记忆。 检查点随运行推进对运行做快照 检查点是 agent 进度的快照,包括它的配置、任务状态、记忆、中间结果、输入和执行历史。 默认情况下,CrewAI 在一个任务完成时创建一个检查点,如果工作流被中断,就能从那个点恢复。 检查点可以存在两种内置存储之一: JsonProvider 把每个检查点存成一个独立的 JSON 文件,方便手动阅读和检查。 SqliteProvider 把检查点都存进一个 SQLite 数据库里,在频繁做检查点和更大负载下表现更好。 from crewai import Crew crew = Crew( agents=[explorer, coder, tester], tasks=[task], checkpoint=True, ) Crew、Flow 和 Agent 都接受一个 checkpoint 参数,子级会继承父级,除非它们设置了自己的值。 在一个修 bug 任务上的全部六层 这是完整 harness 在一个任务上的样子,执行循环、工具、规划、子 agent、沙箱和记忆协同工作: 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: """Sync ./workspace into the sandbox, then run pytest there.""" return E2BExecTool().run(command=sync_and_test_command(path)) explorer = Agent(role="Codebase Explorer", goal="Map repo, surface relevant files.", tools=[read_file, list_dir], llm=llm) coder = Agent(role="Software Engineer", goal="Implement requested change.", tools=filesystem_tools, reasoning=True, llm=llm) tester = Agent(role="Test Runner", goal="Run tests in sandbox, report pass/fail.", tools=sandbox_tools + [read_file] + [run_tests], llm=llm) manager = Agent(role="Engineering Lead", goal="Delegate steps, finish once tests pass.", allow_delegation=True, llm=llm) task = Task( description="In ./workspace, {objective}. Explore, edit, test, report.", expected_output="Summary of changes and test 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": "fix failing tests in account.py"}) Agent harness 在成功可以被自动检查时最容易评估。一套测试套件给了 agent 一个具体的目标,这样它就可以规划、编辑、测试、重复,直到一切通过。 所以这是在一个小型代码库上测试的,一个 BankAccount 类,有两个真实的 bug 和五个测试,其中三个失败。规则是只修实现,不修测试。 这映射了 Anthropic 内部评估编码 agent 的方式。一个已发布的例子是让 Claude 对照一大套失败的测试,重建一个 claude.ai 界面的克隆。 在这里,harness 把这个项目从 3 个失败、2 个通过带到全部 5 个通过,而"只改实现"这条规则堵死了编辑或删除失败测试这条捷径。 [图片:测试套件从 3 失败 / 2 通过到 5 通过的终端截图] 框架在提示词、环境和工具选择处止步 系统的某些部分不是框架替你构建的: 提示词。每个 agent 的行为来自它的角色、目标和背景故事。把这些做好需要测试和迭代,没有任何配置标志能替代它。 执行环境。沙箱,无论是 E2B 还是自管的 VM,都必须自己搭好并接线。 工具选择。每个 agent 拿到哪些工具、哪个 agent 该有权访问什么,是框架不做的一项设计决策。 harness 本身也有成本。规划、子 agent 和循环都会增加 API 调用,所以一个复杂的 agent 设置最终可能比一次模型调用就能直接解决的任务更贵。 还有一点值得记住的长期局限。随着模型进步,有些脚手架会变得不再必要,因为今天建进 harness 里的有些东西,是对今天模型局限的一种变通,而不是永久需求。 Anthropic 最初用上下文重置来防止 Claude Sonnet 4.5 过早结束任务,而到了能力更强的 Claude Opus 4.5,这些就不再需要了。 [图片:Anthropic 关于在 Sonnet 4.5 和 Opus 4.5 之间退役上下文重置的说明] harness 自始至终才是重点 这就是全部发现。一个编码 agent 的能力主要活在 harness 里,而一个编排框架交给你的 harness,比你想象的要多。 循环、规划、委派、沙箱和记忆都以配置的形式到来,而提示词、执行环境和工具选择仍然是你的。 如果你想针对自己的代码库运行这个,CrewAI 文档覆盖了这里用到的每个功能,而且这个框架完全开源。 查看 CrewAI 文档 → 查看 CrewAI GitHub(100% 开源)→ 就到这里! 如果你喜欢这篇文章。 找我 → @akshay_pachaar ✔️ 每天,我都会分享关于 AI、机器学习和 vibe coding 最佳实践的教程和洞见。 Prompts from crewai.tools import tool import subprocess @tool("run_tests") def run_tests(path: str = "tests/") -> str: """Run the pytest suite at the given path and return the result.""" 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 from crewai import Task task = Task( description=( "In the working directory ./workspace, {objective}. " "Explore the code first, make the change, then run the tests and report." ), expected_output="A summary of the files changed and the final test output.", human_input=True, ) while True: reply = model(messages, tools) calls = [b for b in reply if b.type == "tool_use"] if not calls: # plain text, no tool call: the job is done return reply.text messages += [reply, run_all(calls)] from crewai import Crew, Agent, Task, Process explorer = Agent( role="Codebase Explorer", goal="Map the repository and surface the files relevant to the task.", backstory="You read directories and files to build a picture of the code.", tools=[read_file, list_dir], llm=llm, ) # Same for other two specialist agents manager = Agent( role="Engineering Lead", goal="Break the request into steps and delegate each to the right specialist.", backstory="You decide who does what, review tests, finish once change is done.", llm=llm, allow_delegation=True, ) crew = Crew( agents=[explorer, coder, tester], tasks=[task], manager_agent=manager, process=Process.hierarchical, ) from crewai import Agent bug_fixer = Agent( role="Bug Fixer", goal="Find and describe the fix for the reported bug in the codebase.", backstory="You read directories and files to build an accurate picture of the code.", tools=[FileReadTool()], reasoning=True, max_reasoning_attempts=3 # Optional: Set a maximum number of reasoning attempts ) from crewai import Crew crew = Crew( agents=[explorer, coder, tester], tasks=[task], memory=True, ) from crewai import Crew crew = Crew( agents=[explorer, coder, tester], tasks=[task], checkpoint=True, ) from crewai import LLM, Agent, Crew, Task bug_fixer = Agent( role="Bug Fixer", goal="Find and describe the fix for the reported bug in the codebase.", backstory="You read directories and files to build an accurate picture of the code.", llm="claude-sonnet-4-6", ) task = Task( description="Find the fix for {objective}.", expected_output="A short description of the fix and which file it belongs in.", ) result = Crew(agents=[bug_fixer], tasks=[task]).kickoff( inputs={"objective": "the overdraft bug in account.py"} ) from crewai import Crew, LLM crew = Crew( agents=self.agents, tasks=self.tasks, planning=True, planning_llm=LLM(model="gpt-4o-mini"), ) from crewai_tools import DirectoryReadTool, FileReadTool, FileWriterTool read_file = FileReadTool() write_file = FileWriterTool() list_dir = DirectoryReadTool() filesystem_tools = [read_file, write_file, list_dir] 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: """Sync ./workspace into the sandbox, then run pytest there.""" return E2BExecTool().run(command=sync_and_test_command(path)) explorer = Agent(role="Codebase Explorer", goal="Map repo, surface relevant files.", tools=[read_file, list_dir], llm=llm) coder = Agent(role="Software Engineer", goal="Implement requested change.", tools=filesystem_tools, reasoning=True, llm=llm) tester = Agent(role="Test Runner", goal="Run tests in sandbox, report pass/fail.", tools=sandbox_tools + [read_file] + [run_tests], llm=llm) manager = Agent(role="Engineering Lead", goal="Delegate steps, finish once tests pass.", allow_delegation=True, llm=llm) task = Task( description="In ./workspace, {objective}. Explore, edit, test, report.", expected_output="Summary of changes and test 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": "fix failing tests in account.py"}) from crewai_tools import E2BExecTool, E2BPythonTool sandbox_tools = [E2BExecTool(), E2BPythonTool()] # run tests / run code 链接 claude.ai github.com/patchy631/ai-engineering-hub/tree/main/build-code-harness docs.crewai.com/ 标签:# X # Claude # AI # 指南 # Sonnet 相关文章 10 个 SEO 外链 Claude 自动化,3 个月内获得 61k 次 AI 提及 外链建设就是不断试错。 SEO AI Claude 自动化

原文参考:https://maxed.wiki/posts/let-s-build-claude-code-s-harness-step-by-step/ (Maxed.wiki,本页为站内中文整理)