ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Agent Harness Runtime 架构深度解析:工具循环、状态外置与长程任务调度实战

Agent Harness Runtime 架构深度解析:工具循环、状态外置与长程任务调度实战 1. 为什么你的 Agent 跑长任务总在半路“失忆”先说一个我踩过的坑。去年做一个自动修 bug 的 Agent单轮任务跑得挺顺一旦让它连续处理十几个文件、跨几十分钟就开始出问题改到第八个文件时忘了最初的目标重复改已经改过的文件最后还自信地宣布“全部完成”结果测试一跑红一片。当时第一反应是模型不行换了个更强的模型好了一点但没根治。后来才想明白问题不在模型参数里而在模型外面那层运行系统。这层系统现在有个名字叫 Agent Harness Runtime也有人把它归到 Harness Engineering 这个方向。用一句工程化的公式概括就是agent model harness。模型负责推理下一步Harness 负责把“下一步”变成可执行、可观测、可纠偏的动作。这篇不讲概念讲落地。我会把 Agent Harness Runtime 拆成三块能直接抄的东西工具循环的调度边界、状态外置的持久化设计、长程任务的断点续跑机制。每一块都给可复制的配置片段和状态存储结构最后演示一次长程任务从触发到恢复的完整验证动作。适合已经在写 Agent、但被长任务稳定性折磨的工程师也适合刚接触 Harness Engineering、想知道运行时到底该管哪些事的人。核心检索词先摆出来Agent Harness Runtime 是一套把模型接入真实工作流的运行时外壳工具循环决定它能行动状态外置决定它能续航长程任务调度决定它不跑偏。这三件事做不好换再强的模型也是白搭。2. TaoToken 前置给 Harness 一个稳定的模型出口在动手写 Runtime 之前得先解决模型调用这一层。Harness 的循环里每一轮都要向模型发请求如果这个出口不稳定工具循环的调度边界根本无从谈起——你连“下一步推理”都拿不到谈何纠偏。我现在的做法是把模型出口统一走 TaoToken。它的 API 地址是 https://taotoken.net/api兼容 OpenAI 风格的接口Harness 里换模型只需要改 base_url 和 model 两个字段不用动循环逻辑。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册和文档都在上面。为什么 Harness 场景特别需要这一层因为长程任务对模型出口有三个硬要求一是稳定循环跑几十轮不能中途断二是可切换不同子任务可能适合不同模型比如规划用强推理、执行用快模型三是可观测每轮调用的 token 和耗时得能统计不然长任务成本失控你都不知道。TaoToken 在这三点上省事。它的接入文档在 https://taotoken.net/doc API Key 在 https://taotoken.net/api-keys 生成。我一般会在 Harness 的配置里把模型出口单独抽成一个 provider 配置这样工具循环里只认 provider 接口不认具体厂商。这里要提醒一句Harness 的模型出口配置不要写死在代码里。长任务调试时你会频繁换模型对比效果写死意味着每次都要改代码重启。抽成配置后改一个 JSON 就能切换这对后面验证断点续跑特别重要——恢复任务时你可能想换个模型继续跑配置化让这件事变成改一行。另外如果你打算长期跑编码类 Agent可以了解下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合高频、长时间的 Agent 调用场景。模型对话入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 调试单个工具调用时我常在那上面先验证 prompt 和工具描述。3. 可复制配置工具循环与状态外置的 Runtime 片段这一节给能直接抄的配置。我把 Harness Runtime 拆成两个配置文件一个是 runtime 主配置管工具循环和模型出口一个是状态存储结构管外置持久化。先看 runtime 主配置。我用 TOML 写路径放在项目根目录的harness/runtime.toml[model] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model_id claude-sonnet-4-5 max_tokens 8192 temperature 0.2 [loop] max_iterations 60 tool_timeout_seconds 120 max_consecutive_tool_errors 3 context_compact_threshold 0.75 offload_tool_output_over_bytes 8192 [state] root_dir ./harness/state plan_file plan.md handoff_file handoff.json tool_output_dir tool_outputs checkpoint_file checkpoint.json git_worktree true [sandbox] allow_shell true deny_commands [rm -rf /, curl * | sh, chmod 777 *] allow_network false network_allowlist [api.taotoken.net] workdir_isolated true [hooks] post_tool_use [ { matcher Edit|Write|MultiEdit, command cd $PROJECT_DIR pnpm tsc --noEmit 21 | head -50 } ] pre_tool_use [ { matcher Bash, command python harness/hooks/guard_bash.py } ] stop [ { command python harness/hooks/check_deliverables.py } ]这份配置里几个关键点值得说。loop.max_iterations是工具循环的硬边界防止模型陷入死循环max_consecutive_tool_errors是连续失败熔断工具连续报错三次就停下来交还控制权而不是无限重试烧 token。context_compact_threshold 0.75表示上下文用到 75% 就触发压缩但压缩有损耗所以配合offload_tool_output_over_bytes把大块工具输出落盘Context 里只留摘要和路径。再看状态存储结构。这是状态外置的核心目录长这样{ task_id: fix-auth-bug-20240612, goal: 修复登录接口在并发下的 token 校验失败问题, created_at: 2024-06-12T09:00:00Z, status: in_progress, current_step: 4, plan: [ { id: 1, desc: 复现并发失败, status: done, artifact: tool_outputs/repro.log }, { id: 2, desc: 定位校验逻辑, status: done, artifact: tool_outputs/grep_result.txt }, { id: 3, desc: 编写修复补丁, status: done, artifact: git:branch/fix-auth }, { id: 4, desc: 跑并发测试验证, status: in_progress, artifact: null }, { id: 5, desc: 提交并写变更说明, status: pending, artifact: null } ], handoff: { last_summary: 已定位到 token 校验的竞态条件补丁在 fix-auth 分支待验证, open_questions: [并发测试是否需要压到 1000 QPS], next_action: 运行 pnpm test:concurrency }, checkpoint: { iteration: 23, last_tool_call: Edit src/auth/verify.ts, context_tokens_used: 61200 } }这个checkpoint.json就是断点续跑的锚点。任务恢复时Harness 先读它知道跑到第几轮、最后调了什么工具、上下文用了多少然后从handoff.next_action继续而不是从头再来。plan数组里每项的status和artifact让进度可追溯artifact指向落盘的工具输出或 Git 分支需要细节时再精确读取。工具循环的调度边界在这份配置里体现为三层max_iterations是总量边界tool_timeout_seconds是单次边界max_consecutive_tool_errors是错误边界。三层叠加循环不会失控。状态外置则体现为state段和 checkpoint 结构凡是不能随 Context 截断而消失的信息全部落盘。4. 验证请求一次长程任务从触发到恢复的完整动作配置写完得验证它真能断点续跑。我设计了一个最小可复现的验证流程你可以照着跑一遍。第一步触发一个会中途“断电”的长任务。我写了个脚本模拟任务跑到一半进程被杀# harness/run_task.py import json, os, time from harness.runtime import HarnessRuntime runtime HarnessRuntime(config_pathharness/runtime.toml) task runtime.start_task(goal修复登录接口并发 token 校验失败) for step in task.plan: if step[status] done: continue result runtime.execute_step(step) runtime.save_checkpoint() # 每步落盘 if step[id] 4: print(模拟进程中断checkpoint 已保存) os._exit(1) # 硬退出模拟断电跑这个脚本任务会在第 4 步“跑并发测试验证”前硬退出。此时harness/state/checkpoint.json里应该记录了iteration、last_tool_call和next_action。第二步恢复任务。另起一个进程调用恢复入口# harness/resume_task.py from harness.runtime import HarnessRuntime runtime HarnessRuntime(config_pathharness/runtime.toml) task runtime.resume_task(task_idfix-auth-bug-20240612) print(f从第 {task.current_step} 步恢复下一步{task.handoff[next_action]}) result runtime.continue_task() print(f任务完成状态{result.status})resume_task内部做的事读checkpoint.json拿到iteration和last_tool_call读handoff.json拿到next_action读plan.md重建进度视图然后从第 4 步继续。关键是不重跑已完成的步骤——plan里status done的直接跳过。第三步验证模型出口。恢复时 Harness 会向模型发请求确认出口通。你可以单独测一下curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 回复 OK 即可}], max_tokens: 16 }返回里能看到choices[0].message.content就说明出口正常。这一步很重要因为长任务恢复时如果模型出口挂了Harness 会误判为任务失败而不是网络问题。实测下来这套流程能把一个原本跑 40 分钟、中途必崩的任务变成可中断、可恢复、可换模型继续跑的任务。恢复后从第 4 步接着跑前面三步的产物repro.log、grep_result.txt、fix-auth 分支都还在不用重来。5. 本篇常见错排查401、local proxy failed 与 reading choices配置和验证跑通之前大概率会撞几个报错。我把踩过的坑列出来对照着查。401 Unauthorized。最常见的原因是TAOTOKEN_API_KEY环境变量没设或者设了但 Harness 进程没读到。检查两点一是echo $TAOTOKEN_API_KEY有没有值二是 runtime.toml 里api_key_env写的变量名和实际环境变量名是否一致。我遇到过在 IDE 里设了环境变量、但终端跑脚本时没继承的情况统一在 shell 里 export 一次就好。local proxy failed / connection refused。这个报错通常不是模型出口的问题而是 Harness 自己的沙箱配置把出网拦了。看 runtime.toml 的[sandbox]段allow_network false时所有外部请求都会被拒。如果你需要访问模型 API把api.taotoken.net加进network_allowlist。注意别把allow_network直接改成 true 了事白名单更安全。reading choices of undefined。这是解析响应时choices字段不存在。原因一般是请求体格式不对或者模型 ID 写错了。检查model_id是否拼写正确请求体里messages是不是数组。还有一种情况是返回了错误对象而不是正常响应比如限流或参数错误这时响应里没有choices解析代码要先判断response.error再取choices。OAuth / token expired。如果你用的是需要 OAuth 的模型出口长任务跑到一半 token 过期循环会卡住。解决办法是在 Harness 里加一个 token 刷新 Hook挂在pre_tool_use上每次工具调用前检查 token 有效期快过期就刷新。或者干脆用 API Key 方式省掉刷新逻辑。checkpoint 写了但恢复时读不到。检查state.root_dir路径。相对路径是相对于进程工作目录的如果你在项目根目录跑脚本、但 Harness 内部cd到了别的地方路径就对不上。建议用绝对路径或者在 runtime 初始化时把工作目录固定下来。工具输出落盘了但 Context 里找不到。这是 offload 的正常行为。工具输出超过offload_tool_output_over_bytes就落盘Context 里只留路径。模型需要细节时得让它用 Read 或 rg 去读那个路径。如果你发现模型“看不到”输出检查它有没有拿到路径以及沙箱是否允许读tool_outputs目录。排查这类问题的通用思路先确认模型出口通curl 测再确认沙箱放行看 allowlist最后确认状态文件路径对看 checkpoint 有没有写进去。三层依次查基本能定位。6. 把 Harness 当成会进化的工程系统写到这里回到最开始那个问题为什么换模型没根治长任务失忆因为失忆的根因在 Harness 层——状态没外置、循环没边界、完成判定不独立。模型再强也救不了一个把关键状态只放在 Context 里的运行时。我现在维护 Harness 有个习惯每一条规则都要能追溯到一次具体失败。Agent 曾经提交过被注释掉的测试那就加一条 PostToolUse Hook 强制跑测试Agent 曾经在上下文快满时焦虑收尾那就加一条 Stop Hook 检查交付物。规则只增不减会变成配置垃圾抽屉所以模型变强后也要删掉过时的提醒。这套机制叫 Ratchet棘轮只往一个方向转让每次失败都沉淀成约束。如果你要动手建议从两个入口开始Skill 和 Hook。Skill 解决“Agent 能做什么”把领域能力封装成按需加载的包Hook 解决“Agent 必须怎么做”把类型检查、危险命令拦截、结束前复核放进运行链路。这两类改动看起来像配置实际上是在改 Agent 的运行时行为。模型出口这层用 TaoToken 把 base_url、Key、Model ID 三件套配好Harness 里只认 provider 接口。API Key 在 https://taotoken.net/api-keys 生成接入文档在 https://taotoken.net/doc 调试单个工具调用可以去模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 先验证 prompt。长期跑编码 Agent 的话Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 更适合高频调用场景。最后留一个实用技巧长任务恢复时先别急着 continue先让 Harness 打印一遍plan和handoff人工确认下一步动作合理再继续。我遇到过 checkpoint 记录正确、但 handoff 里的 next_action 已经过时的情况直接续跑会做无用功。多这一步确认能省不少返工。
返回列表