Agent Harness Runtime 架构深度解析:从工具循环到状态外置的 Sandbox 落地骨架
1. 为什么你的 Agent 跑十分钟就开始失忆先说一个我踩过的坑。去年做一个自动修 CI 的 Agent单轮任务跑得挺顺一旦让它连续处理十几个失败用例到第七八个就开始胡来明明前面已经改过的文件又改回去测试命令重复跑最后还自信地宣布全部修复完成实际上一半用例还是红的。当时第一反应是模型不行换了个更大的权重结果只是把崩溃点从第七个推迟到第十个。问题不在模型参数里而在模型外面那层运行系统。业界现在管这层叫 Agent Harness Runtime——你可以把它理解成模型的操作系统外壳模型只负责推理下一步该干什么Harness 负责把这一步变成可执行、可观测、可恢复的动作。它决定了模型能看到什么上下文、能调用什么工具、在什么环境里执行、失败后怎么被拉回来、长任务跑偏时谁来纠偏。用一句工程化的公式概括就是agent model harness。同一个模型、同一个任务、同样的预算只调整 harnessCoding Agent 的表现可以差出一个数量级。所以这篇不聊模型选型专门拆 Harness Runtime 在 Sandbox 场景下的落地骨架工具循环的调度边界在哪、状态外置怎么选型、长程任务中断后怎么恢复。文末给一份可直接复制的 config.toml 和 settings.json再走三步验证启动 Runtime、触发一次工具循环、中断后按状态外置恢复任务。适合谁看正在把 Coding Agent 往生产环境推的工程师被跑一半就崩折磨过的同学以及想搞清楚 Harness 到底管哪些事的人。2. 工具循环的调度边界谁来决定下一步工具循环Tool Loop是 Harness 的心脏。它的基本形态很朴素模型输出一个工具调用意图 → Harness 校验并执行 → 把结果回灌给模型 → 模型决定下一步。循环直到模型不再调用工具或者触发退出条件。听起来简单但调度边界一旦模糊Agent 就会失控。我把它拆成四个必须明确的边界。2.1 单轮工具调用的数量上限模型一次可能吐出多个工具调用并行 tool calls。Harness 必须设上限否则一个帮我重构整个项目的指令可能瞬间触发几十个文件写入。建议单轮并行调用不超过 5 个超出的排队到下一轮。这个值写在 config.toml 里别硬编码。2.2 工具结果的截断与落盘这是最容易被忽略的边界。工具输出不能无脑塞进 Context。几千行日志、完整网页、巨型目录树全部塞进去会瞬间吃光窗口还会让后续推理被噪音淹没。正确做法是 Tool-call Offloading大输出落盘Context 里只留摘要、文件路径和可继续查询的线索。模型需要细节时再用 rg 或 Read 精确取片段。2.3 循环的退出条件退出不能只靠模型说完成了。Harness 要维护一个显式的退出判定任务清单是否全部勾选、必须通过的测试是否真的绿了、有没有未提交的变更。这些条件由 Stop Hook 在模型尝试退出时校验不满足就把控制权打回去。2.4 单步超时与整体预算每个工具调用要有超时比如 120 秒整个任务要有 token 和 wall-clock 预算。超预算时 Harness 主动中断并落盘状态而不是等模型自己发现我好像跑太久了。把这四个边界写进配置工具循环才从能跑变成可控。3. TaoToken 前置给 Runtime 一个稳定的模型入口Harness Runtime 本身不产生推理能力它需要一个模型入口。在 Sandbox 里跑长任务模型入口的稳定性比峰值性能更重要——因为一次连接抖动可能让跑了二十分钟的任务前功尽弃。我现在的做法是把模型调用统一走 TaoToken 的 API 入口好处是接口格式稳定、便于在 Harness 里做统一的重试和超时封装。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。拿 Key 的路径很直接进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建一个密钥。建议给 Harness 单独建一个 Key方便按任务维度统计消耗也方便出问题时快速吊销。如果你只是想先验证模型连通性可以直接用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 发一条消息确认 Key 和网络都正常再往 Runtime 里接。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面把请求格式、流式返回、错误码都列清楚了。Harness 里做重试时重点看 429 和 5xx 两类错误前者退避重试后者可以换一次连接再试。注意Harness 里不要把 Key 写死在代码或配置文件里。用环境变量注入Sandbox 启动时从宿主环境读取避免密钥随镜像或日志泄漏。4. 可复制配置config.toml 与 settings.json 骨架下面这份骨架是我在 Sandbox 场景里实际用过的精简版去掉了业务耦合保留 Harness Runtime 的核心结构。你可以直接抄进项目再按需改。4.1 config.tomlRuntime 主配置[runtime] name sandbox-harness max_parallel_tool_calls 5 # 单轮并行工具调用上限 step_timeout_seconds 120 # 单步工具调用超时 task_budget_tokens 800000 # 整体 token 预算 task_budget_seconds 3600 # 整体 wall-clock 预算 [model] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不落盘 model claude-sonnet-4-5 max_retries 3 retry_on [429, 500, 502, 503] [state] # 状态外置文件系统 Git 记忆库三层 plan_file .harness/plan.md handoff_dir .harness/handoff tool_output_dir .harness/tool-output memory_store .harness/memory git_worktree true # 每个任务独立 worktree auto_commit true # 每完成一个子任务自动提交 [sandbox] workdir /workspace network deny # 默认禁出网 network_allowlist [taotoken.net] deny_paths [/etc, /root/.ssh, **/.env] preinstalled [git, rg, jq, yq, pnpm, python3] [loop] exit_requires [plan_complete, tests_green, no_uncommitted] offload_threshold_bytes 8192 # 超过 8KB 的工具输出落盘几个关键点解释一下。max_parallel_tool_calls和step_timeout_seconds是工具循环的硬边界。state段是状态外置的核心plan 文件、handoff 目录、工具输出目录、记忆库四者分工明确。sandbox.network deny配合白名单是通用 Bash 能力的安全底线。loop.exit_requires定义了退出判定缺一不可。4.2 settings.jsonHook 与工具权限{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: python3 .harness/hooks/guard_bash.py } ] } ], PostToolUse: [ { matcher: Edit|Write|MultiEdit, hooks: [ { type: command, command: cd $HARNESS_WORKDIR pnpm tsc --noEmit 21 | head -50 } ] } ], Stop: [ { hooks: [ { type: command, command: python3 .harness/hooks/check_exit.py } ] } ] }, permissions: { allow: [Read, Edit, Write, Bash(git:*), Bash(rg:*), Bash(pnpm test:*)], deny: [Bash(rm -rf:*), Bash(curl:*)] } }Hook 的设计精神是成功静默失败喧哗。PostToolUse 里的 typecheck 通过时不返回任何信息避免污染 Context失败时把错误塞回下一轮模型必须修。这样你就不需要在项目规则文件里反复写改完记得跑类型检查——纪律已经从提示词变成运行时逻辑。guard_bash.py负责拦截危险命令check_exit.py负责在模型宣布完成前校验 plan、测试和未提交变更。这两个脚本是 Harness 的确定性防线比任何提示词都可靠。5. 三步验证启动、循环、恢复配置写完不算完得跑通三步才算 Harness 真的立起来了。5.1 第一步启动 Runtimeexport TAOTOKEN_API_KEY你的密钥 export HARNESS_WORKDIR/workspace # 初始化状态目录 mkdir -p .harness/{handoff,tool-output,memory,hooks} # 启动 Runtime python3 -m harness.runtime --config config.toml --settings settings.json启动成功的标志是日志里出现runtime ready并且.harness/下四个目录都建好了。如果报api_key_env not found检查环境变量是否导出如果报workdir not writable检查 Sandbox 挂载权限。5.2 第二步触发一次工具循环给 Runtime 一个最小任务比如统计当前目录下所有 .py 文件的行数写入 .harness/plan.md。python3 -m harness.runtime --task 统计当前目录下所有 .py 文件的行数结果写入 .harness/plan.md预期行为模型先调用 Bash 执行find . -name *.py | xargs wc -lHarness 校验命令通过白名单后执行输出如果超过 8KB 就落盘到.harness/tool-output/Context 里只留摘要。然后模型调用 Write 把结果写进 plan 文件PostToolUse 触发 typecheck这里没有 TS 文件会直接通过最后 Stop Hook 校验退出条件。验证成功的标志.harness/plan.md里有统计结果.harness/tool-output/下可能有落盘文件日志里能看到完整的工具调用链。5.3 第三步中断后按状态外置恢复这是最关键的一步。手动中断 RuntimeCtrlC 或 kill然后重新启动并指定恢复python3 -m harness.runtime --config config.toml --resume恢复逻辑是这样的Runtime 读取.harness/plan.md看哪些子任务已完成读取.harness/handoff/里最后一次交接摘要从 Git worktree 恢复代码状态然后从断点继续。如果 plan 文件里第一项已勾选恢复后应该直接从第二项开始而不是重头再来。验证成功的标志恢复后的日志显示resuming from checkpoint且不会重复执行已完成的子任务。如果它从头开始跑说明状态外置没生效——大概率是 plan 文件没写成功或者--resume没读到正确的 handoff 目录。6. 本篇常见错排查跑不通的时候按下面这几类对号入座。工具循环停不下来检查loop.exit_requires里的条件是不是永远无法满足。最常见的是tests_green依赖一个根本不存在的测试命令Stop Hook 每次都判定失败模型就一直修。先把退出条件简化成plan_complete单项跑通再加。Context 爆炸offload_threshold_bytes设太大或者工具输出没走落盘逻辑。检查.harness/tool-output/目录是不是空的——如果是空的但 Context 还是爆说明落盘钩子没接上。恢复后重复执行plan 文件的勾选状态没持久化或者--resume读的是旧 handoff。检查.harness/plan.md的修改时间确认中断前最后一次写入成功了。Git worktree 如果没自动提交恢复时也会丢状态。Hook 报错但模型看不见PostToolUse 的失败输出必须回灌到下一轮 Context否则模型不知道自己错了。检查 Hook 的 stderr 是不是被吞了。成功静默、失败喧哗喧哗的部分要确保模型能收到。Sandbox 网络全禁导致模型调不通network_allowlist里要加上模型 API 的域名。如果用的是 TaoToken 入口把taotoken.net加进白名单否则 Runtime 连模型都够不着。并行工具调用冲突多个工具同时写同一个文件后写的覆盖先写的。max_parallel_tool_calls调小或者给文件写入加锁。Git worktree 隔离能缓解但不能根治关键还是调度层要识别写冲突。7. 把 Runtime 接进你的工作流Harness Runtime 的价值不在于配置多漂亮而在于它把等模型升级变成了今晚就能改的工程对象。工具循环的边界、状态外置的选型、长程任务的恢复策略这三件事每一件都能独立优化也都能独立验证。如果你正在做长期编码或 Agent 类项目建议把模型入口和 Runtime 配置分开管理。模型侧走 TaoToken 的 API 入口 https://taotoken.net/api Runtime 侧按上面的骨架落地。需要看具体接入参数就去文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 需要管理多个任务的 Key 就去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果是团队长期跑 Agent 工作流Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在配额和稳定性上更适合持续任务。最后留一个我自己的习惯每次 Agent 翻车先别急着换模型去.harness/目录里翻 plan 文件和 handoff 摘要。十次里有七次问题就写在那几行状态里。