他从零写了一个 agent harness:把模型当 20%,剩下 80% 全是工程
最近几个月,几乎所有团队都在堆 agent。
但很少有人讲清楚一个 agent 内部到底是什么。
不是模型。是模型周围那一整套东西。
Mohit Goyal 花了几个月时间在 Python 里从零搭了一个完整的 harness——没有框架捷径,包括流式 agent 循环、强类型工具调用、审批关卡、prompt injection 隔离、上下文压缩、MCP 接入、子 agent、持久化,以及一整套测试。项目叫 AgentForge,开源、可装、能跑。整篇文章不是发布稿,是它教会作者的"agent 到底是什么、为什么没自己造过的人对它的理解是危险的"。
最核心的一句话,作者写得很重:
一个 agent 不是一个模型。agent 是一个 runtime,它控制模型怎么"看、动、重试、记忆、停止"。
模型大概只占 20% 的工程量。
剩下 80% 都在它外面:动作空间、审批策略、观测格式、上下文预算、恢复路径、持久化层。
第一个对象不是模型客户端,是 Session
很多人画 agent 循环就是这种 naive 形态:
prompt → model → response
聊天机器人够用。coding agent 不够。
一个 coding agent 启动时必须先知道:当前在哪个目录、有哪些工具、激活的是哪个模型、审批模式是哪种、已经发生过什么、加载了哪些 skill、接了哪些 MCP server、上下文还剩多少、是不是 plan 模式、能不能事后恢复、有没有正在形成 tool/action 死循环。
所以 AgentForge 里的第一个对象不是 model client,是 Session。
Session 持有一切:tool registry、skill loader、MCP manager、context window、operating mode。在第一次模型调用之前,harness 已经决定了"模型能看到哪些工具、哪些 skill、哪些 MCP 工具、当前 mode 如何塑形上下文"。
模型不自己发现世界。harness 在第一个 token 生成前就把世界摆好了。
作者把这叫"runtime owns the model, not the other way around"。
Agent 循环是策略引擎,不是 ReAct 图
教科书里的循环是:think → act → observe → repeat。
真的循环要处理:上下文压力、模型失败、回退模型、tool 预算、plan/build 模式、重复动作、流式输出、crash checkpoint。
在模型被调用之前,harness 已经做了这些事:
- plan 模式拿到更小的 turn 预算
- 回退模型排好序
- 失败的模型可以被熔断
- 工具 schema 按 mode 过滤
循环运行时还要实时盯上下文压力——到 80% 自动 compact,但必须保留足够多的近期状态以避免重做已完成的工作。
但循环更要知道"什么时候该停"。
AgentForge 有一个 LoopDetector,盯连续多轮里出现完全相同的工具调用。比如连续三次 read_file 同一个路径但中间没有编辑——这不是在推进,是在转圈。检测到就直接强制模型产出一个 final answer,而不是无限 spin。
熔断器在模型层级工作:某个 provider 开始持续报错,就把那条路开路,自动切到下一条链。模型会挂。harness 不为这个做准备的,那不叫 harness,叫 demo。
一句话总结这节:
Agent 循环不是循环,是"进度"的策略引擎。 它决定继续、停止、藏工具、压历史、问用户、放弃模型、还是把重复行为判为 stuck。
只建 happy path,你做的是 demo。 建了 stop conditions,你才开始做 harness。
工具是模型的神经系统
作者说这是他学到的最重要的事:tool design is agent design。
模型只能通过你给它的动作空间行动。如果工具名重叠,它会犹豫;如果 schema 含糊,它会瞎猜;如果结果不透明,它恢复不了;如果错误只写"failed",模型会 loop 或 hallucinate 下一步。
所以 AgentForge 的每个工具都返回强类型的 ToolResult,带四个关键字段:
- summary:用平实语言告诉模型发生了什么
- artifacts:告诉它什么变了、什么可以接着检查
- next_actions:告诉它安全的下一步
- recovery_hint:告诉它怎么避免在同一失败上盲目重试
工具结果不是日志行,是 agent 推理循环里的下一次"观察"。 那次观察的质量直接决定下次决策的质量。
失败调用也走同一个合约。error_result 工厂在每次失败时自动塞默认的 recovery hint 和 next actions。裸异常只告诉模型"东西坏了";结构化错误告诉它"哪里坏了、看什么、下一步安全动作是什么"。这两者的差距,就是 loop 的 agent 和 recover 的 agent 的差距。
所有结果——成功和失败——都过一遍 registry:清理、redact、prompt-injection 标记、hooks,然后再送到模型面前。工具在世界层执行,registry 把结果再翻译回安全的"观察"。
这才是 harness 边界。
文件工具暴露隐藏状态
作者原以为文件工具很无聊。结果不是。
第一个版本的 read_file 只会返回 text。但 coding agent 要的不止是 text:行号、offset/limit、二进制检测、是否被截断、trailing-newline 状态(这决定了 patch 能不能干净应用)。
trailing-newline 这个标志看起来是细节,但 patch 失败就是因为文件最后没换行而模型不知道。 行号看起来是细节,但模型要精确编辑、又只能从内容里推位置的时候就是死路。
edit 工具要求精确 old_string 匹配。匹配不到的时候不会静默失败——它会找文件里相似行返回给模型,再给一条 recovery hint:"先 re-read 一下文件,context 里的版本可能已经过期"。
这是把恢复合约写进文件操作里。
apply_patch 进一步:先校验 patch 路径(拒绝绝对路径、拒绝父目录穿越),用 git apply --check 做 dry-run 校验,git 不可用时还有 fallback parser。
一节话总结:
细节变成模型行为。坏工具强迫模型推断隐藏状态;好工具把模型需要安全行动的状态直接摆出来。
安全是被强制出来的,不是被"请"出来的
你可以要求模型小心点。
但你还得在模型外层强制安全。
AgentForge 有五种审批模式:on-request、auto、auto-edit、never、yolo。审批层看:可变性、命令模式、受影响路径、danger flag、配置的策略。
def classify(self, action: Action) -> Decision:
if action.is_mutating and self.policy == ApprovalMode.ON_REQUEST:
return Decision.ASK
if action.command_matches_danger_pattern():
return Decision.DENY
if action.affects_protected_path():
return Decision.DENY
return Decision.ALLOW
这段代码故意写得很平淡。这才是重点。
模型不应该负责决定"在当前上下文下 rm -rf 是否安全"。harness 先把动作分类、套上策略,在命令运行前批准、拒绝或问用户。
这也是为什么 plan mode 绝不能只是 prompt 里写一句"别编辑文件"。AgentForge 的 plan mode 是在 registry 层过滤动作空间——模型在 plan mode 收不到写工具,根本没法调。
模型可以被指示避免某事,但仍然去做。不暴露工具的 harness 让它"结构上不可能"去做。
安全属于策略和强制,不只是 prompt 文案。
Prompt injection 是 read 问题,不是 web 问题
一开始听 prompt injection,像是网页浏览的问题。
搭完 coding agent 你会发现,每次文件读取也是 prompt 输入。
仓库文件可以含指令。shell 命令可以打印指令。网页可以含指令。MCP server 可以返回指令。
如果这些输出原样回到模型,模型就可能当作指引。
所以 AgentForge 把工具观测包成"untrusted content":
<untrusted_tool_output source='web_fetch'>
...
</untrusted_tool_output>
这不是完整沙箱。shell 命令和 MCP server 不会被这一层魔法般变安全。恶意的文件仍可能用更隐蔽的方式尝试注入。
但它建立了一个 prompt 自己不可靠建立的边界:每次观测都被显式标注"来自某个具体源的数据"。模型同时看到包裹和指令。这种分离是结构性的,不是对话性的。
MCP 接入不能绕开 registry
AgentForge 接 MCP 时做了一件关键事:MCP 工具不带特权。
一个 filesystem MCP server 的 read_file 变成 filesystem__read_file。一个 GitHub server 的 create_issue 变成 github__create_issue。命名规则简单,消除一整类"名字撞了"的 bug。
但更重要的是:MCP 工具不绕开 registry。它们被当作一等公民工具注册进来,所以完整 pipeline 仍然生效——schema 暴露、mode 过滤、审批检查、输出清理、redact、prompt-injection 标记、hooks。
MCP server 返回网页、文件、结构化 API 响应,仍然是外部内容。仍然被包成 untrusted。仍然被 redact 掉密钥。信任边界不会因为工具来自 MCP 而不是本地代码就消失。
扩展系统只有在保留 harness 合约时才有意义。给外部工具开"二等通道"的插件架构,是带洞的安全模型。
子 agent 是工具,不是编排
AgentForge 里的子 agent 是一个工具。
父 agent 传一个 goal 进去,工具启一个子 agent,带着 scoped config、allowed tools、max turns、硬超时。一个输入,一个结果,父 agent 保持控制。
内置的子 agent 故意全是只读:explorer、debugger、codebase investigator、code reviewer、test planner、architect。默认没有写文件、没有 mutating shell 的能力。父 agent 决定怎么用它们返回的结果。
作者给了一个真实场景:他让 AgentForge 查一个 shell tool 间歇超时的问题。父 agent 调 subagent_debugger,传一个 goal 进去。debugger 跑最多 6 轮,只用只读工具(read_file、grep、安全命令的 shell),最后返回一条聚焦发现:超时是从进程 spawn 开始计的,而不是从第一字节输出,慢文件系统上就出现表观方差。父 agent 看完,做了针对性修复。
子 agent 没有写权限。父 agent 不用亲自管理调查。边界让委派安全,让结果可用。
至于 swarm(多 agent、共享状态、冲突处理、聚合写),那是编排问题。作者没做,并且认为是正确的判断:先把子 agent 当工具,边界会逼你定义合约;合约稳了再谈规模化。
持久化就是系统编程
一开始作者以为持久化很容易。存 JSON、读 JSON、完事。
等 harness 开始碰真实机器状态就不一样了。
session 快照、checkpoint、event log 要有地方放。AgentForge 用 platformdirs 拿平台数据目录(Linux 上 ~/.local/share/agentforge,macOS 上 ~/Library/Application Support/agentforge),用 owner-only 权限写文件。
这暴露了一个不光彩但真实的问题:测试不应该依赖开发者家目录的脏状态。
作者自己机器上,session 相关测试正在静默读写真实 platform data 目录——然后通过/失败取决于上次手动跑留下什么状态。同一份测试在干净机器上通过,在他机器上挂。
修法很无聊。可靠的测试命令变成:
HOME=/tmp/agentforge-test-home python3 -m pytest -q
这恰恰是重点。
agent harness 不是 prompt 实验,是读写文件、写状态、起进程、管权限、还要从部分失败里活下来的软件。持久化层反映的就是这些:
with os.fdopen(fd, "w", encoding="utf-8") as fp:
json.dump(data, fp, indent=2)
fp.flush()
os.fsync(fp.fileno())
os.replace(tmp_name, file_path)
os.chmod(file_path, 0o600)
原子写。owner-only 权限。crash-safe 替换。JSONL event log 追加不覆盖。
不刺激。必要。
到这一节,"agent 工程"开始不像 AI-only 的活,更像把 LLM 塞进内部的"普通系统编程"。
测边界,不测散文
大多数人会以为 agent 难测是因为模型输出不确定。
模型散文是不确定的。
harness 合约是确定的。
AgentForge 有 278 个通过的测试,覆盖:config 加载、session 状态、plan mode 工具过滤、上下文管理与压缩、loop detection、tool schema、文件工具行为、patch 校验、shell 工具策略、输出清理、跨结果/审批/导出的 redact、prompt-injection 包裹、持久化快照、报告、skill、MCP 邻接行为。
全套用隔离的 home 目录跑:
HOME=/tmp/agentforge-test-home python3 -m pytest -q
278 passed
可以测:危险命令是不是在执行前被拦。可以测:密钥是不是在到达模型前被剥掉。可以测:edit 在不能唯一定位时是否拒绝。可以测:plan mode 是不是从 schema 列表里过滤掉了写工具。可以测:session 快照是不是 round-trip 回同一状态。可以测:prompt-injection 包裹是不是被应用到每一次外部观测。
这些测试一个都不需要真的模型调用。
这是关键洞察:agent 可靠性的很大一部分来自确定性 harness 行为,跟模型智能毫无关系。harness 合约是破的,再聪明的模型也救不回来。
Test the boundaries, not the prose.
AgentForge 是真实的 Python 包,不是带代码块的博文。278 个测试通过。它们不证明 agent 聪明,证明 harness 合约立得住。
这个区分才是重点。
怎么开始:不要从框架开始
不要从大框架开始。
从一个小 harness 开始。
- 一个 model adapter
- 循环带 stop conditions
- 三个工具:read_file、edit、shell
- 工具带类型
- 工具结果带 summary、next_actions、recovery_hint
- 写操作前要审批
- 文件读取带行号
- 失败路径带 recovery hint
- 上下文剪枝
- 一个 checkpoint
- 一个 skill
- 一个 MCP server,工具用 namespace 命名
- 一个失败的工具调用测试,验证模型拿回有用的 observation
这个练习比再读一篇 agent 抽象解释教得更多。
因为一旦开始建 harness,那些"真的问题"会逼出来:
- 应该存在哪些 action?
- 模型应该永远不被允许直接做什么?
- 工具运行后模型应该看到什么?
- 送到模型前应该 redact 什么?
- 什么算 untrusted?
- 循环什么时候该停?
- 什么状态必须从 crash 里活下来?
- 哪些部分能不用模型调用就测?
这是 agentic engineering。
不只是 prompting。
是 action-space design。 是 observation design。 是 context design。 是 recovery design。 是 safety design。 是 runtime design。
AgentForge 把这些事显式化了。
每个认真做 agent 的工程师,都应该从头建一个小 harness。不是为了 ship。是为了理解框架在替你藏什么。