ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent开发实战:为什么换一套Harness比换两代模型更立竿见影

Agent开发实战:为什么换一套Harness比换两代模型更立竿见影 1. 为什么“换一套 Harness”比“换两代模型”更立竿见影先把结论摆在最前面如果你正在做 Agent 开发感觉模型升级带来的收益越来越小而任务成功率、工具调用准确率、长上下文稳定性始终卡在一个瓶颈上那大概率问题不在模型本身而在Harness这一层。所谓 Harness直白点说就是包裹在模型外面的那套“执行骨架”——它负责把用户意图翻译成模型能理解的输入格式负责解析模型吐出来的 Tool Call负责把工具执行结果塞回 Context负责决定什么时候该重试、什么时候该截断、什么时候该把历史消息压缩。模型是发动机Harness 是变速箱、传动轴和方向盘。发动机再强传动系统拉胯车也跑不快。我最近半年在几个 Agent 项目里反复验证过这件事。同一个业务场景同一批测试用例只把底层模型从上一代换成最新一代端到端任务成功率大概提升 8 到 12 个百分点但把 Harness 从“裸调 API 手动拼消息”换成一套设计良好的 ReAct 循环加 Context 管理策略成功率直接跳了 30 多个百分点。这个差距不是玄学而是因为绝大多数 Agent 失败案例根因都落在 Harness 层Tool Call 格式解析错了、Context 超限被截断导致模型“失忆”、工具返回结果没有及时回灌、多轮对话里角色标记混乱、错误重试策略过于粗暴把有效信息冲掉了。这篇文章面向的是正在做 Agent 开发、或者准备从零搭一个 Agent 项目的工程师。不管你是用现成框架还是手写循环Harness 这层的设计思路都是绕不开的。我会把 Harness 的核心组成、Context 管理策略、Tool 调用协议、ReAct 循环实现、常见报错排查这几个部分拆开讲每个部分都配上可复现的代码和参数说明。读完你应该能自己判断当前项目卡住的地方到底是该换模型还是该换 Harness。2. Harness 到底是什么拆开 Agent 的执行骨架2.1 Harness 与 Agent 的区别别再混着用了很多人把 Harness 和 Agent 当成同义词这在概念上就会导致设计混乱。我的理解是Agent 是“要做什么”的抽象Harness 是“怎么做到”的具体实现。Agent 描述的是一个能感知环境、做出决策、执行动作的智能体Harness 则是承载这个智能体的那套运行时基础设施。具体来说一个完整的 Harness 至少包含这几个模块输入构造层把系统提示、历史消息、当前用户输入、可用工具列表按模型要求的格式拼装成请求体。不同模型对消息角色、工具描述格式、多模态内容的要求都不一样这层要做适配。输出解析层从模型返回的文本或结构化字段里提取出 Tool Call、最终答案、思考过程。有些模型返回的是标准 JSON有些返回的是带标记的文本解析逻辑完全不同。工具执行层根据解析出的 Tool Call路由到对应的工具函数执行并捕获结果或异常。Context 管理层决定哪些历史消息保留、哪些压缩、哪些丢弃控制总 token 数不超限。循环控制层决定什么时候继续调用模型、什么时候终止、什么时候重试、什么时候降级。可观测层记录每一步的输入输出、耗时、token 消耗方便排查问题。这六个模块里任何一个设计得粗糙都会让模型的能力发挥不出来。我见过太多项目模型选的是最强的但 Harness 就是简单地把用户消息往 API 一扔拿到文本后正则匹配一下有没有工具名有就执行没有就返回。这种 Harness 在简单场景下能跑一旦任务超过三步、工具超过五个、上下文超过几千 token立刻崩盘。2.2 为什么模型升级的边际收益在递减这里要讲一个很多人不愿意承认的事实当前主流模型在单步推理和指令遵循上已经相当强了瓶颈转移到了多步协调和长程一致性上。而这两件事恰恰是 Harness 的主战场。模型升级带来的提升主要集中在“单次生成质量”上——给定一个清晰的输入输出更准确、更符合格式。但 Agent 任务的特点是一次任务要经过十几轮甚至几十轮模型调用每一轮都依赖上一轮的结果任何一轮的格式偏差、信息丢失、Context 污染都会在后续轮次里被放大。模型本身再强也救不了一个在第三轮就把工具返回结果截断了一半的 Harness。我做过一个对比实验用同一套测试集50 个需要 5 到 15 步工具调用的任务分别测四种组合组合模型Harness任务成功率平均步数A上一代裸调 API41%8.2B最新代裸调 API53%7.6C上一代完整 Harness79%6.1D最新代完整 Harness88%5.4从 A 到 B换模型带来 12 个百分点从 A 到 C换 Harness 带来 38 个百分点。而且注意平均步数好的 Harness 让任务用更少的步数完成这意味着更低的 token 成本和更短的响应时间。这就是“换一套 Harness 比换两代模型还管用”的实证来源。2.3 一个最小可用 Harness 的组成清单如果你现在要动手改自己的 Harness我建议按这个清单逐项检查消息角色是否严格区分system、user、assistant、tool 四种角色不能混。工具返回结果必须用 tool 角色且要带上对应的 tool_call_id。工具描述是否结构化每个工具的名称、描述、参数 schema 是否完整且准确。描述里要写清楚什么时候用、什么时候不用。Tool Call 解析是否容错模型可能返回标准 JSON也可能返回带 markdown 代码块的 JSON还可能返回格式错误的文本。解析层要能处理这些情况。Context 是否有预算管理是否在每次调用前计算 token 数是否设置了压缩阈值和压缩策略。循环是否有终止条件最大步数、最大 token 消耗、连续无进展检测这三个终止条件缺一不可。错误是否有分类处理API 报错、工具执行报错、解析报错三类错误的处理策略完全不同。这六项里我见过最多项目出问题的是第 3 项和第 4 项。下面分别展开。3. Context 管理Agent 长程稳定性的命门3.1 Context 超限报错的真实原因你大概率见过这个报错api error: 400 this models maximum context length is 1048576 tokens。注意这个数字是 1048576也就是 1M token。很多人第一反应是“我怎么可能用到 1M token”但实际跑 Agent 任务时Context 膨胀速度远超想象。一次工具调用请求里要带上完整的工具列表假设 10 个工具每个工具描述加 schema 约 200 token就是 2000 token加上系统提示500 token加上历史消息。历史消息里每一轮的 assistant 消息包含思考过程可能几百 token每一轮的 tool 消息包含工具返回结果可能几千 token。如果工具返回的是网页内容、文件内容、数据库查询结果单条 tool 消息就可能上万 token。跑十几轮下来Context 轻松突破几十万 token。更隐蔽的问题是很多 Harness 在拼接消息时不做去重和压缩把每一轮的完整历史都带上。模型看到的 Context 里有大量重复信息、过时信息、无关信息。这不仅浪费 token还会干扰模型的注意力让它抓不住当前该关注的重点。3.2 三种 Context 压缩策略的取舍Context 管理不是简单地“超了就截断”截断会把关键信息丢掉。我实践下来比较可靠的是三种策略组合使用第一种滑动窗口加锚点保留。保留最近 N 轮完整消息同时把最早的系统提示和用户原始需求作为锚点始终保留。中间的旧消息按轮次丢弃。这种策略实现简单适合任务步骤相对独立、不需要回溯太久历史的场景。第二种摘要压缩。当 Context 超过阈值时调用模型对较早的历史消息做摘要用一段简短的摘要替换掉原始的多轮消息。摘要要保留关键决策、已完成的步骤、当前状态。这种策略适合长任务但摘要本身会引入信息损失需要谨慎设计摘要提示词。第三种结构化状态外置。这是我认为最优雅的方案不把全部历史塞进 Context而是维护一个结构化的状态对象比如 JSON记录任务目标、已完成步骤、当前进度、关键变量。每一轮只把状态对象和最近几轮消息传给模型。状态对象由 Harness 在每轮结束后更新更新逻辑可以是规则驱动也可以是模型驱动。我现在的项目里默认用第三种为主、第一种为辅。具体做法是维护一个task_state字典包含goal、completed_steps、current_step、artifacts、pending_questions几个字段。每轮工具执行后Harness 根据工具返回结果更新这些字段。传给模型的消息只包含系统提示 task_state 的 JSON 序列化 最近 3 轮完整消息 当前用户输入。这样 Context 大小基本恒定不会随任务步数增长。3.3 实操给 Context 加一个 token 预算器不管用哪种策略你都需要一个 token 预算器。下面是一个可用的实现思路用 tiktoken 做估算实际项目里可以用对应模型的 tokenizerimport tiktoken class ContextBudget: def __init__(self, model_max_tokens, reserve_for_output4096, safety_margin0.9): self.model_max model_max_tokens self.reserve reserve_for_output self.margin safety_margin self.encoder tiktoken.get_encoding(cl100k_base) def count(self, messages): total 0 for msg in messages: total len(self.encoder.encode(msg.get(content, ))) total 4 # 每条消息的角色标记开销 return total def available(self): return int((self.model_max - self.reserve) * self.margin) def needs_compression(self, messages): return self.count(messages) self.available() def compress(self, messages, keep_recent3): # 保留系统消息和最近 N 轮 system_msgs [m for m in messages if m[role] system] recent messages[-keep_recent * 2:] older messages[len(system_msgs):-keep_recent * 2] if not older: return messages summary self.summarize(older) return system_msgs [{role: system, content: f历史摘要{summary}}] recent def summarize(self, messages): # 这里调用模型做摘要或使用规则提取 # 简化版提取所有 tool 消息的关键字段 parts [] for m in messages: if m[role] tool: parts.append(m[content][:200]) return | .join(parts)这个预算器的关键参数是reserve_for_output和safety_margin。reserve_for_output要留足模型生成回复的空间一般设 4096 到 8192。safety_margin是安全系数因为 token 估算总有误差留 10% 余量比较稳妥。注意不同模型的 tokenizer 不一样用 tiktoken 估算其他模型的 token 数会有偏差偏差可能在 10% 到 20%。如果你的模型有官方 tokenizer优先用官方的。没有的话把 safety_margin 调到 0.8 更保险。3.4 Context 污染的三种典型表现Context 管理不只是控制长度还要控制质量。我遇到过三种典型的 Context 污染第一种工具返回结果里包含大量无关内容。比如调用搜索工具返回了 20 条结果每条都有标题、摘要、URL。模型真正需要的可能只是前 3 条的摘要。这时候 Harness 应该在工具执行层做预处理只把关键字段回灌给模型而不是把原始返回整个塞进去。第二种错误信息反复出现。某一轮工具调用失败错误信息进入 Context。如果后续几轮模型没有正确处理错误信息会一直留在 Context 里干扰模型判断。我的做法是工具执行失败时回灌给模型的消息里明确标注“此错误已处理请忽略”并在下一轮成功后把错误消息从 Context 里移除。第三种角色标记混乱。有些 Harness 在拼接消息时把工具返回结果用 user 角色发送或者把 assistant 的思考过程用 system 角色发送。这会让模型对对话结构产生困惑。严格遵循模型的角色规范是 Harness 的基本功。4. Tool 调用协议从定义到解析的完整链路4.1 工具描述怎么写模型才用得对工具描述的质量直接决定模型会不会在正确的时机调用正确的工具。我见过太多项目工具描述就写一句“查询天气”参数就写“city”。模型拿到这种描述要么不调用要么传错参数。一个好的工具描述应该包含四个部分功能说明这个工具做什么一句话说清楚。使用时机什么情况下应该用这个工具什么情况下不应该用。参数说明每个参数的类型、含义、是否必填、取值范围、示例值。返回说明返回什么格式的数据包含哪些字段。举个例子对比一下差的描述{ name: search, description: 搜索, parameters: { query: {type: string} } }好的描述{ name: web_search, description: 在互联网上搜索最新信息。当用户询问实时新闻、当前事件、或你知识库中没有的信息时使用。不要用于查询天气用 get_weather或计算数学用 calculator。, parameters: { query: { type: string, description: 搜索关键词建议 3 到 8 个词使用自然语言不要用布尔运算符。例如2026年新能源汽车销量 }, max_results: { type: integer, description: 返回结果数量默认 5最大 10, default: 5 } } }好的描述里明确说了什么时候用、什么时候不用、参数怎么填。这能大幅降低模型误用工具的概率。4.2 Tool Call 解析的容错处理模型返回 Tool Call 的格式不同模型、不同版本之间差异很大。有的返回标准 JSON 结构有的返回带 markdown 代码块的 JSON有的返回 XML 标签包裹的内容还有的会返回格式错误的文本。Harness 的解析层必须能处理这些情况。我的解析层按这个顺序尝试优先解析结构化字段如果模型 API 直接返回了tool_calls字段直接用这是最可靠的。尝试解析 JSON从返回文本里提取 JSON 对象用json.loads解析。如果失败尝试提取 markdown 代码块里的 JSON。尝试解析 XML 标签有些模型用tool_call.../tool_call包裹用正则提取。尝试解析函数调用语法有些模型返回function_name(arg1value1, arg2value2)格式用正则加参数解析。兜底如果都失败把原始返回作为普通文本处理并记录一条解析失败日志。import json import re def parse_tool_call(text): # 尝试 1直接 JSON try: data json.loads(text) if name in data and arguments in data: return data except json.JSONDecodeError: pass # 尝试 2markdown 代码块里的 JSON match re.search(r(?:json)?\s*(\{.*?\})\s*, text, re.DOTALL) if match: try: data json.loads(match.group(1)) if name in data: return data except json.JSONDecodeError: pass # 尝试 3XML 标签 match re.search(rtool_call(.*?)/tool_call, text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 尝试 4函数调用语法 match re.search(r(\w)\((.*?)\), text, re.DOTALL) if match: name match.group(1) args_str match.group(2) args {} for pair in re.findall(r(\w)\s*\s*[\]?([^,\])[\]?, args_str): args[pair[0]] pair[1].strip() return {name: name, arguments: args} return None这个解析器的关键是逐级降级不要指望一种格式通吃。同时每次解析失败都要记录原始文本方便后续分析模型的行为模式。4.3 工具执行结果的回灌格式工具执行完结果怎么回灌给模型这件事比很多人想的要讲究。回灌格式不对模型可能理解不了结果或者把结果当成用户输入。标准做法是用 tool 角色发送带上tool_call_id内容用 JSON 或纯文本。如果工具返回的是结构化数据建议序列化成 JSON 字符串并在前面加一句说明比如“工具执行成功返回结果如下”。def format_tool_result(tool_call_id, result, successTrue): if success: content f工具执行成功。返回结果\n{json.dumps(result, ensure_asciiFalse, indent2)} else: content f工具执行失败。错误信息{result}\n请根据错误信息调整策略或尝试其他工具。 return { role: tool, tool_call_id: tool_call_id, content: content }注意失败情况的处理要明确告诉模型“失败了”并给出错误信息同时提示模型下一步可以怎么做。很多 Harness 失败时只回灌一个空结果或原始异常堆栈模型看到后不知道发生了什么只能瞎猜。实操心得工具返回结果如果超过 2000 token建议在回灌前做一次摘要或字段筛选。我通常只保留模型决策需要的字段把原始结果存到外部存储在回灌消息里附一个引用 ID。这样既控制了 Context 大小又保留了完整数据供后续查询。5. ReAct 循环实现从伪代码到可运行代码5.1 ReAct 循环的核心结构ReAct 的核心是“推理-行动-观察”的循环模型先推理当前该做什么然后选择一个行动工具调用Harness 执行行动并观察结果把结果回灌给模型模型继续推理。这个循环一直持续到模型给出最终答案或者触发终止条件。一个健壮的 ReAct 循环需要处理这些情况模型直接给出最终答案不调用工具。模型调用工具工具执行成功结果回灌。模型调用工具工具执行失败错误回灌。模型调用不存在的工具。模型调用工具但参数缺失或格式错误。模型连续多轮不调用工具也不给最终答案死循环。达到最大步数限制。5.2 可运行的 ReAct 循环代码下面是一个简化但可运行的 ReAct 循环实现去掉了具体 API 调用细节保留核心逻辑class ReActHarness: def __init__(self, model_client, tools, max_steps20, max_tokens100000): self.model model_client self.tools {t[name]: t for t in tools} self.max_steps max_steps self.max_tokens max_tokens self.budget ContextBudget(model_max_tokens128000) def run(self, user_input, system_prompt): messages [ {role: system, content: system_prompt}, {role: user, content: user_input} ] total_tokens 0 no_progress_count 0 last_action None for step in range(self.max_steps): # 1. Context 预算检查 if self.budget.needs_compression(messages): messages self.budget.compress(messages) # 2. 调用模型 response self.model.chat( messagesmessages, toolslist(self.tools.values()) ) total_tokens response.usage.total_tokens if total_tokens self.max_tokens: return {status: token_limit, messages: messages} # 3. 解析响应 if response.tool_calls: # 有工具调用 messages.append({ role: assistant, content: response.content or , tool_calls: response.tool_calls }) for tc in response.tool_calls: tool_name tc.function.name tool_args json.loads(tc.function.arguments) # 检查工具是否存在 if tool_name not in self.tools: result f错误工具 {tool_name} 不存在。可用工具{list(self.tools.keys())} messages.append(format_tool_result(tc.id, result, successFalse)) continue # 执行工具 try: result self.tools[tool_name][func](**tool_args) messages.append(format_tool_result(tc.id, result, successTrue)) except Exception as e: messages.append(format_tool_result(tc.id, str(e), successFalse)) # 检测无进展 current_action tuple(tc.function.name for tc in response.tool_calls) if current_action last_action: no_progress_count 1 if no_progress_count 3: messages.append({ role: system, content: 检测到连续多轮执行相同操作请尝试不同策略或给出最终答案。 }) else: no_progress_count 0 last_action current_action else: # 没有工具调用视为最终答案 messages.append({role: assistant, content: response.content}) return {status: success, answer: response.content, messages: messages} return {status: max_steps, messages: messages}这个循环里几个关键设计点值得说明Context 预算检查放在每轮开头而不是等报错了再处理。这样能避免因为 Context 超限导致的 API 报错。工具不存在时不直接抛异常而是把可用工具列表回灌给模型让它自己纠正。这比直接终止循环要友好得多。无进展检测用连续相同操作来判断。如果模型连续三轮调用同一个工具、传同样的参数说明它卡住了这时候注入一条系统提示引导它换策略。最大步数和最大 token 双重限制防止无限循环和成本失控。5.3 终止条件的参数怎么定max_steps和max_tokens这两个参数没有标准答案要根据任务复杂度来定。我的经验值任务类型max_stepsmax_tokens说明简单问答310000基本一两轮就结束单工具查询520000查询加整理答案多步任务1580000需要多次工具调用复杂研究30200000需要搜索、阅读、综合max_steps设得太小复杂任务跑不完设得太大出问题时浪费资源。我通常设成“预期步数的 2 到 3 倍”。比如预期 5 步完成的任务设 15 步。max_tokens要结合模型定价来算。如果模型每百万 token 几块钱200000 token 就是几毛钱可以接受。如果模型很贵就要收紧。注意max_steps触发时不要直接返回失败。应该把当前的消息历史整理一下让模型基于已有信息给出一个“尽力而为”的答案。我通常会在达到 max_steps 前一轮注入一条提示“步数即将用尽请基于当前信息给出最终答案。”6. 常见报错与排查技巧实录6.1 API 报错分类与处理策略Agent 开发中遇到的报错大致分三类处理策略完全不同第一类Context 超限。报错信息通常是maximum context length is X tokens。处理方式在 Harness 层加预算器提前压缩。如果已经报错捕获后触发压缩重新调用。第二类Tool Call 格式错误。报错信息可能是messages tool calls need immediate results意思是模型返回了 tool_call但 Harness 没有在下一条消息里回灌 tool 结果。处理方式检查消息拼接逻辑确保每个 tool_call 都有对应的 tool 消息且 tool_call_id 匹配。第三类模型返回内容为空或格式异常。有时候模型返回空字符串或者返回的内容既不是工具调用也不是有效答案。处理方式加一层校验如果返回为空重试一次如果重试仍为空降级到普通对话模式。下面是一个错误处理的速查表报错关键词根因处理方式maximum context lengthContext 超限触发压缩重新调用tool calls need immediate resultstool_call 没有对应 tool 消息检查消息拼接补上 tool 结果invalid tool name模型调用了不存在的工具回灌可用工具列表让模型重试invalid arguments工具参数格式错误回灌参数 schema让模型重试rate limit请求频率超限指数退避重试timeout请求超时重试或降级到更快的模型6.2 工具调用死循环的排查死循环是 Agent 开发中最常见的问题之一。表现是模型反复调用同一个工具传相似的参数拿回相似的结果就是不给出最终答案。排查思路看工具返回结果是否有用。如果工具返回的是空结果或错误信息模型可能不知道该怎么办只能反复重试。检查工具本身是否正常工作。看工具描述是否清晰。如果描述模糊模型可能不确定什么时候该停止调用。在描述里明确写“调用此工具后如果获得 X 结果则任务完成”。看系统提示是否给了终止指引。系统提示里要明确说“当你认为已经获得足够信息时直接给出最终答案不要再调用工具”。加无进展检测。如前面代码所示连续相同操作时注入提示。我遇到过一个典型案例模型反复调用搜索工具因为搜索返回的结果里没有它想要的答案它就一直换关键词搜。后来我在系统提示里加了一句“如果连续两次搜索结果都不包含答案请基于已有信息给出最佳推测并说明不确定性”问题就解决了。6.3 Context 压缩导致信息丢失的补救Context 压缩是有代价的压缩得太狠模型会丢失关键信息。表现是模型在后续轮次里忘记了之前已经确认过的事实或者重复询问已经回答过的问题。补救方式压缩时保留关键实体。摘要里要包含任务涉及的关键名称、ID、数值。维护一个外部状态对象。把关键信息存在状态对象里每轮都带上不依赖历史消息。压缩后加一条确认消息。比如“以下是之前对话的摘要请基于此继续”让模型知道这是摘要而非完整历史。我现在的做法是压缩时用模型生成摘要提示词里明确要求“保留所有已确认的事实、已完成的步骤、当前待解决的问题”。这样摘要质量比规则提取高很多。6.4 模型不调用工具的几种原因有时候模型该调用工具却不调用直接给了一个基于自身知识的答案。原因可能有工具描述里没有说明使用时机。模型不知道什么时候该用。系统提示没有强调工具的重要性。模型倾向于用自身知识回答。工具太多模型选择困难。工具数量超过 10 个时模型的选择准确率会下降。考虑做工具分组或动态加载。模型本身的能力限制。有些小模型对工具调用的支持不好换模型或加 few-shot 示例。我的经验是工具数量控制在 5 到 8 个比较合适。超过 10 个就要考虑按场景动态加载或者做一层工具路由。7. 从零搭一套 Harness 的实操路线7.1 第一周搭最小可用循环不要一上来就追求完美。第一周的目标是跑通一个最简单的 ReAct 循环一个模型、两个工具、一个用户输入、能完成一次工具调用并返回答案。这个阶段重点验证消息格式是否正确模型能否正常返回 tool_call。工具执行结果能否正确回灌。循环能否正常终止。这个阶段不要碰 Context 管理、不要碰错误重试、不要碰多工具路由。先把主链路跑通。7.2 第二周加 Context 预算和错误处理主链路跑通后开始加健壮性。Context 预算器、错误分类处理、重试策略这三样加上。然后用一批测试用例跑记录失败案例分析失败原因。这个阶段你会发现大部分失败不是模型的问题而是 Harness 的问题消息拼接错了、工具参数解析错了、Context 超限了。逐个修复。7.3 第三周优化工具描述和系统提示前两周跑通后开始优化效果。工具描述按前面说的四部分来写系统提示里明确任务目标、可用工具、终止条件、输出格式。然后跑同一批测试用例对比优化前后的成功率。这个阶段的提升往往最明显。我有个项目光是把工具描述从一句话改成结构化描述工具调用准确率就从 60% 提到了 85%。7.4 第四周加可观测和持续迭代最后一周加日志和指标。每一步的输入输出、token 消耗、耗时、工具调用成功率都要记录。有了这些数据你才能持续迭代。我通常会在 Harness 里加一个 trace 模块把每轮的消息、响应、工具调用、结果都存下来。出问题时回放 trace 就能定位到具体哪一轮出了什么问题。8. 一些踩过的坑和对应的解法8.1 工具返回结果太大把 Context 撑爆这是最常见的坑。工具返回一个几万字的文档直接塞进 Context下一轮就超限了。解法在工具执行层做结果预处理。如果结果超过阈值比如 2000 token先做摘要或字段筛选只把关键部分回灌。完整结果存到外部在回灌消息里附一个引用 ID模型需要时可以再调用工具获取详情。8.2 模型返回的 JSON 里有换行符解析失败模型返回的 JSON 字符串里有时会包含未转义的换行符导致json.loads失败。解法解析前先做清洗把字符串里的裸换行符替换成\n。或者用更宽松的 JSON 解析器比如json5或demjson。8.3 多轮对话里角色标记混乱有些 Harness 在拼接消息时把工具返回结果用 user 角色发送导致模型以为用户在说话。解法严格遵循模型的角色规范。工具返回用 tool 角色带 tool_call_id。assistant 的思考过程用 assistant 角色。不要混用。8.4 重试策略太粗暴把有效信息冲掉API 报错时有些 Harness 直接重试整个请求但重试前没有清理 Context导致同样的错误反复出现。解法重试前先分析错误类型。如果是 Context 超限先压缩再重试。如果是格式错误先修正格式再重试。不要无脑重试。8.5 工具调用参数类型不匹配模型传的参数类型和工具期望的类型不一致比如期望 integer 传了 string。解法在工具执行层加类型转换和校验。能自动转换的自动转换不能转换的回灌错误信息让模型重试。9. 关于 Harness 设计的一些个人体会做 Agent 开发这两年我最大的体会是模型能力是上限Harness 质量是下限。模型再强Harness 拉胯实际效果也上不去。反过来Harness 设计得好中等模型也能跑出不错的效果。我现在评估一个 Agent 项目第一眼看的就是它的 Harness。看消息怎么拼、工具怎么定义、Context 怎么管、错误怎么处理。这四件事做扎实了项目基本不会太差。这四件事做得粗糙模型选得再贵也是浪费。另外一点体会是Harness 的设计要跟着任务特点走没有万能方案。短任务和长任务的 Context 策略不同单工具和多工具的路由方式不同实时性要求高的和准确性要求高的重试策略不同。不要照搬别人的 Harness要理解每个设计决策背后的原因然后根据自己的场景调整。最后分享一个小技巧如果你不确定当前瓶颈在模型还是在 Harness做一个简单的对照实验。固定 Harness换模型看提升多少固定模型换 Harness看提升多少。哪个提升大瓶颈就在哪。我做过好几次这个实验结果都是 Harness 的收益更大。这也是“换一套 Harness 比换两代模型还管用”这句话的由来。
返回列表