从0到1手写 AI Agent Harness:为什么护城河不在模型,而在工程外壳
📖 摘要:本文从 2026 GitHub 趋势(deepseek-harness 周增 1.4 万星)与"护城河在 Harness 不在模型"讨论切入,讲清 Agent Harness 的定义、四大职责与手写最小可运行实现——ReAct 循环 + 权限门禁 + 可回放会话。并给出把记忆/协议/技能/可观测/评测/安全拼进同一控制平面的思路与三条生产避坑。读完你将拥有可跑的 Harness 骨架。
🏷️ 关键词:Agent Harness,AI Agent,工程外壳,工具调度,权限沙箱
目录
- 一、背景与痛点:模型不再是瓶颈
- 二、什么是 Agent Harness
- 2.1 定义:Harness 是模型的"驾驶舱"
- 2.2 Harness 的四大核心职责
- 2.3 一个信号:GitHub 趋势从模型转向系统
- 三、手写最小可运行 Harness
- 3.1 整体架构设计
- 3.2 ReAct 主循环 + 工具调度
- 3.3 权限门禁:把工具调用关进笼子
- 3.4 可回放会话:让调试可追溯
- 四、把"六件套"装进 Harness
- 4.1 记忆 / Skills / 协议如何接入
- 4.2 可观测 / 评测 / 安全如何接入
- 五、生产避坑
- 5.1 星标暴涨 ≠ 可生产
- 5.2 上下文污染与回放陷阱
- 5.3 最小权限与默认拒绝
- 六、总结
一、背景与痛点:模型不再是瓶颈
过去两年,大家的注意力都在"哪个模型更强"——榜单、参数、上下文长度。但 2026 年 8 月的一组信号很说明问题:GitHub Trending 周榜第一是deepseek-harness(一周暴涨约 1.4 万星),紧随其后的diagram-design、prime-agent、TencentDB-Agent-Memory、agent-skills、cloudflare/computer几乎全在解决怎么把模型"框"起来、管起来、跑起来,而不是训练新模型。
社区里的共识正在收敛:模型质量趋同之后,真正的护城河是控制模型行为的系统。一个原始 LLM 只会"聊天",要让它"干活"——读写文件、调接口、跑命令、跨会话记住上下文、多人协作——你需要一层包裹模型的东西。这层东西,就是 Agent Harness。
💡 一句话:模型是发动机,Harness 是车架、方向盘和刹车。发动机再强,没有车架它上不了路。
二、什么是 Agent Harness
2.1 定义:Harness 是模型的"驾驶舱"
Agent Harness(工程外壳)是介于"裸 LLM"和"可用 Agent"之间的一层工程代码。它不直接产生智能,而是负责:循环驱动模型、调度工具、管理上下文、控制权限、记录过程。你可以把它理解为模型的"驾驶舱"——模型负责思考,Harness 负责让它安全地"踩油门、打方向、踩刹车"。
它和普通的"调一次 API 拿到回答"有本质区别:
| 维度 | 裸 LLM 调用 | Agent Harness |
|---|---|---|
| 交互方式 | 一问一答 | 多轮循环 + 工具反馈 |
| 上下文 | 单次窗口 | 可压缩、可持久、可回放 |
| 能力边界 | 只有文字 | 能读文件、跑命令、调外部服务 |
| 安全 | 无 | 权限门禁、沙箱、审计 |
| 可调试 | 重跑难复现 | 事件日志可追溯、可回放 |
2.2 Harness 的四大核心职责
一个合格的 Harness 至少承担四件事:
- 循环驱动(Loop):把"模型输出 → 解析动作 → 执行 → 结果回填 → 再问模型"串成一个闭环,直到任务完成或步数耗尽。这是 ReAct 模式落地的骨架。
- 工具调度(Dispatcher):把模型想用的"工具名 + 参数"路由到真实函数,处理序列化、异常、超时。模型只描述意图,Harness 负责落地。
- 权限控制(Permission):默认拒绝。只有当工具在白名单内、参数通过校验,才放行;危险动作(删库、公网请求)必须显式授权或 human-in-the-loop。
- 过程记录(Replay):把每一轮的目标、思考、工具调用、结果、报错都落盘成事件流。出事后能像看日志一样还原"当时为什么这么做",而不是靠记忆复现一个不稳定的交互。
这四件事单独看都不神秘,但把它们拼成一个稳定、可审计、可恢复的循环,正是 Harness 真正的工程价值。
2.3 一个信号:GitHub 趋势从模型转向系统
2026 年 8 月 GitHub Trending 最明显的特征是:最快增长的项目几乎都在解决 Agent 的六个瓶颈之一——上下文、记忆、技能、编排、机器访问、边缘执行。模型选择已经"够用",开发者不再等一个稍好的基座模型,而是围绕模型组装可替换的组件。
这带来一个重要判断:看 Trending 时,星标衡量的是"需求",架构、验证、发布节奏、许可证、集成成本才决定"能不能落地"。本文要做的,就是抛开星标,亲手把 Harness 的核心拼起来。
三、手写最小可运行 Harness
下面用一个纯标准库、零外部依赖的最小 Harness 把上面四件事跑通。生产里你只需把mock_llm换成真实模型 API,把工具函数换成你的业务函数即可。
3.1 整体架构设计
┌─────────────────────────────┐ 用户目标 → │ Harness (控制平面) │ │ ┌────────┐ ┌───────────┐ │ │ │ Loop │→│ Dispatcher │→ 工具(读文件/跑命令/调API) │ └────────┘ └───────────┘ │ │ ┌────────┐ ┌───────────┐ │ │ │ Gate │ │ SessionLog │ │ ← 权限门禁 + 事件回放 │ └────────┘ └───────────┘ │ └─────────────────────────────┘ ↓ SessionLog (JSONL 事件流)3.2 ReAct 主循环 + 工具调度
核心是一个Harness类:run()里跑 ReAct 闭环,mock_llm仅作演示(生产替换为真实 API)。为了可运行,我把"模型决策"用步数驱动,方便你直接python harness.py看到完整流程。
importjson,uuid,datetime,os# ---------- 1. 会话事件日志(可回放) ----------classSessionLog:def__init__(self,path):self.path=pathdefrecord(self,event):event["ts"]=datetime.datetime.utcnow().isoformat()event["id"]=uuid.uuid4().hex[:8]withopen(self.path,"a",encoding="utf-8")asf:f.write(json.dumps(event,ensure_ascii=False)+"\n")defreplay(self):ifnotos.path.exists(self.path):return[]withopen(self.path,encoding="utf-8")asf:return[json.loads(l)forlinfifl.strip()]# ---------- 2. 权限门禁(默认拒绝) ----------classPermissionGate:def__init__(self,allow=None):self.allow=set(allowor[])defcheck(self,tool_name,args):iftool_namenotinself.allow:returnFalse,f"tool '{tool_name}' not in allowlist"returnTrue,"ok"# ---------- 3. 真实工具函数 ----------deftool_read_file(path):withopen(path,encoding="utf-8")asf:returnf.read()[:2000]# 只读前 2000 字符,避免把大文件塞爆上下文TOOLS={"read_file":tool_read_file}# run_shell 故意不注册,演示"默认拒绝"# ---------- 4. 模拟 LLM(生产替换为真实 API 调用) ----------defmock_llm(step):ifstep==0:return{"action":"call","tool":"read_file","args":{"path":"config.example.json"}}ifstep==1:return{"action":"call","tool":"run_shell","args":{"cmd":"rm -rf /"}}# 危险动作return{"action":"answer","text":"已读取配置;危险命令被权限门禁拦截,任务安全结束。"}# ---------- 5. Harness 主循环 ----------classHarness:def__init__(self,session_path,gate,max_steps=10):self.log=SessionLog(session_path)self.gate=gate self.max_steps=max_stepsdefrun(self,user_goal):self.log.record({"type":"goal","text":user_goal})forstepinrange(self.max_steps):decision=mock_llm(step)# ← 生产: 真实模型决策ifdecision["action"]=="answer":self.log.record({"type":"answer","text":decision["text"]})returndecision["text"]tool,args=decision["tool"],decision["args"]ok,reason=self.gate.check(tool,args)# 权限门禁self.log.record({"type":"tool_call","tool":tool,"args":args,"allowed":ok,"reason":reason})ifnotok:continue# 被拦截,进入下一轮,不执行iftoolnotinTOOLS:self.log.record({"type":"error","text":f"unknown tool{tool}"})continuetry:result=TOOLS[tool](**args)self.log.record({"type":"tool_result","result":str(result)[:500]})exceptExceptionase:self.log.record({"type":"error","text":str(e)})return"max steps reached"3.3 权限门禁:把工具调用关进笼子
注意第 3 节代码里run_shell没有注册进TOOLS,而且PermissionGate默认拒绝白名单外的工具。当mock_llm在 step 1 返回危险的rm -rf /时:
- 门禁
check("run_shell", ...)返回False; - 循环
continue,真实命令永远不会执行; - 事件流里留下一条
allowed: false的记录,事后可审计"谁、什么时候、想干什么、被拦了"。
这就是"安全从感知层移到执行层"的落地:与其在提示词里求模型"别干坏事",不如在 Harness 这一层用代码物理阻断。
3.4 可回放会话:让调试可追溯
SessionLog把所有事件写进 JSONL。出问题时,不需要重跑一遍不稳定的交互,直接replay()就能看到完整时间线:
if__name__=="__main__":gate=PermissionGate(allow=["read_file"])# 仅放行只读工具h=Harness("session.example.jsonl",gate)print("结果:",h.run("读取配置文件并检查风险项"))print("\n--- 会话回放(事件流)---")forevinh.log.replay():print(f"[{ev['ts']}]{ev['type']}:{json.dumps(ev,ensure_ascii=False)}")运行后你会看到:goal→tool_call(read_file, allowed=true)→tool_result→tool_call(run_shell, allowed=false)→answer。一条被拦截的危险调用清晰可查,这就是 Harness 相对"裸调 API"的核心优势。
四、把"六件套"装进 Harness
社区这几年把 Agent 的各个能力点都磨得很成熟了:记忆、工具协议(MCP/A2A)、Skills、可观测、评测、安全——单看都好用,问题是怎么拼起来。Harness 恰好是那个"控制平面",把这些模块接成一张网。
4.1 记忆 / Skills / 协议如何接入
- 记忆:在
run()每轮开始前,从记忆中心拉取与当前目标相关的上下文(对话/文档/代码),注入系统提示;工具产出再写回记忆。跨会话复用靠它。 - Skills(声明式技能):把
TOOLS从硬编码函数升级为"元数据驱动的技能清单"——每个 Skill 有名称、描述、输入 schema。模型按描述匹配,Harness 懒加载,避免把所有指令塞进上下文。 - 协议(MCP/A2A):
Dispatcher不只调本地函数,还能通过 MCP 连外部工具服务、通过 A2A 把子任务转发给另一个 Agent。协议是"工具/Skill"的 transport 层,Harness 只管调度,不关心工具在哪。
4.2 可观测 / 评测 / 安全如何接入
- 可观测:把
SessionLog的每一条事件加上trace_id/span,导出到 OpenTelemetry 后端(Langfuse、Phoenix),就能看到每个工具调用的耗时、成本、成功与否。 - 评测:在
run()外层包一个 Eval 循环——跑一批固定任务,用"确定性 + 启发式 + LLM-as-Judge"三层打分,作为 CI 质量门禁,防止改了 Harness 把准确率带崩。 - 安全:
PermissionGate只是第一道。再叠加输入注入检测(识别提示词攻击)、输出脱敏(拦截泄露的密钥)、出口白名单(只允许访问审批过的域名),就构成执行层防护。
一句话:记忆给上下文、Skills 给能力、协议给扩展、可观测给眼睛、评测给标尺、安全给刹车——而 Harness 是把它们拧在一起的轴。
五、生产避坑
5.1 星标暴涨 ≠ 可生产
deepseek-harness一周 1.4 万星很炸裂,但星标衡量的是注意力,不是可靠性。落产前请查四项:贡献者分布是否健康、有无未解决的安全 issue、是否有打 tag 的正式 release、依赖风险与维护响应速度。任何能碰仓库或 shell 的 Agent,都必须先过这几关再进生产。
5.2 上下文污染与回放陷阱
回放能还原"发生了什么",但还原不了"当时的完整上下文"——如果上下文里混入了错误的中间结论,回放看到的是"被污染后的决策链",容易误判根因。对策:回放时同时存快照版本号,调试优先看tool_result原始值而非模型复述;并定期做可恢复性演练,确认日志真能重建现场。
5.3 最小权限与默认拒绝
永远从"默认拒绝"出发:TOOLS白名单 +PermissionGate双保险。不要因为"模型一般不会乱来"就放开危险工具。把危险动作(写库、公网请求、删文件)设为必须显式审批或 human-in-the-loop。可参考上一条原则:安全要在执行层用代码物理阻断,而不是在提示词里靠模型自觉。
六、总结
2026 年的 Agent 竞争,已经从前两年的"模型军备赛"切换到"系统工程赛"。Harness 就是这场比赛里最关键的控制平面:它用 ReAct 循环驱动模型、用 Dispatcher 落地工具、用 PermissionGate 物理阻断危险动作、用 SessionLog 让一切可追溯。
本文给的最小实现只有几十行标准库代码,却把四件核心职责跑通了。把它和记忆、Skills、协议、可观测、评测、安全这"六件套"接起来,你就拥有了一个能生产落地、可审计、可恢复的 Agent 框架骨架——而不是又一个只能在 Demo 里惊艳的玩具。
🚀 动手建议:先把本文的
harness.py跑起来,再逐步把mock_llm换成真实模型、把TOOLS换成你的业务函数,最后接上一条可观测链路。欢迎在评论区聊聊你踩过的 Harness 坑。
示例数据声明:文中config.example.json、example.com、run_shell等均为通用化演示素材,不对应任何真实系统或业务。