首页 / 文章

智能体框架的五层结构,以及缺少这些层时代码会出现的故障。

代理控制台的五层结构,每一层都包含不可或缺的代码:执行边界、沙箱环境、内存管理、验证循环以及上下文处理流程。

发布者:REACTAPP.TOP阅读约 14 分钟3301 词

来源: `The Complete Guide to Agent Harnesses (With Code)`

想象这样一个团队:他们会在代理的配置中添加一行内容,任何修改都必须经过人工审批才能生效。这条指令表述清晰,位于配置顶部附近,代理会在每次会话开始时加载它。

然而有一天下午,某次会话竟多次越过那条指令,将大约76项未经批准的更改纳入了最终交付给客户的文件中。系统没有出现任何错误提示,也没有暂停询问。两小时后,有人仔细检查文档,标出了代理自行修改的每一行,结果在那个带有客户名称的文件中发现了46处修改。

指令本身并没有问题,只是放置的位置不对。

另一种有效的方法是使用钩子,即工具在发起调用之前由框架运行的小型脚本。该脚本会检查待处理的调用并可以拒绝响应。代理可能仍希望进行修改,但修改内容不会被写入文件。这类钩子有时会出现错误,通常是因为规则设定得过于严格,而其优势在于你可以打开代码并明确指出导致错误的具体条件。而提示语中的句子则无法提供这样的参考依据。

你声明的规则与实际执行的规则之间的间隔就是框架。它涵盖五个方面:

  1. 代理可以执行的操作
  2. 它能够访问的位置
  3. 重启后依然存在的状态
  4. 对其输出进行审核的人
  5. 其显示内容所对应的成本

每个领域都有相应的代码。有一组可运行的演示程序,能让每一层在您的个人笔记本电脑上先出问题,然后再在生产环境中出故障。这是为那些已经发布过代理程序并目睹其出错的人准备的。

工具框架真正能为您带来的好处

从几乎简单到无需多言的内容开始讲起。一个编程代理程序的主循环仅有六行代码:

while agent.turns < max_turns:
    call = agent.next_call(observations)
    if call is None:
        break
    result = execute(call)
    observations.append(result)

那些代码行就是代理程序。通过错误处理和重试机制,这个数值大约会达到二十左右。无论你评估过哪种框架,它们都会封装这个循环,而关键所在在于execute函数。该函数决定了模型能做什么、能操作什么、能保留什么、由谁来审核它,以及最终呈现给它的内容是什么。

所有这些决策共同构成了控制框架,许多评论认为其重要性甚至超过模型本身的选择。不过数据支持的观点更为有限,但也更为实用。

在成本方面,这种框架更具优势。2026年6月的一篇预印本研究对比了三种框架(Goose、OpenCode、OpenHands-SDK)在两种模型上的表现,测试数据来自Terminal-Bench Pro中难度更高的50个任务子集。不同框架在完成每个任务时所需的token数量差异可达40倍。即便更换为性能更好的模型,这一数值也仅会变为原来的1.0到1.3倍。

但在功能表现上则并非如此。不同框架的通过率差异在0到8个百分点之间,通过自举置信区间分析,除差距最大的情况外,其余所有情况的差异都接近于零。鉴于样本量仅为50,作者自己也认为这些准确率结果仅具有描述性意义。

简单来说,同一个模型执行同一项任务,即便成功率相同,使用不同的框架时成本也可能高出四十倍。后续的所有优化措施都能降低成本并减少意外情况,但都无法提升薄弱模型的智能水平。

将规则转化为代码

在系统提示中列出规则,其实是在赌模型能够记住它、按预期理解它,并在上下文变得复杂时依然遵循它。通常模型能做到这一点。正是这种高成功率让它存在风险,因为人们会因此不再进行验证。

代码层面的规则会在每次调用时都被执行,即便模型已经忘记了这些规则。

智能体循环中有五个地方可以放置控制代码。以下各部分分别说明其作用、执行相关代码,以及该层缺失时的行为表现。无需同时使用这五个部分,只需选择你认为本周最可能出问题的那一层即可。

