Agent Harness 拆解345 行代码实现主 Agent 与 Sub-Agent 双层循环很多人第一次读 Agent 代码注意力会停在提示词上。真正决定它能不能持续干活的是外面的循环、工具表和路径沙箱。这篇文章拆一份三百多行的最小实现一个主 Agent 循环、一个子 Agent 循环、四个文件工具、一个 task 工具。读完后你能说清六大模块分别落在哪些函数上也能拿到一份照着改的清单。说明本文只做静态阅读未运行验证行为结论以源码为准涉及密钥的配置只确认存在不展开内容。一、Harness 解决的是模型不可信一次 LLM 调用是纯函数输入 messages输出 message。它没有状态、没有手、也没有边界。Harness 在外面加了三样东西状态messages 列表让多轮对话能接上能力工具表让模型能读写文件、执行命令边界工作目录与超时让错误不至于扩散。readme 把它拆成六大基础模块Loop、Tool、Context、Environment、Memory、Observability。这六个词不是分类学而是六个必须回答的工程问题谁来循环、能调什么、送什么上下文、在哪执行、记什么、怎么看清发生了什么。二、六大模块的代码落点模块代码位置现状Loopagent_loop、run_subagent有终止判断父循环缺上限ToolCHILD_TOOLS、PARENT_TOOLS、TOOL_HANDLERSSchema、handler、路由齐全Contextmessages、sub_messages只追加无压缩EnvironmentWORKDIR、safe_path、subprocess.run(cwd...)路径受限无网络与资源隔离MemoryCLI 内的history进程结束即丢失Observability若干print无结构化日志、无 Trace这张表更值得看的是右列。readme 描述的是目标形态代码实现的是骨架Loop、Tool、Environment 有雏形Context 只做了最朴素的拼接Memory 和 Observability 基本空缺。三、safe_path路径沙箱的两个关键调用文件工具的第一步都是把用户或模型给的路径规范化并确认它没有跑出工作目录。defsafe_path(p:str)-Path:path(WORKDIR/p).resolve()ifnotpath.is_relative_to(WORKDIR):raiseValueError(fPath escapes workspace:{p})returnpath两个调用分工明确resolve()把..、.和符号链接展开成规范路径。少了它../../etc/passwd能绕过前缀比较。is_relative_to()按路径段判断包含关系。字符串startswith会把/work-evil误判成/work的子目录它不会。一个容易被忽略的点WORKDIR Path.cwd()没有resolve()。如果启动目录本身经过符号链接两边归一化后的形态可能不一致比较就会失真。这是静态推断未运行验证。另一个边界是它只保护文件工具。run_bash用shellTrue执行命令命令里的路径不经过safe_path。也就是说沙箱的强度取决于最弱的那条入口。四、工具注册三件套Schema、Handler、路由新增一个工具要改三处JSON Schema、执行函数、路由表。这份实现把三者拆得很清楚。TOOL_HANDLERS{bash:lambda**kw:run_bash(kw[command]),read_file:lambda**kw:run_read(kw[path],kw.get(limit)),write_file:lambda**kw:run_write(kw[path],kw[content]),edit_file:lambda**kw:run_edit(kw[path],kw[old_text],kw[new_text]),}三个细节值得抄父子工具集用列表相加复用。子 Agent 只有四个文件工具父 Agent 多一个task能力差异只体现在一张表上不需要两套代码。lambda **kw做参数适配。模型给的是 JSON 对象handler 要的是关键字参数中间需要解包limit是可选参数所以用kw.get(limit)而不是下标。未知工具名返回字符串。handler(**args) if handler else ...保证模型编造工具名时循环还能继续而不是直接崩。但同一行也埋了坑handler(**args)外面没有 try。模型漏传必填参数时TypeError会直接终止进程文件工具内部的异常捕获救不了它因为异常发生在函数调用之前。五、Sub-Agent隔离的不是进程是上下文源码注释说子 Agent “在全新的子进程运行”但实现里没有subprocess。它做的是另一件事新建一个sub_messages用独立的 system prompt 和工具集跑自己的循环。defrun_subagent(prompt:str)-str:sub_messages[{role:user,content:prompt}]for_inrange(30):responseclient.chat.completions.create(modelMODEL,messages[{role:system,content:SUB_SYSTEM}]sub_messages,toolsCHILD_TOOLS,max_tokens8000,)msgresponse.choices[0].message sub_messages.append(msg.model_dump())ifresponse.choices[0].finish_reason!tool_calls:breakresults[]fortool_callinmsg.tool_calls:argsjson.loads(tool_call.function.arguments)handlerTOOL_HANDLERS.get(tool_call.function.name)outputhandler(**args)ifhandlerelseUnknown toolresults.append({role:tool,tool_call_id:tool_call.id,content:str(output)[:50000],})sub_messages.extend(results)returnmsg.contentor(no summary)三个事实共享文件系统不共享对话历史。子 Agent 和父 Agent 读写同一批文件但 messages 完全隔离。父层只拿最终字符串。中间几十轮的工具调用留在子 Agent 内部主 Agent 的窗口不会被噪声撑满。子 Agent 有独立 system prompt。完成给定任务然后返回简洁最终答案约束的是输出形态不是能力。这就是 readme 说的多 Agents 业务隔离的代码级解释专属提示词、独立上下文、子进程式的失败隔离。注意最后一点只是式异常和内存仍然是共享的。六、双层循环的终止条件层级终止条件风险子 Agentfinish_reason ! tool_calls或跑满轮数上限超限时返回的未必是总结主 Agentfinish_reason ! tool_calls没有轮数上限CLIq、exit、空串、CtrlC正常子 Agent 的轮数上限有一个边界如果最后一轮仍然是tool_calls循环自然结束然后返回最后一轮的msg.content。此时模型可能还在准备调用工具的状态。生产实现通常会在超限后追加一次收尾调用要求模型基于已有信息给结论。主循环的问题更直接while True没有轮数、没有 token 预算、没有墙钟超时。如果模型持续返回tool_calls这个循环没有自停机制。defagent_loop(messages:list):whileTrue:responseclient.chat.completions.create(modelMODEL,messages[{role:system,content:SYSTEM}]messages,toolsPARENT_TOOLS,max_tokens8000,)msgresponse.choices[0].message messages.append(msg.model_dump())ifresponse.choices[0].finish_reason!tool_calls:returnresults[]fortool_callinmsg.tool_calls:argsjson.loads(tool_call.function.arguments)iftool_call.function.nametask:outputrun_subagent(args.get(prompt,))else:handlerTOOL_HANDLERS.get(tool_call.function.name)outputhandler(**args)ifhandlerelseUnknown toolresults.append({role:tool,tool_call_id:tool_call.id,content:str(output),})messages.extend(results)还有一个不对称子 Agent 的工具结果截断到固定长度父层str(output)没有截断。同一份run_bash输出在两条路径上的上下文成本不同。七、这份实现里值得警惕的四个坑问题触发条件后果最小修复父循环无上限模型持续调工具卡死、成本失控加最大轮数handler(**args)无兜底模型漏传必填参数进程崩溃包 try回填错误字符串子串黑名单等价命令、跨平台命令危险命令放行白名单 参数校验history只增不减长会话成本线性上升保留最近若干轮 摘要黑名单这条值得单独说。[rm -rf /, sudo, shutdown, reboot, /dev/]用的是子串匹配。rm -rf /*、变量拼接、别名都能绕过在 Windows 上rm根本不存在黑名单几乎不覆盖真实危险命令。它更像一个示例不是安全机制。八、改造清单与自检表按优先级排的七件事主循环加最大轮数与 token 预算工具调用统一包 try返回结构化结果父层工具输出做与子层一致的截断WORKDIR初始化为Path.cwd().resolve()命令执行改为白名单 shlex参数校验用logging替换print记录轮次与工具名实现最小上下文压缩自检问题子 Agent 跑满轮数后返回内容是否可能是半成品父 Agent 看不到子 Agent 的中间过程出问题靠什么排查文件工具把异常转成字符串外层还能区分成功与失败吗结语Harness 的难点不在于把循环写出来而在于给循环装上刹车、仪表盘和后视镜。这份实现把骨架讲清楚了剩下的都是工程。标签AI Agent, Harness, Sub-Agent, Python
阅读完成 · 觉得有帮助?