news 2026/9/26 3:21:10

第 21 篇 · S9·上:Agent 工程化总纲:Harness 六大支柱与评估体系(轨迹 + benchmark)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第 21 篇 · S9·上:Agent 工程化总纲:Harness 六大支柱与评估体系(轨迹 + benchmark)

1. 为什么你的 Agent 跑通 demo 却上不了生产

如果你已经跟着前面的系列把单 Agent 工具调用、多 Agent 编排都跑通了,大概率会遇到一个很尴尬的阶段:demo 里一切顺滑,一放到真实输入上就开始出洋相。要么多跑几步陷进死循环,要么 token 烧得比预期快十倍,要么工具调错参数还一路错到底,多 Agent 一并发直接把 LLM 配额打爆。这些问题不是模型不够聪明,而是你缺了一套围绕模型的工程系统。

这套系统我习惯叫它 Harness,中文可以理解成“驾驭系统”。核心等式很简单:Agent = Model + Harness。模型是引擎,负责理解、推理、生成;Harness 是方向盘和刹车,负责上下文管理、约束执行、验证循环、状态隔离。一辆没有方向盘和刹车的跑车,引擎再强也是灾难。一个没有 Harness 的 Agent,模型再强也会在复杂场景里失控。

这篇是 S9 三部曲的开篇,先给方法论总纲,再落地第一块硬能力——Agent 评估。评估不只看最终结果对不对,更要看轨迹:工具选对没、参数填对没、用了几步、有没有兜圈。配合 SWE-bench、Tau-bench 这类 benchmark 和在线 A/B,才能建立可复现的评估闭环。下面我会给出可直接复制的 Harness 配置骨架、轨迹埋点方案和 benchmark 跑分验证动作,你可以边看边搭。

2. TaoToken 前置:把模型接入层先固定下来

在搭 Harness 之前,得先把模型接入这层固定住,否则后面换模型、做 A/B、跑 benchmark 都会乱。我自己的做法是统一走一个兼容 OpenAI 协议的入口,这样 Harness 里的模型适配层只需要改一个 base_url 和 key,不用动业务代码。

TaoToken 的 API 地址是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions格式。你需要在控制台创建一个 API Key,然后把它写进环境变量,别硬编码进代码。创建入口在控制台的 API Keys 页面,模型对话调试可以在模型对话页直接试,长期跑编码类 Agent 任务的话可以看下 Coding Plan。

这里要强调一点:Harness 的可拆卸性支柱,要求模型相关部分做成可插拔适配层。所以你的配置里,模型名、base_url、key 都应该是外部注入的,而不是写死在 Agent 逻辑里。这样模型迭代时,你只需要换配置,不用重写 Harness。

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

环境变量设好之后,先别急着写 Agent,用一条 curl 确认接入层是通的。这一步很关键,很多后面排查半天的“Agent 不工作”,其实只是接入层没通。

curl -s "$TAOTOKEN_BASE_URL/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "只回复两个字:通了"}], "max_tokens": 16 }'

返回里能看到choices[0].message.content是“通了”,说明接入层没问题。接下来所有 Harness 配置都基于这个入口。

3. 可复制的 Harness 配置骨架

Harness 的配置我建议分成两块:一块是 Agent 运行时配置(settings.json),管模型、工具、循环上限、预算;一块是评估与轨迹配置(config.toml),管埋点、benchmark、A/B 分流。分开的好处是运行时和评估解耦,改评估不影响线上行为。

3.1 settings.json:运行时骨架

这份配置覆盖了六大支柱里的上下文架构、架构约束、自验证循环、可拆卸性。字段我都加了注释,你按自己场景改值就行。

{ "harness_version": "s9.1", "model": { "provider": "taotoken", "base_url_env": "TAOTOKEN_BASE_URL", "api_key_env": "TAOTOKEN_API_KEY", "name": "claude-sonnet-4-20250514", "fallback_models": ["qwen-max", "gpt-4o"], "adapter": "openai_compatible" }, "context": { "max_context_ratio": 0.4, "compress_threshold": 0.35, "keep_recent_turns": 6, "summary_model": "qwen-turbo" }, "loop": { "max_steps": 25, "max_tool_calls_per_step": 3, "dead_loop_detection": { "enabled": true, "repeat_action_threshold": 3, "repeat_observation_threshold": 2 }, "self_verify": { "enabled": true, "checkpoint_every_steps": 5, "verify_prompt": "检查当前进展是否偏离目标,若偏离请给出纠正动作" } }, "budget": { "max_tokens_per_task": 120000, "max_cost_usd_per_task": 0.5, "on_exceed": "abort_with_trace" }, "tools": { "whitelist": ["search", "read_file", "write_file", "run_test"], "deny_dangerous": true, "require_approval": ["write_file", "run_shell"] }, "isolation": { "per_agent_context": true, "shared_memory": false, "subagent_max_depth": 2 } }