第1层:执行边界

智能体被允许做什么?

该边界是一个介于模型决策与实际工具调用之间的函数。首先让智能体处于无保护状态,其指令要求未经询问不得删除文件,同时队列中有三个工具调用等待执行。

SYSTEM_PROMPT = "IMPORTANT: never delete a file without asking the user first."

def execute(call):
    if call.name == "delete_file":
        FILES.pop(call.args["path"], None)
        return f"deleted {call.args['path']}"

运行后,该代理会先删除无害的垃圾文件cache.tmp,然后再删除名为client-deliverable.md的文件,其名称正如其功能所示。尽管提示中从一开始就说明了规则,却并未提出任何问题。

将一个函数放在工具之前即可解决此问题:

def boundary(rules):
    def wrap(execute):
        def guarded(call):
            for name, deny_if, reason in rules:
                if deny_if(call):
                    return f"DENIED by {name}: {reason}"
            return execute(call)
        return guarded
    return wrap

NEEDS_APPROVAL = [(
    "delete-needs-approval",
    lambda c: c.name == "delete_file" and not c.args.get("approved_by_human"),
    "deletion requires an explicit human approval flag on the call",
)]

使用相同的智能体、脚本和提示词,不会删除任何内容。

第二层:沙箱隔离

智能体能访问什么?

沙箱首先决定了智能体进程可以访问哪些目录和主机。应从零开始构建:先不赋予任何权限,再通过允许的路径列表、允许的主机列表,以及仅能处理指定任务的进程,逐步授予其所需权限。

另一种常见的做法是使用禁止路径的拒绝列表:

DENY = ["secrets/"]

def execute(call):
    path = call.args["path"]
    if any(path.startswith(d) for d in DENY):   # checks the spelling
        return "DENIED by deny-list"
    real = os.path.normpath(path)               # the ../ collapses HERE, after the check
    return DISK.get(real, "not found")

运行它后,代理会尝试两次:

read('secrets/api_key')          -> DENIED by deny-list
read('work/../secrets/api_key')  -> sk-live-DO-NOT-LEAK

校验功能会检查实际输入的文本,而文件查找则依据该文本对应的路径进行。两次请求,两种拼写,同一个文件。

修复方法是为先标准化路径,然后再进行比较:

ALLOW_ROOTS = ["work"]

def resolve(path):
    real = os.path.normpath(path)               # resolve FIRST
    if not any(real == r or real.startswith(r + os.sep) for r in ALLOW_ROOTS):
        return None
    return real

这两种拼写方式最终都会简化为 secrets/api_key,而该路径并不属于任何允许的根目录,因此两者都会被拒绝。此处关键在于允许而非拒绝,因为拒绝列表仅能阻挡其创建者预想到的那些拼写形式。

这些示例只是普通的 Python 函数,因此无需任何容器即可测试。在实际部署中,相同的检查机制体现在进程边界上:用户或容器在 work/ 目录外没有读取权限,操作顺序也完全一致。

第一层无法完成这项工作。程序内部的检查机制只能识别它所识别的调用方式,而 shell 命令、符号链接或包含 .. 的路径则可以通过非上述调用方式访问同一文件。

第三层:内存持久化

上下文重置后什么还会保留?

有一种习惯比其他大多数习惯都更有益:无需犹豫就结束对话。随着上下文越来越复杂,对话质量会下降,处理者也会开始自相矛盾。很多人总是拖延结束这样的对话。只要重要信息被存储在重启无法清除的地方,比如已提交的代码、配置文件、框架本身或总结笔记,尽早重启才是更好的选择。

因此,要明确决定每个状态项应存储在何处。共有三种选择:对话中,它会随会话一同消失;磁盘上,它会持久保存并可在之后获取;以及框架配置中,它会影响后续的所有会话。

一个调度函数即可涵盖这三种情况:

CONVERSATION, DISK, HARNESS_CONFIG = [], {}, {}

