深度讲述 agent 架构:模型易遗忘和发散,Harness(工具、内存管理、循环控制)保证长任务完成。涵盖开源和闭源 agent 工程。
原文发布于 harshkedia.com。全文转载于此。
一个 AI 智能体的框架是指所有围绕语言模型的部分,它将语言模型转化为一个智能体:支持它行动的循环、它与之交互的工具,以及管理其记忆的机制,使其能够继续执行。模型提供智能,框架提供可靠性。这是一篇从零开始的综述,阐述什么是框架以及它的各个部分如何使一个聪慧却容易遗忘的模型在足够长的时间内保持专注以完成真实工作,内容以开源智能体和公开的闭源智能体工程文档为基础。
给一个现代语言模型分配一个下午才能完成的工作。重构一个模块,运行测试,读取失败信息,修复它们,移到下一个文件,继续进行直到测试套件全部通过。前十分钟内它就像魔法一样。它读取代码,制定合理的计划,以良好的品味进行编辑。然后,大约在第二十步左右,它开始失神。它重新读取它已经改过的文件。它忘记了它二十分钟前做出的决定。它在三个测试仍然失败的时候宣布胜利。到第四十步,它已经在自信地绕圈子。
模型在第十步和第四十步之间没有变蠢。是其他东西坏了。理解什么坏了,以及人们为了阻止它坏掉而构建的机制,这就是本文的全部主题。这个机制有一个名称,它已经悄悄成为应用 AI 中最重要的词汇之一:框架。
一个语言模型本身只做一件事:你给它文本,它预测更多文本。它不能读取文件、运行命令或检查自己的工作。除了当前摆在它面前的词汇,它没有记忆。要把它变成一个智能体,必须有某样东西在循环中运行它,给它提供工具,反馈结果,并判断何时完成。那样东西就是框架。Mitchell Hashimoto 把这个整个概念压缩成了一个沿用至今的公式:智能体 = 模型 + 框架。
一个大语言模型智能体在循环中运行工具以实现目标。
循环是它的跳动的心脏。Anthropic 描述其形状为:收集上下文、执行行动、验证结果、重复。模型查看它知道的东西,调用一个工具(读取文件、运行测试、搜索网络),框架执行那个工具并将结果追加到对话中,模型再次查看。它一直循环,直到停止请求工具。除了模型本身之外的所有东西都是框架:循环、工具集合以及它们是如何描述的(Anthropic 称这为智能体-计算机接口,并警告你应该像对待人类 UI 一样投入精力于它)、上下文的管理方式、权限检查,以及当事情出错时的恢复。一个有用的 2026 年简写将其归纳为四个必要部分:智能体循环、工具接口、上下文管理和控制。本文中的所有内容都是这四者中的一个。
Martin Fowler 的团队提供了一个干净的方式来将框架分成两部分。有指南,它们在智能体行动前塑造它(系统提示、AGENTS.md 或 CLAUDE.md、工具描述),也有传感器,它们在智能体行动后给予反馈(linter、测试结果、类型错误)。最好的传感器不仅报告失败,它们的措辞方式使模型知道如何修复它。一个好的框架最大化智能体第一次就做对的机会,然后在人类看到之前捕获它做错的东西。

