从0到1手写 AI Agent Harness:为什么护城河不在模型,而在工程外壳
📖 摘要:本文从 2026 GitHub 趋势(deepseek-harness 周增 1.4 万星)与"护城河在 Harness 不在模型"讨论切入,讲清 Agent Harness 的定义、四大职责与手写最小可运行实现------ReAct 循环 + 权限门禁 + 可回放会话。并给出把记忆/协议/技能/可观测/评测/安全拼进同一控制平面的思路与三条生产避坑。读完你将拥有可跑的 Harness 骨架。
🏷️ 关键词:Agent Harness,AI Agent,工程外壳,工具调度,权限沙箱
目录
- 一、背景与痛点:模型不再是瓶颈
- [二、什么是 Agent Harness](#二、什么是 Agent Harness)
- [2.1 定义:Harness 是模型的"驾驶舱"](#2.1 定义:Harness 是模型的"驾驶舱")
- [2.2 Harness 的四大核心职责](#2.2 Harness 的四大核心职责)
- [2.3 一个信号:GitHub 趋势从模型转向系统](#2.3 一个信号:GitHub 趋势从模型转向系统)
- [三、手写最小可运行 Harness](#三、手写最小可运行 Harness)
- [3.1 整体架构设计](#3.1 整体架构设计)
- [3.2 ReAct 主循环 + 工具调度](#3.2 ReAct 主循环 + 工具调度)
- [3.3 权限门禁:把工具调用关进笼子](#3.3 权限门禁:把工具调用关进笼子)
- [3.4 可回放会话:让调试可追溯](#3.4 可回放会话:让调试可追溯)
- [四、把"六件套"装进 Harness](#四、把"六件套"装进 Harness)
- [4.1 记忆 / Skills / 协议如何接入](#4.1 记忆 / Skills / 协议如何接入)
- [4.2 可观测 / 评测 / 安全如何接入](#4.2 可观测 / 评测 / 安全如何接入)
- 五、生产避坑
- [5.1 星标暴涨 ≠ 可生产](#5.1 星标暴涨 ≠ 可生产)
- [5.2 上下文污染与回放陷阱](#5.2 上下文污染与回放陷阱)
- [5.3 最小权限与默认拒绝](#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 看到完整流程。
python
import json, uuid, datetime, os
# ---------- 1. 会话事件日志(可回放) ----------
class SessionLog:
def __init__(self, path):
self.path = path
def record(self, event):
event["ts"] = datetime.datetime.utcnow().isoformat()
event["id"] = uuid.uuid4().hex[:8]
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
def replay(self):
if not os.path.exists(self.path):
return []
with open(self.path, encoding="utf-8") as f:
return [json.loads(l) for l in f if l.strip()]
# ---------- 2. 权限门禁(默认拒绝) ----------
class PermissionGate:
def __init__(self, allow=None):
self.allow = set(allow or [])
def check(self, tool_name, args):
if tool_name not in self.allow:
return False, f"tool '{tool_name}' not in allowlist"
return True, "ok"
# ---------- 3. 真实工具函数 ----------
def tool_read_file(path):
with open(path, encoding="utf-8") as f:
return f.read()[:2000] # 只读前 2000 字符,避免把大文件塞爆上下文
TOOLS = {"read_file": tool_read_file} # run_shell 故意不注册,演示"默认拒绝"
# ---------- 4. 模拟 LLM(生产替换为真实 API 调用) ----------
def mock_llm(step):
if step == 0:
return {"action": "call", "tool": "read_file", "args": {"path": "config.example.json"}}
if step == 1:
return {"action": "call", "tool": "run_shell", "args": {"cmd": "rm -rf /"}} # 危险动作
return {"action": "answer", "text": "已读取配置;危险命令被权限门禁拦截,任务安全结束。"}
# ---------- 5. Harness 主循环 ----------
class Harness:
def __init__(self, session_path, gate, max_steps=10):
self.log = SessionLog(session_path)
self.gate = gate
self.max_steps = max_steps
def run(self, user_goal):
self.log.record({"type": "goal", "text": user_goal})
for step in range(self.max_steps):
decision = mock_llm(step) # ← 生产: 真实模型决策
if decision["action"] == "answer":
self.log.record({"type": "answer", "text": decision["text"]})
return decision["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})
if not ok:
continue # 被拦截,进入下一轮,不执行
if tool not in TOOLS:
self.log.record({"type": "error", "text": f"unknown tool {tool}"})
continue
try:
result = TOOLS[tool](**args)
self.log.record({"type": "tool_result", "result": str(result)[:500]})
except Exception as e:
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() 就能看到完整时间线:
python
if __name__ == "__main__":
gate = PermissionGate(allow=["read_file"]) # 仅放行只读工具
h = Harness("session.example.jsonl", gate)
print("结果:", h.run("读取配置文件并检查风险项"))
print("\n--- 会话回放(事件流)---")
for ev in h.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 等均为通用化演示素材,不对应任何真实系统或业务。