从零构建 Claude Code 式编码 Harness:基于 CrewAI、E2B 与 OpenRouter 的分层 Agent 实现
【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub
导读
本文围绕仓库build-code-harness/项目展开,该项目在 README 中明确描述其目标:从零重建(rebuilds from scratch)类 Claude Code 的编码 Agent harness——一个能对真实 bug 修复任务进行探索、编辑、测试并汇报结果的完整系统,同时逐层内置了规划、记忆、检查点、沙箱执行与人工审批(human-in-the-loop)能力。文中将完整覆盖环境配置与运行步骤,并深入 code_harness.py 源码,讲清每个"Claude Code 能力"在 CrewAI 中的对应实现机制。读完你既能直接跑通这个开源示例,也能理解如何用 CrewAI 分层搭建一个可靠的编码 Agent。
说明:原文档正文还包含 Newsletter 订阅推广与 Contribution 引导等与编码 harness 技术无关的内容,本文不展开。
技术栈与设计思路
项目由三个核心服务协作而成,README 对此有明确分工:
- E2B:沙箱化的 Shell 与 Python 执行环境——对应 Claude Code 中命令运行所依赖的隔离环境;
- CrewAI:用于构建层级式(hierarchical)Agentic 工作流——对应"子 Agent 委派与协作"的编排层;
- OpenRouter:底层 LLM 提供商——通过一个 key 即可访问多个模型,同时兼容任何 LiteLLM 支持的模型字符串。
项目入口是单个 Python 文件 code_harness.py,源码注释用一段"能力映射"把 Claude Code 的概念逐一翻译到 CrewAI 实现上:模型 = Brain,文件工具 = Hands,子 Agent = Helpers,Manager = Orchestrator,Crew.kickoff = Loop,planning=True = Deep Agent 开关。理解这套映射后,后续每层能力都能对号入座。
环境配置与依赖安装
1. 获取并配置 API Key
E2B API Key(必需):访问 E2B 官网注册账号,在 Dashboard 创建新 Key,然后写入.env文件(需将.env.example重命名为.env):
E2B_API_KEY="..."OpenRouter API Key(必需,或任意 LiteLLM 支持的 provider key):
OPENROUTER_API_KEY="..." MODEL="openrouter/anthropic/claude-sonnet-4-6"OpenAI API Key(仅用于记忆模块):README 特别说明——本 crew 开启了memory=True,而 CrewAI 的记忆系统需要 embedding 模型先把文本转成向量才能保存/召回。默认 embedder 是 OpenAI 的text-embedding-3-large,它与 agent 本身使用哪个 provider 无关,因此即使项目中所有 LLM 调用都走 OpenRouter,这个 key 依然必须配置。
OPENAI_API_KEY="..."README 同时给出了两条规避方案(从 .env.example 与源码可相互印证):一是将 embedder 切换到其他 provider,例如embedder={"provider": "ollama", ...}(CrewAI memory 文档支持);二是直接关闭memory=True。.env.example中对应的注释也确认了这一点。
2. 安装依赖
项目要求Python 3.11 或更高版本并安装 uv:
[project] name = "build-code-harness" version = "0.1.0" requires-python = ">=3.11" dependencies = [ "crewai[tools,litellm]", "crewai-tools[e2b]", "python-dotenv", "pytest", ]其中crewai[tools,litellm]表明 LLM 接入基于 LiteLLM,crewai-tools[e2b]引入 E2B 沙箱工具,pytest用于承载workspace中的测试套件。执行同步:
uv sync运行项目
进入项目目录并启动主脚本:
cd build-code-harness uv run python code_harness.pyREADME 提醒了两点运行期行为:
- 任务设置了
human_input=True:运行在得出答案后会暂停,在终端等待你的批准后才算完成; checkpoint=True会在每个任务完成后把进度写入./.checkpoints/:该目录可在两次运行之间安全删除,建议加入.gitignore。
从源码看,主流程在if __name__ == "__main__":分支中调用crew.kickoff(inputs={...}),把一个真实的 bug 工单作为 objective 注入任务模板,最终print(result)输出结果(code_harness.py)。
内置的演示 Bug:测试套件驱动整个 Harness
项目自带一个微型演示仓库workspace/。README 明确:其中预置了两个真实 bug,配套 pytest 套件初始状态为3 failing / 2 passing,一次完整运行应探索代码、只修复实现,并把套件驱动到5 passing。
逐个对照源码可定位到两个 bug:
withdraw缺少透支检查——account.py 中withdraw直接self.balance -= amount,没有余额校验;对应测试 test_account.py 期望在余额不足时抛出InsufficientFunds并保持余额不变;transfer把款项记到了错误账户——account.py 中transfer调用的是self.withdraw(amount)后紧接self.deposit(amount),钱回到了自己账户而非other账户;对应测试 test_account.py 期望alice余额减 30、bob余额加 30。此外transfer_respects_overdraft测试还把透支约束也覆盖了(test_account.py)。
由于两个 bug 都只在account.py中,而 harness 被要求"只修account.py,永不修改测试",这条工单天然地迫使完整循环跑起来:探索仓库 → 规划修复 → 编辑 → 运行测试 → 迭代至全绿。源码中的实际任务描述也正是如此(code_harness.py)。
逐层拆解:Harness 的架构实现
第 1 层:大脑——可插拔模型
CrewAI 的 LLM 抽象让"模型可替换"成为可能。模型串从环境变量读取并带默认值:
MODEL = os.getenv("MODEL", "openrouter/anthropic/claude-sonnet-4-6") # swap here llm = LLM(model=MODEL)所有 agent 与 manager 共享同一个llm实例;默认模型是经 OpenRouter 访问的anthropic/claude-sonnet-4-6,任何 LiteLLM 支持的模型字符串都可替换,只需修改该行或环境变量。
第 2 层:双手——文件工具与沙箱工具
Claude Code 的read_file/write_file/ls直接映射为 CrewAI 的文件工具(code_harness.py):
read_file = FileReadTool() write_file = FileWriterTool() # overwrites; "edit" = read-then-write list_dir = DirectoryReadTool() filesystem_tools = [read_file, write_file, list_dir]write_file是覆盖写,所以"编辑"本质是"先读后写"。
沙箱是编码 agent 与普通聊天 agent 的关键差异。CrewAI 生态中受支持的路径是接入真实沙箱服务,E2B 提供临时 VM 中的 Shell + Python,其 Shell 足以承载 grep/glob(底层即真实的grep/find)。源码在检测到E2B_API_KEY后才导入并构建沙箱工具(code_harness.py):
sandbox_tools = [] exec_tool = None if os.getenv("E2B_API_KEY"): from crewai_tools import E2BExecTool, E2BPythonTool exec_tool = E2BExecTool() sandbox_tools = [exec_tool, E2BPythonTool()] # run tests / run code自定义工具:run_tests
源码注释点出一个重要事实:内置工具没有"运行测试套件并报告通过/失败"的语义,只有通用 shell 执行。因此项目用@tool装饰器手写了一个"测试形态"的薄封装,把命令提交给上面的exec_tool,从而与其他编码、测试动作运行在同一个隔离 VM 中(code_harness.py):
from crewai.tools import tool if exec_tool is not None: @tool("run_tests") def run_tests(path: str = "tests/") -> str: """Run the pytest suite at the given path inside the sandbox and return the result.""" return exec_tool.run(command=f"pytest {path} -q") custom_tools = [run_tests] else: # No E2B key means no sandbox at all, so there is nowhere safe to run this. custom_tools = []注意它的命名空间:工具名"run_tests"是代码中唯一由项目自行设定的字符串,后面审批门禁的集合构建正依赖这一点。同时注意"无 E2B Key 即不定义该工具"是有意的设计——一个无处路由的命令不算值得交给 agent 的工具。
第 3 层:帮手——三个专职子 Agent
Claude Code 通过派生子 agent 来拆分工作、隔离上下文。在 CrewAI 中,"子 agent"就是 manager 可以委派的对象,而role/goal/backstory三件套即该 agent 的系统提示词——可靠性正是在这里被调优的。
项目定义了三个角色(code_harness.py):
| Agent | role | 负责内容 | 挂载工具 | 关键参数 |
|---|---|---|---|---|
explorer | Codebase Explorer | 在改动前读目录与文件,建立仓库全貌,绝不猜测文件内容 | read_file,list_dir | llm=llm,verbose=True |
coder | Software Engineer | 编辑磁盘文件,实现最小且正确的改动 | filesystem_tools + sandbox_tools | reasoning=True,verbose=True |
tester | Test Runner | 在沙箱内运行测试并如实汇报 pass/fail,不运行就绝不说通过 | sandbox_tools + [read_file] + custom_tools | verbose=True |
值得展开的是coder上的reasoning=True。源码注释强调它与 crew 级planning=True的区别:这是agent 级的规划表面——该 agent 在执行任务前自行反思并草拟一份简短计划,而非由 crew 为所有人提前统一规划一次。作者特别说明,之所以值得为coder单独开启,是因为编辑磁盘文件是整个 crew 中最难撤销的动作(code_harness.py)。
第 4 层:编排者——允许委派的 Manager
Process.hierarchical的本质是一个 manager agent 向上述帮手委派工作,这正是 Claude Code 派生子 agent 的 CrewAI 对偶(code_harness.py):
manager = Agent( role="Engineering Lead", goal="Break the request into steps and delegate each to the right specialist.", backstory=( "You own the outcome. You decide who does what, review their results, and " "only finish once the change is implemented and the tests have actually run." ), llm=llm, allow_delegation=True, # delegation is off by default; the manager needs it on verbose=True, )注释强调:allow_delegation默认是关闭的,manager 必须显式开启它。backstory("你拥有最终结果……只有改动落地且测试真正跑过才算完")本质是在约束 manager 的验收标准。
第 5 层:任务——开放目标 + 人工审批
任务被刻意设计成一个开放式目标,不预先指派给某个 agent,而是由 planner + manager 自行拆解、自由委派(code_harness.py):
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, )human_input=True是 CrewAI 的人机协同门禁:一旦 crew 得出答案,会在 CLI 上暂停、请求批准或反馈,然后运行才算完成。这与第 7 层的"命令级审批"是两个不同位置的门禁(一个在任务完成后,一个在工具调用前)。
第 6 层:主循环与"深度 Agent"开关
crew.kickoff()就是循环本身,而planning=True是把浅层工具调用 crew 变成深度 agent的开关(code_harness.py):每次迭代前会有一个 AgentPlanner 写出逐步计划并注入任务——这相当于 Claude Code 的 todo-list 规划工具,可理解为"把规划当作上下文工程(planning as context engineering)"。注意planning 默认使用gpt-4o-mini,实际运行代码则把它显式设为 OpenRouter 上的同名模型(见第 8 层 Crew 构造)。
记忆系统:跨会话的长期记忆
memory=True开启 CrewAI 的统一记忆系统:crew 不仅记住单次运行内发生了什么,还能记住过去多次kickoff()调用的经验。源码注释把这一点定位为"跨会话击败上下文窗口限制"的手段,也正是 Claude Code 长期记忆与持久化的对应物——它与第 8 层的 checkpointing 是两回事:memory 是跨运行的事实回忆,checkpoint 是中断运行的恢复。
第 7 层:命令审批门禁(在工具运行之前拦截)
这是全文最值得细读的安全机制。前面的Task(human_input=True)审查的是已经完成的答案,而这里的 hook 是更早的一道闸:在某个特定工具调用真正执行之前拦截它,并且可以直接阻止。源码注释强调这是 Claude Code 权限层最接近的对偶物——约束在模型之外强制执行,而不是一条要求模型"小心"的提示(code_harness.py):
from crewai.hooks import before_tool_call GATED_TOOLS = {write_file.name, "run_tests", *(t.name for t in sandbox_tools)} @before_tool_call def require_approval(context): if context.tool_name in GATED_TOOLS: response = input( f"Approve {context.tool_name} with input {context.tool_input}? [yes/no] " ) if response.strip().lower() != "yes": return False # blocks the call; the agent is told it was denied return None几个实现细节值得逐一说明:
- 门禁集合的构建方式:
GATED_TOOLS由上文已导入并实例化的真实工具对象集合组成(write_file.name、sandbox_tools的工具名)。"run_tests"以字面量字符串加入——因为它是项目通过@tool("run_tests")自行设定的名字,是集合中唯一一个百分之百确定的名称; run_tests为何值得纳入门禁:它如今通过exec_tool提交真实命令(而非非沙箱执行),尽管其命令被约束为比裸E2BExecTool更窄的pytest {path} -q,但仍在沙箱内执行,因此与其他沙箱工具走同一道审批;- hook 的返回值语义:
ToolCallHookContext只暴露tool_name、tool_input、tool、agent、task、crew、tool_result,没有内置"询问人类"的方法,因此在 stdin 上阻塞与Task(human_input=True)的工作方式保持一致。返回False会阻止该调用(agent 会被告知请求被拒),返回None则放行不变。
第 8 层:Crew 组装与断点续跑
最后把所有部件组装成一个 Crew(code_harness.py):
crew = Crew( agents=[explorer, coder, tester], tasks=[task], manager_agent=manager, process=Process.hierarchical, planning=True, planning_llm=LLM(model="openrouter/openai/gpt-4o-mini"), memory=True, checkpoint=True, verbose=True, )Checkpointing对应 README 的运行说明:checkpoint=True在每个已完成任务之后把 crew 状态写入./.checkpoints/。若一次长运行中途被杀,可以从最后保存的检查点继续,而不必从头重来——这是长任务扛住中断的 CrewAI 方案,与memory=True的"跨运行事实回忆"明确区分。
完整能力映射速查表
为便于快速引用,将 Claude Code 能力与本文实现要点汇总如下:
| Claude Code 能力 | 本项目实现 | 关键证据 |
|---|---|---|
| 模型/推理 | LLM(model=MODEL),可经 OpenRouter 换模型 | code_harness.py |
| 文件读写 | FileReadTool/FileWriterTool/DirectoryReadTool | code_harness.py |
| 沙箱 Shell/代码执行 | E2B 的E2BExecTool/E2BPythonTool(临时 VM) | code_harness.py |
| 测试执行 | 自定义run_tests薄封装,路由进exec_tool | code_harness.py |
| 子 Agent / 上下文隔离 | explorer/coder/tester三专职 agent | code_harness.py |
| 编排与委派 | Process.hierarchical+manager_agent+allow_delegation=True | code_harness.py |
| todo 计划工具 | planning=True+ 独立的planning_llm | code_harness.py |
| 长期记忆 | memory=True(默认 OpenAItext-embedding-3-largeembedder) | code_harness.py |
| 命令审批 | before_tool_callhook +GATED_TOOLS | code_harness.py |
| 断点续跑 | checkpoint=True,写./.checkpoints/ | code_harness.py |
| 结果人工审批 | Task(human_input=True) | code_harness.py |
运行验证建议与限制
- 若想让完整门禁生效,务必先配置好
E2B_API_KEY;无此 key 时沙箱工具与run_tests都不会被定义,coder/tester实际可用的能力会显著受限(这是源码中的有意设计,非缺省兜底); memory=True强依赖 OpenAI embedder key;不想引入第二个 provider 时,可如 README 所述改用其他 embedder 或关闭 memory;- 运行后产生的
./.checkpoints/目录为运行期产物,两次运行之间可安全删除,建议加入.gitignore; - 本项目是教学性重建(in-depth tutorial),目标是把成熟的编码 agent 能力逐层还原在开源的 CrewAI + E2B + OpenRouter 组合上,供开发者学习每个能力层的底层原理后自行裁剪扩展。示例代码位于 code_harness.py,被修复的缺陷与测试用例位于 workspace/account.py 与 workspace/tests/test_account.py,可作为最小复现与验证载体。
【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考