图 1 · 模型预测;框架做其他所有事情。收集上下文、行动、验证、重复,直到模型停止请求工具。
这是完整的朴素框架:一个 while 循环,调用模型,运行它请求的任何工具,将输出追加到对话中,然后再次调用模型。这对于短任务来说确实是你需要的全部,并且接近真实智能体运行的方式。问题在于'追加'这个词。每一步都使对话变得更长,而对话就是模型的全部工作记忆。在一个长任务中,这个记忆不仅会填满。它会腐烂。
朴素框架,全部代码
messages = [system_prompt, user_task]
while True:
reply = model(messages) # predict the next action
messages.append(reply)
if not reply.tool_calls: # no tool call: it thinks it is done
break
for call in reply.tool_calls:
result = run_tool(call) # read a file, run a test, search
messages.append(result) # the context only ever grows
腐烂是可以衡量的。Chroma 测试了十八个前沿模型,发现性能随着输入增长而下降,通常在达到公布的上下文限制之前,在大约 100,000 到 500,000 令牌之间下降最陡。经典的"中间丢失"结果发现,当相关事实位于上下文的中间而不是末尾时,准确率下降三十分或更多。从业者谈到窗口中间 40 到 60 百分比处的"哑区",在这里召回率悄悄崩溃。一个充满旧工具输出的记录不是中立的填充物。它是主动干扰。
然后是一个没有任何模型升级能够逃脱的算术。如果任务的每一步有百分之九十五的成功率,并且这些步骤相互依赖,十步任务大约百分之六十的时间完成,二十步任务大约三分之一的时间完成。在更现实的每步百分之九十的成功率下,二十步下降到大约百分之十二。METR 的测量给出了上限数字:今天的模型完成几乎所有人类需要几分钟的任务,几乎完成不了人类需要几小时的任务。长地平线可靠性不是一个知识问题。这是一个结构问题。
一个长任务不会因为模型变蠢而失败。它失败是因为其工作记忆充满了噪音,其小错误复合了。
甚至还有一个心理因素。Cognition 发现模型表现出他们称之为上下文焦虑的现象:当窗口填满时,模型开始匆忙,走捷径,并宣布它即将用尽空间,即使实际上没有。他们的修复几乎很有趣。他们给模型一百万令牌的窗口,但将实际使用量限制在远低于此的水平,所以它相信它有充足的跑道并停止恐慌。框架其余部分做的一切,或多或少,都是对这四个问题的响应:窗口填满、中间腐烂、错误复合、模型恐慌。所以让我们一次一个地构建修复。
如果模型不能在长任务中信任自己的记忆,把记忆放在它的脑子外面。最简单的版本是智能体写下并重新读取的待办事项列表。这就是为什么几乎每个严肃的编码智能体都有一个待办工具。Claude Code 推出了 TodoWrite,之后转向了结构化的 Task 工具集;opencode 是完全开源的,有一个 todowrite 工具,直接持久化到本地 SQLite 数据库。这个列表做两件事:它强制模型预先承诺一个计划,并且,因为智能体被提示持续咨询它,它保持目标在新鲜注意力中,而不是让它漂移到上下文腐烂的中间。
第一步:保持计划在磁盘上,而不是在模型的记忆中
write_file('TODO.md', plan) # commit to a plan up front
# many steps later, feed the plan back so it cannot drift out of attention
todo = read_file('TODO.md')
messages.append(user('Here is your plan. Update it, then continue:'))
messages.append(user(todo))
同一想法的更大版本是将文件系统视为记忆。智能体写下笔记、进度日志和决定到文件,然后稍后读回它们。Anthropic 关于长时间运行的智能体的文章描述了跨越多个会话的工作的两智能体模式:初始化程序设置环境和进度日志,每个后续会话读取日志,取得增量进展,并在用尽空间之前用描述性消息提交其工作到 git。他们甚至发现模型不太可能破坏 JSON 状态文件而不是 Markdown 文件,所以持久记忆是 JSON。交接班的心理模型是:每个会话到达时对上一个没有记忆,所以上一个必须留下好笔记。
笔记有所帮助,但实时对话仍在不断增长。因此,运行框架学会了有意识地遗忘。这就是压缩:当上下文接近容量上限时,总结对话中较早的部分,逐字保留最近几轮对话,然后基于摘要继续。opencode 的实现很值得一读,因为它是开源的。它会预留一部分空间,原样保留最近几千个 token 的对话,把其余内容序列化为由同一个模型生成的摘要,然后从“摘要+近期对话”开始下一轮。Claude Code 也会自动执行这一操作;Anthropic 的文档称,它会“在接近上下文限制时压缩对话历史,保留重要的代码和决策,同时释放空间”。
动作 2:窗口填满时进行压缩
def maybe_compact(messages, limit, keep_recent=8000):
if count_tokens(messages) < limit:
return messages # still room; do nothing
recent = tail_by_tokens(messages, keep_recent) # keep the last turns verbatim
older = messages[:-len(recent)]
summary = model(summarize_prompt(older)) # let the model compress the rest
return [system(summary)] + recent # continue from summary + recent
压缩功能强大,也略带危险,而坦诚地说,我们必须承认这一点。摘要从定义上就是有损的,真正的技巧在于如何调优,使其保留那些起支撑作用的细节(尚未解决的 bug、架构决策、为什么要这样做),同时丢弃重复的工具输出。如果处理不当,你可能恰好把唯一重要的事实总结没了。Simon Willison 讲过一个警示案例:某个智能体在一次压缩过程中丢失了最初的指令,结果删除了那个它明明只被要求审查的收件箱。有意识地遗忘是必要的。遗忘了错误的东西,则会让长期运行的智能体在悄无声息中犯下灾难性的错误。
这种危险正是压缩不能靠打开一个开关就万事大吉的原因,也是在高风险工作中必须决定哪些内容根本不允许被遗忘的原因。设想一个智能体用一百轮对话起草合同。如果它把客户的硬性要求——“赔偿责任以已支付费用为上限”——压缩成一句轻描淡写的“讨论过责任条款”,它就会起草出错误的内容,同时表现得无比自信。解决办法是:让有损的部分仅限于可丢弃的推理过程,而必须保持不变的事实则以无损方式保存,也就是完全不让关键事实进入摘要器。
因此,那些起支撑作用的事实会连同来源一起,逐字提取到一个持久化的结构化存储中(事项状态:相关方、适用法律、必备条款、禁用条款、截止日期)。压缩永远不会触碰这份存储,而智能体在起草前会重新读取它。三条规则可以保证安全。第一,在压缩之前而不是之后,将关键事实提取到持久化记忆中。第二,绝不能让模型凭自己的记忆起草关键条款;必须让它重新读取来源(检索,而非回忆)。第三,将完整对话记录保存在磁盘上,让“已压缩”意味着从窗口中裁剪,而不是删除;这样,人或一个全新的智能体始终能够审计被移除的内容。对于真正重要的工作,这项审计应由人在检查点完成。压缩管理的是模型的短期记忆,而不是存放事实来源的地方。
安全压缩:绝不总结那些必须保持不变的事实
# critical facts never go through the summarizer
facts = extract_facts(messages) # verbatim, with their source
save_json('MATTER.json', merge(load('MATTER.json'), facts))
messages = maybe_compact(messages, limit) # only the chatter gets summarized
# before drafting anything critical, re-read the source. do not trust memory.
clause = draft(read_json('MATTER.json'), retrieve(source_docs, topic))
动作 3:将子智能体用作上下文防火墙
有些工作天生就会消耗大量上下文。“弄清楚这个代码库中的身份验证是如何运作的”,可能意味着要阅读四十个文件,最后只得出三句话的结论。如果主智能体亲自完成这项工作,它的上下文就会被四十个之后再也用不到的文件填满。解决办法是整个运行框架中最优雅的一招:启动一个子智能体。子智能体在自己独立的上下文窗口中运行,完成繁杂的阅读工作,然后只向主线程返回一份压缩后的摘要。探索发生在别处,主线程保持整洁。Anthropic 指出,他们的子智能体在探索时往往会消耗数万个 token,最终只返回一两千个 token。这就是一道上下文防火墙。