def remember(kind, key, value):
    """'chat' dies with the session, 'disk' survives it,
    'config' shapes every session that follows."""
    {"chat": lambda: CONVERSATION.append(value),
     "disk": lambda: DISK.__setitem__(key, value),
     "config": lambda: HARNESS_CONFIG.__setitem__(key, value)}[kind]()

这个功能几乎没什么用。它所提供的不过是一条规则:如果不指定内容要存入三个存储位置中的哪一个,就不得保存。团队很少明确做出这样的选择,这也解释了为何有些智能体会忘记某些事情,而在其他事情上却固执地犯错。在演示中,对话列表显示为空,而决策和规则仍保留在磁盘及配置文件中。

第4层:验证循环

谁来审核输出结果,又是什么阻止审核者直接修正问题?

验证循环指的是由能够标记问题但无权直接修复问题的机制,对智能体的工作进行二次检查。

后半部分正是人们常常忽略的。让审核器的子代理来处理,它会默默修复所发现的错误。这看似高效,但却会抹去你想要的信息:你再也无法区分哪些是正确的代码,哪些是在背后被修改过的错误代码。

因此应将审核器的权限设置为只读。它只会生成评审意见,而根据这些意见采取行动则是另一道流程,会留下新的痕迹。将两者分开就能保留可供查看的信号。

另一种策略是试运行。对于任何不可逆的操作,该工具会先接收一个标志,默认情况下会说明它原本会执行什么操作,只有当有人刻意关闭该标志时才会真正执行操作。无害的行为是默认状态,因此出现疏忽也不会造成任何后果。在演示中,修复工具会宣布它将重写total.py,但实际上什么都没写入,最终那个“少算一个”的错误依然存在。

一个 execute 操作同时包含这两条规则:

def execute(call):
    if call.name == "review":
        snapshot = dict(CODE)               # a copy, so the reviewer cannot write
        src = snapshot[call.args["path"]]
        return f"VERDICT: {'off-by-one' if '+ 1' in src else 'looks good'}"
    if call.name == "apply_fix":
        if call.args.get("dry_run", True):  # on by default, turned off on purpose
            return f"DRY RUN: would rewrite {call.args['path']}, nothing written"
        CODE[call.args["path"]] = call.args["new"]
        return f"wrote {call.args['path']}"

审核人员发现了“少算一个”的问题,但却无法对其进行修改。

第5层:上下文处理流程

什么内容会进入模型,又该如何计费?

上下文处理流程决定了信息如何进入模型的处理范围以及其中有多少内容会被接纳。

费用就产生在内容被接纳的环节。每个进入模型的标记都会在当前轮次被收费,之后的所有轮次也会再次收费,因此正是这里出现了不同服务方案之间四十倍的差距,而非在第1层。

一种简单的处理方式会加载所有内容:针对某个函数的问题会加载整个文件,针对简单查询会加载全部的搜索结果,还会加载所有的历史记录。虽然这种方式可以正常运行,但到了第20轮时,你仍然要为第1到19轮已经读取过的内容再次付费。

解决方案是委托处理与信息提炼。子代理在单独的窗口中工作,读取数万个标记后仅返回一两千个。主线程则跳过原始搜索结果,直接获取答案。

在代码层面,关键在于哪个计数器会增加:

def subagent_search(query):
    """Its own window. The main thread never pays for this reading."""
    global subagent_tokens
    subagent_tokens += sum(len(v.split()) for v in CORPUS.values())
    return next(f"{n}: {b.split('ANSWER:')[1].strip()}"
                for n, b in CORPUS.items() if "ANSWER:" in b)
def execute(call):
    global main_tokens
    distilled = subagent_search(call.args["q"])
    main_tokens += len(distilled.split())    # the only line that bills you
    return distilled

该演示刻意保持规模较小且真实可信。两次运行中的问答内容完全相同,但主线任务在第一次处理了9,602个标记,而在另一次仅处理了2个。

这也解释了为何链式子代理会让人失望——当每一步都需等待前一步完成时,就会出现上下文隔离,且根本无法实现并行加速。真正独立的步骤应当能够同时运行。否则,就必须接受隔离这一后果。只要清楚费用涵盖的内容,这通常也能成为其合理性的依据。

