1. 从一次工具调用失败说起智能体 Harness 到底在解决什么你大概率遇到过这种场景本地用 LangChain 搭了个能查数据库、能读文件的智能体demo 跑得挺顺一放到真实任务里就崩——模型把工具名拼错、参数少传一个字段、上下文越滚越长最后超了窗口、某个工具超时之后整个链路直接挂掉。这些问题的根子不在模型本身而在模型外面那层「壳」没搭好。这层壳就是 Harness。Harness 这个词直译是「马具」套在马身上让马的力量能被驾驭。放到智能体里它的角色完全一样模型负责推理和生成Harness 负责把模型的输出变成可靠的动作。行业里有个很直白的公式Agent Model Harness。模型是大脑Harness 是神经、骨骼和护栏。工具调用的协议约束、上下文的动态裁剪、执行循环的终止判断、出错后的重试与降级全归 Harness 管。为什么现在要专门聊这个因为智能体已经从「能不能跑通」进入「能不能稳定跑」的阶段。LangChain 1.0 把可组合性做到了极致Runnable 接口让组件像积木一样拼OpenClaw 走的是另一条路用控制面-数据面解耦的云原生架构把长周期任务的稳定性当成第一目标。两者不是替代关系而是分别对应「开发期快速验证」和「运行期生产管控」两个阶段。这篇就按工程化落地的视角把 Harness 的分层设计拆开给你一套能直接复制、能在本地跑通最小闭环的配置骨架。适合谁看已经用 LangChain 或类似框架写过智能体、但被工具调用不稳定和上下文膨胀折磨过的开发者准备把智能体从脚本升级成服务、需要一套可维护执行层的团队。下面所有配置和命令都以本地可复现为准不依赖任何特殊网络环境。2. TaoToken 前置准备把模型接入层先固定下来在动 Harness 之前得先把模型接入这一层固定住。原因很简单Harness 的工具调用协议、上下文格式、重试逻辑全都建立在「模型接口稳定」这个前提上。如果模型接入层今天换一个地址、明天换一个 KeyHarness 的调试根本没法收敛。我试过把模型接入层单独抽出来用统一的 OpenAI 兼容接口对接Harness 只认 Base URL Key Model ID 这三件套。这样无论底层换哪个模型Harness 代码一行不用改。TaoToken 在这里的角色就是提供这个稳定的接入层它的 API 地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions和/v1/models接口工具调用function calling走标准格式。先拿 Key。打开https://taotoken.net/api-keys登录后创建一个新 Key复制出来。注意 Key 只在创建时完整显示一次丢了就得重建。拿到之后不要硬编码进代码放到环境变量里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api/v1验证接入层是否通先用最朴素的 curl 打一发确认模型能正常返回curl -s $TAOTOKEN_BASE_URL/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 只回复两个字通了}], max_tokens: 16 }返回里能看到choices[0].message.content是「通了」说明接入层没问题。这一步看着简单但它是后面所有 Harness 调试的地基。如果这里就不通别急着写 Harness先排接入层。模型 ID 怎么选Harness 场景下优先选工具调用能力强的模型。Claude 系列在 function calling 的稳定性上表现不错参数格式很少出错如果你要做代码相关的智能体可以选带 coding 能力的模型。具体有哪些可用模型直接查https://taotoken.net/api/v1/models或者到模型对话页面手动试几个看哪个在你任务上的工具调用准确率高。选型不用纠结太久Harness 的抽象层做好之后换模型成本很低。这里要强调一个工程习惯把模型接入层和 Harness 执行层在代码里分成两个模块。接入层只负责「发请求、收响应、解析 tool_calls」Harness 只负责「决定调哪个工具、怎么处理结果、什么时候停」。这样调试的时候能快速定位问题出在哪一层。很多人把这两层揉在一起写结果工具调用失败时分不清是模型没返回对、还是 Harness 解析错了。3. 可复制的 Harness 配置骨架工具协议、上下文与执行循环这一节是核心给你一套能直接落地的 Harness 骨架。我把它拆成三个配置文件加一个执行循环每个都能单独替换。3.1 工具协议定义用 JSON Schema 把工具钉死工具调用的第一原则是「描述即协议」。每个工具必须有严格的 JSON Schema名称语义化参数必填项明确。下面是一个settings.json风格的配置片段路径放在项目根目录的harness/tools.json{ tools: [ { name: query_sales_db, description: 查询销售数据库返回指定日期区间的销售额汇总。仅支持只读查询。, parameters: { type: object, properties: { start_date: { type: string, description: 起始日期格式 YYYY-MM-DD }, end_date: { type: string, description: 结束日期格式 YYYY-MM-DD }, region: { type: string, enum: [north, south, east, west], description: 销售区域不传则查全国 } }, required: [start_date, end_date] } }, { name: read_local_file, description: 读取本地工作区内的文本文件返回文件内容。路径必须在 workspace 目录下。, parameters: { type: object, properties: { path: { type: string, description: 相对于 workspace 的文件路径 } }, required: [path] } } ] }注意几个细节。description里明确写了「仅支持只读查询」「路径必须在 workspace 目录下」这是在给模型划边界减少越权调用。region用enum限定取值避免模型编造出不存在的区域。required字段一个都不能少否则模型可能漏传参数。工具名称用query_sales_db而不是q1语义化命名能让模型的意图识别准确率明显提升。这不是玄学模型在选工具时靠的就是名称和描述的语义匹配。3.2 上下文管理配置分层 压缩阈值上下文管理是 Harness 里最容易失控的部分。我的做法是分三层系统层角色定义 工具清单、会话层当前任务的历史消息、临时层工具返回的大块数据。配置放在harness/context.toml[context] max_tokens 120000 compact_threshold 0.75 keep_recent_messages 8 [context.system] role_file harness/prompts/system.md tools_file harness/tools.json [context.compact] strategy summarize_then_truncate summary_model claude-sonnet-4-5 preserve_tool_calls true [context.tool_result] max_inline_chars 4000 overflow_action write_to_workspace workspace_dir ./workspacecompact_threshold 0.75的意思是上下文用到 75% 就开始压缩别等到 100% 才处理那时候模型已经开始丢信息了。keep_recent_messages 8保证最近 8 条消息原样保留压缩只动更早的部分。preserve_tool_calls true很关键——工具调用的请求和返回不能被摘要掉否则模型会忘记自己调过什么。overflow_action write_to_workspace是处理大块工具返回的策略如果某个工具返回超过 4000 字符不直接塞进上下文而是写到 workspace 文件里上下文里只留一个文件路径和摘要。这样既保留了数据又不撑爆窗口。3.3 执行循环带重试和终止判断的主循环执行循环的骨架用 Python 写放在harness/loop.py。核心逻辑是「请求模型 → 解析 tool_calls → 执行工具 → 把结果塞回消息 → 再请求」直到模型不再调工具或达到最大轮次import json import os import time from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) MAX_TURNS 12 TOOL_RETRY 2 def load_tools(pathharness/tools.json): with open(path) as f: return json.load(f)[tools] def execute_tool(name, args, registry): if name not in registry: return {error: funknown tool: {name}} for attempt in range(TOOL_RETRY 1): try: return registry[name](**args) except Exception as e: if attempt TOOL_RETRY: return {error: str(e), attempts: attempt 1} time.sleep(2 ** attempt) def run_agent(user_input, registry, system_prompt): tools load_tools() messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] for turn in range(MAX_TURNS): resp client.chat.completions.create( modelclaude-sonnet-4-5, messagesmessages, tools[{type: function, function: t} for t in tools], tool_choiceauto, ) msg resp.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: args json.loads(call.function.arguments) result execute_tool(call.function.name, args, registry) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) return 达到最大轮次任务未完成这段代码里有三个 Harness 的关键设计。第一MAX_TURNS是硬性终止条件防止模型陷入无限调用循环。第二execute_tool带指数退避重试工具超时不会直接崩掉整个任务。第三工具返回统一用 JSON 字符串塞回role: tool的消息里tool_call_id必须和请求对应否则模型会报错。把这三个配置文件和循环拼起来就是一个最小可用的 Harness。工具协议管「能调什么」上下文配置管「记多少」执行循环管「怎么跑」。三者解耦任何一层要换都不影响其他两层。4. 本地验证跑通最小闭环并观察执行轨迹配置写完得验证它真的能跑。验证分两步先跑一个单工具任务再跑一个多工具协同任务。先准备一个最简单的工具注册表放在harness/registry.pyimport os def read_local_file(path): base os.path.abspath(./workspace) target os.path.abspath(os.path.join(base, path)) if not target.startswith(base): raise ValueError(path escapes workspace) with open(target, encodingutf-8) as f: return {content: f.read()[:2000]} def query_sales_db(start_date, end_date, regionNone): return { start_date: start_date, end_date: end_date, region: region or all, total: 128400, currency: CNY, } REGISTRY { read_local_file: read_local_file, query_sales_db: query_sales_db, }在workspace/下放一个notes.txt随便写几行内容。然后跑单工具任务from harness.loop import run_agent from harness.registry import REGISTRY system 你是一个数据分析助手。需要读文件时调用 read_local_file需要查销售数据时调用 query_sales_db。 print(run_agent(帮我读一下 notes.txt 的内容, REGISTRY, system))预期结果是模型调用read_local_file返回文件内容然后模型用自然语言总结给你。如果这一步通了说明工具协议、执行循环、模型接入三层都对齐了。再跑多工具协同print(run_agent( 先读 notes.txt然后查 2026-01-01 到 2026-01-31 的销售数据把两者结合起来给我一段总结, REGISTRY, system ))这一步会触发两次工具调用中间还涉及上下文里同时保留文件内容和查询结果。观察执行轨迹的方法是在run_agent里加日志把每一轮的msg.tool_calls和工具返回打出来print(f[turn {turn}] tool_calls{[c.function.name for c in msg.tool_calls] if msg.tool_calls else none})正常的话你会看到类似这样的轨迹[turn 0] tool_calls[read_local_file] [turn 1] tool_calls[query_sales_db] [turn 2] tool_callsnone三轮结束模型给出总结。如果轮次明显偏多或者工具名调错说明工具描述还不够清晰回去改tools.json里的description。验证上下文压缩是否生效可以故意让工具返回一大段文本把max_inline_chars调小到 500看它是否按配置写到 workspace 而不是塞进上下文。这一步能帮你确认压缩策略真的在跑而不是配置写了没生效。5. 常见报错排查401、tool_calls 解析失败与循环不终止跑不通的时候错误基本集中在几个地方。下面按真实报错对照排查。401 Unauthorized。最常见的原因是 Key 没读到或者带了多余空格。先确认环境变量echo $TAOTOKEN_API_KEY | head -c 8如果输出为空说明 export 没生效检查是不是在子 shell 里设的。如果 Key 开头不是sk-可能复制时漏了。还有一种情况是 Base URL 写成了https://taotoken.net/api而没带/v1OpenAI SDK 会拼成/api/chat/completions路径不对也会 401 或 404。正确写法是https://taotoken.net/api/v1。local proxy failed / connection error。这类报错通常是本地网络配置问题不是 Key 的问题。先确认能不能直接访问 Base URLcurl -s -o /dev/null -w %{http_code} $TAOTOKEN_BASE_URL/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY返回 200 说明网络通。如果返回 000 或超时检查本机 DNS 和防火墙设置。注意不要在代码里硬编码任何代理地址保持直连。reading choices of undefined。这个报错说明resp.choices是 undefined通常是响应体结构不对。打印完整响应看看print(resp.model_dump_json(indent2))常见原因是模型 ID 写错了接口返回了错误对象而不是正常的 completion 结构。确认model字段用的是/v1/models里列出的真实 ID。另一个原因是请求体里messages格式不对比如 system 消息放在了 user 之后。tool_calls 解析失败 / arguments 不是合法 JSON。模型返回的function.arguments偶尔会是空字符串或截断的 JSON。加一层防御try: args json.loads(call.function.arguments or {}) except json.JSONDecodeError: args {} messages.append({ role: tool, tool_call_id: call.id, content: json.dumps({error: invalid arguments, please retry with valid JSON}), }) continue把解析失败当成一次工具调用错误返回给模型模型下一轮通常会修正格式。这比直接抛异常中断任务要好。循环不终止 / 达到最大轮次。如果模型反复调同一个工具检查工具返回里有没有明确的成功标志。如果工具返回{error: ...}模型可能会一直重试。在工具返回里加上status: ok或status: failed并在 system prompt 里说明「如果工具返回 status 为 failed不要重试直接告知用户」。另外MAX_TURNS别设太大12 轮对大多数任务够了设太大反而掩盖了逻辑问题。OAuth / 认证方式混淆。如果你之前用过 Claude Code 或 Codex 的 OAuth 登录流程注意那套认证和 API Key 是两回事。Harness 里统一用 API Key不要混用 OAuth token。如果你在用 CC Switch 或 Cline 这类工具配置里要写全三件套Base URL 填https://taotoken.net/api/v1Key 填你的 API KeyModel ID 填具体模型名。三件套缺一个都会报认证或模型找不到的错。排查的顺序建议固定下来先 curl 验证接入层再单工具跑再多工具跑最后压上下文。每层通了再进下一层别跳步。6. 把 Harness 用起来从最小闭环到长期编码任务最小闭环跑通之后下一步是把它接到真实任务上。如果你主要做的是长周期的编码或 Agent 任务比如让智能体持续读代码库、改文件、跑测试那 Harness 的上下文管理和执行循环会承受更大压力。这时候可以考虑用 Coding Plan 这类面向长期编码场景的方案它在会话保持和工具调用配额上更适合连续任务不用每次从零搭上下文。接入方式还是那三件套Base URL 用https://taotoken.net/api/v1Key 用你在 API Keys 页面创建的Model ID 按任务类型选。配置写进你的 Harness 配置文件里执行循环不用改。如果你想先手动验证某个模型在工具调用上的表现可以到模型对话页面直接试给它一段带工具描述的任务看它返回的 tool_calls 格式对不对、参数全不全。这一步能帮你在写代码之前就筛掉工具调用能力弱的模型。文档里对工具调用格式、上下文长度限制、各模型的能力差异有更细的说明遇到配置项不确定的时候翻一下比猜快。接入文档在https://taotoken.net/doc里面有完整的请求示例和参数说明。最后说一个实操经验Harness 的配置不要一次写全先让工具协议和执行循环跑起来上下文压缩最后加。因为压缩策略会改变消息结构如果一开始就加工具调用出问题时你分不清是协议错了还是压缩把关键信息删了。等基础循环稳定了再逐步调compact_threshold和max_inline_chars每次只改一个参数观察执行轨迹的变化。这样调出来的 Harness 才是你能掌控的而不是一堆配置堆在一起碰运气。