图 2 · 子智能体充当上下文防火墙。它用掉一个很大的上下文进行探索,然后只交回一小份摘要。主线程永远不会看到那些杂乱内容。
动作 3:将子智能体用作上下文防火墙
def spawn_subagent(question):
# a fresh context window the main thread never sees
sub = [subagent_prompt, user(question)]
answer = run_agent_loop(sub) # burns its own context exploring
return summarize(answer, max_tokens=1000) # hand back only a small summary
# the main thread stays lean
findings = spawn_subagent('how does auth work across this codebase?')
messages.append(user('findings: ' + findings))
这也是多智能体的炒作遭遇现实边界的地方,而坦诚的答案需要具体分析。阅读非常适合并行化:同时派出十个子智能体研究十件事,每个都有自己的上下文,最后合并它们的摘要。Anthropic 报告称,在他们的内部评测中,多智能体研究系统的表现比单智能体高出约 90%,但 token 成本约为后者的 15 倍。写作则恰恰相反。Cognition 在《Don't Build Multi-Agents》中直言不讳地指出,当两个智能体并行编辑时,双方都会做出一些彼此无法看到的隐含决策,最终各部分无法契合。他们提出的规则是“单一写作者原则”:由一个智能体负责写作并掌握完整上下文;子智能体是侦察员,而不是共同作者。真正有效的模式是:由单一主线程派遣只读的子智能体,并始终把笔握在自己手中。
动作 4:设置验证关卡,因为“完成”必须自证
即使测试仍然一片红,模型也会愉快地告诉你它已经完成。它是一个文本预测器,而“任务已完成”是一句出现概率很高的话。因此,运行框架不能轻信“完成”。“完成”是一项声明,而运行框架的职责是要求它拿出证据。成本最低的证据,是设置一个在允许完成之前运行的验证器:运行测试套件,运行类型检查器;如果失败,就把失败结果作为一个新问题反馈到循环中,而不是接受已经完成的声明。更强的版本也是 Anthropic 所采用的做法:使用一个拥有全新上下文的评估智能体。它是一个独立的只读智能体,其上下文从未见过构建过程,因此能够在不受构建者动机性推理影响的情况下判断结果。
动作 4:让“完成”自证
def is_done(project):
# 'done' is a claim. demand proof before believing it.
if not run_tests(project).passed: # a deterministic sensor
return False, 'tests are red'
check = fresh_agent(project.diff) # a read-only judge that never saw the build
return check.ok, check.reason
done, why = is_done(project)
if not done:
messages.append(user('not done: ' + why + '. keep going.'))
完成的另一半,是能够从崩溃中恢复,而这正是一个真正的开源运行框架作为示例展现其价值之处。opencode 会在任何副作用发生之前,将每个步骤写入持久化的 SQLite 事件日志;它会在每一步之前为文件系统创建快照,使任何更改都能撤销;重新启动时,它还会扫描那些执行到一半的工具调用,将其标记为失败,以便循环能够恢复。这正是持久化执行领域的人们(Temporal、Restate、Inngest 等)不断强调的区别:保存检查点不等于保证完成。检查点表达的是“这是你当时所在的位置”。持久化执行表达的是“即使中途崩溃,这项任务也会一直运行到结束”。Dex Horthy 的 12-Factor Agents 从另一个角度说明了同一点:那些能在生产环境中存活下来的智能体,大多只是工程质量良好的普通软件,仅在决策点中加入了模型,而不是什么自主运行的魔法。
动作 5:依据验收标准复述目标
到目前为止的一切都让智能体在长任务中保持活跃。最后一步要确保它始终指向正确的目标,因为长时间运行的智能体会出现漂移。运行到第四十步时,它忙着优化某个三个弯路前自己发明的子问题,并悄悄地失去了关键的线索。漂移的根本原因与衰退相同:真实的目标在最开始陈述过一次,现在却被腐烂的上下文中间埋没了,而最近的每一轮都在讨论一个题外话。每个修复都是一个想法的变体:让目标的声音压过噪声。
在工作开始之前,把目标和完成定义写下来,作为持久的、可检查的准则,而不是凭感觉:必须包含条款 X 和 Y,必须不包含 Z,当测试通过且评审智能体签字时即为完成。然后重新注入。Manus 称之为"诵读"——每隔几步重新陈述目标和计划,使它们位于上下文的新鲜边缘,而不是已经死掉的中间部分。在关键节点根据这些准则检查进度,这样越轨的工作被捕捉到时只浪费了一步,而不是三十步。还要关注轨迹本身:如果一个智能体开始重复阅读同一个文件、重复操作或持续颠簸,这是在实时展示漂移,正确的做法是停止并重新定位它,或重置上下文,而不是让它继续螺旋式下降。对于任何高风险的工作,检查点应该是人为的,智能体在每个门点提议而不是径直冲向终点。
举措 5:诵读目标,根据准则检查
goal = load_goal() # {objective, must_include, must_not_include, done_when}
# 每隔几步诵读一遍目标,使它停留在上下文的边缘
if step % 5 == 0:
messages.append(user('Recite the goal and acceptance criteria, then continue:'))
messages.append(user(render(goal)))
# 在关键节点检查准则,不只是最后才检查
unmet = check(current_draft, goal.done_when)
if unmet:
messages.append(user('Off track. Still unmet: ' + unmet))
这些都不是什么奇特的东西。这是一个项目经理的工作,用代码写出来了:设定规范、根据规范跟踪、在关键点评审、有异常时升级。这是长期地平线智能体的不舒服的教训。框架必须扮演项目经理的角色,因为模型在只拥有一个大目标和不断增长的上下文的情况下,会自信地徘徊并永远注意不到。
转折:最好的框架几乎不存在
经过所有这些机制之后,结论是反直觉的:最好的框架是侵略性地简单。用一个专门智能体的复杂图来包装模型的本能几乎总是会让事情变糟,因为每一层都是另一个需要调试的东西,也是另一种上下文碎裂的方式。构建这些工具的人直言不讳。创建 Claude Code 的 Boris Cherny 这样描述它:几乎与聪明的相反。
所有的秘密调料,都在模型里。而这是对模型最薄的可能的包装。
— Boris Cherny,谈构建 Claude Code
简单性也体现在工具中。给一个智能体一小把锐利、通用的工具(读、编辑、运行 shell 命令、搜索),它就会将它们组合成任何东西。给它一百个专属工具,性能就会下降,因为它会把注意力花在选择它半懂不懂的工具之间。而且有一个更深层的理由不要过度构建:框架有保质期。你添加的每一个手工编码的变通方案都存在是为了掩盖模型还不能可靠完成的东西,而模型在不断进步。看过这种情况发生的工程师主张你应该把框架当作一个九十天的制品,随着模型逐渐胜任工作而移除结构。为你现有的能力水平添加脚手架,然后把它撤下来,因为昨天的脚手架会成为明天的瓶颈。
这种变动在工具本身中是可见的。到 2026 年中期,更尖锐的问题已经悄悄地从"你运行哪个模型"转向"你运行哪个框架",一个新的类别出现在它们之上:元框架,让你在保持上下文和底层策略相同的情况下,在会话中途交换一个引擎。框架从管道变成了你要选择的东西。
没有人能衡量的部分
这提出了我无法停止思考的不舒服的问题。这篇文章中的每一个举措都是一个设计决策。你如何知道它们中的任何一个是否真的有帮助?当然,你可以通过在基准测试中运行智能体来衡量它。除了基准测试分数不是模型的一个属性。它是整个系统的属性,包括框架。拿一个固定的模型,在相同的任务上在九个不同的框架内运行它,分数会波动超过二十点,和相邻模型代之间的差距一样大。
一个编码智能体是一个系统框架,是模型、框架、上下文、环境和反馈信号的复合,其中任何一个都可以移动基准测试分数,幅度相当于相邻模型代之间的幅度。
— 《编码基准与智能体软件工程不一致》(2026)
所以大多数框架工程都是半盲目地进行的。你改变了压缩策略,分数波动了,你真的无法说你是改进了框架还是只是从一个嘈杂的仪器中获得了幸运的抽样。如果这听起来很熟悉,它应该是:这和相信一个你从未验证过的评分者的评估分数是同一个问题。框架是评估者的孪生,另一个无测量的仪器,无声地决定你的结果。我在这里不是中立的观察者,因为诚实地衡量这些系统是我选择从事的问题,但这一点本身成立:你无法改进你无法衡量的框架,而几乎没有人衡量他们的。
那么,框架是什么
它是把一个聪慧、健忘、偶尔过度自信的文本预测器指向一个目标足够长时间以到达它的机器。一个让它行动的循环。它行动的工具。一个笔记本使它不会忘记。压缩使它能忘记正确的事物。子智能体使混乱的工作发生在别处。验证使"完成"意味着完成。以及足够的克制来保持所有这一切都薄,因为模型正在爬向它需要更少你帮助的日子,而不是更多。模型提供思考。框架提供记忆、纪律和第二次机会。
模型思考。框架记忆、检查工作,并拒绝让它提前退出。
Anthropic,《构建高效智能体》(2024)。规范的工作流对智能体的区分和智能体-计算机接口思想。
Anthropic,《AI 智能体的有效上下文工程》(2025)。压缩、笔记记录和子智能体上下文策略。
Anthropic,《我们如何构建多智能体研究系统》(2025)。读取并列化,