几个关键点解释一下。max_context_ratio设 0.4 是因为上下文利用率超过 40% 后推理质量会明显下滑,这是实测出来的经验值。dead_loop_detection里repeat_action_threshold设 3,意思是同一个动作重复三次就判定为兜圈,直接中断并记录轨迹。budget里的on_exceed设成abort_with_trace,超预算时不是静默失败,而是带着完整轨迹退出,方便你回放定位。

3.2 config.toml:评估与轨迹骨架

评估配置单独放一份,管轨迹埋点、benchmark 任务集、A/B 分流比例。

[harness] version = "s9.1" trace_dir = "./traces" trace_format = "jsonl" [trace] enabled = true capture = ["thought", "action", "action_input", "observation", "step_index", "token_used", "latency_ms"] redact_keys = ["api_key", "authorization"] sample_rate = 1.0 [benchmark] suite = ["swe_bench_lite", "tau_bench_retail", "gaia_dev"] run_on = ["prompt_change", "model_change", "tool_change"] baseline_file = "./benchmarks/baseline.json" report_dir = "./benchmarks/reports" [eval] judge_model = "claude-sonnet-4-20250514" judge_rubric = "./eval/rubric.md" human_spot_check_ratio = 0.1 regression_set = "./eval/regression_tasks.jsonl" [ab_test] enabled = true split = { control = 0.9, treatment = 0.1 } metrics = ["task_success_rate", "p95_latency_ms", "cost_per_task"] min_sample_size = 200 significance_level = 0.05

trace.capture里我特意把thought、action、observation都抓了,因为轨迹评估的核心就是这三样。redact_keys防止 key 泄漏进轨迹文件。ab_test里的min_sample_size和significance_level是为了做统计显著性判断,避免把随机波动误判成优化效果。

4. 轨迹埋点与 benchmark 跑分验证

配置写好了,接下来是让它真正跑起来产生数据。轨迹埋点和 benchmark 跑分是评估闭环的两条腿,缺一不可。

4.1 轨迹埋点:每一步都留痕

轨迹埋点的最小实现是在 Agent 循环里插一个 recorder,每一步把 thought、action、observation 写进 jsonl。下面是一个 Python 骨架,你可以直接嵌进自己的 Agent 循环。

import json, time, os from datetime import datetime class TraceRecorder: def __init__(self, trace_dir, task_id, sample_rate=1.0): self.path = os.path.join(trace_dir, f"{task_id}.jsonl") self.sample_rate = sample_rate self.step = 0 os.makedirs(trace_dir, exist_ok=True) def record(self, thought, action, action_input, observation, token_used, latency_ms): self.step += 1 entry = { "ts": datetime.utcnow().isoformat(), "step_index": self.step, "thought": thought, "action": action, "action_input": action_input, "observation": observation, "token_used": token_used, "latency_ms": latency_ms, } with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") def close(self, final_result, success): summary = { "ts": datetime.utcnow().isoformat(), "type": "summary", "total_steps": self.step, "final_result": final_result, "success": success, } with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(summary, ensure_ascii=False) + "\n")

在 Agent 循环里这样用:

recorder = TraceRecorder("./traces", task_id="task_001") for step in range(max_steps): t0 = time.time() thought, action, action_input = agent.plan(state) observation = agent.execute(action, action_input) recorder.record(thought, action, action_input, observation, token_used=agent.last_token_used, latency_ms=int((time.time() - t0) * 1000)) if agent.is_done(observation): recorder.close(observation, success=True) break

跑完一个任务后,./traces/task_001.jsonl里就是完整轨迹。你可以写个分析脚本算工具选择准确率、步数效率、有无重复动作。

import json from collections import Counter def analyze_trace(path): steps = [] with open(path, encoding="utf-8") as f: for line in f: obj = json.loads(line) if obj.get("type") != "summary": steps.append(obj) actions = [s["action"] for s in steps] action_counts = Counter(actions) repeated = {a: c for a, c in action_counts.items() if c >= 3} total_tokens = sum(s["token_used"] for s in steps) return { "total_steps": len(steps), "repeated_actions": repeated, "total_tokens": total_tokens, "avg_latency_ms": sum(s["latency_ms"] for s in steps) / max(len(steps), 1), } print(analyze_trace("./traces/task_001.jsonl"))

输出里如果repeated_actions非空,说明 Agent 在兜圈,需要回去看那几步的 thought 和 observation,定位是工具返回不清晰还是 prompt 没约束好。

4.2 benchmark 跑分:横向标尺

自建任务集容易自说自话,benchmark 提供客观横向对比。我一般跑三个:SWE-bench Lite 测代码修复、Tau-bench 测工具使用加业务策略、GAIA dev 测多步推理加工具加多模态。跑分脚本的核心是固定任务集、固定模型配置、记录轨迹和结果。

