做 Agent 开发这几年,我最大的一个感受是:模型本身的聪明程度,往往不是最难受的瓶颈。真正让人抓狂的,是套在 Agent 外面那层 harness——提示词、工具描述、记忆策略、评测规则、执行沙箱。你费尽力气把一个 Agent 跑通,换一个任务场景,可能就要从头调一遍。而调 harness 的时间,往往比调模型还要多。这也是为什么我一直觉得,“让 Agent 自动优化自身 Harness”这件事,不是锦上添花,而是迟早要做的必修课。
这篇是系列第十七篇,和大家聊一个我自己折腾了很久的玩法:Meta-Harness。简单说,就是不再靠人肉去改 Agent 的外部配置,而是让 Agent 在运行过程中自己观察、自己诊断、自己改自己的 harness,改完再自己验证。听起来有点自举的味道,但真落地之后,效果比想象中稳,坑也比想象中多。下面我把整个思路、架构、代码骨架和踩过的坑都拆开讲。
1. 先搞清楚:Harness 到底在管什么
1.1 Agent 不等于模型,Harness 才是骨架
很多人聊 Agent,默认它就是一个大模型加几个工具调用。但实际开发过的人都知道,Agent 能稳定跑起来,靠的是一整套外部支撑结构。模型只是“大脑”,而大脑需要身体、神经、反馈系统才能工作。这个身体,就是 harness。
我一般把 Agent 系统拆成五层:模型内核、上下文管理、工具接口、执行环境、评测反馈。后四层基本都属于 harness 的范畴。举个例子,同样是调用一个天气查询工具,模型只知道“有个函数能查天气”,但工具的入参格式、超时时间、返回结果怎么解析、查不到时怎么兜底,这些全是 harness 在管。
- 系统提示词:告诉模型你是谁、你要遵守什么规则、输出什么格式。
- 工具描述与 Schema:决定模型能不能正确调用工具。
- 记忆策略:决定哪些历史信息该保留,哪些该丢掉。
- 评测规则:决定“这个任务算不算完成”。
- 沙箱与权限:决定 Agent 能碰哪些资源。
这些配置,单独看每一个都很简单,但组合到一起,就是一个极其容易失衡的系统。你调好了 prompt,工具描述又含糊了;你把工具描述写详细,token 开销又上去了;你压缩了上下文,模型又开始“失忆”。这就是 Agent 开发里最常见的“跷跷板困境”。
1.2 为什么手工调 Harness 撑不住迭代
传统做法里,harness 是工程师手工写的。写完之后,它在每次 Agent 运行中是静态的。问题在于,Agent 本身是动态的:同一个模型,在不同任务、不同用户的请求下,行为差异非常大。你今天针对“检索类任务”调好的工具描述,放到“代码修改类任务”上,可能就频繁误调用。
我做过的项目里,最典型的一个场景是:Agent 需要从一堆业务文档里提取结构化信息。一开始提示词里写了“必须使用 extraction 工具”,工具描述也写得很细。测试集上跑得很稳,上线后发现,只要用户问题里带一点歧义,Agent 就绕开工具自己“编”答案。后来一查,是工具描述的措辞在特定语境下产生了误导,而这个问题在测试集里根本看不出来。
这就是手工调 harness 的根本矛盾:harness 的好坏,要等 Agent 跑起来才知道;而 Agent 一旦跑起来,它的行为又反过来受 harness 影响。靠人肉去调,只能覆盖“你预期到的问题”,覆盖不了“Agent 自己发现的问题”。所以很自然,我想到了能不能让 Agent 在跑任务的同时,把“我为什么没用对工具”“我的提示词哪里让我困惑”这些信息反馈回来,自动生成修改方案。
2. Meta-Harness 的思路:给 Harness 加一条反馈回路
2.1 从“人写配置”到“Agent 改自己的脚手架”
Meta-Harness 的核心思路,就是给 harness 加一条自动化的反馈回路:Agent 在执行任务的时候,同时记录自己的行为轨迹;执行完之后,用另一个分析模型看这些轨迹,判断哪些 harness 配置拖累了性能;然后生成修改补丁;打补丁之后,跑一轮验证;验证通过就保留,不通过就回滚。
听起来像是“让 Agent 自己给自己看病”。但这里有一个关键点:负责诊断的模型,和负责执行任务的模型,不能是同一个运行时实例。因为它是 Agent 执行完一轮之后,拿着完整 trace 做离线分析,所以不会影响线上任务。这就像程序员写完代码之后,跑一轮测试,再让 code review 模型看日志提修改建议,人工确认后再合入。
我把这个循环叫“一次自举轮次”。每一轮,harness 都有机会变得更贴合当前任务。迭代几轮之后,harness 就不再是工程师拍脑袋写的“通用配置”,而是经过 Agent 自己实践验证过的“特化配置”。
2.2 自动优化一个轮次里发生了什么
一个完整的 Meta-Harness 轮次,我拆成五步:
- 运行一组任务,同时采集完整 trace。包括每步的模型输入输出、工具调用参数、返回结果、错误信息、耗时、token 消耗。
- 基于 trace 做诊断,找出“harness 层面的问题”。比如工具描述有歧义、提示词缺少边界条件、记忆策略导致关键信息丢失、评测规则太宽松。
- 根据诊断生成候选补丁。补丁是结构化的,不是直接改自然语言,而是改配置项。
- 把补丁应用到一份“候选 harness”上,重新跑评估。必须用预先准备的评测集,不能只用刚才的任务,否则容易过拟合。
- 对比评估结果,如果候选 harness 的指标不比当前版本差,就合入;否则丢弃,保留原版本。
这个流程里,最重要的是“诊断”和“验证”两步。诊断如果错了,后面全部白搭;验证如果不到位,Agent 很容易学会“作弊”——比如把评测阈值调低来提升通过率,而不是真正提升能力。所以我后面专门加了一个约束:模型只能修改 harness 配置,不能修改评测规则本身。评测规则一旦被允许修改,整个优化就失去意义了。
2.3 和现有 Auto Prompt、Auto Function 方案的区别
现在市面上已经有一些自动优化 prompt 的工具,像 DSPy 那套,本质上是在搜索更好的 prompt 组合。Meta-Harness 和我理解的那些方案有一个本质区别:它优化的对象不限于 prompt,而是整条 harness 链路。
- DSPy 类工具:目标是找到更好的示例、指令、格式。优化空间集中在 prompt 层面。
- Auto Function Calling 优化:目标是调整工具 schema 和描述。优化空间集中在工具层面。
- Meta-Harness:目标是把 prompt、工具、记忆、评测、环境约束作为一个整体来优化。诊断一个错误,可能最终发现问题出在记忆长度限制,而不是 prompt 写得不清楚。
打个比方,Auto Prompt 是在调整“话术”,Meta-Harness 是在调整“工作制度”。话术重要,但制度不改,话术再漂亮也没用。这也是为什么我坚持要把反馈环建在 harness 全链路之上,而不是只盯着 prompt。
3. 系统拆解:Meta-Harness 的四个核心模块
3.1 Observer:先搞清楚 Agent 是怎么跑的
观察模块是整个系统的基础。没有高质量的 trace,后面的一切分析和诊断都是空中楼阁。
我实现的 Observer 是在 Agent 的执行循环里埋了一个钩子,每执行一步,就把下面的信息追加到 trace 文件里:
- 模型请求和响应全文,包括 system、user、assistant 内容。
- 工具调用的函数名、参数 JSON、返回结果、异常堆栈。
- 每一步的耗时、token 数量、是否超时、是否触发重试。
- 当前上下文里的记忆片段摘要(不需要全文,摘要就行)。
一开始我踩过一个坑:只记录“成功”和“失败”这种粗粒度信息,结果诊断时完全看不出问题出在哪。后来改成记录“每一步发生了什么事情”,比如“模型调用了 search 工具,但参数里把 query 写成了空字符串”,这类细节才是诊断的关键。
Observer 设计上有一个原则:不能影响 Agent 本身的执行。所以 trace 是异步写的,而且只记录不下发,不会污染上下文。这个看起来简单,但实际开发中很容易被忽略。有人图省事直接把 trace 塞进 system prompt,结果 Agent 的上下文一下就爆了。
3.2 Diagnoser:把问题翻译成可执行的修改点
有了 trace,下一步是诊断。诊断模块的输入是一整个任务的 trace 集合,输出是一份“问题清单”,每一项都包含问题描述、证据引用和问题模块。
我尝试过两种诊断方式:
第一种是规则式诊断。比如检测“工具调用了三次以上且每次都报 JSON 解析错误”,就认为是工具输出格式和模型预期不匹配。这种方式稳定、可解释,但覆盖不了语义层面的问题。比如“模型连续两次用同一个错误参数调用工具”,规则就很难判断是参数生成逻辑问题,还是工具描述引导性太强。
第二种是 LLM 辅助诊断。把 trace 中相关的片段截取出来,让分析模型回答“这个任务失败的原因是什么?harness 的哪个配置可能误导了模型?”。这种方式灵活,能发现很多规则覆盖不到的问题,但需要控制输入长度和幻觉风险。
我最后用的是混合方案:先用规则扫出明显的硬错误,再用 LLM 分析剩余的不确定场景。LLM 的输入不只是原始日志,而是经过裁剪的“关键事件摘要”,比如“模型在该步骤重复调用了 three times,均返回 invalid date format”。这样既降低了 token 消耗,也减少了幻觉空间。
诊断结果中,每个问题必须能映射到一个具体的 harness 配置项。比如“工具描述没有说明日期格式”,映射到 tools.weather.description;“系统提示词没有约束输出语言”,映射到 system_prompt。如果诊断结果无法映射到配置项,就直接丢弃。这个约束很关键,它保证后面的 Mutator 能落地。
3.3 Mutator:用 LLM 生成 Harness 变更
Mutator 是整个系统里“创意”的部分。它接收诊断结果和当前 harness 配置,生成一个或多个候选补丁。
补丁必须是结构化的,不能是“请把提示词写好一点”这种模糊指令。我定义了几类补丁格式:
- text_replace:替换配置项的某一段文字。
- add_rule:在配置的约束规则列表里新增一条。
- remove_rule:删除一条规则。
- update_param:修改某个参数值,比如上下文窗口长度、工具超时时间。
- add_example:往 few-shot 示例列表里追加一个示例。
每一类补丁都包含目标路径(比如 harness_config.tools["search"].description)、改动内容和变更理由。
生成补丁时,我会让 LLM 遵守几个硬性约束:一次只改一个模块,不要既改 prompt 又改记忆策略;每次改动必须是可逆的,也就是说保存前一份配置;补丁里必须附带一条“预期效果说明”,用来和实际评估结果做对比。
这个设计一开始被很多人问过:为什么不让 LLM 一次性输出整套新配置,直接替换?原因很简单:整套配置的搜索空间太大,LLM 一次生成的方案成功概率很低,而且一旦失败,你根本不知道是哪一步引起的。一次改一个模块,配合评估结果,才能形成可学习的反馈信号。
3.4 Validator:不把系统改坏
Validator 是最后一道闸门。它的任务很简单:在候选 harness 上重新跑一遍评测,看看相比当前版本,指标是升是降。
我这里说的评测,不是拿刚才那几条任务重跑,而是用一份独立的评测集。这个评测集里既有历史回归用例,又有新任务的代表性样本。回归用例是为了防止“修了 A 问题,却搞坏了 B 功能”。新任务样本是为了确保优化方向确实朝着业务目标走。
Validator 的结果有三种:
- 指标明显提升,合入。
- 指标持平,建议人工确认后合入。
- 指标下降,直接丢弃,并记录失败原因。
还有一个细节:Validator 必须跑多次取平均,尤其是任务本身有随机性的场景。我见过只跑一次评估,结果因为采样波动把好补丁过滤掉的情况。后来我改成每个补丁至少跑三轮,取中位数,稳定性好很多。
另外,Validator 还要做一次“合理性检查”。比如检查补丁有没有偷偷修改评测阈值;检查新配置有没有把某个工具完全禁用;检查记忆策略参数的数值是否在合理范围内。这些规则看着死板,但确实能拦住不少 Mutator 的“创业冲动”。
4. 动手落地一个最小的 Meta-Harness
4.1 环境与基础 Harness 准备
聊完思路,直接上代码。这个示例是我在测试环境里跑通的极简版本,目的是演示整个自举链路,不一定适合生产,但足够帮助你理解每一环是怎么串起来的。
我用的环境是 Python 3.10 + OpenAI SDK,模型部分用了两个实例:一个负责执行任务(gpt-4o-mini),一个负责诊断和生成补丁(gpt-4o)。为什么用两个不同的模型?因为诊断和补丁生成对推理能力要求更高,如果也用 mini 模型,生成的补丁质量会很差;而执行任务用 mini 模型,既省成本又能让 harness 的问题更容易暴露。
基础 harness 直接用一个字典表示:
HARNESS_CONFIG = { "system_prompt": ( "你是一个智能助手,请根据用户问题调用合适工具完成目标。" "如果工具返回的信息不足,请明确说明缺少什么。" ), "tools": [ { "name": "search", "description": "搜索业务文档,返回相关段落。", "parameters": { "query": {"type": "string"}, "top_k": {"type": "integer", "default": 3} } } ], "memory": { "type": "buffer", "max_messages": 10 }, "eval": { "pass_rate_threshold": 0.8, "metrics": ["pass_rate", "tool_success_rate"] } }这个配置看起来没什么问题,实际上第一次跑任务集时,tool_success_rate 只有 0.45。原因吗?工具描述里没有说明 query 应该用什么形式,模型经常把一整段需求塞进去,导致检索结果一团糟。这正是 Meta-Harness 应该发现的典型问题。
4.2 实现自动优化循环(代码骨架)
核心循环的伪代码大概长这样:
import copy import json def run_agent_with_trace(task, harness): # 执行一次 Agent 任务,返回 result 和 trace trace = [] # 这里省略 Agent 内部循环细节 # 关键是在每一步把 model_response/tool_call/tool_result/error 追加到 trace return result, trace def evaluate(harness, task_set): # 在给定任务集上跑评估,返回指标 dict results = [] for task in task_set: result, _ = run_agent_with_trace(task, harness) results.append(result) return compute_metrics(results) def diagnose(traces): diagnosis = [] # 第一步:规则扫描 for error in extract_hard_errors(traces): diagnosis.append(rule_based_diagnosis(error)) # 第二步:LLM 分析 llm_diag = llm_diagnose(build_diagnosis_prompt(traces)) diagnosis.extend(llm_diag) return [d for d in diagnosis if d["target_path"] is not None] def mutate(harness, diagnosis): patch_text = llm_generate_patch(harness, diagnosis) patch = parse_patch(patch_text) candidate = apply_patch(harness, patch) return candidate, patch def meta_optimize(harness, task_set, eval_set, max_rounds=3): baseline = evaluate(harness, eval_set) for round_idx in range(max_rounds): traces = [] for task in task_set: _, tr = run_agent_with_trace(task, harness) traces.extend(tr) diagnosis = diagnose(traces) if not diagnosis: break candidate, patch = mutate(harness, diagnosis) cand_metrics = evaluate(candidate, eval_set) if cand_metrics["pass_rate"] > baseline["pass_rate"]: harness = candidate baseline = cand_metrics log_to_changelog(patch, cand_metrics) else: log_reject(patch, cand_metrics) return harness注意几个细节:
- evaluate 用的是 eval_set,不是 task_set。task_set 是生成诊断用的,eval_set 是验证补丁效果的。两者不能混淆。
- 每次 evaluate 最好跑三轮取中位数,示例里为了简洁没写,但实际必须做。
- 日志一定要写清楚哪条补丁被接受、哪条被拒绝、原因是什么。这个后续排查问题时非常有用。
4.3 在真实任务上看效果
我用一个“文档检索 + 信息抽取”的场景做了验证。任务集是 20 个查询,包括“找出某个合同里甲方违约条款的日期”“列出近三个月所有价格变更记录”之类。
第一轮跑完,Diagnoser 给出的问题非常明确:search 工具的 description 没有说明“query 应使用自然语言关键词,不要包含完整问题句式”。LLM 分析证据是:超过 60% 的失败调用中,工具返回的结果都是空列表,而模型在拿到空列表后还会继续尝试完整句子,反复失败。
Mutator 生成的补丁是把 description 改成:
"搜索业务文档,返回与关键词匹配的段落。query 应为自然语言关键词, 例如:价格变更、合同违约日期;不要发送完整句子或问题原文。"Validator 用独立评测集测了三次,tool_success_rate 从 0.45 提升到 0.78,pass_rate 从 0.55 提升到 0.71。补丁合入。
第二轮优化时,系统又发现一个新问题:即使工具调用成功了,模型也经常“忘记”把抽取到的日期格式转换成统一标准。于是补丁在 system_prompt 里加了一条“输出日期时统一使用 YYYY-MM-DD 格式”。这次提升没有第一轮明显,pass_rate 只涨了 0.03,但确实有效。
三轮之后,系统趋于稳定,不再生成新的补丁。整个过程中,我没有手写任何一条 prompt。最后看 changelog,每一步改动的理由都很清楚,这让我觉得这个方向远比手动调优可维护。
5. 实测中必踩的坑
5.1 优化循环失控:改坏配置还自嗨
最容易犯的错,是让 Mutator 自由发挥,结果它把 system_prompt 越改越长,最后上下文爆了。我的处理方式是在补丁生成约束里加一条:每次改动只允许修改一个模块,且新增文字不得超过 200 字。如果补丁文本超长,直接丢弃并让 Mutator 重新生成。
更严重的问题是 Agent 通过修改 harness 来“偷懒”。比如它发现自己频繁调用某个工具失败,不是想办法改进调用方式,而是把工具描述改成“只有在绝对必要时才调用”,结果通过率上涨是因为它根本不调用工具了。这是典型的“用规避代替解决”。Validator 里必须检查工具的调用频率:如果一个补丁导致工具调用次数骤降,要拉高怀疑级别。
5.2 评估指标和真实目标不一致
validator 的评测指标设计不好,整个优化系统就会往错误方向跑。比如只看 pass_rate,不关心 token 成本,Agent 就可能通过增加迭代次数来提升通过率。这在实际业务里是不可接受的。
我后来给评估表加了一组复合指标:pass_rate、tool_success_rate、average_steps、average_tokens。补丁合入的前提是 pass_rate 不能降,average_steps 不能涨太多。这个“多重门槛”比单独看一个指标靠谱得多。
还有一个容易忽略的点:评估任务集本身也需要定期更新。如果一直用同一份 eval_set,优化几轮之后,Agent 对这份 eval_set 就会过拟合,看起来指标很高,换个新场景立刻打回原形。
5.3 成本爆炸:自动优化不是无限试错
Meta-Harness 每一轮优化都要跑大量任务,而且为了验证一个补丁,还要重新跑评测。如果设置不当,成本会非常难看。
我的经验是先在小任务集上跑通整个循环,再逐步扩大。一次优化轮次,如果诊断出三个问题,不要一次生成三个补丁全试,而是按优先级排序,一次只验证一个补丁。这样虽然轮次变多,但总成本更低,因为你能明确知道是哪个补丁产生了效果。
另外,可以在 Mutator 阶段先用一个小的验证集快速筛掉明显不行的补丁,再进入完整 eval_set 评估。这样能省掉不少无谓的模型调用。
5.4 版本管理与环境隔离
Harness 是代码,所以必须纳入版本管理。我把每次合入的补丁都做成一个 commit,commit message 里带上诊断依据和评估结果。这样如果后续发现问题,可以随时回滚,也能回溯“当时为什么要这么改”。
环境隔离也很重要。Meta-Harness 的优化过程会产生很多候选配置,如果直接在线上环境里跑,风险太大。即使是在测试环境,也要保证候选 harness 不访问真实业务数据。我做过一次很惨的教训:候选 harness 的 memory 策略被改坏了,结果它在评估时把上一条任务的敏感信息泄漏到了下一条任务里。所以沙箱和数据隔离是底线,不能省。
6. Meta-Harness 还能往哪走
6.1 Harness 是 Agent 的“技能树”
这段时间做下来,我越来越觉得,harness 就是 Agent 的“技能树”。每一版配置都对应着 Agent 在某一类任务上的能力上限。而 Meta-Harness 做的事情,就是让 Agent 自己爬这棵技能树。
往深处想,这就引出了一种新的开发范式:工程师不再写死配置,而是设计“怎么生成配置的规则”。从这个角度看,Meta-Harness 其实也是给 Agent 套了一层新的 harness——只不过这一层 harness 控制的是“Agent 如何成长”。等这套成长机制成熟了,理论上可以复用到不同 Agent 上,形成一套通用的“自举引擎”。
6.2 再往上一层:Meta-Meta?
如果把 Meta-Harness 本身也看作一个 Agent 系统,它也有自己的 harness——比如诊断 prompt、补丁生成约束、验证策略。这些设定目前还是我手工写的。那我是不是也可以让系统自动优化这些设定?理论上可以,但实际上,每一层优化都会带来新的不确定性,调试成本也会指数级上升。
我的个人看法是:先别急着追求 Meta-Meta。把一层的自举做扎实,比叠再多层更有价值。你真正需要的是积累足够多的“harness 变更日志”。这些日志本身就是极其宝贵的训练数据。将来不管是微调一个小模型来做自动诊断,还是提炼一套可复用的优化策略,都离不开这些真实场景下的反馈对。
最后分享一个小技巧。在 Meta-Harness 里加一条“changelog 记录”的硬性要求:每次补丁合入的时候,都必须用自然语言记录“诊断到了什么问题、改了什么、为什么有效”。别小看这一条,它让整个系统的行为始终可解释。我踩过几次坑之后,发现凡是不写清楚理由的补丁,大概率过两个月就变成了一堆没人敢动的“屎山配置”。反倒是那些有详细 changelog 的优化记录,后来帮我总结出了好几条通用的 harness 设计经验,可以直接迁移到新项目里。按照我现在的习惯,已经不再问“这个 Agent 应该怎么配置”了,而是问“这份配置是怎么演化来的”。Meta-Harness 给我最大的收获,不是自动加了几个规则,而是让整个演化过程变得清晰、可追溯、可复用。