ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Coding Agent Harness 实战:用 Python 搭一个可部署的 Harness-Demo 沙箱

Coding Agent Harness 实战:用 Python 搭一个可部署的 Harness-Demo 沙箱 1. 从一次失败的 Agent 任务说起为什么需要 Coding Agent Harness你可能遇到过这种场景让大模型帮忙改一个 Python 项目里的 bug它信誓旦旦地输出了一段代码你复制粘贴进去跑起来报错再问它它又给一版来回几轮之后上下文全乱了最后你只能自己动手。问题不在于模型不够聪明而在于它没有一套能真正“动手”的运行时框架——这就是 Coding Agent Harness 要解决的事。Coding Agent Harness 可以理解成包裹在大模型外面的控制平面它决定 Agent 能看到哪些文件、能执行哪些命令、工具调用怎么走、上下文怎么压缩、沙箱怎么隔离、失败怎么重试。模型是引擎Harness 是底盘和方向盘。同一个模型换一套 Harness任务通过率可能差一倍以上这在 SWE-Bench 这类真实仓库评测里已经被反复验证。这篇面向想亲手跑通 Agent 循环的开发者用 Python 写一个精简版 Harness-Demo覆盖三件核心事沙箱隔离、工具调用、循环控制。你会拿到可复制的目录结构、依赖清单、启动命令以及一次完整任务执行的验证步骤和日志观察点。适合谁写过一点 Python、想让 Agent 在自己机器上真正跑起来、又不想一上来就啃重型框架的人。我试过从零手写这套循环最大的感受是——难点从来不是调模型 API而是把“模型输出”翻译成“安全可执行的动作”再把执行结果喂回去。下面按这个思路一步步搭。2. 前置准备用 TaoToken 统一接入模型接口Harness 的循环里必须有一个稳定的 LLM 调用入口。为了让 Demo 不绑定某一家模型我们用 OpenAI 兼容接口通过 TaoToken 统一转发这样 Claude、DeepSeek、Qwen 这些模型只要改一个 Model ID 就能切换Harness 代码本身不用动。TaoToken 在这里扮演的是模型接入层它提供 OpenAI 兼容的 Base URL 和 API Key你不需要为每个模型单独写适配代码。对 Harness-Demo 来说这正好把“模型调用”和“Agent 逻辑”解耦开——循环控制、沙箱、工具解析都是我们自己的代码只有client.chat.completions.create这一行走 TaoToken。先拿 Key。打开控制台创建 API Key地址是 https://taotoken.net/api-keys 登录后新建一个复制出来只显示一次记得存好。然后在控制台里确认你要用的模型 ID比如claude-3-5-sonnet-20240620或deepseek-coder这类具体以控制台模型列表为准。拿到两个东西就够了Base URLhttps://taotoken.net/api注意不要加 UTM 后缀这是给代码用的API Keysk-开头的那串如果你更想先验证模型通不通可以直接在模型对话页发一条消息试试 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 。确认能正常返回再往下写 Harness。这里有个容易踩的坑很多人把 Base URL 写成带/v1的完整路径结果 OpenAI SDK 又自动拼一次变成/v1/v1/chat/completions直接 404。TaoToken 的 Base URL 就是https://taotoken.net/apiSDK 会自己补/v1别手动加。依赖清单很轻三个包pip install openai python-dotenv dockeropenai负责调模型python-dotenv读.envdocker是 Python 的 Docker SDK用来在代码里起容器。本机需要提前装好 Docker 并启动服务docker ps能正常输出就说明 OK。如果你还没装 Docker Desktop先去官网装一个这一步绕不过去因为沙箱隔离是 Harness 的核心不能省。3. 可复制配置目录结构、.env 与 harness.py这一节给出完整可跑的代码。目录结构先摆出来simple_harness/ ├── .env ├── harness.py └── sandbox_workspace/ # Agent 工作目录会映射进容器sandbox_workspace是宿主机上的目录容器启动时挂载到/workspaceAgent 写的文件最终落在这里任务结束容器销毁但文件保留方便你检查它到底干了什么。.env配置如下把 Key 换成你自己的LLM_BASE_URLhttps://taotoken.net/api LLM_API_KEYsk-你的key MODEL_NAMEclaude-3-5-sonnet-20240620 MAX_ROUND15 DOCKER_IMAGEpython:3.11-slimMAX_ROUND是循环上限防止 Agent 无限调用工具。DOCKER_IMAGE用官方 slim 镜像体积小、启动快里面自带 Python跑 pytest 够用。下面是harness.py的完整代码分四块安全 Hook、沙箱封装、工具定义、主循环。import os import docker from dotenv import load_dotenv from openai import OpenAI load_dotenv() client OpenAI( base_urlos.getenv(LLM_BASE_URL), api_keyos.getenv(LLM_API_KEY), ) docker_client docker.from_env() MAX_ROUND int(os.getenv(MAX_ROUND)) DOCKER_IMG os.getenv(DOCKER_IMAGE) WORKSPACE_HOST os.path.abspath(./sandbox_workspace) # ---------- Hook 安全拦截层 ---------- FORBIDDEN_COMMANDS [rm -rf /, sudo, chmod 777 /, mkfs] def security_hook(cmd: str): for bad in FORBIDDEN_COMMANDS: if bad in cmd: return False, f拒绝执行高危指令{bad} return True, # ---------- 沙箱容器封装 ---------- class SandBox: def __init__(self): self.container docker_client.containers.run( DOCKER_IMG, detachTrue, ttyTrue, volumes{WORKSPACE_HOST: {bind: /workspace, mode: rw}}, working_dir/workspace, removeTrue, ) def run_command(self, cmd: str) - str: ok, msg security_hook(cmd) if not ok: return msg try: res self.container.exec_run(cmd, stdoutTrue, stderrTrue) return res.output.decode(utf-8, errorsignore) except Exception as e: return str(e) def destroy(self): try: self.container.stop() except Exception: pass # ---------- 工具定义 ---------- TOOLS_PROMPT 你只能调用下面工具输出格式固定 TOOL:tool_name|args 可选工具 1. read_file|file_path 2. write_file|file_pathcontent 3. run_shell|command 完成全部任务后输出DONE # ---------- Agent Harness 主循环 ---------- def agent_harness(task: str): sandbox SandBox() conversation [{role: system, content: TOOLS_PROMPT}] conversation.append({ role: user, content: f开发任务{task}所有文件写在/workspace目录, }) round_count 0 print( Agent 启动 ) while round_count MAX_ROUND: round_count 1 resp client.chat.completions.create( modelos.getenv(MODEL_NAME), messagesconversation, temperature0.1, ) reply resp.choices[0].message.content.strip() print(f\n[Round{round_count}] LLM输出:\n{reply}) if reply DONE: print(Agent 完成任务) break if reply.startswith(TOOL:): body reply.removeprefix(TOOL:) if read_file| in body: _, path body.split(|) full_path os.path.join(WORKSPACE_HOST, path) if os.path.exists(full_path): with open(full_path, r, encodingutf-8) as f: obs f.read() else: obs f文件 {path} 不存在 elif write_file| in body: _, content body.split(|, 1) fp, text content.split(, 1) host_fp os.path.join(WORKSPACE_HOST, fp) with open(host_fp, w, encodingutf-8) as f: f.write(text) obs f成功写入文件 {fp} elif run_shell| in body: _, cmd body.split(|, 1) obs sandbox.run_command(cmd) else: obs 工具参数错误 conversation.append({role: assistant, content: reply}) conversation.append({role: user, content: f工具返回结果:\n{obs}}) else: conversation.append({role: assistant, content: reply}) conversation.append({ role: user, content: 请使用TOOL格式调用工具不要自由对话, }) sandbox.destroy() print(沙箱销毁Harness 执行结束) if __name__ __main__: task_input 写一个加法函数并编写pytest单元测试 agent_harness(task_input)几个设计点值得说明。第一工具调用没有用 OpenAI 的 function calling而是用纯文本协议TOOL:tool|args好处是任何模型都能用不依赖特定 API 能力坏处是解析要自己写但对 Demo 来说足够。第二write_file直接写宿主机目录而不是容器内因为目录已经挂载进容器两边看到的是同一份文件省去docker cp。第三run_shell走容器执行read_file/write_file走宿主机这样即使容器挂了文件也不丢。如果你后续想换成 MCP 协议或 function calling只需要替换工具解析这一段主循环结构不变。这也是 Harness 分层的意义循环控制是骨架工具协议是可插拔的皮。4. 验证请求跑一次完整任务并观察日志配置写完直接运行cd simple_harness python harness.py第一次运行会拉取python:3.11-slim镜像可能等一两分钟。之后你会看到类似这样的日志 Agent 启动 [Round1] LLM输出: TOOL:write_file|add.pydef add(a, b): return a b [Round2] LLM输出: TOOL:write_file|test_add.pyfrom add import add def test_add(): assert add(1, 2) 3 [Round3] LLM输出: TOOL:run_shell|python -m pytest -q [Round4] LLM输出: DONE Agent 完成任务 沙箱销毁Harness 执行结束日志里有几个关键观察点。第一每一轮的LLM输出是模型原始回复你能看到它是否严格按TOOL:格式走如果它开始自由对话说明 prompt 约束不够可以加强 system 提示。第二工具返回结果是回灌给模型的 observation这是 Agent 循环的“感知”环节如果这里返回空或报错模型下一轮就会乱。第三DONE是终止信号模型自己判断任务完成Harness 只负责识别。跑完后检查sandbox_workspace目录应该能看到add.py和test_add.py两个文件。再手动验证一次cd sandbox_workspace python -m pytest -q如果显示1 passed说明 Agent 写的代码和测试都是对的整条链路跑通了。这一步很重要——不要只看 Harness 打印了DONE就信一定要自己复跑测试因为模型可能输出DONE但测试其实是失败的。想验证模型接口本身是否正常可以在模型对话页单独发一条消息对比输出风格 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 。如果那边正常、Harness 里报错问题多半在代码而不是 Key。日志观察还有一个进阶技巧把每轮的conversation长度打印出来。随着轮次增加上下文会越来越长如果某轮突然暴涨说明工具返回了超大文件内容这时候就该在 Harness 里加截断逻辑只回灌前 N 行。这是上下文管理最朴素的起点。5. 常见报错排查401、local proxy failed 与 reading choices跑 Harness 最容易卡在几个固定报错上逐个说。401 Unauthorized。最常见的原因是 Key 没读到或写错。先确认.env和harness.py在同一目录load_dotenv()默认从当前工作目录找。如果你在别的目录运行python simple_harness/harness.py.env就找不到api_key变成None直接 401。解决办法是cd进目录再跑或者用load_dotenv(dotenv_path...)指定绝对路径。另外检查 Key 有没有多余空格复制时容易带上换行。local proxy failed / connection error。这个报错通常出现在client.chat.completions.create那一行本质是网络请求没发出去。先确认LLM_BASE_URL写的是https://taotoken.net/api不要带/v1也不要带任何查询参数。然后单独用 curl 测一下连通性curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer sk-你的key能返回模型列表就说明网络和 Key 都没问题报错就在代码里。如果 curl 也失败检查本机网络环境是否正常。reading choices of undefined。这是 JavaScript 风格的报错在 Python 里对应的是resp.choices为None或resp结构不对。原因通常是 SDK 版本和接口不匹配或者返回体是错误 JSON。打印resp原始内容就能看到多半是{error: {...}}。这时候检查 Model ID 是否拼错——模型名写错时接口会返回错误对象而不是正常 completionresp.choices自然取不到。去控制台模型列表核对一遍 ID。容器起不来 / docker.errors.DockerException。先docker ps确认 Docker 服务在跑。如果报权限错误Linux 上常见把当前用户加进 docker 组sudo usermod -aG docker $USER然后重新登录。Windows/Mac 上确认 Docker Desktop 已启动。Agent 一直不输出 DONE跑满 MAX_ROUND。这不是报错但很常见。原因一般是任务描述太模糊模型不知道什么时候算完成。解决办法是在 system prompt 里明确“完成标准”比如“测试全部通过后输出 DONE”。另外把MAX_ROUND设小一点比如 8先观察别一上来就 50 轮烧 token。排查顺序建议固定成先 curl 测接口 → 再单独跑一个最小client.chat.completions.create→ 最后才跑完整 Harness。这样能把“模型接入问题”和“Harness 逻辑问题”分开省很多时间。6. 从 Demo 到可用下一步怎么扩展这个 Demo 只实现了单 Agent、三个工具、本地 Docker 沙箱属于最小可用。真实项目里你会需要几样东西多 Agent 调度规划、编码、测试分工、MCP 协议适配对接 Git、数据库等外部工具、分层记忆短期会话 长期项目架构、失败自动回滚。这些都可以在现有主循环上叠加骨架不用推倒。如果你打算长期做编码类 Agent建议把模型调用固定走 TaoToken 的 Coding Plan这样多模型切换和额度管理都在一处Harness 代码里只留一个 Base URL https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 里面有各语言 SDK 的配置示例照着改base_url和api_key就行。最后留一个实用技巧把每次任务的完整conversation落盘成 JSON任务结束后回看你会发现模型在哪一轮开始跑偏、哪个工具返回让它误判。这份轨迹比任何评测分数都更能告诉你 Harness 该改哪里。
返回列表