import json, subprocess def run_benchmark(suite_name, agent_config, tasks_file): results = [] with open(tasks_file, encoding="utf-8") as f: tasks = [json.loads(line) for line in f] for task in tasks: agent = build_agent(agent_config) recorder = TraceRecorder("./traces/bench", task_id=task["id"]) result = agent.run(task["input"], recorder=recorder) recorder.close(result, success=result["success"]) results.append({ "task_id": task["id"], "success": result["success"], "steps": result["steps"], "tokens": result["tokens"], }) summary = { "suite": suite_name, "total": len(results), "success_rate": sum(r["success"] for r in results) / len(results), "avg_steps": sum(r["steps"] for r in results) / len(results), "avg_tokens": sum(r["tokens"] for r in results) / len(results), } with open(f"./benchmarks/reports/{suite_name}.json", "w") as f: json.dump({"summary": summary, "details": results}, f, ensure_ascii=False, indent=2) return summary print(run_benchmark("swe_bench_lite", agent_config, "./benchmarks/swe_lite.jsonl"))

跑完之后和baseline.json对比,看成功率有没有退化、步数有没有变多、token 有没有涨。改动 prompt 或换模型后必须重跑,这是防退化的底线。

4.3 在线 A/B:离线到线上的闭环

离线 benchmark 过了,上线后用 A/B 验证真实效果。按 config.toml 里的 90:10 分流,跑够 200 个样本后用统计方法判断差异是否显著。

from scipy import stats def ab_significance(control_success, control_n, treatment_success, treatment_n): p1 = control_success / control_n p2 = treatment_success / treatment_n p_pool = (control_success + treatment_success) / (control_n + treatment_n) se = (p_pool * (1 - p_pool) * (1/control_n + 1/treatment_n)) ** 0.5 z = (p2 - p1) / se if se > 0 else 0 p_value = 2 * (1 - stats.norm.cdf(abs(z))) return {"control_rate": p1, "treatment_rate": p2, "p_value": p_value, "significant": p_value < 0.05} print(ab_significance(180, 200, 22, 20))

三指标要一起看:成功率、P95 延迟、单任务成本。强模型能提成功率但会增成本和延迟,得权衡。我踩过的坑是只看成功率就全量切新版,结果成本翻倍、延迟涨了 40%,后来改成按场景分流才稳住。

5. 本篇常见错排查

轨迹文件为空或只有 summary:检查TraceRecorder.record是否真的在循环里被调用,以及trace.enabled是否为 true。常见原因是 Agent 循环提前 break 了,没走到 record。

benchmark 跑分波动大:先确认任务集是否固定、模型温度是否设成 0。温度非 0 时同一任务多次跑结果会飘,评估必须固定随机种子或温度。

A/B 判断显著但线上没感觉:检查样本量是否够、分流是否真的随机。90:10 分流下 treatment 只有 20 个样本时 p 值不可信,至少跑到 min_sample_size。

死循环检测误杀:repeat_action_threshold设太小会把正常的重试也判成兜圈。建议先设 3 到 5,观察轨迹后再调。

上下文压缩后质量下降:compress_threshold设太低会频繁压缩,丢失关键信息。先设 0.35,配合keep_recent_turns保留最近几轮原文。

模型适配层换模型后报错:检查adapter是否统一走 openai_compatible,以及新模型名是否在 fallback 列表里。可拆卸性支柱要求换模型只改配置,如果还要改代码,说明适配层没做干净。

6. 把评估闭环跑起来,再谈优化

Harness 六大支柱里,评估对应的是自验证循环和熵治理的落地基础。没有轨迹和 benchmark,你根本不知道改动是让 Agent 变好还是变坏。所以顺序上,先把接入层固定(TaoToken 的 API Keys 和接入文档),再把 settings.json 和 config.toml 配好,然后跑通轨迹埋点和 benchmark 跑分,最后接上在线 A/B。这套闭环建起来之后,后面第 22 篇的可观测 Trace、Loop Engineering、容错硬边界,第 23 篇的成本压缩、安全白名单、部署版本化,才有验证和迭代的依据。

如果你现在还在调模型对话阶段,可以先去模型对话页把接入层跑通;如果已经在做长期编码类 Agent,Coding Plan 那条线更适合你;接入和排障相关的细节都在接入文档里。评估闭环不是一次性的活,是每次改动都要跑的例行动作,跑顺了,Agent 工程化才算真正起步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:19:07

基于SpringBoot3+Vue3的工作量统计系统设计与实现

做工作量统计系统&#xff0c;说穿了就是把团队每个人每天干了啥、干了多少、花了多长时间&#xff0c;变成一张张能汇总、能穿透的报表。但真的动手写过的人都知道&#xff0c;这种系统看着简单&#xff0c;实际踩坑的地方一点也不少&#xff1a;统计口径怎么定、日期按哪个时…

作者头像 李华
网站建设 2026/9/26 3:18:55

众呈道具产品质量好不好,满意度怎么样

从国内线下商业陈列行业萌芽生长&#xff0c;到如今品牌线下终端视觉体系成为营销转化的核心抓手&#xff0c;商业陈列定制赛道已经走过了十余年的升级迭代。消费市场对线下场景体验的要求不断提升&#xff0c;品牌对陈列道具的加工精度、交付稳定性、全链路配套服务的要求也水…

作者头像 李华