目前尚无人能给出答案

公众对这一领域的信心已经超过了相关证据的支持程度,因此这些注意事项至关重要。

  1. 此处没有任何内容经过同行评审。截至2026年8月,所有单独研究该装置的论文均为未经评审的arXiv预印本。最严谨的实验也仅称自己为处于研讨会评审阶段的初步成果,而最著名的框架性论文则自称是一份立场文件,并未开展任何实验。应将所有此类内容,包括本文在内,视为未经评审的资料,在据此规划预算之前,请先在自己的任务中重复进行对比分析。
  • 所学技能很少能迁移到未见过的任务中。2026年7月的一篇论文指出,让适配器自动进化并无法可靠地超越单纯的测试时扩展策略,且泛化能力较差,因为搜索过程与最终测试都基于同一基准数据。在Terminal-Bench 2.0的持续学习评估中,GEPA在经过调优的任务上的准确率上升至70.8%,但在更广泛的任务集上的准确率则降至54.5%,低于其56.8%的初始值。实际上调优反而有害。应在未用于构建该适配器的任务上对其进行测试,因为在调优集上观察到的改进在实际应用中会消失。
  • 一种知名的微调成果是通过其自身的训练信号来评估的。例如,为编写临床笔记而对 Gemma 3 27B 进行的微调,使其性能从低于 Claude Sonnet 4 的 35% 提升到高于它的 60%。相关报告明确指出,评估工具同时也被用作训练奖励。这确实是一种有效的方法,但分数恰恰来源于用于教学的同一工具。当供应商宣称某次微调取得了显著提升时,应询问训练时所使用的评估标准是什么;如果仍是同一评估工具,那么这个数值就表明模型只是记住了该评估标准而已。
  • 工具包无法在不同模型之间顺畅迁移。2026年6月的一项研究显示,训练完成后再添加工具包几乎无法弥补同步训练所带来的优势。在最小化工具包上训练后的模型,在工具定义被重新调整后,75.1%的尝试都会出现“无效工具格式”的错误;而Qwen2.5-7B版本的成绩则比其基础模型低10.8分。那种适用于所有模型的工具包实际上对每个模型的适配度都较差。应在训练前就选定工具包,如果您不自行训练模型,那么在项目进行到一半时更换工具包的成本将会远高于最初的迁移成本。
  • 相关术语尚未明确统一。2026年5月,Hugging Face发布了区分“harness”与“scaffold”的术语表,同时承认Claude Code、Codex等工具实际上仍将整个技术栈统称为“harness”。将技术栈划分为五层仅是一种分类方式;按照更狭义的定义,上述大部分研究其实衡量的是不同的概念。请选定一套术语,在团队内部统一使用,并仔细阅读不同研究之间的对比分析。正因如此,五层分类法更适合作为自我搭建系统的检查清单——因为检查清单不会随术语变化而失效,而分类体系则容易产生冲突。在认为两个智能体输出结果存在差异之前,先弄清楚它们分别涵盖了哪些层次。
  • 随着模型性能的提升,Harness的效果似乎会逐渐减弱。Harness-Bench的报告显示,性能更强的模型后端能获得更高的平均分数,且不同Harness之间的差异也更小。某些辅助结构可能只是在支撑那些正在消失的缺陷。应将投资重点放在能长期发挥作用的成本控制上,而非那些即将推出的新模型可能免费带来的更高精度上。
  • 配套脚本

    该方案附带十个简短的脚本。每个层次都有相应的保护机制,而同一代理在没有保护机制的情况下也会运行。这些脚本可在离线环境下运行,无需API密钥,因为脚本替代了模型,并会发出固定的一系列工具调用指令。通过实际运行对比来观察差异,比单纯依赖信任更为可靠。

    git clone https://github.com/paoloap-py/agent-harness-guide
    cd agent-harness-guide
    
    python3 run_all.py        # all five layers, guard off then on, side by side
    python3 test_harness.py   # asserts every difference above, 10 checks
    

    README将这五层结构视为一个审核清单,其中有一个反复出现的问题:是代码在强制执行这一规则,还是仅靠提示语中的句子?任何无法通过指向文件路径来打勾的项,都只是你期望其被遵守的规则而已。

    实际应用效果

    即便规则本身合理,但如果放置在不恰当的位置,也几乎起不到作用。在上面的案例中,它导致了15次违规,其中11次就发生在同一个下午,还有一整天时间完全无法正常使用。只有将规则移至合适位置,它才能真正发挥作用,而规则的内容却丝毫未变。

    这就是问题的核心所在。模型是你购买的产品,而围绕它的所有要素都需要你自己构建,其中后者决定了智能体的使用成本以及它执行未经你批准操作的频率。这两者都属于工程学范畴,但只有后者是你能够自主塑造的。

    常见问题

    那么,什么是代理的框架呢? 就是围绕模型存在的所有组件:工具循环、执行前的检查、沙箱环境、会话间的数据持久化、审核机制,以及决定每轮处理内容的流程。这个循环本身只有六行代码,加上错误处理后则有二十行,其余所有内容都属于该框架。

    框架比模型更重要吗? 从成本角度来看,确实如此,因为每个任务处理所需的标记数可能相差40倍之多。但从任务能否被解决的角度来看,则并非如此。不同框架在通过率上的差异在0到8分之间,且会出现零分的情况,《Terminal-Bench》的作者们认为,对于性能而言,选择模型通常更为重要。

    哪个部分的成本最高? 在于上下文。模型在每一步都会收到一个标记,而这个标记本身就需要付费。正因如此,第五层才会显得最为昂贵,这也解释了为何将搜索任务交给子代理后,费用会发生变化但答案却依旧不变。

    权限规则算安全措施吗? 不算。框架检查仅能查看代理实际发出的调用,而shell命令、符号链接或..路径却可以在不被检测的情况下访问同一文件。真正的限制来自操作系统:初始时让进程没有任何访问权限,再根据任务需求逐步赋予相应权限。

    是自行构建框架还是直接使用现成的? 建议直接使用现成的框架并坚持使用。更换框架会清除配置文件中积累的所有知识,而且每次真正出错后新增的规则都需重新获取。

    相关阅读

  • 量化研究智能体的上下文工程:证明层级与数据存储 — WorldQuant BRAIN研究智能体如何区分规则、观测结果与信念,并将上下文分散存储在账本、事件存储、图结构及文件中。
  • Claude Code的上下文与内存机制:基于LangGraph智能体的重构 — Claude Code如何构建API调用、运行子智能体以及压缩长时会话,同时还介绍了利用检查点工具、内存文件与技能的简化版LangGraph实现方式。
  • Agent Harnesses:用于技能与参考资料的文件夹标准 —— 通过HARNESS.md文件、技能与参考资料目录、渐进式展示功能以及针对大型智能体的嵌套布局,了解如何扩展智能体的技能。
  • Pi:极简的编程智能体框架——扩展功能、技能、会话管理及安全性 —— 为何一个仅包含四种工具且系统提示简短的终端编程框架,就能胜过那些功能繁杂、涉及模型、会话树、扩展功能、技能及沙箱环境的智能体:安装、模型、会话树、扩展功能以及安全隔离。
  • 智能体技能即代码:界定范围、公开信息、编写脚本并加以测试 —— 为何SKILL.md是生产环境中的逻辑框架:仅记录流程化知识,调整触发条件,逐步加载功能,将具体任务交由脚本处理,建立评估机制并淘汰无效方案。
  • 利用AutoGen开发多智能体AI:概念、组件及学习路径 —— 对微软AutoGen框架的通俗解读:智能体、群组聊天、人工干预机制、相关工具、十步学习路径、应用场景以及该技术的发展方向。
  • 为何编码代理仍难以处理私有公司代码 — Real-SWE对基于授权的私有代码库进行了八种编码代理的测试。表现最好的代理仅能解决38.8%的问题,而五种失败类型则揭示了代理出错的原因。