斯坦福 CS329Z 智能体工程笔记(二)
把四种方法并排看,它们分别把学习重点放在不同位置。以下是四种方法的分工对比。
-
ToolFormer把调用表示成文本,通过自监督损失筛选事例,在微调整个语言模型,实验主要围绕五类工具。
-
ToolToken把每个工具变成新增token,只学习工具输出向量,调用时机与工具选择由这个向量参与决定,参数补全主要交给上下文示范,知识库实验扩展到234种关系工具。
-
ToolLLM使用ChatGPT构造带思考、行动和返回的多步轨迹,微调LLaMA并配合检索,在16000多个API中完成组合调用。
-
Gorilla则用GPT-4生成的指令数据与检索文档训练更准确的API选择和调用代码,目录约1600个接口。
表中“学会核时”和“学会什么”的划分是帮助理解,不是完全隔离的模块。例如ToolFormer的损失过滤只是生成监督,最终仍靠微调形成行为;ToolToken的向量也参与选择具体工具,并非只决定是否调用。比较这些方案时要同时问:调用如何表示、训练哪些参数、监督从哪里来、工具如何检索,以及执行和验证由谁负责。
智能体工具使用:06:接口层REST、函数调用与MCP 🔌
上一节我们比较了多种工具学习方法,本节中我们转向系统集成。模型和应用不断变化时,怎样让工具方便连接,这就需要标准接口。之後再用评测判断连接起来的系统是否可靠。
智能体工具使用:06-1:两种集成思路与三层接口 🧱
左边的思路是改变模型,让它适应工具,通过ToolFormer、ToolToken、Gorilla这样的研究,学习调用形式、参数和使用时机。右边的思路则是统一工具的描述,让模型在运行时读到名称、功能和参数结构,再决定怎样使用。这样接入一个新服务时,就有机会主要修改工具描述和适配层,而不是专门为它重训模型。不过,图中“每个新API都要在训练”是一种强调集成成本的概括,不能当成绝对结论。前面已经看到模型可以结合文档和检索,泛化到没有见过的接口。同样,提供一个schema或接入MCP也不会让任意模型突然获得可靠的工具使用能力。训练提供理解和决策基础,标准接口降低连接成本,两者可以互相配合。
课件把接口演进分成三个层次。REST API提供网络端点和服务文档,调用者要知道HTTP方法、参数以及认证方式。Function Calling再把工具包装成模型容易读取的JSON schema,模型输出工具名称和参数,由宿主程序验证并分派执行。MCP则把工具发现与调用组织成共享协议,让服务端的能力能够被不同宿主复用,并且除了工具,还可以暴露资源和提示模板。
图中把REST标成“没有机器可读合同”,说的是只提供零散端点和人工文档的集成方式。REST完全可以搭配OpenAPI提供机器可读说明。后面的例子正会用到。三者也不是互斥替代:模型可能用函数调用形式表达意图,宿主通过MCP发起请求,MCP服务端最终再访问REST API。理解各自负责哪一层,比把它们当成三个竞争名词更有用。
智能体工具使用:06-2:天气REST例子与函数调用合同 🌦️
天气例子展示了最直接的网络调用。请求用GET访问当前天气端点,并在查询参数里指定城市Springfield。服务器返回状态码200,以及包含temp_F和conditions的JSON。这里的74是华氏温度,sunny表示晴天。图下方用读取、创建、更新、删除帮助记忆GET、POST、PUT、DELETE的常见用途,但具体业务语义仍然要看接口约定。对模型来说,困难不只是生成一个看起来像URL的字符串,而是正确知道地址、参数名、单位和响应结构。如果把华氏温度当成摄氏温度,即使网络请求成功,答案仍然会错。工具包装的价值之一,就是把这些容易混淆的细节封装成清楚稳定的接口。
左边的schema描述get_weather功能是查询当前天气,参数是一个对象,其中city必须是字符串,而且被列在required里。右边则是模型为这次请求提出的调用,选择同一个工具,并把city填成Springfield。请把这两件事分开:schema定义允许怎样调用,tool_call描述本轮想调用什么。模型写出这段JSON时,天气查询还没有实际发生。宿主必须解析调用,检查参数,找到对应实现,执行之后再把结果交给模型,模型才有依据回答用户。图中提到跨API需要重写,主要是在提醒不同平台的声明格式、消息包装和调用机制可能需要适配,业务逻辑并不一定全部重写。可以保留共同的天气查询实现,只转换外层合同。格式正确也只是起点,程序是否只读、是否有权限,仍需要程序和业务规则检查。
{
"name": "get_weather",
"arguments": {
"city": "Springfield",
"unit": "imperial"
}
}
智能体工具使用:06-3:MCP的Host、Client与Server 🧩
MCP的全称是Model Context Protocol。图左边是聊天应用、代码编辑器和其他AI应用,右边是数据库、文件系统、开发工具以及办公服务,中间用统一协议连接。箭头是双向的,应用可以发现能力、发起请求,服务再返回数据或执行结果。如果每个应用都为每个外部系统写一套独有接口,集成组合会越来越多。共享协议让一个服务端有机会被多个宿主使用。图中的具体产品是课件举例,真正需要记住的是这种连接关系。协议统一了沟通方式,并没有替代工具自身的业务逻辑,也没有替代宿主对权限、上下文和模型行为的管理。我们仍然要把工具设计得容易理解,并验证模型是否能够正确使用。
再把中间这一层拆开看。Host是用户实际使用的AI应用,它负责模型交互、上下文组织和工具调用流程。Client是宿主内部负责MCP协议通信的组件,可以把它理解为一条会话连接的管理者。Server则负责暴露工具或其他能力,并在收到请求后执行相应逻辑。图中一个Host包含4个Client,有的连接本地文件服务,有的连接本地数据库,另外两个连接远程服务。这里的Client不是另一个大模型,Server也不必是一台独占机器,这些名称描述的是协议角色。模型可能提出查询数据库的调用意图,宿主决定怎样处理,再通过相应Client发送给Server。这样就能清楚地区分模型决策、应用编排和外部能力三个部分。
发现工具相当于先问服务端:“你有哪些能力”。左上角的请求使用tools/list方法,ID用来把响应与这次请求对应起来。返回的tools数组包含天气工具和计算工具。天气工具有程序调用名称weather_current,也有面向人的标题和功能描述。input_schema说明输入是一个对象,location是字符串,并且必须提供,unit只能取metric或imperial。宿主拿到这些描述之后,才能把合适的能力介绍给模型。这里也要识别教学代码的简化:图中响应省略了完整JSON-RPC包装,并额外画出了TTM,不能把这段截图直接当成标准报文模板,应当以所实现的协议版本为准。对这段流程最关键的理解是:工具集合和参数合同可以在运行时发现,而不是完全写死在每个应用的代码里。发现了工具,也不代表已经调用了工具。
{
"method": "tools/list",
"id": 1
}
发现之后,宿主就可以通过tools/call发起一次具体执行。请求里的name选择weather_current,arguments指定地点Springfield,并选择imperial,也就是英制单位。服务端收到请求后,先按照工具合同处理参数,再执行天气查询。图中响应的content数组包含一个文本块,给出华氏68度、局部多云以及风向风速。它是这次工具执行的观察结果。接下来由宿主交还给模型,再组织成用户能够理解的回答。这个闭环里有三个不同产物:模型提出的调用、服务端真实返回的结果,以及模型最终写给用户的话。不能因为调用描述格式正确,就假定工具一定成功;同样也不能在没有拿到结果时,替外部服务编造一个答案。
{
"method": "tools/call",
"params": {
"name": "weather_current",
"arguments": {
"location": "Springfield",
"unit": "imperial"
}
}
}
智能体工具使用:06-4:课程管理系统与认证边界 🔐
课程管理系统的例子把这些层串了起来。左边已有3个REST路由:读取作业、读取提交记录、写入成绩。右边希望把它们暴露为list_assignments、get_submissions和post_grade。代码先建立一个指向课程系统的异步HTTP客户端,再读取OpenAPI,最后把这份说明和客户端交给fastmcp的from_openapi方法。OpenAPI提供端点、参数和返回结构,HTTP客户端负责真正访问源服务,MCP包装层负责对外暴露能力。示例里的工具名称不是单凭路径就能唯一推出,实际命名还取决于OpenAPI的operation ID和映射配置。这样的自动转换适合建立起点,之后仍要检查描述是否清楚、哪些端点应该开放。尤其是post_grade会写入成绩,和只读查询不同,不能因为能自动生成工具,就默认所有调用者都应该拥有这项权限。
同一个MCP服务同时面对两个认证方向。向上看,Agent调用课程MCP服务,这是入站请求,代码用AGENT_TOKEN判断谁可以进来,并把示例调用者标记为Agent。向下看,MCP服务访问课程系统,这是出站请求,HTTP客户端在Authorization请求头里使用另一个LMS_TOKEN。两个凭证保护的对象不同,有权访问MCP并不自动意味着有权修改所有课程数据,能访问课程系统的下游凭证也不应该交给模型自由拼进参数。图里的做法把令牌保存在服务端环境配置中,而不是放入模型上下文。它使用静态令牌校验来说明边界,并没有展示完整OAuth授权流程。采用OAuth时,面向MCP的访问令牌和面向下游API的令牌也应分别面向自己的资源,不能简单透传混用。最后一行启动HTTP服务,但这段教学代码还没有完整展示权限范围、用户身份映射和业务授权检查。最应记住的是:认证和凭证管理由宿主与服务端承担,模型负责提出业务调用意图。
智能体工具使用:07:BFCL、ToolBench与τ-bench评估 📏
上一节我们说明了标准接口怎样连接系统,本节中还要判断连接之后做得怎样。一个Agent会写出合法调用,不代表能完成复杂任务。我们用三类评测逐步扩大观察范围:调用本身、多步工具链,以及工具、Agent和用户共同参与的完整交互。
智能体工具使用:07-1:BFCL与AST检查 🧪
BFCL是伯克利函数调用评测体系。图中展示的是2025年11月的排行榜快照,并非实时排名。表格除了总体准确率,还分别列出网络搜索、记忆、多轮调用以及单轮的不同测试级,所以不能只盯住最左边的总分。比如图中首行总体准确率约为77.5%,但它的多轮得分和单轮得分并不相同,说明工具使用不是一个单一能力。成本列还呈现了另一种取舍,不同系统完成评测的费用可以相差很大,而这个数字也不是某一次普通用户查询的统一价格。模型名称后面标注的FC或prompt表示使用的调用方式也可能不同。阅读这样的表应该先确认测试版本、任务类型、提示和预算,再判断哪一组结果与自己的应用更接近。
BFCL不只是比较模型输出的字符串是否相同。第一种方法是基于抽象语法树,也就是AST离线检查函数名和参数结构,这样可以忽略一部分格式差异,但它仍然主要判断调用描述,不能替代真实执行。第二种方法实际执行函数,观察返回结果。第三种方法检查多步多轮交互之后的状态,例如数据库是否变成任务要求的样子,而不是只看最后一句话是否声称完成。数据来自真实应用场景,并区分不同调用类型,覆盖Python、C++、Java、SQL等不同表达形式。
还有一个经常被忽略的能力:当提供的函数都不适合用户问题时,模型应该识别不相关,而不是为了调用工具而强行选一个。于是评估既要检查该调用时能否调用对,也要检查不该调用时能否停下来。
三个小例子把任务类型分开了。用户问2加3,同时提供add和mul,模型应选择加法,并填入2和3。这里的multiple functions值表示可选择的函数有多个,不代表必须把它们全部调用。用户再问2加3与4加5,则需要两次add调用,两组输入互不依赖,因此具备并行执行的条件。第三个问题是2乘3,而候选只有加法。图中的期望行为是识别没有直接匹配的乘法工具,不能虚构一个mul,或把加法结果冒充乘法结果。这个事例考察给定任务约束下的相关性检测,并不是说数学上不能通过重复加法计算乘积。右侧的分类图进一步说明完整基准还覆盖单轮、多轮、缺少参数、缺少函数、长上下文以及搜索和记忆等Agent能力,不同错误落在不同类别里,综合分相近的模型也可能有完全不同的短板。
智能体工具使用:07-2:ToolBench的数据与任务层次 🗂️
ToolBench把视野扩大到大量真实接口。这里按源论文的统计口径理解:筛选后保留16464个API,来自3451个工具,覆盖49个类别。图左侧先从RapidAPI收集接口和文档,再生成需要这些接口的用户指令。随后标注能够解决问题的调用路径,得到的数据既可以用于训练工具检索器,也可以对LLaMA做监督微调,形成ToolLLaMA。右侧是推理过程:用户提出任务,API检索器先选出相关接口,模型结合这些描述,经过多轮调用和真实返回结果,逐步形成答案。最后由ToolEval评估任务是否完成,以及不同解题路径的质量。这里同时包含数据构建、模型训练和系统评估,不只是一个静态题库。它关注的是:面对大量候选工具时,能否找对接口,并把多步工作真正串起来。
左边的层次结构说明,一个工具不等于一个API。平台先有金融、电影、招聘等类别,也可以有二维码接口这样的专题集合。电影类别里有影视可用性查询、电影搜索等工具,每个工具内部又包含查询服务、按片名搜索、读取类型等端点。文档不仅写功能,还写必填参数、可选参数、代码示例和响应形状。图中的按片名搜索需要片名和国家,还可以指定节目类型、输出语言等选项。右边利用这些文档构造三种指令:只使用一个工具,在同一类别中组合多个工具,以及在同一专题集合中组合多个工具。这样比从整个工具库随意拼几个毫不相关的接口,更容易生成自然任务。比如为“山间电影之夜”提出建议,可能需要组合影视搜索和其他相关服务。训练样本不仅包含用户问题,还要标明相关API,并进一步寻找能实际完成任务的执行路径。
智能体工具使用:07-3:τ-bench、用户交互与pass^k 🛫
τ-bench加入动态的用户交互。左侧的Agent能访问预定、取消和查询等工具,同时必须遵守领域政策。下面的模拟用户则有自己的需求和行为说明,这些隐藏设定并不等于Agent直接知道用户的一切。右侧的航空事例中,用户想改航班,Agent先查询预定,发现是基础经济舱。按照这个模拟环境的规则,基础经济舱不能改签,但如果仍在预定后24小时内可以取消。于是Agent解释限制,提出取消后重订的方案,并询问用户。用户接着说“那就只取消”,Agent应当更新对需求的理解,只执行取消,而不是继续按照先前设想擅自重订。最后,数据库状态变为canceled,提供了可验证结果。这些是基准里的业务规则事例,不是现实航空公司的统一政策。评测的难点在于同时处理工具状态、政策约束和用户新信息,只生成正确函数名远远不够。
为了衡量稳定性,τ-bench强调pass^k,意思是抽到的K次试验全部成功。它不同于pass@k,后者只要求至少一次成功。看公式,某个任务一共运行N次,其中成功C次。从这N条结果里不放回选K条,一共有$\binom{N}{K}$种组合,全部成功的组合数是$\binom{C}{K}$,所以:
$$
\mathrm{pass}^k=\frac{\binom{C}{K}}{\binom{N}{K}}
$$
而至少成功一次的概率是:
$$
\mathrm{pass@}k=1-\frac{\binom{N-C}{K}}{\binom{N}{K}}
$$
最后分别对任务求平均。比如10次试验成功8次,选两次,全成功是28除以45约62%,至少一次成功是44除以45约98%。这说明“总能挑到一个成功案例”和“重复使用都可靠”差别很大。这里的重复试验应在相同任务初始条件下评估,不是对同一笔订单连续执行写入。下方零售环境有500用户、50产品、1000订单、7个写工具和8个非写工具、115个任务;航空环境有500用户、300航班、2000预定、6个写工具和7个非写工具、50个任务。环境规模不同,但都能通过可验证状态检查是否完成。
这组早期实验结果正好展示两个指标为何不能混用。左侧表格里,GPT-4o在零售任务的一次成功率是61.2%,在航空任务是35.2%,平均48.2%。这个平均是两个领域等权,而不是按照115和50个任务数加权,所以不能直接当成混合任务级的总体成功比例。其他模型也显示,单轮看似具备工具能力,进入政策与用户交互场景后仍会失败。右上角比较函数调用、ReAct和只行动等方法,在这组零售实验里,函数调用方式整体更好,但结论受当时模型和评测配置限制。右下角最关键:虚线的pass^k随尝试次数增加而上升,因为找到一次成功越来越容易;实现的pass^k反而下降,因为要求所有抽到的试验都成功越来越严格。因此,评估面向真实用户的服务,不能只展示最好的那一次轨迹,还要测重复运行时的稳定性。
课件中的时间线说明,评测本身也在扩展。2024年的原始τ-bench关注零售和航空,让Agent与模拟用户对话,并通过数据库结果和重复可靠性来评分。2025年的τ²加入双控制场景,用户也能操作环境,有些步骤只能由用户完成,Agent必须指导与协作,而不是代替用户做一切,并扩展到电信领域。图中2026年的记录又包括对50多个任务进行审计修复,纠正含糊指令、错误期望和不可能满足的条件。知识版本把检索与政策推理结合,使用约700份文档,并加入银行场景。语音版本则把同类任务放进有打断、口音和背景噪声的全双工对话。这些是课件采用的版本快照。它们共同说明,可靠工具使用不仅取决于模型能否调用函数,也取决于信息获取、人与系统协作,以及评测任务本身是否定义清楚。
智能体工具使用:08:失败分类与课程总结 🧭
上一节我们介绍了评估体系,本节中我们把失败拆成8类,排查时就更有针对性。单次调用有三类:选择错误,选错工具或本来不需要工具却调用;参数错误,编造字段、填错类型或单位;幻觉工具,调用一个根本不存在的API。多步链条有两类:顺序错误,跳过前置查询就执行后续写入;恢复错误,面对失败只重复同一个请求,不检查原因、不退避或调整方案。对话层还有三类:政策错误,业务规则不允许却仍然行动;交互错误,本应向用户澄清的关键条件被模型自行假设;非确定性,同一个任务这次通过,在运行却失败,所以需要刚才的可靠性指标。
比如健康管家安排预约,选错查询工具、把日期写错、没有确认意图就修改预约,分别属于不同层面的问题,修复方法也不同。结构校验能够拦住部分参数错误,却不能自动判断用户真实意图。重试机制能处理部分临时故障,却可能把错误写入重复执行。把失败归类,才能找到应该改模型、工具合同、执行流程还是交互策略。
现在可以把工具使用这条主线完整接起来。工具让模型获得训练中没有包含的最新信息,也让它触及参数之外的计算和真实系统。ToolFormer展示了怎样借助模型生成与筛选训练样本,学习调用什么、何时调用、怎样填参数;ToolToken探索只训练工具嵌入的路线;ToolLLM则结合工具检索与多步调用轨迹扩大任务范围;Gorilla把范围扩大到约1600个API,并利用文档和结构匹配研究调用正确性与幻觉。到了工程集成,REST提供服务访问,Function Calling提供模型可生成的调用合同,MCP再提供可发现、可复用的连接协议。接通之后,还要用BFCL、ToolBench和τ-bench从调用结构、多步执行,一直检查到用户交互与重复可靠性。最后,遇到失败,不要只说模型不够聪明,而要定位是选错工具、参数有误、顺序不对,还是政策和交互出了问题。可靠的Agent来自模型能力、工具设计和运行时流程共同配合。完成一次调用只是起点,持续正确地完成用户任务才是目标。
本节课中我们一起学习了从RAG局限到工具定义、ToolFormer与ToolToken的学习方法、ToolLLM和Gorilla的大规模工具选择、REST与Function Calling及MCP接口集成,以及BFCL、ToolBench和τ-bench的评估方法。核心结论是:工具扩展了模型能力,但工具调用必须经过真实执行、参数校验、权限控制和可靠性评估,不能把“写出调用”误当成“完成任务”。
斯坦福 CS329Z 智能体工程:05:框架与编排总览
在本节课中,我们将学习斯坦福 CS329Z 人工智能智能体工程第五讲“框架与编排”的核心内容。我们要解决一个很实际的问题:当语言模型需要调用工具、经历多个步骤,并且要稳定完成任务时,怎样把这些能力组织成可开发、可评估、可维护的系统。接下来会从框架提供的抽象出发,逐步讲到工作流、智能体运行框架,以及可靠性和可观测性。为了让初学者也能看懂,下面会用更直白的方式解释概念,并用公式、代码和示例把关键结构串起来。
CS329Z智能体工程:01.1:课程公告与示例仓库 📌
这份课件由 dieonmichaelryan 和张样署名。开始前保留课程当时的几项公告。项目提案需要按小组在 gre book 提交,避免成员各自重复上传。同学还需要申请 Google 额度,并确认已经获得 lo 提供的 OpenRouter 额度。课件列出的提案截止时间是 10 月 9 日晚 11 点 59 分。第一次作业是 10 月 30 日同一时间,并提醒尽早动手。这些是原课程的安排,观看视频时,应按自己实际参加的课程通知执行。
下面先列出课程示例仓库的操作顺序,方便跟着代码练习。
-
使用 git clone 下载 CS329Z 组织下的 lecture5-frameworks 仓库。
-
进入项目目录。
-
运行 setup 脚本,完成课程环境准备。
后续演示基于这套材料。课件估计,当时完整运行讲一遍,基本会消耗大约 1 美元或更少的 OpenRouter 额度。实际支出取决于所用模型和调用量。学习原理时,也可以先沿着代码示例理解每一步的数据流。
CS329Z智能体工程:01.2:从 PyTorch 的分工边界理解框架 🧩
先想一个小问题:你用过 PyTorch 吗?即使没有,也可以带着这个问题往下看。训练一个神经网络时,我们愿意亲自写出哪些步骤,又希望工具替我们处理哪些步骤。框架的价值就藏在这条分工边界里。
左右两段代码做的是同一件事:拟合预测值等于权重乘输入再加偏置,并使用均方误差。左边先求预测误差,把它乘 2,再除以样本数量,得到损失对预测值的导数。接着按照链式法则,权重的梯度是这些导数乘输入后求和,偏置的梯度则直接求和,最后分别减去学习率乘梯度。右边仍然保留前向计算,但把求导交给 loss.backward,把参数更新交给优化器的 step。开头的 zero_grad 用于清除已有梯度。数学过程没有消失,只是被封装成稳定的接口。
可以用公式把这件事写清楚:
[
\hat{y}=wx+b,\qquad L=\frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i-y_i)^2
]
手写训练循环的核心步骤可以概括为下面的伪代码:
# 手写版本的核心思路
pred = w * x + b
loss = ((pred - y) ** 2).mean()
grad_w = (2 * (pred - y) * x).sum()
grad_b = (2 * (pred - y)).sum()
w -= lr * grad_w
b -= lr * grad_b
智能体框架也采取类似思路,把常见的模型调用、状态流转和执行规则封装起来,让我们能够在更高一层组织系统。
CS329Z智能体工程:01.3:学习目标与课程路线 🎯
这里有一个需要把握的学习目标:框架会隐藏一部分底层细节,而工程师仍然需要理解这些细节。就像使用自动求导时,也要知道梯度为什么会被累积、参数什么时候更新;使用智能体框架时,也要知道消息怎样传递、工具何时执行、失败后从哪里继续。课程其他部分会深入这些系统设计。今天先学习抽象本身,弄清每一种接口替我们承担了什么工作,又留下了哪些决策需要我们自己完成。
学完之后,希望你能够完成三件事。下面先列出这三项学习目标。
-
识别智能体工程中常见框架分别处在哪一层,避免只记住一串名字。
-
把框架里的术语对应回已经讨论过的系统设计,例如一个模块如何封装模型调用,一个图如何描述执行流程。
-
评估工具对编排和可靠性到底有没有帮助。衡量时要回到自己的任务:它能否表达所需流程,能否支持调试,以及能否让整个系统更容易稳定运行。
讲解按 5 个主题展开。下面先列出课程路线,括号里的分钟数是原课堂的时间安排。
-
先建立智能体工程的全局视角。
-
再进入工作流和程序框架,理解如何组合模型能力。
-
之后讨论开源智能体运行框架,也就是模型外面的执行支撑。
-
接着讨论编排与遥测,观察任务如何被调度,如何留下可分析的运行记录。
-
最后收束到最佳实践和工程挑战。
我们会围绕这些主题,把框架的接口和它所解决的问题连起来。
CS329Z智能体工程:02:工程化智能体的全局地图 🗺️
在挑选框架之前,我们先回答更基础的问题:什么叫工程化一个智能体。让模型偶尔完成一次任务,这是起点。工程化还要明确任务输入、组织执行过程,并且建立判断结果好坏的方法。下面用三个支柱把这些工作放到同一张图里。
从左到右看,蓝色是数据,包括训练数据、智能体运行轨迹、示范和人类反馈。运行轨迹记录一次任务中的行动与观察,可以帮助我们分析系统怎样成功、怎样失败。绿色是系统或模型,它可能是单个语言模型,也可能是由多个组件组成的工作流,或者能够动态选择行动的智能体。红色是评估与指标,包括任务通过率、答案精确匹配、语言模型评分以及成本和延迟。三个支柱分别回答:依据什么学习或改进,用什么结构执行任务,以及怎样知道改进是否有效。只看最后一句回答是否流畅,还不足以描述整个系统的质量。
可以用一个简化的公式表示这种工程视角:
[
\text{工程化智能体}=\text{数据}\times\text{系统结构}\times\text{评估指标}
]
CS329Z智能体工程:02.1:系统只是中间支柱 🧱
现在左右两侧淡下来,绿色区域被单独突出。日常讨论人工智能系统时,我们很容易把注意力集中在这里:用了什么模型、加入了哪些工具、流程看起来有多复杂。这些当然重要,但这张图刻意提醒我们,系统结构只是中间的一根支柱。即使结构完全相同,输入数据和评价方式不同,最终得到的能力判断也可能截然不同。
当三种颜色重新一起出现,视角就扩展到了人工智能系统工程。举个帮助理解的例子:一个检索问答系统总是答错某类问题,我们需要从数据中收集失败样本,分析是检索没找到证据,还是模型没正确使用证据,然后修改对应组件,再用评估集检验效果。这里数据提供改进依据,系统承担修改,指标判断修改是否值得保留。也正因为这三者相互配合,选择框架时才不能只看代码写起来是否简短。
CS329Z智能体工程:02.2:课程地图与框架版图 🧭
课程安排也沿着这个结构展开。绿色部分集中在系统和模型,包括智能体概念、面向开发者的语言模型、检索增强生成、工具调用、框架编排、设计模式、记忆架构、多智能体和优化。蓝色部分讨论智能体需要什么数据,以及如何选择数据、保证质量。红色部分讨论评估基础、基准设计、语言模型评委和评估基础设施。粉色则扩展到安全与护栏、编程智能体、主动式智能体以及前沿开放问题。我们现在讨论的框架与编排位于绿色区域,但它会持续用到蓝色的数据和红色的评估。
理解框架版图,关键是看最上方的箭头:控制流程的决策权逐渐有多少交给模型。左端是模型调用,作为更大系统中的一个组件。图中列出 OpenAI 和 Anthropic 的接口以及 LiteLLM。中间是工作流或复合人工智能系统,执行逻辑主要写在程序里,课件用 DSPy 和 LangGraph 作为代表。右端是智能体,模型更多参与决定下一步行动,图中列出 SWE-agent 和 OpenHands。
可以用一个简单例子区分:固定执行检索再回答,是程序预先安排的流程;让模型根据检索结果决定是否继续查找、调用哪个工具,则增加了模型对流程的控制。这个横轴表达的是设计方式,并不是越往右就一定越好。
CS329Z智能体工程:02.3:可靠性与可观测性 🛠️
除了控制流从左到右的分工,还要看顶部两项横向能力。可靠性关注任务遇到中断或失败后,能否按预定规则继续执行,课件把 Temporal 放在这里。可观测性关注运行过程是否看得清:调用了什么模型、传入什么内容、经过哪些步骤、时间和成本花在哪里,图中举出 Langfuse 和 MFlow。它们服务于下方多种系统形态。无论使用固定工作流还是更自主的智能体,都需要知道任务是否可靠完成,以及出现问题时能够从记录中找到原因。
红色边框进一步画出了后面的三个技术部分:中间这一块先讲工作流和程序框架,右边再讲智能体运行框架,最后回到上方的可靠性与可观测性工具。这样的顺序有一个好处:先理解任务怎样表达和执行,再理解运行中的任务怎样得到保障、怎样被观察。框架的名字会变化,而这几类工程职责可以作为我们阅读新工具时的参照。
CS329Z智能体工程:02.4:工具使用线索与版图小结 🔍
这些工具有没有实际使用者?课件提供了两类线索。左侧是制作课件时的代码仓库收藏数:DSPy 约 38500,LangGraph 约 42800,Temporal 约 23500。右侧是包含 DSPy 和 LangGraph 关键词的招聘搜索结果截图。它们说明课程选择的工具已有社区关注,也出现在一些岗位描述中。不过,收藏数不等于生产部署数量,搜索摘要中的岗位总数也不能直接当作经过核验的市场统计。这里把它们作为当时的背景材料,真正选择工具时,仍要回到功能、任务适配和工程成本。
到这里,我们已经有了阅读框架的基本地图。工程工作同时包含数据、系统和评估;系统设计可以从单次模型调用,延伸到预设工作流,再延伸到更多由模型决定行动的智能体。不同位置有不同的框架,可靠性与可观测性工具又为这些系统提供共同支撑。带着这张地图,我们就能进一步追问一个具体框架:它提供了什么抽象,让哪一部分工程工作变得更清楚。
CS329Z智能体工程:03:工作流与程序框架 ⚙️
我们先从工作流和程序开始。这里的重点是用明确的程序结构组合语言模型能力,让每个步骤承担清晰的任务,再把这些步骤连接起来。理解这一层,也为后面讨论自主决策和长时间运行打好基础。
红色定位标记落在谱系中间,程序主要负责执行逻辑,模型负责其中需要语言理解和生成的部分。课件重点选取 DSPy 和 LangGraph。前者帮助我们把语言模型任务组织为可组合、可优化的程序,后者会用图的方式表达状态和执行关系。它们位于同一个宽泛区域,但提供的抽象并不相同。
CS329Z智能体工程:03.1:DSPy 的签名、模块与优化器 🧩
先从 DSPy 入手,看看一个语言模型任务怎样成为程序中的一个可调用组件。DSPy 的名字旁边是一组拼图,它很适合提醒我们这套思路:一个复杂任务可以拆成接口明确的小块,再通过程序把它们组合起来。要理解这些小块,先认识三个核心概念:签名、模块和优化器。
下面先列出这三个概念各自的职责。
-
签名回答“做什么”,它明确模型接收哪些输入、需要产出哪些输出,形成任务接口。
-
模块回答“怎样做”,把签名与调用方式、程序逻辑组合成可以执行的函数。
-
优化器根据数据和评价指标,改进程序使用的提示词,或者在相应设置下改进模型权重。
举一个帮助理解的问答例子:输入问题和上下文,输出答案是签名;如何调用模型、怎样把步骤串起来是模块;用一批样本评估并改进这个程序是优化过程。这里的优化器不应简单理解成神经网络里的梯度下降,它优化的对象也可能是提示词。先把这三者的职责分清,才能看懂后面的代码。
CS329Z智能体工程:03.2:最小签名与类型化签名 ✍️
我们从描述任务接口的签名开始。沿着一个具体任务把接口、模块和优化串起来:把自然语言邮件转换成可以使用的日历信息,再扩展成能操作日历的智能体。课件保留了课堂演示的代码和结果,我们据此逐步分析每一次改动。
先从最小接口开始。签名要回答两个问题:调用者提供什么,程序必须返回什么。把这层约定写清楚,后续才能替换模型、调整推理方式,同时让调用代码保持稳定。代码中的 message 指向 response,就是一个最简单的字符串签名。箭头左边是输入消息,右边是输出回复;没有显示写类型时,这里按字符串处理。接着把签名交给 DSPy 的 Predict,得到可以调用的 chat 对象。调用时使用 message 这个命名参数传入问候语,返回结果则通过 response 字段访问。这样外部程序面对的是清楚的输入输出接口,底层如何把字段组织成模型消息、如何解析回复,由框架的适配层处理。
注意字段名称也传达任务含义。response 让模型理解要做出回复;把它改成 sentiment,程序表达的任务就变了。把输出字段换成 sentiment,就得到一个情感分类器。示例分别输入开心、生气和困惑的句子,打印出的结果是正面、负面和负面。红字指出的直接问题是三个字符串没有遵循同一格式:一个 negative 的首字母大写,另一个全小写。人能把它们看成同一类别,按字符串精确判断的程序却可能把它们当成不同标签。更深一层,只有一个字段名称也没有限定合法类别,模型还可能写出一句解释或者另一个近义词。所以结构化输出字段解决了结果放在哪里的问题,标签的取值范围还需要进一步声明。困惑归哪一类,则属于标注规则与语义正确性问题。
现在给签名加上类型。输入 message 是字符串,输出 sentiment 使用 Literal,把合法标签限定为 positive、negative 和 neutral,另加一个浮点型 confidence。下面的 prediction 对象就有两个明确字段,标签拼写也统一了。开心句子对应正面和 0.98,生气对应负面和 0.9,困惑仍然被分为负面,数值是 0.86。这里要区分两种保证:类型帮助约束格式,却不保证分类结论符合业务定义;模型自己生成的 confidence 也不等于经过统计校准的正确概率。假如要用这个数值设置自动处理阈值,还得在标注数据上验证它与实际错误率的关系。
字段变多之后,类签名更容易维护。ExtractEvent 继承 DSPy Signature 类,类中的说明文字定义任务:从邮件提取事件详情。email 是输入,事件名、日期、开始时间和分钟数是输出;日期使用 date 类型,时间使用 time 类型,时长是整数,每个输出又允许 None 表示信息缺失。字段描述还明确要求 24 小时制。把这个类交给 Predict,就能用邮件调用抽取器。截图中的结果是会议、2026 年 10 月 9 日 10 点整、时长为空。时长为空是合理的,因为邮件没有提供持续多久;但输入只写了 10 月 9 日和星期五,年份依赖上下文,附件时应明确参考日期。邮件还提到太平洋时区,而这里打印的 time 没有时区信息。因此,有了类型之后,仍然需要处理年份和时区的业务约定。
下面给出一个简化后的 DSPy 签名示例,帮助理解接口约定:
from datetime import date, time
from typing import Literal, Optional
import dspy
class ExtractEvent(dspy.Signature):
"""从邮件提取事件详情。"""
email: str = dspy.InputField(desc="邮件正文")
event_name: Optional[str] = dspy.OutputField(desc="简短事件名")
event_date: Optional[date] = dspy.OutputField(desc="标准日期")
start: Optional[time] = dspy.OutputField(desc="24小时制开始时间")
duration_min: Optional[int] = dspy.OutputField(desc="持续分钟数")
CS329Z智能体工程:03.3:推理策略、小签名与完整工作流 🔄
输入输出约定清楚了,接下来选择完成它的计算流程。同一个签名可以交给不同模块:直接预测、先生成解释再给结果,或者把多次模型调用与普通 Python 逻辑组合起来。
这里保留 ExtractEvent 签名,只把 Predict 换成 ChainOfThought。输入仍是那封会议邮件,原来的 4 个输出字段也没有变,结果中多了一个 reasoning 字段,用文字说明抽取依据。这展示了接口和推理策略可以分开修改,下游仍然读取事件名、日期、时间和时长,不必因为增加解释就重写业务逻辑。对于需要多步判断的任务,这种策略可能有帮助,但不能仅凭解释更长就认定准确率更高。截图中的日期和时区问题,也不会因为有 reasoning 字段就自动消失。应该在同一批样本、同一个评价指标下比较两种模块,再判断新增的模型输出是否值得它带来的时间与成本。
为了给日历事件配图标,再定义一个小签名 PickEmoji。它只接收事件名称,输出一个表情符号,字段描述明确要求恰好一个表情。这里也使用 ChainOfThought。示例输入 dentist,也就是牙医预约,结果选择了牙齿图标,并附带解释。这个步骤的作用很独立:它不负责判断日期,也不负责写入日历,只把事件名称映射成合适图标。这样的边界方便单独测试和替换。不过,“恰好一个”目前是写给模型的要求;若应用严格依赖它,仍可在输出后增加验证。把每个模型调用的职责收窄,后面组合时就更容易知道错误来自哪个环节。
完整工作流写成 UpdateCalendarFromEmail。它继承 DSPy 的 Module,初始化时建立两个子模块:一个抽取事件,一个选择图标。真正的数据流留在 forward 里。先把邮件交给抽取器,得到 details;接着用普通 Python 条件判断检查事件名、日期和开始时间是否有缺失,只要其中之一是 None 就直接返回,没有找到事件不进入日历写入;信息齐全时,再把事件名送给图标模块,随后调用 TinyTail 的添加事件函数,把标题、日期、开始时间、时长和图标逐项传入。注释特别说明时长为空时,日历工具默认使用一小时。最后返回事件对象,可以添加状态。因此,这个系统里有两次模型调用,也有确定性的分支和工具调用;哪些步骤依赖模型判断,哪些由程序严格控制,在结构上是分开的。
把这个模块实例化后,课件用两封邮件检查两条路径。第一封给出会议安排,返回结果里有事件对象,包含标题、日期和开始时间,状态是已经加入日历。第二封只是感谢对方发送讲义,并没有安排活动,返回 event 为空,状态是没有找到事件。这个对照很关键:一个抽取系统不只要在有信息时提取正确,也要在没有事件时停止。否则只测试正例,很容易忽略误创建事件的问题。从代码结构看,第二种输入一旦触发缺失字段的判断,就不会继续调用图标模块和日历写入函数,这也是把业务条件明确写进程序的价值。
CS329Z智能体工程:03.4:用评估和 GPA 优化提示词 📈
日历界面把刚才的结构化结果变成了可见状态。星期五 10 月 9 日出现一场会议,时间从上午 10 点到 11 点,并带有日历图标。邮件只提供了开始时间,这里的 11 点结束来自工具默认的一小时时长,所以不能把它误认为模型从邮件中抽出了结束时间。其余日期没有新增事件,也与那封感谢邮件被忽略的结果一致。到这里,从输入邮件、模型抽取字段、检查、选择图标到日历展示,整个链条已经能够逐项对应。
接下来要解决的是:这个程序遇到更多不同写法的邮件时,能否依然稳定工作。能处理两个例子只说明流程接通了,要改善真实任务上的表现,需要把“做得好”变成可测量的标准,再让优化器利用数据和反馈寻找更合适的指令。
图中 3 块内容构成一次优化的基本条件。下面先列出这三块内容。
-
左边是数据,可以是带标签样本、实际执行轨迹、成功示范或者人工反馈。
-
中间是被优化对象,可以是单次模型调用、由多个组件组成的工作流,也可以是会自主选择工具的智能体。
-
右边是评价方法,包括任务通过率、精确匹配、模型评审以及成本和延迟等约束。
三者必须彼此对应。例如邮件抽取任务,数据要覆盖不同的日期与时间写法,指标要衡量抽取结果能否被日历正确使用,系统才有明确的优化方向。只换提示词却不看稳定的评价结果,就无法区分真实提升与偶然碰巧答对。
示例准备了 50 封带标准答案的邮件,从 JSON 文件读取。展示的邮件是一条理发预约提醒:与 Jessie 在 2026 年 10 月 28 日星期三见面,下午 2 点 45 分开始,持续 1 小时。标准答案把自然语言转成 4 个字段:事件名为 haircut,日期采用年月日格式,开始时间转换成 14 点 45 分,时长是 60 分钟。代码先打印邮件,再打印 email 以外的所有字段,让输入和标签直接对照。注意邮件签名里的店名不等于事件名称,下午时间也必须正确换成 24 小时制。一个短例子已经包含了语义抽取、日期解析、时间换算和单位转换。
每条记录接着被包装成 DSPy 的 Example,并调用 with_inputs 明确只有 email 是模型输入,其余字段留作评价时使用的标准答案。这个声明防止把答案字段也作为正常输入传给预测程序。随后使用固定随机种子 0 打乱样本,让划分可以复现。前面 30 条构成训练集,剩下 20 条构成测试集。优化器通过训练样本寻找改进,而测试集用来比较优化前后的表现。如果反复根据测试集结果修改程序,这批测试数据也会逐渐参与开发决策,所以正式实验还应保留独立的最终测试集。这里的 50 条数据适合演示流程,不能代表所有真实邮件的分布。
评价函数接收标准答案 gold 和模型预测 pred,逐项检查三个字段:日期转成字符串后比较,开始时间转成字符串只取前 5 个字符,也就是小时和分钟,时长直接比较整数。三个条件全部满足才得一分,只要有一个不满足就得零分。这样衡量的是整条记录是否通过,不是三个字段的平均正确率。事件名是自由文本,示例没有给它打分;时间截到分钟,也意味着秒数和时区没有被检验。函数还收集错误字段,把预期值和实际值写成 feedback,最后一起返回分数和反馈。分数用于比较候选程序,具体错误文字则帮助 GPA 的反思模型知道该改什么。最下面的 evaluate 把这套标准应用到测试集,设置 8 个并行线程,并显示进度。指标写了什么,后面的 100% 就只能证明什么。
优化小模型之前,先用课件中默认配置的参照模型,截图标注为 GPT-5.4n,运行同一个 ExtractEvent 签名,确认任务和评价流程基本可用。截图显示,20 条测试样本全部通过,平均分是 100%。这里的 base model 是这个演示里的相对称呼,可以把它理解为作者选用的较强参照,不必仅根据变量名推断参数规模。更重要的是比较条件:接口、数据和指标保持一致。20 条全部通过,说明它在这批样本的三个受检字段上全部匹配,并没有检验每个事件名称、时区处理或后续日历操作。这个结果提供一个有用的参照,也帮助判断小模型还有多大可改善空间。
然后换成 GM3 的 12B 指令模型。代码先通过 DSPy 的 LM 建立模型对象,设置服务入口、密钥、输出长度等调用参数,再创建同样使用 ExtractEvent 的 Predict,并通过 set LM 把这个预测器绑定到小模型。签名没有改,评价器没有改,测试数据也没有改,因此更容易观察模型替换带来的差异。课件记录的优化前结果是 20 条中 11 条通过,正确率 55%。也就是说,在按整条记录判对的严格指标下,有 9 条至少一个受检字段不匹配。接下来要改进的是这个小模型上的任务指令,而不是直接把整个工作流换回较强模型。
GPA 的配置把前面的各部分接在一起:metric 指向事件评价函数,max_metric_calls 设为 100,限制优化过程的指标调用预算;reflection_LM 使用参照模型,让它分析小模型的错误并提出新的指令;8 个线程用于并行评价。然后 compile 接收小模型预测器和 30 条训练样本,返回优化后的预测器。理解这个过程可以想成一个反馈循环:让小模型按当前指令抽取,查看哪些字段出错,把具体错误交给反思模型,再形成候选指令并重新测量。这里优化的是提示词,没有训练 Gemma 的模型权重。100 次指标调用也不等于 100 轮训练或者 100 次全部样本评测。课件写的几分钟是演示预算的说明,实际时间还取决于服务速度、并行设置以及缓存情况。
把优化后的程序交给同一个评价器,课件记录的结果从 55% 提高到 100%,也就是从 11 条通过提高到 20 条通过,提升 45 个百分点。在这个例子里,小模型配合更合适的指令,达到了参照程序在同一测试集上的分数。这说明模型能力之外,任务如何被说明也会显著影响结果。不过,样本只有 20 条,每多通过一条就改变 5 个百分点。一次满分并不意味着今后的邮件都不会失败。合理的下一步是查看优化究竟改了什么,并用更多未参与开发的样本验证,尤其要检查评价函数没有覆盖的事件名称、缺失信息和时区情形。
打印优化后签名的 instructions,就能直接看到优化产物。课件说明 GPA 在这个演示里只改了这段指令文字。新的说明明确输入是邮件正文,可能夹杂问候、签名和预约提醒,列举了日期、开始时间和持续时间的不同写法;输出则规定短事件名、标准日期格式、时间格式和整数分钟数。它还提醒事件名应来自主要活动,而不是发件人名称。这些细节把原来笼统的“抽取事件”拆成了更明确的操作规则。但这里有一个值得认真检查的地方:指令要求在缺少时区时默认使用 UTC。这个规则是优化产物里的约定,不能当作用户所在地的事实。前面的指标又只比较时间前 5 个字符,根本没有检查时区。因此,得分提高之后,仍要审阅生成的规则,把它们与真实业务要求对齐,必要时补充数据和指标。
CS329Z智能体工程:03.5:ReAct 与 RLM 的组织方式 🤖
刚才的执行顺序由程序固定:抽取、检查、选图标、写入。现在把任务扩展为需要先查询已有日程,再决定如何修改的请求。这时可以把目标和工具交给智能体模块,让它根据返回的信息逐步选择行动。
代码用 DSPy ReAct 创建智能体,签名是 request 指向 summary,同时传入 TinyCalendar 提供的工具集合。这个工具集合决定智能体能够查询或操作什么,框架则组织推理、工具调用和结果反馈的循环。示例请求包含两件事:10 月 7 日下午上完课之后,与 Zheng 安排 30 分钟咖啡;给同一周的每一节课程加上书本图标。要正确完成,它需要先识别那一周的课程,确定指定日期课程结束时间,再选择合适的半小时时段,并更新相关事件。课件记录的摘要显示,周一和周三的两节课都加了书本图标,咖啡被安排在周三 15 点到 15 点 30 分。这里一行代码隐藏的是框架提供的循环机制,任务本身仍然包含多次观察和决策。摘要是否属实,还应和实际日历状态对应。
日历截图提供了这种对应关系:星期一和星期三各有一节课程,时间都是 13 点 30 分到 14 点 50 分,两个标题前都出现了书本图标;星期三还增加了 15 点到 15 点 30 分的咖啡约会。因此,确实是在课程结束之后,并满足 30 分钟时长。星期五原来的会议仍然显示在日历里。这个例子也说明,智能体任务的评价不能只检查最后的文字摘要。它说“已完成”是一种输出,环境里真正出现了正确的新增和更新,则是另一层证据。对有工具操作的系统,执行轨迹和最终状态通常都值得保留,方便定位漏操作、误操作以及摘要不一致的问题。
另一类任务不需要修改日历,而是要从大量邮件里找出答案。代码先读取原来的邮件数据,用分隔符把所有正文连接成 inbox。RLM 的签名接收 inbox 和 question,输出两个字符串列表,分别存放事件名称和日期。这里问的是哪些活动在 G 大楼举行,分别是哪天。课件展示的结果找到 Vision 的论文答辩,日期为 2026 年 12 月 7 日星期一。max_iter 设为 8,限制交互循环次数,并不表示只能读取八封邮件,也不等于最多 8 次内部模型调用。RLM 的特点是把长输入作为程序环境中的数据,让模型通过代码查看、筛选和拆分,并在需要时调用子模型理解片段。这样模型可以围绕问题逐步访问相关内容,而不是要求一次提示中同时消化全部邮件。
把智能体内部循环展开,可以看清 ReAct 为什么要交替进行推理与行动。图中任务是在查找苹果遥控器原本控制的那个软件,还能用什么设备控制。第一步先搜索 Apple Remote,观察结果指出它对应 Front Row 媒体中心。模型据此发起第二次搜索,但直接查 Front Row 没有找到准确词条,工具返回了几个相似名称。于是第三步把查询改成带 software 限定的 Front Row,排除歧义,得到正确的软件条目。最后结合查到的说明,回答键盘功能键,并调用 finish 结束。图中的 thought 是下一步行动的文字依据,act 是工具操作,obs 是外部返回的信息。关键在于,第二次搜索失败并没有终止整个任务,而是成为修正查询的线索。每一轮都能根据新观察调整后续行动。
RLM 的结构图则强调上下文如何被访问。示例要从一本长书中找出大灾难之前制造的物品。左边最外层是根调用,深度为零。绿色区域是 Python 环境,长文先作为变量放在这里。红色叉号表示没有把全部长文直接塞给顶部模型。模型先运行 print 查看开头的一小段,了解内容结构,再按章节标记拆分文本,把局部材料和子问题传给 LLM_query。右边两个深度为一的子调用各自处理更小的问题,返回银色水壶、介质等候选信息。结果回到左边环境后,可以保存、组合和检查,最后汇总成最终回答。所以递归指的是主过程能够发起更局部的模型调用,长文本的切分和结果组合又可以利用普通代码。它是一种推理时的组织方式,并没有把基础模型的上下文窗口物理变大。最终质量仍依赖是否找到相关片段、子问题是否提得合适以及汇总是否遗漏条件。
把这段时间放在一起看,DSPy 的分层就很清楚了。签名规定输入输出契约;模块规定怎样完成任务,可以直接预测,也可以先生成解释,或者像 BestOfN 那样尝试多个候选再按评分选择较好结果;多个模块与普通 Python 逻辑组合形成完整程序;优化器利用数据和评价标准改善程序的提示词,其他知识训练的优化方式也可以调整权重。但刚才的 GPA 演示只改指令。ReAct 和 RLM 则把工具交互、代码执行或子模型调用封装为模块,让同样的接口思想扩展到智能体工程上。重点是分别看清接口、执行策略和评价目标,让系统能够被检查、比较和改进。
CS329Z智能体工程:03.6:LangGraph 的状态、节点与边 🕸️
理解了用签名和模块来组织模型程序,我们再换一个观察角度。如果最关心的是任务执行到哪里、有哪些中间结果,以及接下来该走哪条路径,应该怎样组织程序。LangGraph 把这些问题放进一张图里。我们会沿着状态、节点和边这三个概念,把一个邮件日历助手逐步搭建起来。
先看上方蓝色区域 state,也就是状态,可以理解成整个工作流共享的一份工作记录。它保存任务目前掌握的信息,而下方的大框代表围绕这份记录运行的计算过程。比如处理一封邮件时,状态最初可以只有邮件原文,随后逐渐补上事件名称、日期、开始时间和处理结果。这样后续步骤能够读取前面步骤留下的内容,不必把所有信息都挤进一个很长的函数参数列表。这里的共享是一次图执行中的共享上下文,不要把它直接理解成所有用户都共用的全局变量。状态的字段设计,实际就在决定整个系统能够记住什么、向后传递什么。
有了工作记录,还需要说明流程从哪里开始、在哪里结束。左右两个黑色原点是入口 START 和终点 END,它们标记执行边界。后面代码中会使用框架提供的特殊标记来连接业务节点。入口的作用是把初始状态交给第一步,到达结束位置则表示这条执行路径已经完成。这里还没有任何具体业务逻辑,因为数据放在哪里和运行从哪里开始是两个不同的问题。接下来我们才把真正负责工作的函数放进图里。
三个红色原点代表节点,每个节点都可以是一个普通函数。红色双向箭头强调,它们围绕同一份状态工作,先读取当前需要的字段,再把计算得到的更新交回来。例如,一个节点读取邮件并抽取事件,一个节点根据事件名称选择图标,另一个节点调用日历工具。并不是每个节点都必须调用语言模型,普通的校验、格式转换和接口调用同样可以成为节点。后面的代码还会展示一个很实用的约定:节点通常只返回自己修改的字段,由图来应用这些更新。理解这一点,就能把复杂任务拆成职责清楚的小步骤。
现在黑色连线把节点连接起来。节点回答“做什么”,边回答“做完以后去哪里”。实线表示预先确定的连接,虚线强调可以根据条件选择后续路径。请注意下方红色节点,它有机会继续到右上方节点,也有机会直接结束。所以一次具体运行不一定经过图里的所有节点。决定分支的依据可以来自共享状态,也可以来自模型的判断,具体采用哪一种由程序设计者决定。图中从起点引出的多条连接,也提醒我们:图结构可以描述多条路径,但不能把所有分叉都自动理解成随机选择,真正的执行规则仍然要看代码怎样定义。
把标注暂时拿掉,再看整张图,就能用两条线索理解一次运行。下面先列出这两条线索。
-
第一条是控制流:当前走到哪个节点,下一步可能去哪里。
-
第二条是状态变化:这个节点读了什么,又写回了什么。
黑色连线并不意味着节点只能拿到紧邻上一个节点的输出,它们仍然围绕上方的共享状态工作。调试时,把“路径走错了”和“某个字段算错了”分开检查,通常会更容易定位问题。
下面我们把这张一般性的工作流图收缩成一个熟悉的智能体循环。现在只保留两个业务节点:上方的语言模型以及下方的工具。共享状态可以包含完整的消息历史。用户请求先进入模型,模型根据已有消息决定是直接给出答案,还是发出工具调用请求。如果需要工具,流程转到下方执行相应函数,再把工具结果加入消息历史交回模型。模型因此能够看到刚刚获得的新信息,继续判断下一步。
图中这条回路很关键。智能体通常不是模型说一句、工具做一次就结束,而是可以反复执行,直到不再需要调用工具,沿右侧路径到达终点。比如调整日程时,需要先知道原来的安排,才能计算新时间并完成修改。具体调用几次、调用什么工具,取决于任务和模型决策,执行框架负责把这些步骤连接起来。
接下来用课程保留的代码示例,把这套机制具体化。我们会先完成最小的邮件事件抽取,再加入条件分支和日历操作,最后观察一个由模型决定工具调用的智能体。讲解会逐步对应代码和展示结果,便于理解每一段程序承担的职责。
CS329Z智能体工程:03.7:LangGraph 最小抽取图 🧱
先从一个只有单个业务节点的图开始:输入一封邮件,输出结构化的事件信息。要实现它,我们依次准备输出结构、共享状态、节点函数以及连接这些部分的图。这样模型调用就有了明确的位置和数据接口。
代码先从 Pydantic 导入 BaseModel 和 Field,再定义 ExtractEvent 这个输出模型。里面有四类信息:事件名称是字符串,日期是日期类型,开始时刻是时间类型,持续分钟数是整数。每个类型旁边都允许 None,表示邮件没有提供信息时可以明确返回空值,而不是编造一个答案。注意允许值为空,不等于可以任意省略字段。Field 的描述进一步解释语义:名称要简短,时间采用 24 小时制,持续时长的单位是分钟。最后一行把这个结构交给模型的 with_structured_output 接口,得到专门用于事件抽取的调用器。这样后续代码读取的是名称、日期等字段,而不需要重新解析一整段自由文本。类型约束解决输出形状的问题,内容是否忠实于邮件仍然需要检查。
模型的输出结构确定以后,再定义图自己的状态。这里使用 TypedDict 描述一个字典包含哪些字段:email 保存邮件原文,类型是字符串;details 保存刚才定义的 ExtractEvent 对象,也允许为空。两者职责不同:原文是输入证据,details 是处理后产生的结构化结果。即使完成抽取,原文仍然可以留在状态里,方便后续核对。这个状态类也没有负责执行抽取,它只是在规定数据接口。可以把它想成一张有固定栏目名称的工作表,先放入邮件,再由负责抽取的节点填写事件详情。
真正的抽取工作由 extract 函数完成。它接收当前状态,先通过 email 这个键取出邮件,再调用前面准备好的结构化模型。模型返回的事件对象被放进一个只包含 details 字段的字典里,作为节点结果交还给图。请留意这个返回值:它没有把整个状态重新构造一遍,也没有把 email 再复制一次,因为这里只更新 details,图会把这项更新应用到当前状态。邮件原文因此仍然保留。这个模式把两件事分开了:节点函数负责一次具体计算,框架负责组织状态的流转。以后替换抽取模型主要修改这个节点,新增后续步骤则主要修改图的结构。
现在把各部分连接起来。第一行导入 StateGraph、START 和 END,随后用 StateGraph 接收刚才的状态类型,得到一个图构建器。add_node 把名字 extract 与实际函数绑定起来,名字用于图中的连接,函数负责运行时的计算。接着两次 add_edge 分别把入口连到抽取节点,再把抽取节点连到终点。右侧流程图正好对应这两条边,从上到下只会执行一次抽取。最后调用 compile 生成可以运行的图对象。这里的编译是整理和检查图结构,形成执行对象,并不表示模型已经读取了邮件,也不表示开始训练模型。真正处理具体输入,要等到调用这个图的 invoke 方法。
下面给出一个简化后的 LangGraph 结构示例:
from typing import TypedDict, Optional
from langgraph.graph import StateGraph, START, END
class State(TypedDict, total=False):
email: str
details: Optional[ExtractEvent]
def extract(state: State) -> dict:
mail = state["email"]
details = structured_model.invoke(mail)
return {"details": details}
builder = StateGraph(State)
builder.add_node("extract", extract)
builder.add_edge(START, "extract")
builder.add_edge("extract", END)
graph = builder.compile()
示例通过 graph.invoke 方法传入一封邮件:会议安排在星期五 10 月 9 日上午 10 点,标注了 PST 时区。下方返回结果中,原始 email 仍然存在,同时新增了 details:事件名称是 meeting,显示的日期是 2026 年 10 月 9 日,开始时刻是 10 点。这就把自然语言转换成了程序可直接访问的字段。不过,读输出时也要看到语义上的限制:原句没有明确写年份,示例结果填入了 2026 年;而时间对象显示的时区偏移,也不能直接当作输入 PST 已被正确处理的证明。真正的日历程序应当结合明确的日期上下文和时区规则校验这些值。我们从截图能够确认的是数据流和输出结构,不应该把一次有类型的输出等同于所有日期细节都已经正确。
CS329Z智能体工程:03.8:条件边、日历写入与两封邮件对照 🌿
能够抽取事件,还不等于每封邮件都适合写入日历。有些邮件只是通知,根本没有事件时间。我们现在加入一个判断:信息足够就继续挑选图标并添加日程,信息不够就结束。这正是条件边要解决的问题。
新定义的 CalendarState 继承之前的 State,因此邮件原文和事件详情两个字段继续保留,又增加 emoji 和 status 两个字符串字段。emoji 用来保存日程图标,status 用来记录处理结果。这是一种逐步扩展状态的方式:已有节点仍然可以读取原来的字段,新节点则补充自己的结果。比如抽取节点填写 details,图标节点填写 emoji,添加日历的节点填写 status。字段属于整个工作流,而不是某个节点的私有变量。也要注意,声明一个字段不表示它一开始就已经有值;它什么时候出现,取决于执行路径是否真的经过负责写入它的节点。
这里新建了两个函数。第一个叫 pick_emoji,它调用语言模型,请模型针对日历事件只回复一个表情符号,随后取出回复文本,用 strip 去掉首尾空白,再把结果放入 emoji 字段。去掉空白只是简单清理,并不会自动验证输出真的只有一个表情。第二个函数叫 add_to_calendar,先把状态中的 details 取出来,简写成 d,再调用 TinyCalendar 的 add_event,传进去的参数依次包含事件名称、日期、开始时间以及持续分钟数和刚选好的图标。调用完成后,它返回 status,内容是“已经添加到日历”。于是这两个节点形成清楚的分工:模型负责一个适合用自然语言判断的小任务,普通代码负责把结构化字段交给工具。注意实际添加日程发生在第二个节点的工具调用处,返回状态文字是在记录这一步的处理结果。
条件边由 route 函数提供判断。它先读取 details,然后检查事件名称、日期和开始时间这三个字段;三个条件都成立,就返回 pick_emoji 这个节点名称,否则返回 END。所以这里决定下一步的是一段普通 Python 布尔逻辑,而不是再次询问模型。返回类型里的 Literal 列出了两种允许的路由结果,帮助表达分支范围。这个函数返回的是“去哪里”,不是要写回状态的新字段。还有一个细节值得结合代码看:时长没有参与判断,因此时长未知并不会在这里阻止流程继续;反过来,缺少开始时间则会结束。这样就把业务规则明确编码成了图中的岔路。检查字段存在也只是最低要求,若要验证日期是否合理、时区是否一致,还需要增加相应的校验逻辑。
构建器这次使用 CalendarState,并注册三个节点:抽取、选择图标以及添加日程。入口仍然先连接到 extract,但抽取之后不再固定连接终点,而是调用 add_conditional_edges 把 route 函数挂在这里。右侧两条虚线就是它的两种结果:信息完整时,沿右边走到 pick_emoji,再通过实线进入 add_to_calendar,最后结束;信息不完整时,则沿左边直接到终点。最后的 compile 生成 update_calendar 这个可执行对象。请把图和代码逐一对应起来:注册节点只是在说明有哪些工作可做,添加边才是在说明先后顺序,而条件函数决定某次运行会选择哪条路径。即使图里有三个业务节点,一封普通通知邮件也可能只运行抽取节点。
两次演示输入让分支的效果变得直观。上面的邮件说,牙医预约已经确认,时间是星期三 10 月 7 日上午 11 点。展示的输出抽取出牙医预约名称、日期和开始时间,随后出现牙齿图标,status 也显示已经添加到日历。这些新增字段说明流程走过了选图标和添加日程两个节点。下面的邮件则是在通知课程的 OpenRouter 额度已经到账,请查看收件箱。返回的 ExtractEvent 对象中,名称、日期、开始时间和持续时长都为空,没有图标也没有添加成功状态,于是条件判断选择了直接结束。这个对照展示的不只是抽取能力,还展示了抽取结果怎样影响控制流。我们可以同时检查路径和状态:应当执行的步骤有没有执行,不应当发生的日历写入有没有被跳过。
日历界面给出了工具侧的可见结果。当前展示的是 2026 年 10 月 4 日到 10 日这一周,星期三的栏里,牙医预约从上午 11 点到 12 点,旁边显示了牙齿图标;星期五上午 10 点还有会议;星期一和星期三下午有课程;星期三下午 3 点有与 Zheng 喝咖啡的安排。把牙医条目与刚才的状态对照,就能看到事件名称、日期、开始时间和图标如何进入最终界面。截图中的牙医条目占了一小时,但前面的邮件并没有写出持续时长,这里沿用了前面介绍的日历工具默认值:没有时长就按一小时处理,并不是从邮件里抽出了一小时。已有日历条目也给下一项任务提供了上下文:除了新增事件,我们还可能要求系统找到已有安排,再调整它的时间。
CS329Z智能体工程:03.9:由模型决定工具调用的智能体循环 🔁
刚才的路径由我们预先规定:抽取、检查、选图标、添加。现在提出一个更灵活的请求,比如把已有的咖啡约会推迟半小时。要完成它,系统需要围绕目标使用日历工具,这就回到了模型与工具循环的智能体结构。
代码先从 LangChain 导入 create_agent,再把语言模型和 TinyCalendar 的工具集合传进去,得到日历智能体。这里要准确区分两层:调用入口属于 LangChain,而生成的智能体执行图由 LangGraph 支撑。右侧图展示了这个默认循环:入口进入 model,模型如果发出工具调用,就转到 tools;工具返回后再回到模型;如果模型不再要求工具调用,就走向结束。工具集合规定了系统由哪些可用操作,模型则根据消息内容和工具结果,决定在这次任务中使用哪些操作。我们不需要手写每一种用户请求的固定节点序列,但仍然需要提供合适的工具接口。简短的创建代码把模型、工具和消息循环装配起来,具体任务的路径要在运行过程中才确定。
调用时输入改成 messages 消息列表,其中放入一条用户消息:请把 10 月 7 日与 Zheng 喝咖啡的安排推迟 30 分钟。这个要求同时指定了日期、事件对象和时间变化。结合之前的日历,原来的开始时间是下午 3 点,结束时间是 3 点半,整体后移 30 分钟后,应当变成 3 点半到 4 点,事件时长保持半小时。下方截图展示的正是这个结果。概念上,系统需要定位对应事件、取得原时间再完成更新。但课件没有展示中间的完整工具调用记录,因此不能仅凭最终界面断言它具体调用了几次工具。这里最重要的是评价方法:既要确认它改对了事件,也要确认开始和结束时间一起移动,同时保留其他日程。这个例子把智能体从生成一段回答推进到了围绕目标改变外部状态,也说明为什么最终结果需要能够检查。
回头看 LangGraph 的核心概念,就可以把每个术语对应到实际例子:状态保存邮件、抽取结果和后续处理信息;节点读取这些信息,执行模型调用或普通函数,再返回更新;边规定执行顺序,条件边则根据结果选择分支;需要灵活使用工具时,可以通过 create_agent 这样的高层入口构建模型与工具循环,而不必从零连接所有节点。这里的关键能力是把应用拆成可观察的状态变化和执行步骤。遇到问题时,我们可以明确追问:输入信息是否正确,哪个节点产生了错误,分支条件是否合理,工具是否执行了预期操作。这比只看最终的一段自然语言回答,多了几个具体的检查位置。
把这一部分串起来,工作流是在把模型调用、工具和程序逻辑组合成一个更大的系统。不同框架提供不同的组织方式。DSPy 用签名明确任务的输入输出,再通过模块组织计算;LangGraph 用状态、节点和边描述执行过程。右侧还列出了 LlamaIndex 和 Haystack 等其他框架,提醒我们可用工具不止一种,但系统设计的问题不会因此消失。我们始终要决定哪些步骤由程序固定,哪些判断交给模型,以及怎样衡量任务是否完成。改进还需要数据和评估:准备能代表真实任务的例子,检查最终结果和必要的中间行为,再比较修改前后的表现。DSPy 的优化器把这一过程变成明确的优化步骤;在日历助手里,正确抽取、正确分支、正确修改事件,同样都可以成为评估对象。理解这些共同原则,才能把框架提供的便利转化成可靠的应用行为。
CS329Z智能体工程:04:开源智能体运行框架 🤖
工作流让我们组织多个步骤,而当模型需要自己决定下一步做什么时,就需要一套承接这些决策的运行环境。接下来讨论开源 agent harness,也就是围绕模型构建的工具、上下文和执行机制,重点是理解这些设计怎样影响智能体的实际表现。
先把 harness 放回整个技术版图。横线左端是模型接口,模型只负责更大系统中的一小部分;中间的工作流由程序规定逻辑和编排;右端的智能体则把更多控制流决策交给模型。红框标出的 SWE-agent 和 OpenHands 属于围绕智能体行动循环构建的系统。这个位置变化的关键是谁决定下一步。程序可以固定检索、总结和回答的顺序;智能体则可能根据测试报错,临时决定继续读代码、修改文件或重新运行测试。不过,模型决定行动并不等于模型直接拥有操作系统能力。把决策变成可执行操作、把执行结果送回上下文,仍然需要 harness 来完成。
沿着图中的箭头看一次完整循环。右侧环境可以是浏览器、桌面系统或数据库,观察结果沿着向左的箭头进入模型;上方的规划、推理和记忆提供额外信息;模型据此决定接下来要做什么,决策沿着向下的箭头交给工具,例如检索、计算、代码执行和记忆读写,再变成底部的具体行动;行动通过向右的箭头改变外部环境,新的观察又返回模型。红框里的这些能力与连接方式,就是理解 harness 的起点。以修复程序为例,报错是观察,检查哪个函数是决策,读取和编辑文件是工具操作,重新测试又产生下一轮观察。harness 要让这条反馈链持续运转,还要决定哪些历史保留、哪些工具可用,以及何时结束。
不同 harness 的差别通常落在四类选择上。下面先列出这四类设计选择。
-
工具暴露什么能力。
-
记忆保存什么信息。
-
上下文怎样组织。
-
动作怎样执行。
同样是执行命令,可以每次启动独立进程,也可以保持一个终端会话;同样是保存历史,可以直接追加消息,也可以先存成事件,再重新构建模型输入。每一种选择都有代价:更多工具可能提高任务覆盖率,也会增加选择难度;保留更多历史有助于追踪状态,同时增加开销。因而,评价 harness 需要同时看任务成功率、费用、等待时间和用户控制能力。一个适合短小代码修复的设计,未必适合长期运行的个人助理。
CS329Z智能体工程:04.1:miniSWE-agent、OpenHands、Pi 与 OpenCode 🧰
miniSWE-agent 把核心循环压得很小,只给模型一个 bash 执行入口,历史也只是不断追加的线性消息列表。读取文件、搜索代码、运行测试都可以通过命令行完成。另一个关键选择是使用独立子进程执行每次动作。比如一次命令切换了目录,下一次命令不会自动继承那个 shell 的当前目录,导出的环境变量也不会自然延续。因此,必要的目录和环境设置需要随命令明确提供。这里的独立指的是进程会话状态,不代表文件修改会消失,写入磁盘的代码仍然保留。这样做减少了维护长时间运行终端的复杂性,也让每次动作的输入和退出结果更清楚,代价是不能依赖隐含的终端状态。
OpenHands 展示了另一种选择。按照课件介绍的实现,每一步先保存成事件,每个事件对应磁盘上的一个 JSON 文件,再根据这些事件重建发给模型的消息。这样执行记录与模型看到的上下文可以分别管理。它还保留持久终端,课件以 tmux 会话为例:切换目录、设置环境变量或激活虚拟环境后,后续命令可以沿用这些状态。这更接近开发者连续操作终端的方式,但系统也需要处理状态失效和终端卡住的问题。第三个设计是重复检测:同一动作和同一结果连续重复 4 次,就把运行判为陷入循环。这是一种节约预算的启发式规则,能拦住无进展的重试,也需要注意区分正常重复与真正卡住,具体后端和阈值会随版本及配置变化。
Pi 同样强调简洁,但它选择了四种清晰的操作:读取、写入、局部编辑和 bash。课件所示的核心系统提示不到 1000 token,默认也不内置计划模式、盒子智能体工具,把扩展能力留给外部机制。它的历史结构很有特点:消息通过父子关系组成一棵树。用户可以回到先前的节点,从那里尝试另一条路径,同时保存原来的分支。这与一直往后追加的线性历史形成对照。课件还强调,核心没有独立的权限审批系统,操作使用启动进程时已有的权限。这描述的是能力边界,不能理解为自动获得隔离保护。实际部署需要由操作系统、容器或扩展明确约束可访问的资源。工具少和控制简单,仍然需要清楚的运行边界。
OpenCode 面向长期存在的助理,因此关心的不只是一次任务内部的循环。课件列出的心跳机制会按计划唤醒智能体,示例间隔是 30 分钟;没有需要报告的事情时,用 no_reply 保持安静。一个长期运行的 gateway 统一管理聊天渠道,让消息可以进入同一套运行体系。它还通过 Markdown 文件提供行为规则、身份、用户信息与记忆,分别对应 AGENTS.md、IDENTITY.md、USER.md 和 MEMORY.md。最后一项是记忆整理,课件描述凌晨 3 点把日常记录中值得保留的内容提升为长期记忆。这些时间与行为应当理解为相应实现和配置下的机制,不能假定任意安装都完全相同。这里的核心取舍是:智能体从一次性程序变成持续运行的服务后,必须显式管理唤醒、消息路由和跨任务记忆。
CS329Z智能体工程:04.2:harness 的测量与 Meta-Harness 📊
这些设计选择会反映在实际测量中。图中每组固定一个模型,比较 Pi、Codex 和 Claude Code 3 种 harness。左侧横轴是每次运行的美元成本,越短越便宜;右侧横轴是成功率,越长越好;细线表示统计不确定性。绿色框标出了各组较优的测量结果。看最上面一组,Codex 的成本约为 0.98 美元,低于 Claude Code 的 1.55 美元,但 Claude Code 的成功率 75.6%,比 Codex 的 72.2% 略高。再看 Opus4.8,Pi 和 Codex 的成功率同为 72.2%,Pi 的成本更低。Sonnet4.6 也体现这种取舍:Codex 成本最低,Pi 的成功率点估计最高。底部 Cyclone4.5 的三种成功率差距则更明显。成本与效果需要分别比较,不能只挑一个指标。这是课件引用的 Terminal-Bench2 实验结果,取决于任务样本、版本和运行设置,有误差线的点估计差异也不能直接当作确定的优劣排名。
既然 harness 的实现影响效果,能不能把这部分代码也纳入自动优化。Meta-Harness 给出了一个外层搜索循环。按图中 1、2、3 的顺序看:第一步,提案智能体读取左侧文件系统,里面保存过去候选方案的源码、执行轨迹和评分,然后提出一版新的 harness 代码;第二步,把这版代码与语言模型组合,在右侧任务集上运行并评分;第三步,把候选代码、推理轨迹和评估结果一起写回文件系统,底部回箭头表示这些经验进入下一轮搜索。这个过程优化的是围绕模型的程序,例如怎样保存、检索和组织信息,并不是直接更新模型权重。保留详细轨迹也很关键,因为一个总分只能告诉我们效果变了,失败在哪一步往往需要回看具体执行记录。左右两张图展示的是不同实验,不能把纵轴数值混为一谈。左图是文本分类,横轴表示评估了多少个 harness,纵轴表示搜索到目前为止最好的表现;红色 Meta-Harness 曲线从约 33% 升到约 57%,高于图中的其他优化方法。曲线只升不降,是因为它记录历史最优值,并不意味着每次新提案都更好。右图换成 Terminal-Bench2,比较以 Claude HighQ4.5 为基础的不同 harness,纵轴是通过率,红柱为 37.6,Go 为 35.5,miniSWE-agent 为 29.8,Claude Code 为 27.5。还要注意,右图纵轴从 20 开始,视觉高度会放大差异,应以标注数字为准。这些结果支持在该实验条件下自动优化 harness 的价值,但不能推断它对所有模型和任务都领先,也不能直接与刚才另一套评测中的成功率横向相减。
把这些例子放在一起,会看到智能体并没有唯一标准架构。工具越多未必越好,持久会话和独立命令各有适用场景,历史既可以线性保存,也可以组织成树或者事件流。课程希望大家亲自尝试这些设计,用真实任务观察结果再决定保留哪些机制。一个有价值的新 harness 可能并不需要更多概念,而是把某类任务的成功率、费用和可控性做得更合适。不过,即使单次任务的循环设计得很好,系统运行久了仍然会遇到网络错误、进程退出和难以定位的问题,这就把我们带到可靠编排与可观测性。
CS329Z智能体工程:05:可靠编排与可观测性 🛡️
现在关注两项基础能力:编排负责让多步任务按照预期推进,遥测则记录运行中发生了什么。对长时间执行的智能体来说,完成任务和解释任务为什么没有完成,同样需要工程支持。
技术版图上方的红框把这两项能力单独标了出来。Temporal 代表可靠执行与编排,Langfuse、MFlow 等工具代表可观测性。它们与下方的模型、工作流和智能体不是互相替代的关系,而是在系统运行时提供配套能力。比如模型调用失败了,可靠性机制要决定什么时候重试、从哪里继续;一次回答质量下降了,可观测性则帮助我们查看用了哪个模型、经过哪些步骤、每一步花费多久。两者也不能互相替代:记录了一条错误日志,不会自动修复失败;流程能够继续执行,也不等于我们理解了其中的行为。构建实际应用时,需要把执行状态和观察数据一起考虑。
这张 Terminal-Bench2 图说明了为什么更复杂的任务会让基础设施变得重要。横轴是模型发布时间,纵轴采用对数刻度,从几秒、几分钟延伸到几小时。每个点表示该模型在这套软件任务上预测成功率达到 50% 时所对应的任务时长,误差线体现估计的不确定性。这里的时长是人类专家完成任务需要的时间,用来衡量任务难度,并不是智能体真的连续运行了这么久,也不是保证完成任意同等时长的工作。虚线概括历史测量中的增长趋势,不应当当成确定预测。顶部斜线区还明确提示,超过 16 小时的测量在当时的任务集上不可靠。图中点位总体向更长任务移动,提示智能体要处理更复杂的依赖与过程,而这些过程能否持续执行,还需要单独解决工程可靠性。虽然人类任务耗时不等于智能体运行时长,但真实智能体确实可能经历多轮调用、等待外部结果,甚至在较长时间内持续工作。步骤越多,遇到接口超时、限流和进程故障的机会也越多。如果所有进度只在内存里,一次重启就可能让系统失去已经完成的工作。因此,我们需要可靠的状态记录、超时和恢复机制。与此同时,调试也不能只看最后一句回答,需要把模型调用、工具参数、返回结果以及错误串成可追踪的过程,才能判断问题出在决策还是执行。这些并非智能体才有的新问题,而是成熟软件工程经验在新系统中的延续。
CS329Z智能体工程:05.1:Temporal 的活动、工作流、工作进程与服务端 🧱
Temporal 是这里介绍的可靠编排工具。理解它可以先抓住一个目标:让多步业务流程在等待、失败和进程重启之后,仍然能够根据已保存的执行记录继续推进。接下来把它拆成活动、工作流、工作进程和服务端四部分。
先看四个红色原点,每个都代表一个 activity,也就是活动。活动封装一个具体的功能单元,例如调用模型、检索数据、运行测试或者写入外部系统。这些操作可能访问网络、依赖外部服务,也可能执行到一半失败,所以图中直接强调活动是会失败的。这样拆分的好处是可以为不同操作明确超时、重试策略和错误处理方式。要注意,能够重试并不意味着外部效果天然只发生一次。如果活动已经写入数据却在回报结果前中断,再次执行就可能重复写入。因此,活动涉及外部状态时,需要考虑幂等性,让同一个请求重复执行也不会产生不希望出现的重复效果。
绿色连线把红色活动连接起来,对应 workflow,也就是工作流。活动负责做具体的事,工作流负责规定它们之间的关系:什么时候启动、等待什么结果,以及成功或失败之后走向哪里。比如先读取需求,再调用模型修改代码,接着运行测试,最后根据测试结果决定结束还是继续修改。示意图画成简单链条,实际也可以表达分支、并行和循环。Temporal 的工作流代码还需要满足确定性要求,以便依据执行历史重放并恢复状态。模型调用、数据库访问这类结果可能变化的外部操作,通常放入活动中;工作流根据已记录的结果做编排决策。这种分工是后面实现故障恢复的基础。
定义了活动和工作流,还需要有人执行它们。左下角新增的两组 worker 就是运行应用代码的工作进程。红色的 activity worker 执行具体活动,绿色的 workflow worker 执行工作流逻辑,决定接下来需要调度什么。这是两种职责,同一个工作进程也可以注册并处理两类任务。这里还要分清客户端、工作进程和右侧服务端:客户端负责发起流程或与流程交互,应用代码的实际计算由 worker 承担,Temporal 服务端负责协调和保存执行信息,并不会替我们直接运行全部业务代码。这样执行模型调用的工作进程和负责其他操作的工作进程,就可以按需要分配不同资源。
右侧补齐了 Temporal 服务端的主要职责。上方任务队列按任务类型分别承接活动任务和工作流任务,让对应的 worker 领取。工作队列把任务产生与实际处理分开,不要求某个进程始终在线等待同一次调用。下方是执行历史与状态,它通过双向箭头连接持久存储。活动什么时候被安排、何时完成、返回什么结果,都会成为恢复流程的重要依据。如果某个 worker 退出,后续 worker 可以结合历史恢复工作流状态,再从需要继续的位置向前推进。这里的持久化主要保存执行事件、输入输出和相关状态,并不是把任意程序的整块内存自动复制到数据库。恢复依赖工作流的确定性,以及活动边界和重试规则的正确设计。把队列、worker 和执行历史连起来,就能进一步理解一次流程启动之后的任务流转。
CS329Z智能体工程:05.2:正常路径、失败重试与长时间等待 ⏳
理解了持久执行的基本思想,我们把它接到前面的日历助手上。接下来的代码和运行截图会把正常完成、活动失败重试以及工作进程停止后恢复这三种情况连起来讲清楚。先从没有故障的情况开始:日历助手仍然完成同样的任务,从邮件提取事件、选择表情、再写入日历。变化在于,我们把容易失败的操作与控制流程分开,让 Temporal 记录执行进度,为后面的恢复建立基础。
代码把三个实际操作定义成 activity,也就是可以独立执行和重试的活动。上半部分的事件提取函数仍然调用 DSPy 模块。如果事件名称、日期或开始时间缺失,就返回空值;否则整理成包含标题、日期、开始时间和持续分钟数的字典。中间的活动根据标题选择表情,最下面的活动调用日历接口,把这些字段和表情一起写进去。注意这三个函数上方都有活动定义装饰器,它们是交给工作进程执行的边界。模型调用、网络访问和日历写入都可能失败,因此适合放在这里。尤其写日历会改变外部状态,生产环境还应使用事件标识和聚重逻辑,避免重试造成重复日程。
有了活动,工作流负责决定它们怎样衔接。代码先配置重试策略:首次重试间隔一秒,最多尝试 5 次,这 5 次包含最初的执行。运行方法先等待提取结果。如果没有事件直接结束;否则把标题交给表情活动;最后将事件详情和表情一起交给日历活动。每次调用都通过执行活动接口发起,并设置单次活动从开始到完成的一分钟超时。这样,业务顺序与失败处理就分开了。工作流本身必须支持确定性重放,相同历史应产生一致的控制决策,因此大模型请求和外部读写放在活动中。截图虽然关闭了工作流沙箱,也没有解除这项确定性要求。重放时,已记录的活动结果会用于恢复状态。
定义完工作流,还需要一个真正执行代码的 worker,也就是工作进程。它连接到 Temporal 服务,监听名为 calendar 的任务队列,并注册自己认识的工作流类型和三个活动函数。活动执行器配置了 4 个线程用于运行这些同步活动,这不代表单个日历流程的三个步骤自动并行,因为它们仍有明确的数据依赖。最后一行把工作进程的运行方法放入异步任务。这里要分清职责:服务端保存历史、管理调度和计时;工作进程拉取任务、执行我们的业务代码。只有服务端而没有合适的工作进程,流程也无法继续进行新的业务计算。
调用方通过客户端启动两次工作流,分别给它们设置独立的工作流标识,但都投递到相同的日历任务队列。第一段输入说 10 月 8 日星期四下午 3 点有答疑时间,包含可以提取的事件,因此截图输出了“已经加入日历”。第二段只是感谢对方发送幻灯片,没有要安排的活动,输出就是“没有发现事件”。这个对照很重要:工作流成功运行,不代表一定要产生一次外部写入。识别到没有事件并正常结束,也是正确的业务结果。客户端的执行工作流方法会等待结果,执行过程中的活动与状态变化则可以在 Temporal 的界面中检查。
把返回值和日历界面对应起来看,星期四 10 月 8 日已经出现下午 3 点到 4 点的答疑日程,旁边还有时钟表情。这张卡片把前面三个活动串在一起:提取模块提供标题和时间,表情模块提供图标,日历接口负责实际保存。其他课程、牙医预约和咖啡会面等日程属于界面的上下文,不应只因为它们同时出现,就认为都是刚才那封邮件生成的。检查这类助手,既要核对程序是否返回成功,也要检查最终可见的日期、开始时间和持续时长,才能确认模型的结构化输出与业务效果一致。
日历结果回答了“完成了什么”,时间线则告诉我们“怎样完成”。横向表示时间,下面三个活动条依次对应事件提取、表情选择和日历写入,上方贯穿整个过程的绿色条表示工作流。先拿到事件详情才能选择表情,这两项都具备后才写入日历,所以它们在时间上前后衔接。界面还能切换是否显示空闲时间,截图中的折叠标记提醒我们,画面上的横向距离未必可以直接当作完整的真实等待时间。排查慢请求时,应进入具体活动查看时间和输入输出。这张截图展示的是一次运行记录,不能据此推断模型服务通常只需要几毫秒。
正常路径已经打通,现在人为加入一个会暂时失败的活动。我们要观察的不是异常有没有出现,而是失败是否被记录、是否按策略重试,以及成功后能否继续完成原来的业务目标。
新的表情函数故意模拟不稳定服务。它读取活动上下文里的尝试次数,只要次数小于 3 就抛出服务暂时不可用的异常。因此,第一次和第二次会失败,第三次才调用真正的表情模块。装饰器明确把这个新函数注册为原来的活动名称,所以工作流仍然请求同一种活动,替换的是工作进程提供的实现。这样可以在不修改业务流程的前提下测试恢复行为。这里的重试针对短暂故障。如果输入本身有问题,或者操作明确不允许执行,就需要分类处理错误,不能把每一种失败都理解成多试几次必然成功。工作进程这次监听 retry 队列,并把活动列表中的表情函数换成刚才的故障版本。
输入要求在 10 月 6 日星期二上午 9 点安排 30 分钟散步。截图的日志依次显示第一次、第二次、第三次尝试,随后返回“已经加入日历”。代码没有再写一个手动重试循环,因为工作流调用活动时设置的重试策略已经交给 Temporal 管理。默认指数退避下,第一次失败后约等待一秒,第二次失败后约等待两秒,再安排新的尝试。成功之前,工作流停在等待这个活动的地方;成功之后才进入日历写入。我们保留已完成步骤的进度,而不是因为表情服务短暂失败,就把整个任务从头重做。
日历中新增的散步卡片位于星期二上午 9 点到 9 点半,标题和持续时间与输入对应,旁边的步行表情则来自最终成功的那次活动。这个结果说明,表情服务经历了短暂失败,业务仍然能够继续完成。但用户界面只展示最终状态,看不出背后已经尝试了 3 次。因此,产品层面的成功率和底层活动的重试率应分别观察。如果用户暂时没有感知失败,重试次数却持续上升,系统可能已经出现服务不稳定或容量不足的问题。成功结果并不能替代对执行过程的监控。
对应的时间线把重试过程显露出来:事件提取很快完成,随后表情活动带有重试符号和数字 3,表示经历了三次尝试,较长的一段包含失败后的等待,日历写入则在末尾才发生。颜色从失败阶段过渡到成功阶段,可以帮助我们定位耗时集中在哪里。这里的长条不能直接理解成模型持续计算了整整 3 秒,因为退避等待也占据时间。排查时要把单次调用耗时、重试次数和两次尝试之间的等待分开。由此才能决定是优化模型请求、调整超时与退避参数,还是修复不稳定的下游服务。
短暂失败可以通过重试处理,但还有另一类常见情况:业务本来就需要等待很久。比如先安排会议,再在稍后提醒用户。这时等待状态也应持久保存,不能依赖某个工作进程一直活着。
提醒工作流先调用日历工作流作为子工作流,并等待它完成,然后通过工作流的 sleep 方法等待指定秒数,最后返回提醒文本。这里复用了已有日历流程,所以工作进程同时注册提醒和日历两种工作流以及它们需要的三个活动。关键是这个 sleep 对应服务端持久记录的计时器。等待时,不需要一直占用线程来睡眠。即使工作进程停止,时间到了这件事仍然可以被保留下来。也要看清示例边界:代码只是等待传入的描述后返回一段字符串,并没有实现短信、邮件或推送发送;真正发送提醒时,应再增加一个处理外部写入的活动。
演示输入是一场星期四上午 11 点开始、持续 60 分钟的实验室会议,同时传入 20 秒的等待时间。客户端使用启动工作流方法拿到句柄,不必在这一行等到工作流完全结束。外层程序等待 10 秒后,取消正在运行的工作进程任务,用来模拟执行者离开。随后查询服务端状态,截图显示工作流仍然处于运行中。这里的运行中表示业务执行尚未结束,不表示此时一定有进程正在执行模型调用。还要区分两个取消对象:停止的是 worker 的异步运行任务,并没有向工作流发送取消请求,所以服务端保存的进度和计时器仍然存在。
恢复时,新工作进程继续监听同一个 reminders 队列,并注册相同的工作流和活动。客户端通过原来的句柄等待最终结果。截图随后返回会议提醒,查询日历也只看到一条实验室会议。原因是前面的日历子流程已经完成,其结果已经记录在历史中,重放用于重建工作流状态,不会再次调用这些已经完成并记录的模型活动。接着,程序依据原先的计时器继续前进。不过,这个例子不能推广成所有外部副作用都天然只执行一次。如果外部写入成功、完成确认却丢失,活动仍可能重试。因此,日历聚重这样的幂等设计依然必要。
最终日历中,星期四上午 11 点到 12 点出现实验室会议,下午 3 点的答疑时间也仍然保留。结合刚才的历史记录可以确认,在这次停止发生于等待阶段的演示里,恢复工作进程没有把已完成的日历添加步骤再做一次。这个检查方法值得保留:恢复后既看工作流是否结束,也核对业务状态是否符合预期,包括事件有没有丢失、时间是否正确、有没有重复。只有业务结果与执行记录能够对应起来,我们才知道恢复机制真正保存了哪些进度,而不只是看到一个成功状态就结束排查。
时间线里,日历子工作流位于最开始,随后一条橙色长条标着 20 秒,表示持久计时器的等待区间,上方绿色条覆盖整个提醒工作流的生命周期。这 20 秒主要是在等待,不等于消耗了 20 秒模型计算或占用了一个线程。即使工作进程中途停止,恢复后也会沿用原先登记的计时状态,而不是重新开始等待完整的 20 秒。如果截止时刻没有可用的工作进程,后续业务代码仍要等执行者恢复才能运行。因此,持久性保证等待不会因为进程消失而被遗忘,但实际提醒延迟还取决于工作进程和服务是否可用。
CS329Z智能体工程:05.3:可观测性工具与运行记录 🔍
Temporal 的历史帮助我们理解活动、重试和计时。进一步调试智能体,还需要观察具体提示词、模型输出、成本和质量反馈。接下来这几种可观测性工具展示了不同层次的观察方式。
MFlow 的截图展开了一次语言模型调用。左边是调用节点和耗时,右边把输入与输出分开放置,还提供属性和事件标签。输入提示词要求模型解释 MFlow 的用途,下方则能检查模型实际返回的内容。这种界面让我们从最终答案追溯到具体请求:提示词是否按预期拼接,传入的数据是否完整,模型到底返回了什么。如果日历助手错误地提取了时间,就可以先定位提取模块,再查看那次调用的输入输出,而不必只根据最后的日历卡片猜测原因。记录调用过程的价值在于保留可检查的证据。
LangSmith 的示例把视角扩展到一组生产运行。上方汇总调用数量、词元用量、延迟和反馈,下面每一行对应一次运行,可以查看输入、耗时以及连贯性、相关性等评价。右侧筛选器还能按反馈来源、运行类型、成功状态或延迟条件缩小范围。比如先筛出超过 10 秒的运行,再进入具体调用链,就能寻找共同的慢步骤。这里的界面数字用于说明功能,不作为严格的性能统计样本。实际分析分位数时,应保证来自同一统计口径。把运行记录与质量反馈连接起来,才有办法判断一次修改究竟改善了哪些任务。
Langfuse 的仪表盘展示了另一层观察方式。左上比较模型延迟,并可切换第 50、第 75 和第 90 分位数;中上按模型展示费用随时间的变化;右上按记录类型观察数量,包括普通记录、调试记录和错误;下面分别汇总模型成本、调用轨迹数量和评价分数。这几块应联合阅读:费用上涨可能来自流量增加,也可能来自重试增多或每次请求变长;中位延迟稳定,也不代表较慢的一部分用户没有受到影响。出现异常趋势后,再下钻到具体轨迹和模型调用,才能把宏观现象和实际原因连接起来。
把编排和可观测性放在一起,日历助手的工程结构就完整了。编排明确各步骤的依赖,保存进度,并处理超时、重试与长期等待。可观测性帮助我们检查输入输出、定位错误,持续关注延迟、成本和质量。这些做法来自成熟的软件工程,也适用于智能体。真正投入使用时,模型能完成一次任务只是起点,还要通过评测制造成功率,通过监控知道系统正在发生什么,通过持久执行制造中断后如何继续。把这些机制接起来,才能让一个可演示的原型逐步成为可以稳定维护的系统。
CS329Z智能体工程:06:最佳实践与工程挑战 🧠
现在我们已经看过工作流、智能体运行框架、持久编排和观测工具,最后把它们收束为几个设计原则:从简单方案开始,为失败和恢复留出路径,用评测指导改进,并理解框架背后的真实行为。
图中的三个位置代表不同程度的控制方式。左边模型只是大系统里的一个组件;中间程序明确组织多个模型或工具,主要控制流仍由代码决定;右边智能体让模型在运行时决定下一步做什么。它们对应不同需求,并不是越靠右越先进。像固定的邮件提取和日历写入流程,明确编排可能已经足够。确实需要智能体时,也要仔细选择工具,因为工具越多,模型面对的选择和系统需要维护的接口也越多。如果拥有足够高质量数据,还可以考虑优化模型本身。复杂度应由任务与测量结果来解释,而不是由框架提供多少功能来决定。
设计失败路径时,要记录模型调用、工具调用、状态变化和错误,并设置超时、重试上限与执行预算。截图底部展示了一个命令被环境阻止的反馈。它不仅说失败,还解释了原因和允许采用的等待方式。把这种有信息量的错误反馈给智能体,才可能让它调整下一步,而不是反复提交同样的命令。当然,反馈要求遵守的限制仍然有效,不能把恢复理解成绕过限制。右侧的工具标志提醒我们,持久执行与日志监控可以互相补充。实践中还应区别暂时性的服务故障、需要修正的参数错误,以及应直接停止的拒绝情况,让每类失败走到合适的处理路径。
Supervisor 将执行轨迹变成原智能体可以操作的对象。左上方的监督者先创建任务,观察模型和工具调用,在错误编辑处介入、回退后分叉出修复路径。右侧代码对应观察、回退和分叉三个工具。下方三组实验分别考察运行时干预、反事实优化和树形强化学习。左图是 Cooper Bench 上的运行时干预:监督者通过观察协作过程及时介入来帮助协调,结对编程通过率从 28.8% 提高到加入不同监督模型后的 45.3% 和 54.7%。中图是 Live Bench 上的反事实优化:修改工作流后,从行为开始分级的位置重放候选路径,比较另一种做法是否更好。纵轴是通过率,横轴是优化所用分钟数。未优化为 30.7%,Meta-Harness 为 40%,GPA 为 48.7%,Pro 为 51%,而且 Pro 的位置比 Meta-Harness 更靠左,表示耗时更少。右图对应 Terminal-Bench2 上的树形强化学习方法,在关键状态处分叉,探索不同后续路径,把结果用于判断哪些决策带来更好的效果。柱状图比较原始基线、普通 GRPO 和树形方法,第一组依次为 26.1%、34.2%、39.4%,第二组为 3.3%、33.8%、37.2%。以上都是特定实验设置的结果。
回退也有环境边界。受控文件状态可以恢复,已经发出的邮件或实际付款并不能被执行轨迹自动撤销。要判断是否值得加入监督智能体、换模型或增加重试,最有说服力的依据是评测。图中的天平提示:我们需要同时衡量质量、成本和延迟,而不是只挑一个漂亮数字。
先定义业务上的成功,再准备具有代表性的任务,覆盖常见输入、边界情况和故障场景。仍以日历助手为例,普通会议邮件、没有事件的感谢邮件、缺少时间的邮件以及日历接口超时,都应该纳入测试。然后用相同任务比较修改前后的结果。评测不仅用于最终验收,也为提示词优化、工具设计和框架选择提供持续反馈,让改进方向有可检验的依据。
左侧把通用编程榜单和系统名称打上叉,右侧强调自定义领域评测。这里不是说公开基准没有价值,而是提醒我们:指标必须对应实际目标。如果你正在开发日历、客服或信息抽取系统,通用代码任务上的提升并不能直接证明业务效果更好。更合适的做法是收集自己的典型任务和失败案例,明确答案、状态变化与成本要求,建立领域评测集。对于通用编程智能体框架,或者旨在改进广泛智能体能力的方法,公开编程基准仍然可以提供有用证据。先明确要服务谁、完成什么,再选择能够衡量这些目标的任务。
CS329Z智能体工程:06.1:理解抽象边界与持续改进循环 🔁
框架提高了开发效率,也让一些底层行为被隐藏起来。右边漫画要求直接展示提示词,表达的就是这种担忧。封装再方便,开发者仍然需要知道实际给模型发了什么。复杂系统失败时,问题可能来自提示词、数据转换、工具参数、重试规则或状态管理。应沿着执行轨迹逐层定位,不能笼统归因为模型不聪明。另一个代价是依赖维护:框架和外部库升级后,接口或默认行为可能变化。因此,要理解关键抽象,保留可观察的边界,并通过评测检查变更是否影响业务。只有能够解释系统为什么这样运行,才能在出问题时可靠地修复它。
把这些原则落实到工作流程,就是“构建、运行、评测、改进,然后再次运行”。先用简单实现建立基线,观察真实任务中最常见的失败,再选择能够解决这些失败的机制。需要恢复能力,就补上明确的活动边界、超时与幂等;需要提升质量,就通过轨迹诊断和领域评测寻找原因。每增加一层复杂度,都应该能够说明它改善了什么,以及付出了多少成本和延迟。这个循环没有一次性完成的终点,因为输入分布、模型与依赖都会变化。持续理解系统、持续验证行为,才是智能体工程真正的日常工作。
CS329Z智能体工程:07:课程总结 📚
本节课中,我们一起学习了智能体框架与编排的完整脉络。各种框架和示例都服务于同一个目标:让模型能力进入一个可以运行、观察、评价和改进的软件系统。我们可以沿着任务表达、控制流程、执行可靠性和效果验证这几个层次,重新串起所有内容。
下面先列出本课最后需要带走的关键结论。
-
工程化智能体意味着持续构建、运行、评测与改进。DSPy 和 LangGraph 展示了如何表达模型程序与控制流程。
-
智能体运行框架承接工具、上下文和执行循环。Temporal 让活动重试、状态恢复和长期等待有明确的运行机制。观测工具则把模型调用和业务行为变成可以检查的记录。
-
最后,选简单且适合任务的方案,用领域评测衡量质量、成本和延迟,并理解每一层抽象的边界。
带着这套思路再看一个智能体系统,就不仅能问它能不能完成任务,还能进一步判断它为什么成功、在哪里失败,以及下一次应该怎样改进。
斯坦福 CS329Z 智能体工程:06:智能体框架与编排
在本节课中,我们将学习智能体系统的设计模式与支撑结构。我们会从控制流程开始,逐步理解常见结构的工作方式,最后把这些选择放进真实的智能体运行框架中观察。进入内容前,先保留原课程的几项安排。这里提醒同学尽快申请谷歌云额度,详情查看课程的 ID 平台。项目提案对格式相对宽松,但其中与期末报告要严格遵守 ICLR 模板。课件写明,第一次作业在 10 月 30 日星期五截止,其中演示是 11 月 4 日,报告是 11 月 6 日。这些是原课程当时的通知,学习视频中的日期不代表对观看者新增的截止要求。
接下来回到设计本身。
设计模式与智能体结构
设计模式这个想法来自软件工程。左边是经典的设计模式著作,右边用单例模式说明模式描述的是可以反复使用的结构。沿着箭头看,客户端通过 getInstance() 取得对象,类内部保存一个 instance 引用。构造函数是私有的,外部不能随意创建新的实例。下面的伪代码先判断引用是否为空,如果没有对象就创建,然后返回同一个引用。这样调用方共享一个受控实例。图中的注释还提醒多线程环境需要在关键位置处理并发。不能直接照抄这几行伪代码,就认为是线程安全。
这个例子和智能体有什么关系?我们也会遇到反复出现的组织问题,比如谁负责分派任务,谁检查结果,失败后回到哪里。给这些结构命名可以帮助团队讨论和复用经验,但具体实现仍要处理自己的运行环境与约束。
马丁·福勒在这里强调,模式是一种尚待完成的方案,落地时需要我们自己补全并适应具体环境。比如刚才的单例图已经告诉我们对象由谁创建、通过什么入口访问,却没有替我们解决所有并发与生命周期问题。同样,画出一个智能体的评估反馈环,并不等于已经有了可靠评估标准,我们还得明确什么算失败、反馈如何表达、最多允许修改几次。学习模式最终是为了看清这些必须由工程师完成的决定。
搭建智能体系统会涉及许多设计决定:哪些步骤固定下来,哪些让模型判断;多个任务要依次执行还是可以并行;历史记录保留多少;什么结果需要再次检查。这些选择共同决定系统的行为。接下来介绍的模式为这些决定提供一组清楚的表达方式。真正实践时还需要按照任务特点组合和调整,并用实际效果判断是否合适。
这一讲有三个学习目标。首先,能说明常见工作流与智能体设计模式,包括它们的输入如何流动、下一步由谁决定。其次,能比较不同的上下文管理方案:哪些信息进入模型,哪些放在外部,怎样控制信息量,同时保留完成任务所需的依据。最后,能够分析真实的智能体支撑框架(agent harness),看看这些模式怎样变成具体的运行机制。课件选择派(PAI)作为案例。这里重要的能力是从一个实际系统中认出设计取舍,并说明为什么这样组织调用、状态和上下文。
我们先回顾工作流与智能体的区别,再学习工作流的组织方式,然后进入推理与行动的设计,以及上下文管理,最后分析派。原课件给这些部分安排了 5、15、20、20 和 10 分钟,那是课堂节奏的参考。这段讲解会随着图示和例子展开,让前一个问题自然引出后一个问题。先抓住贯穿全讲的一条线索:系统下一步做什么,到底由谁决定。
工作流与智能体:控制权分布
很多系统都会调用语言模型,也都会使用工具,所以只问有没有模型、有没有工具,还不足以区分它们。更有用的观察方法是跟着一次任务的执行过程,看看分支、循环和终止条件由谁控制。掌握这一点,后面的各种模式就容易放到同一张地图里理解。
图中从左到右把模型、工作流和智能体放在一条连续线上。左边的模型是更大系统中的一个组成部分,接收输入并生成输出。下面列出的模型名称只是课件中的事例,关键在于单次模型调用本身还没有规定完整业务如何运行。中间是工作流,也叫复合人工智能系统,它把多个模型和工具组合起来,但整体逻辑与编排写在程序里。例如邮件分类器可以先读邮件、再分类、最后保存标签;新闻摘要系统也可以按固定顺序抓取、筛选、总结。右边是智能体,模型承担更多控制流程的职责,根据已经看到的结果决定接下来调用什么工具、是否继续调查、何时结束。客服智能体与编程智能体都是这样的应用例子。这条线强调的是控制权的分布,而不是模型能力从低到高的排名。一个系统也可以把固定流程和自主决策放在不同层次。
控制权的变化会带来可预测性与灵活性的取舍。左图横轴是自主程度,越往右,模型越能决定行动;纵轴是可预测性,越往上执行路径越容易提前掌握。工作流化在左上方,因为分支与调用顺序来自程序,开发者更容易检查会走哪些路径;智能体化在右下方,因为它会根据新的观察改变行动,适应性更强,但具体会调用几次工具、绕过哪些中间步骤,往往事先无法完全确定。这里是一张定性示意图,曲线不是实验测出来的准确函数,也不意味着自主性增加就一定按这个速度降低可靠性。实际设计中可以给模型明确的工具范围、预算与退出规则,保留一定灵活性,同时限制执行的不确定性。我们的目标是让控制权的安排与任务真正需要的自由度相匹配。
现在沿着两张实际流程图走一遍。左边从开始节点进入 extract(提取信息),随后有两条虚线分支:一条直接到结束,另一条进入选择表情的节点。进入后流程固定向下,先选表情,再把事件加入日历,最后结束。比如在日历助手里,提取后发现没有可处理的日程就可以提前结束,发现有效日程才继续后面的步骤。这是对分支用途的一个说明,图本身并没有展开具体判断代码。重点在于允许出现哪些节点、它们按什么顺序执行,已经由开发者安排好了。
再看右边:开始后先调用模型,模型可以直接结束,也可以选择进入工具节点。工具执行以后,结果重新回到模型,于是模型根据新观察再次决定是继续调用工具还是给出最终回答。这里形成的是“模型→工具→模型”的循环。比如工具报告日历冲突,模型可能决定重新查询时间,再尝试另一个方案。因此,两边都能使用语言模型和工具,也都存在分支,差别是左边把业务步骤预先展开,右边让模型在循环内不断选择行动。图中节点数量更少,不代表实际执行步骤一定更少。
把刚才的观察整理起来,可以从四个维度比较。第一是控制流:工作流主要由代码控制,智能体更多由语言模型决定。第二是适应方式:工作流也能有分支、路由和循环,只是这些可能的路径事先编好了;智能体会结合观察结果选择下一步。第三是适用任务:结构清楚、反复出现的任务适合固定工作流;需要探索、不能提前写出全部调查步骤的任务更适合使用智能体。第四,看客服例子:工作流可以固定执行工单分类、检索相关政策、起草自动回复;智能体则可能需要查询订单、检查操作记录、继续追问线索,用工具逐步找到并解决问题。
表格底部还有一句很关键的话:真实系统经常把二者结合起来。例如,外层先按固定规则校验工单、分配权限,遇到复杂情况再交给智能体调查。处理完成后,回到固定的验收与记录步骤。因此,这不是一次非此即彼的选择,而是决定在系统的哪一部分给模型更多控制权。
五种工作流模式:提示链、路由、并行化、编排者与工作者、评估者与优化者
理解控制权以后,先来看那些可以预先组织好的工作。我们需要回答几个具体问题:有依赖的步骤怎样串联?不同类型的请求怎样分流?相互独立的工作怎样同时执行?结果不够好时又怎样返回修改?接下来的 5 种工作流模式分别为这些问题提供基本结构。
课件采用了 Anthropic 关于构建有效智能体的工程文章中的分类。我们会保留图中的每个角色,顺着箭头解释一次输入怎样变成输出。这些名称可以帮助我们沟通架构,但真正值得记住的是数据依赖、任务分工和控制逻辑。理解了结构,就可以用自己熟悉的代码或编排框架实现。
先建立一个整体印象。提示链把大问题拆成前后相接的小步骤,前一个结果成为后一个步骤的材料。路由先判断请求属于哪一类,再把它交给合适的模型或提示。并行化让多个调用同时处理独立工作,最后合并结果。编排者与工作者模式先由一个模型根据当前任务确定子任务,再分派出去完成。评估者与优化者模式则让生成和评价形成循环,依据反馈逐步修改输出。它们对应的是依赖、选择、并发、动态分工与反馈这五类结构问题。接下来要特别分清:路由是选择一条合适的处理路径,并行是启动多条工作路径,而动态分工还要先决定这些工作究竟是什么。
提示链(Prompt Chaining)
先看提示链。输入从左边进入第一次模型调用,生成第一个中间结果。这个结果并不马上交给下一个模型,而是先经过中间的检查关卡。检查通过,沿上方箭头进入第二次调用;第二个结果再传给第三次调用,最后形成输出。检查失败,就沿下方虚线提前退出。因此,提示链既包含顺序依赖,也可以加入程序化检查,避免错误材料一路传到最后。
举一个具体的文档处理例子。第一步,从会议记录里提取决定和代办事项;检查关卡确认必要字段齐全,而且每条事项能找到记录中的依据。通过以后,第二步按负责人整理行动清单;第三步把清单写成面向团队的简明通知。如果第一步提取不完整,就先停止,提示需要补充材料。这样每次模型调用承担的任务都更集中,中间结果也更容易检查。代价是后面的步骤必须等待前面完成,多次串行调用会增加延迟。它适合那些能拆出稳定步骤、拆分后又确实更容易做对的任务。拆得越细并不自动越好,还要避免在阶段之间丢失信息,并评估最终准确性是否真正提高。
路由(Routing)
如果不同请求需要不同处理方式,就可以使用路由。图中输入先进入路由器,它判断当前请求属于哪一类,然后选择上、中、下三条路径中的合适一条。每条路径都有自己的模型调用,处理结果再到右边输出。图里的多条箭头表示可选择的分支,不意味着每个请求都要把三个模型全部调用一遍。
这里的专门化可以发生在两个层面。一是模型相同,但提示、工具或者业务流程不同。例如,客服请求中退款问题走退款政策处理,账号登录问题走故障排查,普通咨询走知识问答。二是模型本身不同,把容易处理的常见问题交给成本较低的模型,把困难或异常问题交给更强的模型。这样既能分别优化各类任务,也有机会降低总体成本与等待时间。
不过,路由本身需要时间,而且分类错误会让请求进入不合适的流程。所以使用它的前提是:类别确实有意义、可以比较可靠地识别,并且专门处理的收益足以覆盖路由开销。对类别不清楚的情况,还应设计明确的兜底路径。
并行化(Parallelization)
接下来把选择一条路径变成同时启动多条路径,就得到并行化。图中输入同时进入三个模型调用,三份结果会到汇总器(aggregator),由它生成最终输出。汇总器可以执行固定合并规则,也可以做进一步判断,具体取决于任务。
课件区分了两种用法。第一种叫分工:各个调用处理相互独立的部分。这里给的例子是一边判断用户请求是否符合安全规则,一边准备对请求的回答。两项工作并行进行,最后由汇总逻辑结合检查结果决定如何输出。检查没有完成前,就不应把本来需要它批准的回答或外部操作提前放出去。第二种叫投票:对同一个输入进行多次处理,再综合这些尝试得出结论。例如让多个调用独立判断一道问题的答案,最后按明确规则选择结果,这属于推理时增加计算量的一种方式。
这两种用法虽然图形相同,但目的不同:分工的目的是让每个调用专注不同方面,投票的目的是获得多个候选判断。并行化可以缩短独立工作的总等待时间,但仍会消耗多次调用的资源,而且最终通常需要等待所需结果到齐。如果多个调用以同样方式犯错,票数更多也不能保证正确,汇总规则和输出差异同样重要。
编排者与工作者(Orchestrator-Workers)
普通并行化假设我们已经知道要分哪些工作。可是面对一个新的复杂任务,子任务的数量和内容可能事先都不确定,这时可以加入编排者。看输入先交给左侧的编排者,它理解任务、动态拆分,交给中间的工作模型。三个工作框只是一个示意,实际需要几个以及每个做什么,要由当前输入决定。各个工作者完成后,结果进入综合处理角色,再组织成最终输出。
例如需要排查一个网页功能为什么失效。编排者读过问题以后,可能安排一个工作者检查前端事件,一个检查后端接口,一个检查测试与配置;换成另一个问题,可能只需要检查两处,或者需要调查数据库迁移。和固定并行流程的区别,就在于这些具体分工是运行时产生的。综合环节还要解决结果之间的关系,例如前后端对字段的判断不一致,不能简单拼接报告就结束。
这种模式虽然让模型参与了任务分解,但外层仍然可以由程序固定执行“分解→分派→汇总”这几个阶段,所以这里把它列在工作流模式中。落地时还需明确工作者的输入、产出、依赖和权限;存在先后依赖的子任务,不能为了并行而强行同时开始。
评估者与优化者(Evaluator-Optimizer)
如果第一次输出还不够好,可以使用评估者与优化者模式。图中输入先进入生成者,生成一个候选方案;沿上方弧线交给评估者。评估者有两种选择:如果满足要求,沿右侧箭头接受并输出;如果不满足,就沿下方弧线把拒绝原因和反馈返回生成者。生成者据此修改,再提交评估。
这个循环中的关键是反馈必须能推动下一次改进。课件列出两个必要条件:第一,评价依据能够说清楚;第二,模型有能力依据这些标准给出有效反馈。此外,生成者也必须能利用反馈改进。比如把一段技术说明改写给初学者,标准可以是术语是否解释、例子是否和结论一致、关键步骤是否遗漏。评估者如果指出“第二段直接使用了‘缓存命中’却没有解释其含义”,生成者就知道要补什么;如果反馈只有“写得不好”,就很难指导具体修改。
这里的优化通常是对当前输出进行改写,并不等于更新模型权重。评估者也可能判断错误,所以应当尽量结合可检查的标准;能够用程序验证的部分,可以加入实际检查。工程上还要设置轮次或成本上限,并记录是否确实改善,避免在反复修改中丢掉原有优点,或者一直循环而无法结束。
真实系统通常会组合这些模式。比如一个技术支持助手,先用路由区分普通问答和复杂故障;普通问题可以沿提示链完成检索与回答;复杂故障则交给编排者动态分派日志分析、配置检查等工作,其中独立的调查并行进行;综合得到方案后,再经过评估与修改,确认依据充分,最后输出给用户。这样 5 种模式就成为可以嵌套的结构。设计时需要持续问:新增这一层到底解决了什么问题?它带来的延迟、成本和失败点是否值得?
案例一:STORM——多视角调查与提纲生成
有了这些基本结构,接下来我们就通过 STORM 和 ADAS 两个案例,观察它们怎样组合成完整系统,并怎样用实验检验组合后的效果。
先看 STORM。左边用户只给出一个主题,想了解 2024 年的花莲地震;右边的输出已经是一篇带目录、分节和引用的长文。目录从地质背景、历史地震展开到这次地震的影响,包括人员伤亡、建筑损坏、余震,再到救援医疗响应、损失评估和长期恢复。这里展示的重点是系统怎样把一个宽泛问题变成覆盖多个方面、可以继续阅读的知识结构。假如只把主题交给模型,让它一次写完,模型很容易围绕自己熟悉的内容反复扩写,却漏掉重要角度。STORM 把工作提前到写作之前:先调查应该了解什么,再搜集证据和组织提纲,最后才写正文。因此,右边长文的质量既依赖生成能力,也依赖前面这套信息发现与组织流程。
顺着这张图,我们把 STORM 的信息流完整走一遍。起点是左上角的主题,记作 T。第一步,先调查相近主题的文章,观察别人从哪些角度组织内容。第二步,据此识别多种研究视角(图中的 P)。以地震为例,地质背景、建筑安全、救援恢复,就可能引出不同问题。这些是帮助理解视角的例子,每个视角都交给一位模拟的百科写作者。第三步,写作者阅读已有对话,并向专家提问(红色 Q 是问题)。右边这个专家角色同样由模型扮演,但回答必须借助检索。第四步,把问题拆成搜索查询;第五步,搜索并筛选来源;第六步,综合证据形成回答(绿色箭头上的 A)。回答又进入对话,让写作者继续追问,因此后续问题可以建立在新发现之上。
不同颜色叠起来的背景代表多组视角对话。对话最终汇总为下方的 C0 到 Cn,同时把可信来源积累为右下角的参考资料集合 R。另一条支路从主题直接生成初稿提纲(记作 OD),这是第七步。第八步,把初稿提纲和多视角对话结合起来,补足内容、调整层级,得到最终提纲 O。随后,写正文时再按章节检索相关证据,生成带引用的段落,并处理重复内容。注意:提纲和证据分别承担结构与事实依据,两者缺一不可。
红色框把刚才的流程重新标成了设计模式。左侧从调查文章到识别视角,以及生成和修订提纲,都有前后依赖,体现提示链。下方并行化的红框指出,直接生成初稿提纲与开展多视角调查是可以分别推进的两条支路;等二者都完成,再汇合到修订步骤。不同视角的调查本身也能分别展开。上面的大框标为编排者与工作者:系统先形成分工,再让不同角色负责各自的调查。不过 STORM 也提醒,这个实例中的工作分配叫“固定”,所以它与并行化非常接近。内层问答循环被标成近似的评估者与优化者,因为新回答会暴露信息缺口,推动下一轮追问;它并不是严格意义上先评分、再修改同一份答案。理解这些标注的关键是根据依赖、分工和反馈关系识别模式,不必强行把每一个框都塞进唯一类别。同一个系统可以同时具有几种模式,不同边界下也会有不同描述。
这套流程是否有价值,可以看表里的两组指标。左边与人工文章比较,R1 和 ROUGE-L 衡量文本重合情况,实体召回衡量参考文章中的实体覆盖。它们都不能直接等同于事实正确率。右边是自动评估器按 1 到 5 分量表给出的趣味性、组织性、相关性和覆盖度。
前三行分别是直接生成、普通检索增强生成(RAG),以及按提纲分解检索和写作的 O-RAG。普通 RAG 的 R1 是 28.52,按提纲组织后变成 44.26,这说明结构化写作很重要。STORM 进一步达到 45.82,ROUGE-L 为 16.70,实体召回为 14.10,高于 O-RAG 的 12.57。右边 STORM 的相关性 4.45、覆盖度 4.88,也高于 O-RAG 的 4.09 和 4.70;趣味性从 3.90 到 3.99,组织性从 4.79 到 4.82。带粗体标记的差异是相对 O-RAG 在配对检验中达到显著性的项目,不能把所有加粗数字都理解为显著提升。
最后一行去掉提纲阶段,R1 跌到 26.77,实体召回降到 7.39,覆盖度降到 4.37,它的组织性反而略高,为 4.87。这提醒我们单一评分并不全面。这组实验支持“先研究、再组织、再写作”的设计,但不能证明每篇文章都可靠,引用与具体事实仍需要核验。
案例二:ADAS——架构搜索与组合
STORM 展示了一套人工设计的组合流程。接下来的 ADAS 更进一步:能否把如何组合组件也交给搜索算法?
图的左边给出四类输入:目标评测任务、允许的推理调用预算、可用的大模型,以及可选的推理时技术。技术清单里有生成、融合、批评、排序、验证,还有测试生成和测试评估。中间的优化器提出一个候选架构,在一部分评测样本上运行得到结果,再据此选择下一套配置。这里采用的贝叶斯优化,可以理解为利用已有试验结果,优先尝试更有希望的配置,减少盲目穷举。右边是最终输出的工作流:多个生成器先产生候选,之后经过批评器、排序器、融合器再输出答案。
这里的架构搜索搜索的是模型调用的数量、角色顺序与组合,并没有重新训练一个神经网络,也没有修改这些模型的权重。还要区分两种开销:寻找好架构本身要花评测成本;部署选中的架构后,每次回答也会承担多次调用成本。
为了真正读懂组合架构,需要把 7 类组件的接口区分清楚。生成器接收任务指令,输出候选答案,每个候选需要一次调用。融合器接收指令和多个候选,重新组织成融合后的答案,每个输出需要一次调用,它会改写内容。批评器也读取候选,但输出各自的优点与缺点,表中记为一次调用,它主要提供评价信息。排序器按质量排列候选,保留前 K 个,表中也是一次调用,它主要负责筛选而不是创造新答案。这四类组件适用范围比较广。
下面三类更针对推理任务。验证器判断候选的推理是否成立。这个实现对每个候选调用两次模型:先形成验证理由,再据此给出判断,所以成本不能只按一个方框算一次。测试生成器从任务描述中形成一组检查条件,调用一次。测试评估器同时读取指令、测试和候选,对每个候选进行一次评估,再给出带分数的结果。特别要注意,ADAS 这里讨论的是模型生成和模型评估的测试,不因看到“单元测试”几个字就自动认定它运行了真实程序或拥有标准答案。实际落地可以另外接执行器,但那是需要明确设计的能力。表中的调用次数也只是成本的一层;候选越多、上下文越长,同样一次调用消耗的 token、时间和费用都可能不同。
看一个针对数学任务搜索得到的具体架构。最左边把同一道题交给四个生成模型:GPT-4o、Llama-3.1 的 4050 亿参数版本、PaLM-2 的 720 亿参数版本,以及 WizardMath 的 8×220 亿参数版本。每个模型采样 5 次,因此初始共有 20 份候选解答。它们进入 Claude-3.5-Sonnet 担任的批评器和排序器,先指出问题,再挑选和排列较好的候选。接着又出现四路并行,但这些角色现在是融合器:它们读取上游筛选后的信息,各自产生改进后的解答,而不是把原题从头独立回答一遍。随后由 GPT-4o 担任验证器,最后再由 Claude-3.5-Sonnet 融合成一个输出。这样就形成了“先扩展→再筛选→再评价筛选→再并行融合→最后验证收束”的结构。
图中同一模型可以承担不同角色,所以角色与模型并不是一一对应。这里列出的型号是论文实验配置,重点是理解这种分工怎样组合,以及错误可能在哪一层被发现或被继续传递,而不是认为这组型号永远是最佳搭配。
再来看性能与预算的关系。横轴是允许使用的最大输入输出 token 预算,从 1 万增加到 10 万;这不是模型参数量,也不是训练数据量。纵轴是三个评测集上的平均分,分别覆盖指令回答、数学和编程,也就是图上列出的 Arena、Auto 和 Math、Contest。蓝色曲线从约 53 分,经过 2 万预算时的约 61 分、5 万时的约 69 分,继续升到 10 万时的约 76 分。橙色 FO 到后面约 63 分,绿色 ADAS 约 65 分,出现较明显的平台。红色曲线是 MoA 的约 61 分参考线,紫色点线是 O1 的约 69 分参考线。参考线只是这个实验中的对照成绩,不表示这两个系统在任何预算下都固定不变。
这张图说明,在这组任务和配置里,额外计算通过恰当的架构组合转化成了收益,但不能据此得出“预算越大永远越好”的结论。收益会受候选质量、验证能力和任务分布限制;增加串行步骤还会增加延迟。另外,纵轴从 40 分开始,视觉差距被放大了。工程上应该比较在相同预算约束下能得到多少提升,再决定是否值得增加这一层流程。
到这里,工作流部分的主线就连起来了:提示链处理步骤依赖,路由负责选择分支,并行化把独立工作展开,编排者与工作者处理任务分解,评估者与优化者利用反馈反复改进。真实系统通常把这些模式组合起来。右边的日历流程就是一个小例子:先提取信息,再决定直接结束,还是选择表情并添加到日历。节点和可走的边由开发者明确限定,因此更容易重现路径、定位哪一步出错。STORM 和 ADAS 则展示了更复杂的组合。工作流最容易调试,并不意味着模型输出完全确定,它只是把控制流程约束得更清楚。当任务路径难以预先列举时,我们才需要进一步讨论让模型动态决定下一步的智能体。
推理与行动的设计:ReAct、Reflection、CRITIC、CodeAct、ReWOO 与 LM Compiler
接下来把关注点从整个工作流移到智能体内部的一次次决策。即使工具已经准备好,模型仍要判断:现在知道什么、缺少什么信息、该采取什么行动,以及失败之后怎样调整。推理与行动设计模式要解决的正是这些问题。
ReAct:推理与行动交替
先从推理和工具使用交替进行的 ReAct 开始。接下来这些方法都是围绕这个闭环中的不同环节提出改进。图里的红框是智能体的核心循环:模型根据任务与当前上下文产生行动,行动作用于环境,环境再把反馈送回来,成为下一次模型调用的输入。比如查询网页后返回的是实际检索结果,执行代码后返回的是输出或错误信息。模型必须依据这些观察更新判断,才能决定下一步。左侧的人可以提出目标、补充信息,必要时介入;下方的停止节点则提醒我们,循环要有明确的退出条件。完成任务可以停止,达到调用或时间预算也需要停止。设计时只有一份工具列表还不够,我们还需要规定模型如何表达行动、怎样识别成功和失败、哪些反馈保留到上下文,以及什么时候需要重新规划。
ReAct 把推理和行动放在同一个过程里。它的出发点是:先根据当前信息判断该做什么,再调用工具,用新的观察继续更新判断。这里强调的是推理与环境交互相互支持,而不是要求每一个简单动作之前都生成一大段文字。复杂决策需要分析,已经明确的步骤则可以直接执行。
我们用一个完整的问答过程来看差别。问题是:“苹果遥控器最初用于控制的那款软件,除了遥控器还能用什么设备控制?”左边有三种失败方式:直接回答猜了 iPod;只写推理的方式把目标软件误认为 Apple TV,于是推导出 iPhone、iPad 等设备,推理看起来连贯,第一步事实却错了;只行动的版本虽然查到了相关软件,但最后给出的却是一个没有回答问题的 “Yes”。
右边的 ReAct 把过程分成四轮。第一轮先明确需要查苹果遥控器,执行搜索,观察中出现了 “Front Row 媒体中心” 这个关键信息。第二轮把它作为中间结果,决定搜索 Front Row,查找其他控制方式;但工具没有直接命中,而是返回多个相近条目。第三轮分析失败原因,看到候选中的 Front Row 软件,于是加上“软件”这个限定词重新搜索。这一步体现了根据观察做消歧,而不是机械重复原查询。第四轮从得到的资料里抽取出该软件可以由苹果遥控器或键盘功能键控制,因此最终调用结束动作,答案是键盘功能键。图中的蓝色搜索和结束动作由模型选择,观察由环境提供;绿色文本表示找到并利用的关键事实。推理负责决定下一步查什么,也负责把证据与问题连接起来;搜索提供新的事实,但搜索本身不会自动完成理解。
不过,知道这个格式并不代表任何模型只靠提示就能熟练执行。两张柱状图的纵轴都是 HotpotQA 的答案完全匹配分数,横轴是 PaLM 模型大小:80 亿、620 亿以及 5400 亿参数。左图只用提示示例:蓝色是直接回答,橙色是思维链,绿色是仅行动,红色是 ReAct。可以看到,80 亿和 620 亿模型的红柱反而最低,让较弱模型同时管理推理、工具调用和观察并不容易。右图改为微调后情况发生变化:80 亿模型的 ReAct 大约达到 25 分,620 亿模型达到 32 分以上,两组中红柱都最高。右图没有 5400 亿的微调柱,不能把空白理解成零分。原论文使用约 3000 条正确轨迹进行微调,说明可以通过训练把“什么时候思考、怎样调用工具、如何利用反馈”变成更稳定的能力。课件把它联系到后训练中的智能体能力培养。这里需要记住的是:这组具体实验支持的结论是“合适的轨迹训练有帮助”,而不是“只要更换提示格式就必然得到最好的智能体”。
这张表把同一基础模型上的两种方式单独比较。使用 PaLM 5400 亿参数模型,HotpotQA 的指标是答案完全匹配率,Fever 的指标是事实核验准确率。仅行动时两个分数分别是 25.7 和 58.9;加入推理后,ReAct 分别达到 27.4 和 60.9,也就是提升 1.7 和 2 个百分点。这个对照说明,在这些任务里,行动之前和观察之后增加必要的推理,有助于综合证据和调整搜索。它并不说明所有任务都需要更长的推理,也没有证明 ReAct 超过其他一切方法。我们应该把推理当做服务于决策的计算:能够帮助分解问题、发现缺口或处理异常时,它才产生价值。
Reflection:带着失败经验重试
一次尝试结束之后,怎样让下一次不再犯同样的错?这就需要引入反思与记忆。Reflection 关注的是跨尝试的改进。一次行动失败后,简单地再试一遍可能重复同样的错误。它让模型结合反馈,写出失败原因和下一次应该采取的调整,再把这段经验交给后续尝试。这里的反思不是空泛地说“我会更仔细”,而是要指出哪个判断错了、漏了哪个条件、下次怎样改变行为。
左边的结构把职责分成三类:Actor 是执行者,读取上下文后产生行动;Evaluator 是评估者,判断结果是否成功;Self-Reflection 是反思者,结合轨迹和反馈把失败转成可操作的经验。这三种角色可以由模型实现,但评估信号也可以来自环境规则或测试,并不要求一定用三个不同型号的模型。沿着底部箭头看,执行者对环境采取行动,环境返回观察和奖励。当前尝试的行动与观察形成轨迹,放进短期记忆;反思生成的经验文本放进长期记忆,供后续尝试使用。这里的“长期”是相对于一次尝试而言,并不是说经验已经写进模型参数。
右边的策略公式把策略表示为由执行模型和记忆共同决定:动作概率在当前状态与上下文的条件下产生。算法先运行初始轨迹,评估结果,生成初始反思并保存;随后在循环中重新尝试、评估、反思、追加记忆,逐轮更新行为。变化的是提示上下文中的经验,模型权重保持不变,所以这不是梯度下降式的强化学习。还有一个实现细节要留意:图中循环条件把“未通过”和“未到次数上限”用逻辑或连接,按字面执行可能越过预算。实际实现应当在“未成功并且仍有预算”时继续,成功或达到上限就停止。理解流程的同时,也要检查伪代码是否能安全地落到真实程序里。
这三列把反思写成了具体行为变化。每列都按“任务轨迹→评估→反思→下一次轨迹”排列。先看左边,任务是清洗锅并放到台面上。第一次执行者以为锅在一号炉灶,试图拿取,环境却回应“没有任何事情发生”。它没有正确处理这个失败,还继续尝试清洗,因此行动建立在错误状态上。评估指出了这种臆测,反思明确定位到锅并不在一号炉灶,下一轮改为到二号炉灶拿锅,后续才有可能完成清洗和放置。
中间是括号匹配程序。原始实现只比较左括号和右括号的总数,自行生成的单元测试没有通过。反思指出遗漏的是顺序条件:比如先出现右括号,即使最终数量相同也不代表合法。正确检查还需要扫描过程中的前缀余额不为负、结束时余额为 0,并根据题目要求检查两种拼接顺序。图中下一轮用检查函数判断候选拼接结果,再返回“是”或“否”,改进来自补上算法条件,而不是换一种措辞。
右边是问答:询问约翰·兰彻斯特与艾伦·迪恩·福斯特共有的职业。第一次把“小说家”和“编剧”都算作交集,环境给出零分。反思指出错误地把各自的多个职业混在一起,下一轮只回答“小说家”。三个例子的反馈来源不同:行动环境、规则或代码测试、答案评估。但共同点都是先获得失败证据,再把证据变成明确的修正策略。
虚线图进一步检验了这种改进。左图横轴是尝试轮次,纵轴是解决任务的比例。灰色虚线是只用思维链或只用 ReAct 重复尝试后,大致停留在三成多。加入 Reflection 后,红色的“思维链+反思”从约 32% 升到 40%,蓝色的“ReAct+反思”则逐步升到约 54%。这说明在这组 HotpotQA 问题里,带着失败经验重试比不积累经验的重试更有效。
右图进一步区分收益到底来自保存历史还是来自反思。图例中的 GT 表示给定题目对应的真实知识文本,所以它与左边需要自己查资料的条件不同,不能直接横向比数值。只有思维链时约 61%;加入最近一次轨迹作为情景记忆(EPM)升到约 66%;再加入反思后达到约 74%,比只保留轨迹又高约 8 个百分点。因此,原始记录和提炼后的经验承担不同作用。但还要看到实验条件:每轮都有环境提供的对错反馈,成功后可以停止、失败才继续尝试。这个结果不是说模型凭自己的信心就能稳定判断答案是否正确。
如果把外部反馈拿掉,只要模型审查自己的回答会怎样?这里引用的研究专门考察这种内在自我纠错。表格使用 GPT-4-Turbo,两个评测分别是数学应用题 GSM8K 以及常识问答。标准提示只调用一次,分数是 91.5 和 84。第一组反馈提示要求“认真审查”,第一次纠错增加到三次调用,却降到 88 和 81.5;第二轮共 5 次调用,回升到 90 和 83,仍低于原始结果。第二组提示要求判断是否正确、错误再找原因,常识问答第一轮跌到 74.5,第二轮 81;数学两轮都是 90。第三组要求验证并解释,数学两轮 91,常识问答从 81.5 到 83.5,仍没有超过基线。为什么多想一次反而可能变差?因为生成答案和判断答案可能共享同一个知识盲区,模型既可能没发现真错误,也可能把原来正确的内容改错。Reflection 的优势部分来自环境或测试提供的可靠反馈,不能简单归因于“反思”两个字。
课件同时提醒,这项研究的模型和实验早于后来的专门推理模型,后者可以在内部推理中进行检查与修正。因此,我们既不应断言所有模型都无法自我纠错,也不能假定增加一轮自评就必然提高质量。是否需要额外的显式反思,最终要在具体模型、任务和反馈条件下评估。
CRITIC:让验证者也能使用工具
CRITIC 沿着这个问题继续前进。既然纯自评不够可靠,就让验证者也能使用工具。左边先由黑盒语言模型生成初始输出,随后进入“验证→提出批评→修正输出”的循环。验证阶段不是只问模型“你觉得对不对”,而是接入右侧的知识库,包括维基百科、搜索引擎、代码解释器、计算器和文本评估接口。不同任务需要不同证据:事实问题要查资料,数学程序要看执行结果,文本属性则可以交给相应接口。
右图横轴是纠错轮数,纵轴是 MMQA 问答集的 F1 分数,反映答案与参考答案的重合质量。起点约 64 分;CRITIC 在第一轮升到约 74 分,随后接近 75 分。没有工具的浅橙色虚线只到约 67 分。紫色 ReAct 参考线约 65 分。蓝色带星号的 oracle 条件进一步接近 80 分,但它预先知道哪些样本答错,只对错误答案纠正,避免把正确答案改坏,这是额外的理想条件,不能当做真实部署时免费获得的能力。CRITIC 的核心对照是:验证有没有获得外部证据,以及能否可靠决定何时修改。顺便更正一处引用:这项方法对应的论文题目是《利用工具交互批评进行自我纠错》,课件页角沿用了前一项研究的链接。现在我们已经看到,行动既可以用于完成任务,也可以用于检验答案。接下来还需要决定动作本身应该用什么形式表达。
CodeAct:用代码作为动作接口
前面讨论的是怎样借助反馈改进推理。现在再问一个更具体的问题:模型决定做什么以后,应该用什么形式把动作交给环境?2024 年的 CodeAct 提出,直接生成可执行代码是一种很有表达力的动作接口。因为代码不仅能调用单个工具,还能保存变量、组织循环、组合多个工具的结果。接下来的例子会让这个区别非常直观。
任务是“比较美国、日本、德国和印度在哪个国家购买名叫 co.back-1 的手机最划算”。这里不能只比较标价,还要把汇率、税费和运输费用放进同一个计算口径。先沿着右侧的动作与环境交替过程往下看:模型先查询德国的汇率和税率,得到 1.1 与 0.19;再查询当地手机价格得到 700;然后把这三个数字填进换汇与计税工具,返回 916.3。按照这里的示例计算,就是 700 × 1.1 × 1.19。注意这还不是含运费的最终价格。下方列出的五类接口还包括查询运费和估算最终价格,图中把这部分交互省略了。随后对日本等其他国家重复同样过程,最后才得出“日本 904 美元”的示例答案。这些是论文演示数据,并不是实际购买建议。
问题在于,每取回一个中间结果,模型往往都要再读一遍历史,再生成下一条调用,数字还得被重新写进参数。橙色文本和紫色 JSON 只是两种动作表示,背后都可能产生很多这样的往返。接下来要改进的就是这些可以由程序负责的重复组织工作。
换成代码动作以后,任务和工具都没有变,改变的是一次交给环境的工作量。先看代码最上方,countries 保存 4 个国家,final_prices 是一个空字典,用来记录每个国家的最终价格。绿色的 for 循环负责逐个处理。循环内部先把查询结果分别放进汇率和税率变量,再取得本地价格;换算工具直接读取这些变量,运输费用和最终价格也按照依赖关系继续计算,最后写入字典。这里的数据传递由程序变量完成,不需要模型每一轮重新抄写 700、汇率和税率。
再看下面另一处绿色标记:min 接收国家作为候选集,但通过 key 参数按字典中对应的价格比较,因此返回的是最便宜国家的名字,而不是字母顺序最靠前的国家。随后再用国家名字取出价格,打印出“日本”和“904”。这样多次工具执行被组织成一次代码动作,模型只需要根据执行结果形成回答。循环提供控制流,变量提供数据流,现成的 min 函数提供可靠的比较逻辑。减少的是模型与解释器之间的交互轮数,不是凭空免掉底层查询。这段普通 for 循环本身也没有自动并行。遇到运行错误时,执行反馈仍可以交给模型修正下一段代码。真实系统还需要为代码配置合适的执行权限与隔离环境。
这种表达方式有没有实际收益,要同时看成功率和交互开销两张横向柱状图。它们使用相同的模型列表,包括 GPT-4、Claude-2、GPT-3.5、Turbo、Pro、DaVinci-003 以及 PaLM-270B。青绿色表示代码动作,粉色表示 JSON,紫色表示纯文本。左图横轴是成功率,越长越好;右图横轴是平均交互轮数,越短越好,而且刻度从 5 开始,不是从零开始。以最上面的 GPT-4 106 版本为例,代码动作的成功率约为 74%,另外两种约为 52 到 53;平均轮数则从接近 8 轮降到约 5.5 轮。把视线往下移,多数模型也表现出代码成功率更高、轮数更少的趋势,符合刚才用循环与变量减少往返的解释。不过图里的 DaVinci 模型就不是代码轮数最低。因此,底部的总结应该理解为总体趋势,而不是每个模型上的绝对保证。
另外,轮数不等于总耗时,也不等于总 token:一轮代码可能执行多个接口。这是论文在当时的模型和测试任务上的结果,实际收益仍取决于模型的代码能力、任务是否容易组合以及执行反馈的质量。从代码动作还能自然走到另一条思路:如果一些后续步骤现在就能确定,为什么一定要执行一步再让模型规划一步?可以先生成计划,再由执行器按照计划运行。这样智能体动态决定的动作序列就暂时变成了一段工作流。关键不是取消模型决策,而是把适合提前决定的部分集中处理,同时保留应对新信息的能力。
ReWOO:规划与观察解耦
ReWOO 把这个过程拆成规划器、执行器和求解器。名称中的 “without observation” 是说规划阶段不必每做一步都等待工具观察,并不是整个系统不需要事实证据。图中问题是:一款叫 T 的鸡尾酒,其主要原料由哪家干邑酒庄生产。左上方规划器先写出四步:第一步查这款鸡尾酒,把结果命名为 #E1;第二步让语言模型从 E1 里提取主要原料,保存为 E2;第三步用 E2 作为查询词再查维基百科;第四步根据原料和第三步的资料提取生产者。蓝色的 E1、E2、E3 是尚未填入实际内容的结果占位符,它们让规划器能提前表达依赖关系,而不必假装已经知道答案。
右上方执行器再逐步落实这些指令:先取回鸡尾酒介绍,再得到“轩尼诗干邑”,随后查询生产者,最终获得轩尼诗公司的名称。每完成一步,就把后续参数中的占位符换成真实结果。下方求解器一起读取计划与收集到的证据,形成最后回答。因此,工作者并不只调用搜索,也可以调用语言模型完成局部提取。这个分工减少了重复规划和重复输入历史的开销。但如果中途发现实体有歧义或者原计划的前提不成立,预先写好的后续步骤也可能失效,这正是需要进一步考虑重新规划的地方。
这张散点图把质量和开销放到了同一张坐标纸上。横轴是 token 消耗,越靠左越省;纵轴是准确率,越靠上越好。颜色区分方法:红色直接回答,蓝色思维链,绿色 ReAct,紫色 ReWOO。圆点代表公开基准,三角形代表研究者整理的基准,比较时要看相同形状。圆点这一组,绿色大约消耗 3600 个 token、准确率约 58%;紫色大约是 1300 个 token、准确率约 61%。三角形这一组也从绿色的大约 1800 个 token、65%,到紫色的大约 1000 个 token、70%。两根红色箭头因此都指向左上方,表示这组实验中同时节省 token 并提高准确率。但紫色并不是最左边:直接回答和思维链更省,只是没有达到相同的准确率。合理的结论是:把规划与观察解耦,改善了这些测试上的质量与成本折中。不能把它解释成任何任务都应该一次规划到底,尤其在强依赖环境反馈的任务里,持续调整仍然重要。
LM Compiler:并行执行与动态重新规划
LM Compiler 在计划执行的基础上进一步强调两件事:并行执行和动态重新规划。看这条从左到右的系统链路。用户要求比较微软与苹果的市值。规划器先生成带依赖关系的任务图:蓝色任务一查询微软,绿色任务二查询苹果。这两个查询互不依赖,可以同时开始;黄色任务三需要前两个结果,所以必须等待;深蓝色任务四再根据计算结果组织回答。中间的任务获取单元相当于一个知道依赖关系的调度器:只有上游结果就绪时,才把对应任务交给右侧执行器;执行器调用搜索、数学或语言模型工具,把结果保存下来,再通知调度器解除后续依赖。这就是图中有向无环任务图的含义,而不是把所有工具盲目同时运行。当已有结果不足以完成目标时,还可以重新生成后续计划。
这里还有一个值得留意的细节:图中任务三写的是“微软市值除以苹果市值”,它得到的是两者比率,本身并不是“微软需要增长的百分比”。若微软当前较低,要计算达到苹果所需的增长率,应当用“苹果市值除以微软市值再减一”,要超过还需要更高一点。任务图负责正确安排先后关系,计算语义是否正确仍然需要检查。
用人物国籍比较来理解并行收益会更直接。问题是:斯科特·德里克森和艾德·伍德的国籍是否相同?左边的 ReAct 路径先让模型决定查第一位导演,等待搜索结果,再把观察追加进提示词;然后模型决定查第二位导演,再等待结果;最后才回答“两人都是美国人”。左侧向下的箭头表示这些阶段依次发生。右边则先由规划器同时列出两个查询,执行器并行发出搜索,等两份结果到齐后,再交给模型比较并结束。两个名字在问题里都已经给出,因此查第二个人不需要先知道第一个人的搜索结果,串行等待在这里是可以避免的。理想情况下,两次搜索的等待从耗时相加变成接近较慢那一次;但总时间还要包括规划、汇总和调度,所以并不一定恰好快两倍。图中标出 1.8 倍,是论文设置下的加速结果。若问题改成“查第一位导演的合作者,再查这位合作者的国籍”,那么第二次搜索对象尚未确定,就出现了真实依赖,不能再用同样的并行安排。
再看实验表。左半边使用 GPT 系列模型,右半边使用 PaLM-270B。每组都报告准确率、以秒为单位的延迟以及相对基线的加速比。任务包括 HotpotQA 问答、电影推荐、Parallel QA 和 24 点。但带小十字记号的 ReAct 是加入了减少重复调用和过早停止提示的版本,因此比较延迟时要认准这条基线。以左侧 HotpotQA 为例,延迟从 7.12 秒降到 3.95 秒,得到 1.8 倍加速,准确率则从 62.47% 变成 62%,基本接近,并没有同时提高所有指标。电影推荐的并行空间更大,延迟从 20.47 秒降到 5.47 秒,加速 3.74 倍,准确率也有所提高。右侧对应的两个任务分别报告 1.4 倍和 2.82 倍。Parallel QA 左侧的延迟从 35.9 秒降到 16.69 秒;24 点比较的是思维树,左侧从 241.2 秒降到 83.6 秒,右侧从 952.06 秒降到 456.02 秒。
表里有一处不能直接照读:右侧 Parallel QA 的两项延迟写成 15.47 和 26.20,却同时写了 2.27 倍加速,这三项数字不自洽,不能据此得出该格的加速结论。整体上,这些实验展示了减少串行等待的潜力。费用还取决于输入输出 token 和工具调用成本,不能直接用延迟倍数推算;实际效果也会随模型、任务依赖和执行环境改变。
到这里,我们已经从推理反思走到了代码动作和计划执行。回头看,这些模式并不是要求每个系统都装上一个同名模块,而是在回答几个具体问题:怎样根据反馈修正判断?怎样表达动作?哪些决策值得交给程序稳定地执行?把这些问题串起来,就能看懂它们在真实智能体里的组合方式。
ReAct 提醒我们,在行动前进行必要推理,并根据观察继续决定。Reflection 强调利用经验和反馈修正后续尝试。课件对当时系统的观察是:一部分自我检查已经融入模型推理,不一定再单独设置显式反思环节。CodeAct 则把动作组织成代码,发挥变量、控制流和现有软件的能力。ReWOO 与 LM Compiler 说明,可以把一部分规划结果落实为工作流,尤其让独立的工具调用并行执行,但这不意味着规划本身不重要。
右侧剪短的循环图把这些思想收在一起:从开始进入模型,模型可以走向结束,也可以走向工具;工具执行后的结果再回到模型,这是下一次判断。表面上只有模型与工具两个主要节点,节点内部却可以使用我们刚才讨论的多种设计。另一个补充是:工作流仍然可以用于子智能体编排。例如,先让不同子任务分别完成,再收集结果统一检查。选择哪一层使用固定流程、哪一层保留自主决策,要根据任务依赖和可靠性需求决定。
上下文管理:聚焦与连续性
行动方式设计好了,还有一个直接影响长任务成败的问题:模型每次重新开始判断时,到底能看见什么?循环越长,工具结果、历史讨论和中间尝试就越多。接下来进入上下文管理,把关注点从怎样决定下一步,转向怎样为下一步准备足够准确而且不过载的信息。
还是熟悉的“人、模型、环境”循环:人提出目标,模型发出动作,环境给出反馈,条件满足时停止。现在模型调用外面多了一个绿色方框,叫上下文管理器,它代表一次调用之前的信息组织工作。用户最初要求是什么?刚刚执行了什么?哪些结果已经确认?下一步需要哪些文件或工具说明?环境返回 10 万行日志,并不意味着这 10 万行都应该直接塞进下一次请求;但如果只留一句“执行失败”,也可能删掉真正需要的错误栈。上下文管理器要在这些选择之间取得平衡,把系统保存的信息转成适合当前判断的输入。它可以包含固定规则、检索、摘要或者模型辅助选择,不必一定是另一个独立大模型。图中的反馈循环也因此多了一层含义:每得到新反馈,不只是追加内容,还要重新判断哪些内容对下一步有用。最终的行动质量既依赖模型能力,也依赖这一轮实际看到了什么。
这张图把信息组织的范围展开了。左边是较简单的单轮场景:系统提示词加用户消息放进上下文窗口,模型生成回答。右边则先有一个可能使用的信息池:文档、多个工具、记忆、文件、完整指令、领域知识和历史消息。中间标着 Selection 的箭头表示按当前任务筛选和组织。最后真正进入窗口的只是系统提示词、选中的两份文档、一份记忆、两个工具,以及当前消息和必要历史。这里的“工具方块”可以理解为让模型知道如何调用工具的相关说明,并不是把工具的所有实现代码搬进来。模型既可能回答用户,也可能发起工具调用;工具返回结果后,沿下方箭头进入消息历史,再成为下一轮可选的信息。这就是一个持续循环的工程过程。
两处剪刀图标提醒我们,窗口有容量限制,但筛选不应等到容量满了才发生。比如正在修一个接口的超时问题,相关调用链和最新失败日志通常比已经结束的另一项功能讨论更重要。提示工程仍然有用,而上下文工程进一步决定哪些材料何时出现、保留多久、如何更新,以及哪些细节暂时存到窗口之外。
大海捞针实验:长上下文并非万能
为什么需要主动筛选?可以具体读一下这张“大海捞针”热力图。测试对象是当时支持 20 万 token 窗口的 Claude 2.1。研究者在一段长文档的某个位置放入一条目标事实,再要求模型找出来,并用 GPT-4 评估回答。横轴是总上下文长度,从大约 1000 逐渐增加到 20 万 token;纵轴是事实插入位置占整篇文档的百分比,顶部是开头,底部是结尾;每个格子对应一种长度和一种位置的组合。右侧色条里,绿色表示检索得分高、接近百分之百,红色接近零,黄色和橙色居中。
先沿水平方向看,左边短上下文整体更绿,往右的长上下文区域出现更多红色。再沿同一列上下看,成绩并不均匀:开头(最顶部)和接近结尾的一些区域仍然较好,而中间和较早位置的许多格子更差。灰色虚线强调的是,这片退化区域不是模型存在某条确定的记忆分界线。还要留意纵轴的百分比并非等间距:底部说明用了 35 个深度和 35 种长度,深度采样按 S 型分布安排,因此格子数量不能直接当做均匀位置上的总体成功率。这张图具体说明:信息放进可接受的窗口,不代表模型总能稳定提取。它也只是一款历史模型、特定提示词和检索任务的结果,不能用来推算当前所有模型的表现,更不能单靠热力图判定内部注意力出了哪种机制性问题。
两条原则:聚焦上下文与保持连续性
从这个现象可以提炼出两条需要一起满足的原则。左边是聚焦上下文:只保留当前任务确实需要的信息,不必每次都带上完整历史。大量无关输出会造成上下文膨胀、增加成本,也可能干扰判断。右边是保持连续性:删减之后,用户要求、最终目标、约束、已经完成的进度和没有解决的问题,仍然要准确地留下来。比如已经确认不能修改公共接口,这项约束比某次成功安装依赖的完整日志更值得保留。
下面还提到 KV 缓存,这是推理效率层面的连续性。对于支持前缀缓存的服务,如果前面那段输入 token 保持一致,就可能复用已经算出的键值,减少重复预填充;若不断改写或重排开头,后面即使文字类似,也可能失去这部分复用机会。不过,保留任务含义和保留缓存不是同一件事。摘要可以保住关键事实,却改变 token 序列;缓存也不是长期记忆数据库,因此不能为了缓存永远把所有旧信息留在窗口里。实践上要把稳定指令尽量放在前面,把当前变化的信息有序组织,再根据质量、长度和计算成本决定何时压缩。
上下文管理的五种模式
原则明确以后就可以讨论具体怎么实现。接下来这组模式分别处理:历史怎样保存、知识怎样移出窗口、冗余内容怎样缩短,以及不同子任务的信息怎样隔离。它们可以相互配合:系统保留完整记录,同时给模型展示一个经过选择的工作视图,长任务才有机会既接得上又跑得动。
先把 5 种模式各自的职责分清。
1. 事件日志(Event Log)
把用户输入、智能体动作和工具观察保存为事件,再按需要重建模型输入。它解决原始记录如何保留的问题。完整记录让我们知道智能体究竟只是声称完成,还是启动过服务、又在浏览器里验证过结果。
2. 记忆(Memory)
把值得长期保存的事实或中间成果写到记忆和文件中,当前窗口就不必持续携带全部细节,需要时再取回。例如一句“生日是 2 月 7 日,男友叫詹姆斯”,比整段闲聊更适合留在可用状态里。
3. 上下文压缩(Context Compaction)
把较长的历史总结为较短描述,保留目标、关键结论和待办。但摘要可能遗漏信息,因此要关注质量。
4. 观察遮蔽(Observation Masking)
主要针对旧工具输出,隐藏或者截短那些已经不再需要的日志与文档片段。让动作过程仍然可追踪,同时减少重复内容。
5. 上下文隔离(Context Isolation)
把特定任务交给子智能体,只给它完成任务所需的信息,再把结果带回主任务。这样子任务的探索过程就不一定污染主任务的窗口。
5 种模式并不互斥。可以先完整记日志,再给子智能体分配局部材料,同时把重要结论写入记忆,并对主上下文做压缩。共同的检查标准是:缩短之后是否还足以支持正确的下一步行动。
OpenHands 的架构图给事件日志一个很具体的落点。先看左边,命令行、网页界面和开发环境插件都可以发来用户消息。历史事件送给智能体,智能体根据历史产生新动作;中间的事件流记录这些动作以及执行后的观察;右边的运行环境负责把动作变成真实执行结果。图中运行环境放在 Docker 沙箱里,提供交互式 Python 以及基于 Playwright 和 VNC 的浏览器,通过统一执行接口连接。
沿中间示例走一遍:用户要求生成 1 到 10 的数字列表,并在 5000 端口用网页显示。智能体先发出 Python 单元动作,创建 app.py 文件,收到“文件已创建”的观察;接着写入服务代码,观察中能看到用 range(1, 11) 生成 1 到 10(右端点不包含 11);然后发出命令型动作,在后台启动 Python 服务,把输出写入日志,再读取日志。返回的观察显示 Flask 服务已经启动,本地地址使用 5000 端口;最后智能体还发出浏览器动作访问这个地址,浏览器返回 1 到 10 的列表。蓝色框是动作,橙色框是观察,黄色开头是用户请求,它们的来源和类型都可以区分。
于是左下角的三个抽象关系就清楚了:事件流是动作与观察的序列;智能体把历史映射为新动作;运行时把动作映射为观察。完整记录让我们知道智能体究竟只是声称完成,还是启动过服务,又在浏览器里验证过结果。
保存完整事件,并不等于每次都原样发给模型。上方流程正好说明了这层分离。最左边是事件列表,先筛选出能转换为模型消息的事件;再按同一次模型响应的编号,把属于它的多个动作分组;接着转换成消息格式,最后形成模型输入。为什么需要分组?因为模型一次响应可能同时提出多个工具调用。事件系统可以把它们逐项记录,但重新构建对话时,要把它们组织回同一条带多个工具调用的助手消息,并正确对应后续结果。这样底层日志可以保持只追加、不随意改写的历史,而模型看到的工作视图可以根据任务进行调整。旧日志被隐藏或总结,并不要求把原始执行证据一并删除。
外部观察者也可以订阅事件了解进度、错误和系统状态,而不必从最终回答猜测发生了什么。需要区分的是:重建上下文指重新组织已记录的信息,不是把所有工具再执行一遍。对于有外部副作用的动作,重放文本记录尤其不能等同于重复发起操作。记录与输入分开,才能同时支持追踪、恢复和灵活的上下文策略。
分层记忆:MemGPT 的例子
另一种办法是把信息分层保存。MemGPT 用一个连续聊天的例子展示它。先看左侧,日期是 2 月 7 日,用户说男友詹姆斯给自己做了生日蛋糕。收到记忆压力提醒以后,系统调用工作上下文的追加操作,把两项关键事实保存下来:生日是 2 月 7 日,男友叫詹姆斯。这样完整闲聊中的重要关系被提炼成短小、可继续使用的记忆。
再看右侧,同一天用户说“去六旗乐园庆祝”。系统没有要求诊断就聊天,始终指着当前窗口,而是调用外部存储的搜索,查询“六旗乐园”,返回三条带日期的历史记录。其中一条说,两个人最初就是在那里认识的。于是回答不止问“玩得怎么样”,而是能问是否和詹姆斯一起去,并联系到他们相识的地点。这个例子里有两种不同动作:把重要事实写入工作记忆,以及从外部历史存储中检索相关片段。不是所有记忆都自动直接进入模型。
分层存储借鉴了操作系统的思路:有限的工作区域装当前必要信息,更大范围的历史放在外部,需要时再搬回来。外部还可以保存文档与归档知识,但眼前这次调用具体检索的是对话历史。窗口大小并没有因此变成无限,效果仍取决于记住了什么、搜索是否命中以及取回的内容是否适合当前问题。只把东西写出去还不够,还要有可靠的取回路径,才有条件安全地删减当前上下文。
上下文压缩与观察遮蔽
把信息移到外部之后,我们还需要处理已经积累在对话里的历史。一个常用办法就是压缩(compaction)。沿着左侧示意图看,系统提示和用户请求在最上方,下面依次是三个时间段,每段都包含推理、行动和观察。右侧保留了系统提示与用户请求,把前两个时间段合并成粉色的摘要,同时原样保留最近一次推理、行动和观察。这样模型下一步依然知道任务是什么、之前已经做过什么,又不必重新读取所有细节。
摘要的目标也不只是缩短文字,它应当保留用户约束、已经验证的事实、失败尝试、当前进度以及还没有解决的问题。比如调试程序时,不能只写成“已经检查代码”,而应说明检查了哪个模块、排除了什么原因、下一步准备验证什么。右边最后一条还提出:压缩过程本身可以带有智能体能力,例如先核对工作文件,再决定哪些结果需要留下。不过摘要是有损表示,因此重要细节最好能从原始记录或外部文件中找回。
压缩是不是一定要调用另一个模型生成摘要呢?这张饼图给出了一个很直接的切入点。它统计的是论文实验中,原始智能体上下文里不同类型的 token 占比:推理占 9.6%,行动占 6.5%,而环境观察(也就是工具返回的文件内容、命令输出和其他结果)占到了 83.9%。注意这里的比例是特定实验的统计,不是所有智能体的固定规律。但它提示我们,在代码任务中,真正把上下文撑大的常常是一次次读取的长文件和测试日志。假如这些旧输出对当前决策已经没有直接价值,先处理它们就能用很简单的方法减少大量输入。接下来的观察遮蔽正是保留行动脉络,同时收缩旧工具结果。
现在把三列放在一起比较。最左边的原始智能体把三个时间段的推理、行动和观察全部保留。中间的观察遮蔽仍然保留第一次和第二次的推理与行动,但把它们占据大块空间的观察替换成紫色的短占位标记,最近一次观察则完整留下。这样模型还能知道先前为什么调用工具、调用了什么,只是不再每轮重复读取那两段长输出。最右边的模型摘要不同:它把前两个时间段整体重新表达成粉色摘要,连推理和行动的原始文字也一起替换。
两种方法省下 token 的方式因此不同。观察遮蔽通常可以用确定性的窗口规则完成,不需要为每次遮蔽额外调用摘要模型;摘要则能提炼跨步骤的信息,但需要生成成本,也可能遗漏细节。遮蔽同样不是无损的:如果后续突然依赖早先的错误日志,就需要重新读取文件,或者从完整事件记录里找回那段输出。实践中可以保留最近若干轮观察,再根据任务特点调整窗口大小,不是把所有工具结果都一刀切地删除。
这三张散点图把取舍展示得很清楚。横轴是每个任务实例的美元成本,越靠左越省;纵轴是任务解决率,越高越好,因此理想方向是左上方。黑色圆点表示不压缩的原始智能体,紫色三角表示观察遮蔽,粉色方块表示模型摘要。实验是在软件修复基准上进行的,比较时要在同一个模型的子图内看。
左侧 Gemini 2.5 Flash 中,两种管理策略都明显向左移动,解决率也比黑点略高:摘要大约 36%,遮蔽大约 35.6%。中间启用思考的版本则揭示了代价:黑点解决率约 44%,观察遮蔽约 36.4%,摘要约 31.4%,虽然费用降低,任务效果也下降了。所以不能概括成“压缩永远无损”。右侧 Codex-480B 遮蔽把每实例成本从约 1.29 美元降到 0.61 美元,解决率则从 53.4 到 54.8;摘要的结果接近。这里的小幅准确率差异也不能仅凭点的位置就宣布具有统计显著性。这些是论文设置下的结果与计价,不能当成现在的报价。真正应当带走的是:先把简单遮蔽当做有竞争力的基线,再同时评估成本、解决率和长任务表现。
上下文隔离与渐进披露
前两种方法都在缩短同一个上下文。另一种办法是从一开始就让不同工作拥有不同的上下文窗口。图上方的规划者把工作分别交给左右两个智能体,每个虚线框里都有自己的模型和网格状的上下文符号,表示它们可以在各自的信息范围内推理,而不必互相携带完整历史。例如让一个智能体调查接口实现,另一个检查测试用例;各自读取的文件和搜索过程可以留在自己的上下文里,再把相关结论交回上层。
图中直接画出的是任务向下分派。工程上还需要定义返回结果的格式,避免最后又把所有原始输出堆回主上下文。这种设计既能支持独立任务并行,也能减少无关信息的相互干扰。但隔离越强,跨任务信息共享就越需要明确的接口;如果两个子任务存在依赖,上层必须把必要事实传过去,否则只是把一个信息拥挤的问题变成了信息缺失的问题。
不创建多个智能体,也可以减少一次性进入上下文的信息,这就是渐进披露。图中展示的是一个处理 PDF 的技能文件。最上面的蓝色区域是元数据,给出技能名称和描述,让模型先知道这个技能能提取文字、处理表格、合并拆分文件或者填写表单。这一层像目录索引,不需要把全部说明书都塞进初始提示。发现任务确实需要处理 PDF 时,再读取下面的正文,了解操作步骤和快速开始示例。正文里又指向参考文档和表单说明;只有遇到复杂功能或填写表单的任务,才进一步加载对应文件。
于是信息沿着“名称与描述 → 主要说明 → 专门参考材料”这几个层次逐步展开。左下角“读取文档并统计页数”的代码是按需获得的具体方法,而不是每次对话都必须携带的内容。这样做的关键不是让模型少知道,而是让它知道去哪里找,并在合适的时候取得足够的信息。初始上下文保持简洁,专门知识仍然可达。当然,技能描述必须写清用途,否则模型可能连该加载哪份说明都判断不出来。
这些策略可以组合,而且真实系统往往需要组合。比如先把已经确认的重要结论写到外部笔记,再压缩较早的对话,这样摘要不必承载全部细节;又比如完整事件日志继续追加保存,每次构造模型输入时再把过旧的工具输出替换成占位标记。缩短的是模型看见的输入,原始记录仍然能够用于检查和恢复。还可以让子智能体分别读取相关技能,只把结果摘要返回主智能体。
这里要特别区分三个层次:持久记录负责保留发生过什么;任务状态负责表达现在做到哪里;当前上下文负责提供下一次决策真正需要的信息。把它们分开设计,就更容易判断一个事实应该一直保留、暂时移出,还是只在被查询时加载。
上下文工程的分类
到这里,我们已经有了保存、缩短和隔离信息的多种办法。先把这些名字整理一下:有些方法负责把信息留在模型之外,有些负责在需要时找回来,有些负责减少当前输入,还有些负责让不同工作各自保有上下文。接下来这张分类图可以帮助我们把具体实现放回更基本的操作之中。
从左到右,这张图把上下文工程分成写入、选择、压缩和隔离四类。左下角的网格小方块统一表示模型上下文。
红色的写入是把信息从当前上下文转存出去。图里区分了跨会话的长期记忆、同一会话内的工作笔记,以及同一会话内保存的结构化状态。它们生命周期不同,不能都理解成永久记忆。
蓝色的选择方向正好相反:把需要的信息取回来,包括相关工具、工作笔记、长期记忆和外部知识。工具也需要选择,因为把大量工具定义全部放进提示同样会占用预算并增加选择难度。
绿色的压缩有两条路线:上方用摘要保留相关内容,下方用裁剪去掉无关内容,对应我们刚比较过的语义改写和直接截断。
橙色的隔离则有三个位置:把信息分区保存在状态中,把大量内容留在环境或沙箱里,以及分配给不同智能体各自处理。隔离的对象是信息与工作上下文,不等于图中每一种做法都天然提供了安全隔离。
四类操作也不是互斥的产品分类。例如写到外部笔记,下一步还需要选择性读取;多智能体隔离之后,返回的结果也可能需要压缩。用这张图分析系统,应该追踪信息怎样流动,而不是只问它有没有某个功能名称。
红色标注指出了课程接下来的两个方向。左边框住长期记忆的写入和读取,这两个环节必须配套:既要决定什么值得记住,也要决定什么时候把它带回当前任务。右边框住跨智能体分区,后续会进一步讨论如何分工与协作。画面上的“本周三”“下周一”是原课程当时的安排,不是我们观看视频时的日期提醒。对知识结构来说,它们分别延伸了“持续积累经验”和“让多个执行者协同完成任务”这两个问题。
把这一部分收拾起来:事件日志、外部记忆、摘要压缩、观察遮蔽和上下文隔离都是围绕两个目标展开的:让上下文保持聚焦,同时保持任务的连续性。聚焦意味着把下一步用不到的冗长内容移开;连续性意味着不能连用户目标、关键约束和已经完成的进度也一起丢掉。右侧绿色大框包围着模型调用,强调上下文管理是模型外围的一项系统职责。每次调用前,系统要决定给模型看什么;调用后,又要决定怎样记录新事件和更新状态。判断一种优化是否成功,也不能只看输入少了多少 token,还要看模型是否重复探索、遗忘约束或者失去恢复工作的能力。
案例三:PAI——极简智能体运行框架
接下来用一个真实的智能体框架,看看这些模式怎样落到工程设计中。我们已经讨论了工作流、推理与行动以及上下文管理,现在进入案例部分,观察 PAI 这个智能体运行框架。这里最值得分析的是它选择保留哪些核心机制,又把哪些能力交给扩展和外围环境。带着这些问题看实现,会比单纯技术功能清单更有收获。
PAI 的定位是极简智能体运行框架。标题里的 harness 指的是围绕模型运行的那层系统:它接收任务、组织模型输入、执行工具调用、保存会话,并把执行结果交回模型。模型提供推理和生成能力,运行框架则把这些能力接到真实环境。极简并不意味着只能处理简单问题,而是基础层尽量少规定具体工作方式,让使用者通过配置和扩展适应自己的任务。下面我们按照设计理念、工具、执行循环、上下文和权限这几个维度,把这个抽象定位逐项拆开。
设计理念
这段设计理念的意思是:让 PAI 适应你的工作流,而不是要求你的工作流迁就它。按照课程所展示的基础配置,它没有内置一套子智能体系统,没有逐字工具调用的权限确认,没有专门的规划模式,也没有专门的智能体待办清单和后台命令管理功能。这些缺省选择体现的是:核心层少做决定,把更具体的工作方式留给扩展。例如,没有专门规划模式并不代表模型不能先思考计划;没有内置待办功能也不代表不能在文件中维护任务进度。相应的,开发者需要自己组合这些能力,不能默认它们已经完整存在。
这里描述的是课件中的基础设计,不应推断所有版本、所有社区扩展都缺少这些功能。软件会演进,实际安装时要结合版本和配置判断。这个案例要教会我们的是区分核心职责与可扩展能力,并理解极简带来的灵活性和集成成本。
工具选择
再看工具选择。右侧强调默认启用的是读取、命令执行、编辑和写入四类工具;左侧表格给出了更精确的含义:read 读取文本文件和支持的图像;bash 执行命令;edit 对已有文件进行精确的文本替换;write 则创建文件或覆盖文件。表里还列出 Windows 上的 PowerShell,以及搜索文件内容的 grep、按路径模式查找文件的 find、列出目录的 ls。它们出现在内置工具表里,不等于这一列全部默认开放给模型。保留较小的默认集合,可以减少工具描述占用的上下文,也减少模型在多个相似接口之间犹豫。
右边说“其他工具都是命令执行的子集”,应当从功能覆盖的角度理解:许多读写和搜索任务都能通过命令及程序组合完成。它不意味着这些接口在权限、输入约束、返回格式和错误处理上完全等价。专门的编辑工具把替换意图表达得更明确,读取工具也能提供更合适的结果表示。因此,能力重叠仍然可以与保留专用接口同时成立。
执行循环
工具准备好之后,PAI 采用类似 ReAct 的循环:模型根据当前任务和已有观察决定下一步,通过结构化工具调用发出动作,运行框架执行之后再把结果交回模型。课件强调行动前的推理,但这不要求每个模型都向用户展示同一种显式思考文本。
下面的树状图讲的是另一层设计,也就是会话历史怎样组织。黑点代表历史节点,箭头连接前后关系。主干走到一个位置后,可以继续向右,也可以从已有节点分出新的对话路径。这样当一种方案走不通,用户可以回到先前位置,尝试另一条路线;原来的分支仍能保留下来。为当前调用构造上下文时,需要沿当前分支读取相应历史,而不是把整棵树的所有分支混在一起。
这里尤其要区分:对话分支回退不等于真实环境自动回滚。之前写入磁盘的文件、执行过的外部操作是否恢复,需要另外的版本管理或环境机制支持,不能仅凭这张会话树做保证。
上下文管理
PAI 的上下文管理把前面三个模式连在了一起。第一,从事件记录转换出模型输入;第二,通过技能文件逐步加载专门知识;第三,在历史过长时做压缩。看下方这条序列:最左边 ST 是系统提示;接着 summary 是压缩记录提供的摘要;后面的 USR 表示用户消息,ASST 表示助手消息,TOOL 表示工具结果,它们共同构成需要保留的近期交互。下方的长方形指出,近期消息从一个名为 first_cap_entry_id 的边界开始,我们可以把它理解为第一条保留记录的标识。这个边界非常关键:摘要负责交代边界之前的任务经过,原始消息负责保留边界之后的具体细节。于是下一次模型调用看到的是系统提示、历史摘要和近期消息,而不是直接复制整份事件日志。
实现压缩时,系统会生成摘要、追加一条压缩记录,再依据保留边界重建下一次输入。没有发送给模型的旧记录仍然可以保存在会话日志中。技能加载解决“哪些专门说明现在需要出现”,压缩解决“历史如何缩短”,事件记录则让这两种加工都有可追溯的基础。这里的图展示的是模型最终看见的输入序列,不能把它误读成原始历史已经全部被删除。
权限边界
理解执行能力之后,还需要准确理解权限边界。课件强调,PAI 以启动它的进程权限运行,并不会在每一次工具调用前弹窗询问。因此,如果那个进程能够读取某个目录、修改某个文件或者访问某项服务,工具通常也会继承相应能力。这里所谓“没有保护”指的是:不要把课件中的基础运行框架当成会自动拦截所有危险操作的审批系统。它不意味着能够绕过操作系统权限,也不意味着外部容器或虚拟机的限制失效。
补充一个版本区别:当前官方文档还说明了项目资源加载时的信任确认,用于决定是否加载项目配置和扩展;但它并不是每次工具执行的审批,更不是工具沙箱。因此,分析这种系统时,要把“界面是否确认”“工具是否可用”以及“进程真正拥有什么权限”分别看待。这个案例中的权限责任主要落在启动方式和外围环境上。
PAI 小结
现在可以把 PAI 的几个选择拼回同一张设计图:它用较小的核心和扩展机制支持定制;用读取、命令、编辑和写入这组基础工具连接环境;用 ReAct 风格的循环让模型根据反馈继续行动;再用事件记录、渐进披露和压缩维持长对话中的有效上下文。课件最后关于内置保护的提醒,也应按刚才的范围理解:基础循环不替代外部权限隔离和治理。
这些能力并不是互不相关的清单。工具输出会进入事件记录,记录经过选择与压缩成为下一轮输入,模型再据此选择动作。一个真正可运行的智能体就是这些机制反复协作的结果。也正因它们相互影响,改变工具集合、历史保留方式或者权限环境,都可能改变同一个模型的实际任务表现。
总结
通过这个案例,我们可以把整讲内容重新连起来。先决定哪些控制流程交给程序、哪些交给模型;再决定模型怎样推理和行动;然后设计信息如何保存、筛选和送入下一次调用;最后把这些选择落成具体的运行框架。
接下来用这条线索回顾本讲的 5 个部分。
第一,工作流与智能体的区别,核心在于控制流由谁决定。
第二,提示链、路由、并行化、编排者与工作者、评估者与优化者提供了组织模型调用的基本结构,真实系统会把它们组合起来。
第三,推理与行动的设计决定模型怎样利用反馈:ReAct 把推理和动作交替进行;Reflection 尝试把经验写回后续上下文;CodeAct 用程序表达更丰富的行动;而规划与编译式方法则组织动作之间的依赖和执行顺序。
第四,上下文管理处理的是信息预算与任务连续性:既要减少无关内容,也要保住目标、约束和进度。
第五,PAI 说明这些模式可以组合成一个具体的运行框架,而且哪些功能放进核心、哪些交给扩展本身也是设计选择。
面对新任务时,不必先寻找最复杂的架构。先找出真正的困难是控制流程、行动表达、反馈质量还是信息管理,再用相应模式解决,并通过任务结果验证取舍。本讲到这里结束。
最后的课堂提醒对应原课程随后安排的记忆主题,它会沿着今天留下的问题继续深入:哪些信息值得跨会话保存?怎样更新和检索?以及怎样让过去的经验帮助当前任务?回顾今天,设计智能体不只是给模型增加工具,更是在安排控制流、行动方式和信息流。理解这些选择,就能更有条理地分析和搭建自己的智能体系统。
斯坦福 CS329Z 智能体工程:07:课程概述与学习目标
在本节课中我们将要学习智能体记忆架构。一个智能体可以推理、调用工具并完成一次复杂任务,但要成为能够长期协作的助手,它还必须回答另一个问题:过去发生的事情怎样影响现在的决策。用户已经解释过的项目规则,下一次不应重新解释;今天做了一半的任务,明天应接着做;用户修改了偏好,助手应及时更新。这里的记忆不只是保存聊天记录,而是把经历、事实和做事方法组织起来,在需要时找到并放入当前决策过程。
这一讲会从实际需求出发,逐步拆开记忆的类型、存储方式、设计模式和评估方法。学习时抓住三个目标,就能避免把“接上一个数据库”误认为已经完成记忆系统。以下是三个学习目标。
-
能够识别多会话、面向用户的系统为什么需要记忆。判断标准不只是助手能否复述往事,而是它能否延续任务、遵守已有约束并适应用户变化。
-
能够根据设计模式区分不同架构。即使两个系统都使用向量数据库,也可能一个保存原始对话,另一个保存反思后的经验,它们的能力和风险并不相同。
-
能够指出仍未解决的挑战。保存更多不等于回答更好,旧信息可能过时,检索可能找错内容,提炼经验也可能把偶然事件总结成错误规则。
记忆闭环 = 观察环境 → 组织当前信息 → 检索过去经验 → 选择行动 → 写回值得保留的结果
明确目标以后,我们回到课程安排和本讲的核心问题,先看通知,再看两个真实任务例子。
📌 智能体记忆架构:02:课程通知与核心问题
正式开始前,先保留原课程中的两项通知。以下是原课程通知内容,需要说明它们来自制作原课件时的安排。
Loy Institute 与亚马逊云服务合作,为课程学生追加 200 美元云服务额度;第一次作业截止日期写为 10 月 30 日。这里展示的是制作原课件时的课程安排,不是当前学习视频的作业或领取通知。
下面进入本讲核心问题。先看员工管理例子。上半部分是员工手册:要求员工按排班时间到岗;如果会迟到,至少提前 30 分钟联系 Lu Na;滚动 30 天内三次无正当理由迟到,会收到正式书面警告;书面警告后仍反复迟到,可能减少工时或终止雇佣;病假应尽早通知,理想情况下至少提前 2 小时;未联系就缺勤,被视为严重违规,可能立即终止雇佣。下方对话中,员工说自己要到 10 点 45 分才能到,助手却回答“完全没问题,商店等你到了再开,而且 10 点到 11 点通常很安静”。员工实际打卡时间是 11 点 08 分。手册里的管理规则与助手眼前的回复没有形成可靠一致性。这个例子说明,系统既要保留规则,也要保留迟到事件和时间,才能在之后正确应用。
仅凭截图不能断定失败一定是遗忘造成。即使规则被检索出来,决策也可能没有遵守。记住信息、找到信息、按信息行动是三个需要分别保证的环节。另一种失败发生在邮件助手中。用户要求检查收件箱,建议哪些邮件可以归档或删除;在得到指示前不要执行。当前任务是分析和提出建议,删除操作还没有被授权。但下面的记录里,助手开始制定清理旧邮件的命令,把 2 月 15 日以前且不在保留名单中的邮件作为清理对象,甚至准备反复执行直到旧邮件全部清掉。用户先后发出“不要这样做”“停止操作”“停止助手”的消息,记录中仍出现后续工具动作。这里提醒我们,任务状态不能只剩下一句“清理邮箱”,还必须保留“只提建议”“等待确认”以及用户最新发出的停止信号。这些限制如果在长流程里被遗漏,摘要虽然更短,行为却会发生根本变化。记忆不能代替权限控制和取消机制,运行系统也需要让停止信号及时中断执行。把目标、约束和当前进度一起保存,才更接近完整的任务状态。
两个例子共同指向核心结论:对长时间运行、跨越多个会话的智能体来说,记忆是基础能力。模型当前看到的上下文有限,新会话开始后,过去对话不会天然全部进入模型。系统必须决定什么值得保留、什么需要更新、什么时候取回。这一讲同时讨论两层问题:第一层是记忆类型,保存的是一个事实、一段经历还是一种做事方法;第二层是记忆架构,谁负责写入、存在哪里、如何检索,以及怎样让这些内容参与行动。把两层分开,才能判断不同系统解决了哪个问题。整讲按五部分展开:用真实任务说明记忆必要性;复习记忆类型;分析记忆架构和设计模式;讨论如何评估记忆是否有效;梳理尚未解决的挑战。课件旁标出的 5 分钟、10 分钟、35 分钟、15 分钟和 10 分钟是原课堂时间规划。更重要的是它们之间的逻辑关系:先确定需求,再选择信息表示和系统结构,最后用评测检查效果。
🧩 智能体记忆架构:03:记忆需求、损失与收益
先从需求出发。下面把缺少记忆的后果拆成三类,再与记忆带来的收益一一对应。这样遇到一种新架构时,就能问清楚它主要是在帮助智能体记住做法、记住事实还是记住任务进度。以下是缺少记忆时的三类损失。
-
做事流程丢失。用户教过助手怎样处理任务,换一个会话后,助手又从头探索。这里丢失的不是最终答案,而是达到答案的步骤和方法。
-
知识丢失。重要事实、规则和个人偏好如果没有跨会话保留,下次交互就可能用不上。例如用户习惯的报告格式,或项目中特定的命名约定,不一定属于模型预训练时已经知道的知识。
-
进度丢失。即使仍在同一个长会话中,智能体也可能忘记之前的目标、已经完成的操作和早先指令,导致重复工作或偏离原约束。右侧困惑的机器人是一个比喻:它未必完全不会做事,而是不知道之前发生了什么,也不知道现在应该从哪里继续。
相应的记忆可以带来三个收益。以下是个性化、经验复用和状态延续三种收益。
-
个性化。同一个问题,面对不同用户可以结合其偏好和实际情况给出更合适的回答。前提是这些偏好确实来自用户,并且仍然有效。
-
从经验中学习。智能体在一次任务里掌握的步骤,可以在之后复用,不必每次重新试错。这里的学习不一定修改模型权重,也可以把验证过的操作流程存起来,下一次作为指导使用。
-
有状态。系统能够延续工作。用户只说“打开我们之前做的那个提案”,系统就能找到对应内容,恢复目标和进度。右侧机器人有了大脑,是对连续性的比喻。
下面用三个例子分别说明个性化、经验复用和状态延续到底需要记住哪些信息。先看跨会话个性化例子。2020 年 2 月 21 日的第二次会话中,用户说自己喜欢意大利菜,尤其喜欢披萨,这时可以记录一项初始偏好。2023 年 4 月 17 日的第 12 次会话中,用户补充自己发现对麸质过敏,于是开始尝试地中海菜,尤其是烤海鲜。绿色标记强调新信息不只是增加了一道喜欢的菜,还改变了后续推荐必须考虑的限制。2024 年 11 月 13 日,用户问“今晚可以做什么”。带笑脸的回答使用较新的约束,询问是否考虑地中海海鲜或其他无麸质选项;带哭脸的回答只记得旧的意大利菜偏好,直接提出意面建议。个性化不是命中任意一条历史记录就算成功。系统需要识别信息的时间和变化,区分旧偏好与当前约束;检索到旧记录后,也要检查它是否已被后来的信息修正。旧偏好可以保留为历史,却不能在当前建议中压过更新更相关的限制。
再看经验怎样转化成能力。左边的简单任务是根据地点名称在地图上找到一个地方:先在开放街道地图中输入地点,再点击搜索。蓝色代码框里填充动作,把地点写进编号 145 的输入元素,点击动作操作编号 147 的元素,分别对应输入和搜索;花括号中的地点是可以换成不同输入的变量。右边的新任务更复杂:用户不只想定位地点,还想得到它的邮政编码。中间直箭头表示智能体可以先复用左边已经掌握的搜索步骤,右上虚线框里仍然是输入地点、点击搜索,因此没有必要重新探索怎样使用搜索框。接下来弯曲箭头指向新增步骤:查看结果中的候选地点,从地图或关联信息中提取邮政编码,再调用发送消息的动作,把结果告诉用户。返回话术里的邮政编码同样是占位变量,要替换成刚查到的真实值,而不是原样输出。这里的增长方式是在旧流程上增加步骤,形成更复杂的工作流。代码里的数字是一段具体交互中的元素标识,并不表示任何网页都可以照抄同一组数字。真正需要迁移的是输入目标、触发搜索、读取结果的方法,以及如何根据当前页面找到相应元素,这就是把一次成功经历抽象成可复用程序的意义。
状态延续的例子是订机票。图上方是用户之前讨论过的预定:从旧金山飞往悉尼,选择联合航空,乘坐经济舱。随后用户开启新会话,说自己准备完成上次讨论的预定。左侧红色叉号表示会话之间的信息没有顺利传递,助手只好重新问具体行程,用户再次说明出发地、目的地,还要补充 3 月 3 日等信息,底部标出这段示例需要十轮交互才能重新确认。右侧使用跨会话记忆:用户的新请求先进入检索器,来自旧会话的信息沿右侧连线进入检索环节,再被整理成出发城市、目的地、航空公司、座舱等级等字段。黄色标记帮助对应这些字段与历史对话中的事实。检索器做的是恢复先前讨论的内容,而不是猜测一张新机票。助手拿到这些字段后,就能直接进入确认步骤,示例中只需要两轮。这里的 1 轮和 2 轮是图中这段对话的对比,不是对所有系统的固定提升比例。它说明任务状态可以从旧对话中恢复出来,让用户不必反复提供同样信息;恢复状态之后仍然有确认环节,记得上次讨论过什么,并不自动等于用户已授权执行最终预定。
🧠 智能体记忆架构:04:记忆类型与 CoALA 认知架构
刚才已经看到三种需求,但它们保存的信息并不一样。本节借助一个认知架构框架,把当前正在处理的信息与长期保存的经历、事实和做法区分开。分类清楚以后,存储与检索的设计才会更有针对性。
这个框架叫 CoALA,可以理解为面向语言智能体的认知架构。它把语言模型、记忆、决策过程和环境交互放进同一张图里。我们借用它的作用建立一套共同语言,解释不同记忆分别服务于什么环节,而不是要求所有系统都必须照着同一个软件结构实现。智能体接触的环境有三类:与人的对话、物理世界以及数字系统。向上的观测箭头把环境信息带进工作记忆,向下的行动箭头把智能体的决定作用到环境中。工作记忆位于右下方,里面的循环箭头表示推理会不断利用和更新当前信息。左边的决策过程与它双向连接,负责围绕当前状态选择下一步行为。因此,工作记忆相当于正在使用的工作台,不能简单等同于永久档案库。
上方三个长期记忆区域分别是情景记忆、语义记忆和程序性记忆。右上角的情景记忆保存过去的经历;中间的语义记忆保存事实和知识。它们各有两种方向:检索箭头向下,把有用内容送入当前处理过程;学习箭头向上,把新信息写回长期存储。左上角的程序性记忆里计划了语言模型,也画了智能体代码。提示送进模型,模型结果经过解析,代码或规则也可以被调用和更新。这里强调的是做事方法,既可能来自模型能力,也可能来自显式代码和可检索流程。红框圈出的四块就是后面要分别讨论的四种记忆角色。理解这张图要抓住完整循环:观察环境,组织当前信息,检索过去经验,选择行动,再把值得保留的结果写回。只实现存储环节,还不能构成有效的记忆闭环。
记忆类型 = 情景记忆(经历) + 语义记忆(事实) + 程序性记忆(技能)
工作记忆 = 当前上下文窗口中真正参与推理的信息
工作记忆通常对应模型的上下文窗口,也就是这一次调用真正看到、正在据此推理的信息。它包含用户输入、当前目标、工具返回的观测,以及与当前任务有关的中间推理内容。把模型调用放在上下文管理器里面,强调送给模型的内容需要由系统组织。假设数据库里保存了 10 万条聊天记录,模型不会因此同时看见 1 万条记录;只有经过选择、检索和组装,进入这次上下文的部分,才成为当前可用信息。所以工作记忆的关键问题是有限空间如何分配,既要放入足够证据,也要保住目标、约束和当前进度。它与长期记忆的联系,就像工作台与资料库的联系。资料库存得再完整,取不到工作台上,这一轮决策也可能用不上。
情景记忆保存智能体过去经历过什么。对于聊天助手,它可以是以前的对话;对于会调用工具的智能体,它还包括观察、动作和结果组成的执行轨迹。记忆流例子中,每一行都有时间戳,然后是一件被观察到的事情,例如 21 点 18 分人物在读书,22 点 33 分他在写日记,稍后又在伸展身体;记录中还有看电影、打扫厨房以及桌子、床和冰箱当时的状态。它记录具体时刻发生的具体事件,而不只是总结一个抽象结论。原始记录的重要价值是保留上下文和来源。以后要知道某项判断从哪里来,或者任务究竟执行到了哪一步,都可以返回这些轨迹。但原始日志通常很长,包含大量与当前问题无关的细节,所以保存全部经历不意味着每次都把全部经历交给模型,后续仍然需要检索、筛选或提炼。
语义记忆保存事实和知识,可以描述世界,也可以描述用户,例如家庭信息、偏好、兴趣和当前计划。它与情景记忆的区别在于重点从“那次说了什么”转向“我们现在知道什么”。记忆管理界面就是一种展示方式:列表中记录用户有一个叫丽娜的 2 岁女儿,女儿喜欢水母;用户希望会议纪要带标题和要点,并在末尾总结行动项;还记录偏好简洁直接的博客写作风格,喜欢旅行,以及考虑 4 月去墨西哥度假。这些条目可以来自多段对话,但使用时不必每次重放完整聊天。列表旁边的删除图标也说明它们可以单独管理。需要留意不同事实的有效期不一样:写作风格可能相对稳定,孩子的年龄和某次旅行计划则会随时间变化。所以语义记忆仍然需要时间、来源和更新机制,不能因为已经提炼成一句话,就把它当成永久不变的事实。
程序性记忆回答“怎样做”,把可以作用于当前任务状态的规则、步骤和技能保存下来,影响智能体接下来的行为。品牌风格技能文件是一个例子:顶部是名称和描述等元信息,用来说明这个技能适用于什么任务;正文给出操作指导,包括如何为演示文稿应用品牌颜色和字体,例如深色用于主要文字或深色背景,浅色用于浅色背景或深色上的文字;标题使用 Topins 字体,不可用时回退到 Arial;正文使用 Lora,不可用时回退到 Georgia。用户提出制作演示文稿时,智能体可以先识别这个技能适用,再读取具体规则,把它们应用到当前文件。这里记住的不只是“某家公司有品牌规范”这个事实,而是做演示文稿时应该怎样遵循规范的方法。程序性记忆既可以是自然语言说明,也可以配套代码和工具。不过,有一个技能文件不代表已经把技能训练进模型,很多系统是在运行时读取这些内容,让模型依据它行动。
除了按内容分类,还可以按信息放在哪里分类。参数记忆存在模型权重中,预训练学到的知识可以承担语义记忆的作用,遵循指令等能力可以承担程序性记忆的作用;继续训练或微调也能改变模型掌握的知识与技能。课件把参数记忆中的情景记忆标成不适用,这里应理解为模型权重通常不承担应用中逐条保存、按用户和时间检索会话事件的职责,不能把它误解成模型绝对不会记住任何训练样本。上下文记忆则把事实检索出来,作为语义信息加入提示;把技能读取出来,作为程序性指导;把过去的对话取回,作为情景证据。上下文管理器负责挑选合适内容,组装进本次模型调用。因此,这是两条不同的分类轴:情景、语义、程序性描述信息的用途和内容;参数与上下文描述模型如何获得和利用这些信息。把编号写入外部数据库,再检索到上下文,通常不需要更新模型权重。
情景记忆 = 存经历
语义记忆 = 存知识
程序性记忆 = 存技能
表中把生成式智能体作为前两类的例子,因为同一个系统既可以记录具体经历,也可以由经历提炼出更抽象的知识;Voyager 则展示了把技能积累起来、供后续任务调用的思路。这三类不是互斥的文件格式,而是内容所承担的角色。某次调试中运行了哪些命令、得到哪些报错,是情景记录;由此确认某个环境使用什么依赖版本,是语义知识;总结出下次怎样排查同类故障,是程序性方法。同一段经历可以产生三类记忆,但它们在下一次任务中被检索和使用的方式并不相同。
🗄️ 智能体记忆架构:05:存储载体:文件、数据库与知识图谱
有了类型划分,现在进入实现层面。我们要把“智能体应该记住”转换成一组明确的系统设计:保存什么,由谁触发,用什么方式读取,以及怎样更新。后面的架构案例就是这些选择的不同组合。
设计记忆系统可以先连续问三个问题。以下三个问题必须一起考虑,否则底层存储再好,也可能因为写入不及时或检索时机错误而帮不上当前任务。
-
存什么、取什么。候选内容包括具体事实、一般知识、操作流程以及对话和任务的推进状态。选定内容后,还要决定保存原文,还是保存结构化字段或经过提炼的摘要。
-
什么时候读写。有的系统把记忆操作做成工具,让智能体按需调用;有的由后台工作进程整理;还有的在特定事件发生时触发,例如会话结束、任务完成或用户修改某项偏好。触发时机不同,会影响延迟,也会影响新信息多久才能被下一轮使用。
-
怎样存、怎样取。文件系统、数据库和知识图谱是后面重点介绍的几种方式。
要让记忆跨越一次模型调用或一次进程运行,就需要某种持久化机制。课件列出三种常见方式:文件系统、数据库和知识图谱。文件系统把记忆写进文件,并借助目录组织;数据库把记忆作为记录存储,通过字段、关键词或向量进行查询;知识图谱把实体及其关系显式连接起来,沿关系寻找信息。这三种方式并不是互相排斥的技术阵营,图谱本身可以由数据库承载,文件内容也可以建立索引。比较它们时,应当关注智能体看到的操作界面是浏览和编辑文件、查询匹配记录,还是围绕实体与关系检索。以下是三种常见存储载体的关注点。
-
文件系统:目录天然提供层次结构,列目录可以发现有哪些资料,搜索命令可以定位关键词,读取和编辑文件完成具体的记忆访问与修改。可读、可检查,也容易复用智能体熟悉的工具;但目录和关键词搜索不自动等于高质量语义检索,文件变多后,命名、更新与重复内容管理仍然需要设计。
-
数据库:向量数据库适合根据语义相似度查找,把问题和记忆编码成向量,再找出意思接近的内容;即使没有使用完全相同的词,也可能找到相关经历。按用户、时间、任务编号或状态筛选时,SQL 这类数据库可以保存元信息,通过明确条件缩小范围。BM25 根据词项匹配进行相关性排序,适合找包含特定名称或术语的记录;BM25 是文本相关性评分,不等于数据库字段的严格相等查询,要确保编号完全一致,仍然应使用精确过滤。
-
知识图谱:把记忆中的关系显式表达出来。人物节点可以连接“认识谁”“关注什么主题”“使用什么产品”“在哪家公司工作”,工作关系还可以标出有效时间。曾经在这家公司工作和现在仍然在那里工作,是不同事实。图谱可以围绕实体追踪关联,或区分不同时间的关系;但抽取关系、合并同一实体、处理变化都可能引入错误,因此图谱最好仍能回到原始事件或文档,说明某条关系从哪里来。
文件系统方案最直接。把记忆放进文件,让智能体使用已有的文件操作能力来管理。目录天然提供层次结构,列目录可以发现资料,搜索命令可以定位关键词,读取和编辑文件完成记忆访问与修改。三层示意中,最上方把智能体说明文件和工具配置文件放在程序性区域,因为它们影响行为方式和可用工具;中间用不同目录组织知识文档,还画出一个技能说明文件;最下方是对话目录,里面按日期保存对话记录,对应情景记忆。这里有一个容易混淆的细节:图中把技能文件划进中间区域,但文件名本身不能决定记忆类型。一个文件如果主要规定完成任务的步骤,在内容分类下仍然属于程序性记忆;如果记录事实,则承担语义记忆。应按内容和用途理解,而不是把目录位置当成严格定义。这种设计的优点是可读、可检查,也容易复用智能体熟悉的工具;局限是目录和关键词搜索不自动等于高质量语义检索,文件变多后,命名、索引、更新与重复内容管理仍然需要设计。文件系统提供操作基础,但不会自动完成全部记忆管理。
数据库方案把记忆组织成可查询的记录。向量数据库适合根据语义相似度查找,把问题和记忆编码成向量,再找出意思接近的内容;即使用户没有使用完全相同的词,也可能找到相关经历。如果需要按用户、时间、任务编号或状态筛选,SQL 这样的数据库可以保存元信息,通过明确条件缩小范围。关键词检索列出 BM25,它根据词项匹配进行相关性排序,适合找包含特定名称或术语的记录。这里要区分:BM25 是文本相关性评分,并不等于数据库字段的严格相等查询;要确保编号完全一致,仍然应使用精确过滤。这和检索增强生成的思路相通:先获取证据,再让模型使用证据。实际记忆系统也可以组合多种手段,例如先限定当前用户和时间范围,再进行向量或关键词召回,最后挑出适合进入上下文的内容。存储与检索方式服务于任务需求,并不需要只选择其中一种。
知识图谱进一步把记忆中的关系显式表达。左侧以一个人物节点为中心,连线分别表示他认识谁、关注什么主题、使用什么产品,以及在哪家公司工作;工作关系还标出有效时间,截止到 2026 年 3 月。这样的时间信息很重要,因为曾经在这家公司工作和现在仍然在那里工作,是不同事实。右侧示意图给出三层组织:最上面的情景层保存原始材料,包括文档、对话和结构化数据;中间的实体层从这些材料中识别出人、事物以及它们的关系;最下面的社区层把关联紧密的内容聚成群组,并形成摘要。箭头从原始材料向下,说明图谱结构和更高层摘要可以由底层证据逐步组织出来。再回到图顶部,编号一是用户问题,编号二是智能体访问记忆,编号三是结合记忆生成最终回答。图中用 Claude 3 作为模型示例,重点不在特定模型,而在模型与外部记忆的分工。图谱结构可以帮助系统围绕实体追踪关联,或区分不同时间的关系;但抽取关系、合并同一实体、处理变化都可能引入错误。因此,图谱最好仍能回到原始事件或文档,说明某条关系究竟从哪里来。
🧱 智能体记忆架构:06:四种设计模式总览
底层怎样存储已经有了概念,现在看更上层的四种设计模式。第一种是从经验中学习做法,回顾成功轨迹,提炼或复用其中的操作流程。第二种是分层记忆,协调短期与长期信息,把当前最需要的部分留在容易访问的位置。第三种是反思与整合,在写入长期记忆前,对经历做提炼,形成更有用的总结或结论。第四种是记忆维护,清理重复内容,删除不再需要的记录,并在事实变化时更新旧记忆。这些模式可以组合使用。一个系统完全可以先从成功执行中提炼技能,再把技能放进长期文件库,通过检索进入当前上下文,同时定期修订失效内容。以下是四种设计模式的对应关系。
设计模式 = 从经验学习程序 + 分层记忆 + 反思整合 + 记忆维护
接下来先看第一种模式,理解智能体怎样把做过的一次任务转化为以后可以继续利用的能力。
🛠️ 智能体记忆架构:07:从经验中学习程序:Voyager、AWM 与技能文件
有了记忆的基本概念,我们先来看第一种架构模式:从经验中学习做事的方法。智能体完成一次任务之后,除了记录发生过什么,还可以提炼出以后能够再次调用的操作步骤。接下来的重点就是经验怎样变成技能,以及这些技能怎样帮助解决更复杂的新任务。
Voyager 把这个思路放进了《我的世界》游戏。沿图从左往右看,它由自动课程、迭代生成程序、技能库三个部分组成。左边的课程模块根据探索进度提出新任务,例如先踩木头、制作工作台、再挑战僵尸,逐步向挖钻石这样的复杂目标推进。这里的课程是一套选择下一项探索任务的机制,不是提前写死的一张任务清单。中间收到新任务后,语言模型生成可执行代码。以对抗僵尸为例,程序先检查背包里有没有石剑;有旧装备,没有就调用制作石剑的技能,随后还可以制作盾牌。右侧向左的蓝色箭头表示这些已有技能会被检索出来,参与新程序的构造。程序向下交给游戏环境执行,环境再把状态变化和执行错误送回来。因此,代码生成不是一次性结束,而是根据真实反馈反复修改。右下角的自我验证模块检查任务是否完成:红色分支表示失败后继续完善程序,绿色分支表示成功后把新程序作为技能存入库中,紫色回路更新探索进度,影响下一项任务的选择。整个系统因此形成闭环:选择挑战,组合旧技能,执行并检查,积累新技能,再挑战更难的问题。技能库让已经解决的子问题可以被复用;但成功判定和环境反馈仍然是这条闭环的重要条件。
再把技能库拆开看。上半部分展示写入过程:左侧是 GPT-4 生成的对抗僵尸程序,程序里包括装备武器、制作盾牌、恢复饥饿值和寻找敌人等步骤;中间用另一个模型把程序转换成自然语言说明,再对说明计算嵌入向量。这个向量充当检索的键,原始可执行程序则作为对应的值,两者一起加入技能库。为什么不只保存一段对话?因为以后真正要复用的是能执行的程序,而自然语言说明更适合和新任务进行语义匹配。下半部分展示读取过程:现在的目标变成制作铁镐,系统先展开任务需要的条件,例如 3 块铁锭和两根木棍,并结合环境反馈形成检索描述,再把描述编码为查询向量。查询进入技能库后,取回最相关的 5 项技能:冶炼铁锭、制作木棍、制作工作台、制作熔炉以及制作木镐。注意这 5 项并不等于已经完成了铁镐,也不必与最终目标的字面名称相同,它们可能是当前目标所依赖的准备工作。模型拿到这些程序之后,还需要根据手头的材料、工具和状态选择、组合与执行。这里把语义检索和程序复用连接起来:向量负责帮助找到可能有用的能力,具体代码负责落实动作,环境反馈负责判断组合是否真的有效。
实验结果要结合表格里的两类数字一起读。主数字表示完成目标所需的提示迭代次数,括号里的分数表示三次运行中成功了几次;标成“不可用”的格子表示没有完成。ReAct 和 Reflection 在这里的几类工具任务上都没有成功。AutoGPT 完成了铁工具,但平均需要 90 多到 100 多次迭代。Voyager 即使移除技能库,仍然能够较快完成前三类目标,说明自动课程和执行反馈本身也有作用。加入技能库后,铁工具的平均次数从 29 降到 21,木工具从 7 降到 6,石工具则从 9 变成 11,所以不能说每一项都会更快。最显眼的是,钻石工具完整系统在一次成功运行中用了 102 次提示迭代,三次运行总共成功一次,其余对照方法在这个实验中均未成功。因此,底下的红字应理解成:技能库帮助系统跨过了一次更长的任务依赖链,而不是保证每次都能成功。评估长期学习系统时,既要看成功后的效率,也要同时看成功率和实验规模。
两张曲线图进一步把过程展开。横轴是生成代码时的提示迭代次数,纵轴不是连续分数,而是从木头、工作台、木镐逐步推进到目标物品的里程碑。左边要制作石剑,右边要收集一桶岩浆,黑色虚线标出最终目标。橙色代表完整 Voyager,黄色代表移除技能库,浅紫色是 AutoGPT,深紫色是给 AutoGPT 接入 Voyager 技能库。左图中,橙色最先到达石剑,黄色随后到达;原始 AutoGPT 停在工作台附近,而接入技能库后,深紫色也能够沿着工具依赖逐步推进,并最终到达石剑。右图中,完整 Voyager 也更早获得岩浆桶;接入技能库的 AutoGPT 推进到了找到岩浆源,却没有在图示预算内完成最后目标。这样就能区分两件事:技能复用确实可以减少重复探索,帮助跨越中间步骤;但完成整条任务链还依赖规划、执行和纠错。把一个技能库接到另一个智能体上,并不自动使两个系统的整体能力相同。
从游戏转向网页操作,同样的思想形成了 AWM,也就是智能体工作流记忆。图中的例子是用户问编号 0130 的订单是谁下的。左下角先获得一段实际行动轨迹:点击订单入口,在页面中向下滚动,找到目标订单,然后把客户姓名艾玛·洛佩兹返回给用户并结束。轨迹可以来自已有标注,也可以来自智能体自己执行。第二步是评估这次任务有没有正确完成。只有通过评价的轨迹,才沿着向上的分支进入工作流归纳;没有通过的轨迹在这里被跳过。第三步不再保存那个具体订单号和具体按钮编号,而是提炼成可复用的“查找订单”流程。右侧上方先描述这个流程的目标:找到指定编号的客户订单;下方再给出一系列步骤,每一步包括当前环境的描述、选择动作的理由以及实际动作。图中订单编号被写成变量,按钮也用语义化名称表示。最后这个工作流沿着箭头进入记忆库,供语言模型解决后续问题时参考。中间环境与智能体之间的环形箭头提醒我们,执行时仍然要反复观察网页状态并选择动作;归纳出来的流程是在帮助决策,并不意味着以后可以不看页面、机械重放某一次成功轨迹。与 Voyager 相比,这里主要积累的是带解释的操作流程,应用场景从游戏代码转成了网页导航。
怎样让模型提炼流程,而不是复述整段经历?提示词给了几个明确约束。输入是一组网页导航任务,每项都包含自然语言目标以及完成任务的动作序列。模型要在不同任务中寻找反复出现的动作子集,把它们归纳成常用的子程序。例如购买某个商品可能包含搜索商品、进入详情、填写地址等子流程;归纳目标不是把一次完整购买过程原样记下来。提示词还要求避免生成相似或相互重叠的工作流,并规定每个流程至少包含两个步骤,防止记忆库退化成一堆孤立的点击动作。最后,可变化的输入文本、按钮字符串等,都要换成有描述意义的变量名。这样换一个商品或订单时,记忆仍然可以提供结构上的帮助。这里所谓学习主要表现为把新得到的流程写入外部记忆,并在后续推理时使用;这段机制本身没有要求通过反向传播更新语言模型参数。
两个具体例子可以帮助看清这种表示。上面的流程是在购物网站浏览指定类别的商品:第一步,把鼠标悬停在主分类菜单上,让子分类出现;第二步,点击目标子分类。这里的主分类标识符和子分类标识符都是绑定的变量,执行时需要从当前页面找到对应元素。下面的流程用于后台编辑并保存字段:先清空目标字段,再填入新值,最后点击保存按钮。先清空再填写,是为了避免新内容意外追加到原有内容后面;保存动作则让编辑真正提交。每个动作前都配有一句解释,把操作目的与操作本身连接起来。底部红字说明这些流程作为上下文放入系统提示,供智能体解决任务时参考。因此,模型仍然要把变量映射到当前界面的真实元素,并根据观察决定下一步。记忆保存的是可迁移的操作结构,而一次实际执行仍然需要环境感知、参数绑定和结果检查。
接下来检查这种记忆是否只是在重复做过的题。左边表格使用 WebArena 的跨模板子集,尽量让不同测试任务不共享同一个原始任务模板。列名下的括号是网站样本数,成功率是表格中的百分数。AWM 的总成功率为 33.2%,高于 ToT 的 23.2% 和只使用无障碍树表示的 BrowserGym 的 2.5%,也略高于使用人工编写工作流的 Step 的 32.1%。按网站拆开看,AWM 在购物、内容管理、论坛、代码协作和地图任务上的结果分别为 24.5%、29.3%、52.2%、31.8% 和 39.4%。它并非每列都独占第一:购物任务低于人工工作流方法,内容管理和论坛与之持平,代码协作则与 ToT 持平。右图单独展示地图任务在线运行时的累计成功率。横轴是已经处理的查询数量,纵轴是截至当前的平均成功比例。最初几十个查询中曲线快速上升,约 40 个查询以后在 40% 附近波动,图中据此标出快速阶段和相对稳定阶段。累计平均会平滑后续变化,所以稳定也不代表每一道新题都有同样的成功概率。这个实验支持的结论是:少量经历中提炼出的流程可以帮助后续任务,并表现出一定跨模板迁移能力;它没有证明所有网站、所有变化都能被这些流程覆盖。
把操作经验保存下来也可以采用更直接的工程形式:技能文件。左边展示一个技能目录,里面既有入口说明,也有补充文档、幻灯片指南和可以执行的模板脚本。右边展开入口文件,可以看到两层结构:顶部是 YAML 格式的元数据,包含名称和描述,用来说明技能做什么、什么任务应该使用它;下面是 Markdown 正文,记录具体规则与操作知识。这个品牌样式技能会告诉智能体怎样为演示文稿应用指定颜色和字体,例如深色、浅色背景分别如何使用,标题字体和正文字体怎样选择。目录里的脚本则可以把部分步骤落实为可重复执行的程序。这样技能并不只是一条很短的提示词,它可以是说明、参考资料和代码组成的一组资源。和从轨迹自动归纳的 AWM 相比,这种文件形式也允许人工编写、审核和更新,从而把稳定的操作规范交给不同任务反复使用。
如果有几十上百种技能,是否要把所有文件都塞进上下文?图里采用的是逐步加载。最上方的系统提示只附带每个技能的一小段说明,像目录一样告诉模型有哪些能力。右上角放大了表格技能的简介,它支持创建、读取和修改表格,保留公式,进行分析与可视化,以及重新计算公式。这些元数据帮助模型判断何时应该选它。中间的实际用户任务却是填写一个 PDF 表单,因此模型选中的是 PDF 技能。第一轮工具调用读取它的入口说明,工具返回结果,才把完整内容加入上下文。随后模型发现入口说明引用了专门处理表单的文档,于是再发起第二轮读取,把这份详细指南加载进来。顺着蓝色和橙色结果框看,就能理解所谓渐进式披露:先知道资源存在,再按任务读取操作指南,最后在需要时深入到某个分支。这样既保留了详细知识,又减少了每次请求都携带无关内容的开销。技能能否被正确选中,也因此依赖元数据是否清楚、入口文档能否把模型导向真正相关的资源。图中的路径和读取方式属于这个示例,具体系统可以通过不同工具实现同样的分层访问过程。
程序性记忆也有一个很实际的边界:过去有效的步骤未必适用于变化后的环境。右上角是熟悉的游戏场景,右下角加入了新的模组、设备和规则。原先的技能库可能仍然知道怎样做木剑,却不知道新材料的配方,也可能因为接口或行为变化而执行失败。网页智能体也会遇到类似情况:按钮位置、字段要求和业务流程改变之后,旧流程就需要重新核对。因而,记住技能之后仍然要观察当前环境,并允许修订或淘汰失效步骤。幻灯片把技能文件列为通用用户型智能体的一种常见实现,因为它便于补充和更新;不过文件可编辑只是提供了维护入口,并不会自动保证内容始终正确。这个限制提醒我们,记忆不仅要解决保存和读取,还需要考虑何时更新,以及怎样判断旧经验还能不能用。
🪜 智能体记忆架构:08:分层记忆:MemGPT 与 Mem0
接下来换一个角度。即使记忆内容本身正确,也不可能把所有历史同时放进有限上下文。第二种架构模式因此关注分层管理:哪些信息常驻,哪些保存在外部,需要时怎样取回,又由谁决定这些读写操作。
MemGPT 的核心想法是给语言模型一组工具,让它参与管理自己的记忆。先看上方有限上下文窗口,8000 个词元只是图中的示意规模。输入部分被分成三个区块:黑色的系统指令规定运行方式和工具用法,只读且相对固定;橙色的工作上下文保存需要经常看到的重要信息,可以通过函数修改;蓝色的先进先出队列保存近期消息,由队列管理器维护。右边还要留出生成答案和工具调用的输出空间,所以不能把所有容量都用于历史输入。下方两类外部存储都不直接出现在模型当前看到的文本里:绿色的归档存储容纳可长期保留的信息,蓝色的回忆存储记录交互历史。函数执行器负责落实模型提出的读取和写入请求,队列管理器负责把消息加入上下文并维护历史记录。箭头表明,模型可以通过工具把外部信息带回当前上下文,也可以把值得保存的信息写到工作区或归档区。这里的关键不是让一次前向计算突然看到无限文本,而是在多轮调用之间改变它实际看到的内容。就像处理大文件时分批读入内存,长期知识可以存在外部,但用于本次推理的那部分最终仍然需要进入有限上下文窗口。
MemGPT 上下文 = 只读系统指令 + 可修改工作上下文 + 先进先出近期消息 + 输出预留空间
外部记忆 = 归档存储(长期知识) + 回忆存储(交互历史)
红色标注进一步强调三种不同的访问语义。上方蓝色消息区采用先进先出:新的交互进入队列,空间不足时较旧的消息需要移出当前窗口;旁边很窄的紫色区域用于保留被移出消息的递归摘要,帮助延续对话线索,详细原文仍可留在外部回忆存储。右下角的回忆存储按字符串或日期查找,适合回答某次对话说过什么、某个时间发生过什么;它由队列管理器持续写入,模型通过函数检索。左下角的归档存储则使用向量数据库这类语义检索方式,更适合寻找与当前问题相关的知识片段,而且模型可以通过工具写入内容。不要把这两个外部区块都理解成同一种检索器:一个偏向保留和查找交互日志,一个偏向组织需要长期使用的知识。无论从哪里检索到信息,返回内容都要重新进入当前上下文,模型才能用它生成回答。分层结构解决的是内容放在哪里、由谁维护、怎样取回,而不是取消上下文容量限制。
看一个完整的写入和更新例子。左边发生在 2 月 7 日,用户说男朋友詹姆斯给自己做了生日蛋糕。系统随后收到上下文空间紧张的提示,模型判断生日和人物关系值得保留,于是调用工作上下文的追加函数,分别写入“生日是 2 月 7 日”以及“男朋友名叫詹姆斯”。这两条简洁事实可以在原始聊天记录移出近期队列后继续留在工作区,不必每次都重新搜索那段对话。右边时间来到 2 月 14 日,用户告诉助手两个人已经分手。此时正确的维护方式不是只继续追加一条互相矛盾的信息,而是调用替换函数,把“詹姆斯是男朋友”更新为“詹姆斯是前男友”。这个动作修改的是当前工作区里对关系的描述,并不意味着过去的聊天记录也被重写。这样系统既可以保留曾经发生过什么,也能在当前回答中使用已经更新的事实。底部还提醒归档区域也有相应的记忆维护工具;而回忆存储的历史消息由队列管理器写入,不采用这里同样的主动改写方式。对长期交互来说,记忆能力不只是记得多,更包括能把新信息与旧事实对齐,并区分当前状态和历史事件。
读取操作也有两种不同例子。左边仍是 2 月 7 日的对话,用户说去六旗乐园庆祝生日。助手对回忆存储搜索“六旗乐园”,得到三条带日期的历史消息,其中包括用户曾经说自己和詹姆斯第一次在那里见面。因此,接下来的回答能够把当前活动和过去的关系联系起来,询问这次是否也和詹姆斯一起去。这项信息并不在当前消息里,而是通过检索重新带回来的。注意这里的日期早于刚才 2 月 14 日的分手更新,不能把这两个示例的先后时间混淆。右边则是文档问答。用户问第一届诺贝尔物理学奖由谁获得。系统在归档存储中搜索相关词,第一批返回 124 条结果中的 10 条,并标明总共 13 个结果页。模型没有在第一批看到充分答案,就带着相同查询和第二页参数继续检索,随后找到 1901 年的获奖者伦琴,并给出回答。这里的页码是检索结果的分页,不是让整个数据库一次性进入上下文。它展示的是一种受控的多步查找:先获得一小批证据,判断是否足够,不够再继续。底部说明也很关键:工作记忆本来就在输入文本中,所以不需要再通过搜索工具读取;只有窗口之外的记忆才需要显式取回。
效果评估分成两个部分。左边是深层记忆检索任务,同时报告准确率和 LCS 召回指标。后者根据参考答案之间的最长公共子序列衡量文本覆盖情况,不能直接等同于事实正确率。GPT-3.5 Turbo 加入 MemGPT 后,准确率从 38.7% 提高到 66.9%,对应的文本召回指标从 0.394 提高到 0.629;GPT-4 从 32.1% 提高到 92.5%,指标从 0.296 到 0.814;GPT-4 Turbo 则从 35.3% 提高到 93.4%,指标从 0.359 到 0.827。这些都是该记忆任务与当时模型配置下的实验结果。右图横轴是一次检索后交给基础模型的文档数量,纵轴是问答准确率。几条带点的线显示,文档增加到一定数量后,正确率不再持续上升,部分模型明显下降。原论文为了让更多文档装进固定窗口,还截短了每篇文档,可能删掉关键证据,因此下降不能只归因于模型被长上下文干扰。两条水平线对应 MemGPT 配置:红色实线使用 GPT-4 或 GPT-4 Turbo,大约在 0.72;橙色虚线使用 GPT-3.5,大约在 0.4,用来比较主动分页查找的表现。这里不能把水平线理解成已经证明任意长历史都不会退化,也不能把文档数量直接当成模型原生上下文上限。这个实验更具体地说明:扩大一次输入的文档数量,需要面对容量截断和证据保留的取舍;按需取回证据,可以在所测任务中更有效地利用有限上下文。
Mem0 把分层思路进一步组织成三个存储区,以及存储、更新、检索和回答生成四类操作。先沿最下方的蓝色虚线看写入路径:右边产生一次回答后,这轮用户问题、模型回答和时间戳组成一条对话记录,被送入左边的短期记忆。短期区按先进先出管理,较早的记录随后转入中间的中期记忆。中期区不是一条没有结构的长列表,而是把相关对话归成主题段,每个主题段包含若干对话页,图中的对话链用来保留连续交流之间的联系。中期到长期的虚线表示进一步提炼:当一个主题的热度超过阈值,系统从中更新长期的用户知识、用户特点或助手特点;不再活跃的主题在容量管理时可以被淘汰。右侧长期区还区分动态信息与相对静态的基本档案。再看读取路径:用户查询从顶部同时触发三层检索;短期区提供最近上下文,中期区先选相关主题段,再选其中最相关的对话页,长期区提供相关的个人知识和角色信息;这些内容沿底部实线汇合,进入回答生成模块,而新的交互又回到短期区。这样图中的各条箭头合起来就是一个持续运行的循环:记录对话,组织主题,提炼稳定信息,再按当前问题组合所需上下文。这里的页和段是记忆组织单位,不是直接调用操作系统的物理内存分页。
红色框把三层分工单独强调出来。左侧短期记忆回答“刚才我们在聊什么”,所以优先保留最近发生的对话。中间中期记忆回答“我们过去围绕这个主题具体讨论过什么”,因此既保存主题组织,也保留可检索的对话细节。右侧长期个人记忆则回答“这个用户有哪些持续相关的背景偏好,以及助手需要维持怎样的角色”。两条红箭头表示并不是所有消息都自动变成永久画像:短期到中期需要组织和规并,中期到长期还要经过热度判断与信息提炼。论文中的热度同时考虑主题被检索的次数、包含的对话页数量以及最近一次使用距今的时间,综合反映经常访问、持续讨论和近期相关这几种信号。长期区域也不是只读不变,图里用户知识、用户特点和助手特点标在动态区,基本档案标在相对静态区。理解这三层时,重点应放在信息用途和更新策略,而不是仅按保存了几天来分类。一次回答仍然可能同时需要三层:最近的提问背景、以前讨论过的细节,以及长期有效的个人条件。
🔁 智能体记忆架构:09:反思式整合与生成式智能体
我们已经讨论了怎样把记忆分层保存。接下来再往前走一步:经历过一件事,是否就意味着真正学到了东西。第三种模式叫反思式记忆整合。它把一次次具体经历加工成教训、规律和较高层次的认识,让后续决策能够复用这些经验。
先用 Reflexion 把这个过程看清楚。左边有三个模型角色,右下方的 Actor 负责行动,动作进入底部环境,环境返回观察和奖励。一次尝试中的动作与观察串起来,就是左下方的轨迹,也就是短期记忆。上面的 Evaluator 评估这次尝试,Self-Reflection 再结合内部评价和外部反馈,生成一段反思文字。这段文字进入右侧的长期经验,再回到 Actor 指导下一次尝试。这里改变的是下一次输入的经验,不是对模型权重做梯度更新。右边的算法逐项对应这个闭环:M、A、E 和 SR 分别表示行动者、评价者和反思者;策略 π 表示在状态 S 下选择动作 A 的规则。这里 π 包含行动模型与记忆 M。先产生初始轨迹 T0,评价它形成反思 S2,再把它放进记忆列表;之后每轮重新行动、评价、反思、追加记忆,并增加尝试次数。轨迹中的 A0、O0 直到 A_i、O_i 就是动作和观察交替出现的序列。还要特别留意循环条件。原图写的是“未通过或者尝试次数小于上限”。如果照这个逻辑直接写代码,失败时可能超过次数上限,成功后也可能继续运行。工程实现通常应当是“尚未成功,并且预算还有剩余”,或者显式写出“成功即退出”和“超限即退出”。比如代码测试失败后,记忆应写成“空输入没有处理,下次先加边界分支”,而不只是保存“我失败了”。能改变下一次行为的具体教训,才是反思的价值。
Reflexion 循环 = 行动 → 评价 → 反思 → 写入记忆 → 下一次行动
有效教训 = 可操作的具体改进,而不是只记录“失败”
反思到底带来了什么?这张 HotpotQA 曲线做了一个消融对比。横轴是尝试次数,从 0 到 4;纵轴是已解决任务的比例。灰色虚线只做思维链推理,基本停在 61%;粉色虚线加入情景记忆,也就是把最近一次尝试带给下一次,提升到大约 66% 后趋于平稳;深紫色实现在情景记忆之外再加入反思,从约 63% 逐渐升到 74%。图例里的 GT 表示给模型提供了题目的真实支持上下文,不是直接把正确答案交给它。因此,这个实验重点观察的是已有证据下的推理能力。到后面几次尝试,带反思的方法比只保存经历的方法高大约 8 个百分点。它支持一个具体判断:保留上一轮的过程有帮助,但把过程加工成明确的错误解释和改进方向,还能进一步帮助推理。同时,曲线最后也出现了平台,并没有无限上升。反思需要可靠反馈,也受模型自身判断能力限制。不能把这张特定任务上的曲线理解成反思次数越多,任何问题都一定能解决。
反思还可以服务于长期行为,而不只用于失败后的重试。Generative Agents 构建了一个有 25 个智能体的小镇。画面左侧有人在公园散步,左下方有人完成晨间活动,中间有人在咖啡馆邀约聊天,右上方有人去学校,右下方的同事则在交流市场选举的消息。这些场景共同提出一个记忆问题:角色不能每次开口都像刚刚出生。它需要知道自己是谁、和谁交谈过、听到了什么消息、今天有什么安排,以及哪些关系值得继续发展。例如咖啡馆里的一次邀约可能影响之后的计划,别人分享的一条消息也可能在更晚的对话中再次被提起。所以这个小镇的重点不只是多个语言模型同时对话,而是每个角色都带着持续积累的经历行动。原始观察、检索、反思和规划需要形成闭环,才能让单次看似合理的回答进一步组成前后连贯的行为。
从最左边的 Perceive 感知开始,智能体看到环境中的事件,就把观察写入中间的 Memory Stream 记忆流。记忆流可以很长,不能每次完整塞给模型,所以经过 Retrieve 检索出与当前情况相关的一组记忆。右侧 Retrieved Memories 再流向 Act,用于决定眼前的行动。这条从左到右的主线回答“现在该做什么”。图里上下两条回路同样重要。上方把检索出的记忆交给 Plan,形成未来安排,然后把计划重新写进记忆流。这样到了后面的时刻,智能体能取回自己答应过的事情,避免每次都从零决定。下方把检索出的记忆交给 Reflect,从多次观察中归纳较高层次的认识,再写回记忆流。这样下一次检索既能找到原始事件,也能找到整理后的理解。例如反复观察到某人花时间查资料、写论文,可以归纳出他重视研究。原论文的反思过程会先提出值得思考的问题,检索证据,再形成有证据记录指向的结论。这里的证据关联很重要,否则一次猜测就可能被长期记忆反复强化。规划则把较大的日程逐步细化为行动,并允许情况变化时重新安排。所以,虚线框里不是一个简单的聊天记录数据库,它持续吸收新观察,又接受规划和反思的改写。记忆决定行动,行动带来新的经历,新的经历又改变记忆,这才让角色具有时间上的连续性。
具体该从记忆流里挑出什么?左边记录了大量带时间戳的事件,有桌子闲置、冰箱闲置,也有写日记、休息、打扫等活动。面对右上方的问题“你现在最期待什么”,把这些内容全部搬过来显然没有必要。右边的检索分数结合三个因素:近期性,衡量这条记忆最近是否被访问过;重要性,区分日常琐事和对角色有意义的经历;相关性,衡量它与当前问题有多接近。原论文把三项归一化后做加权求和。这个例子里权重都取 1。排在最上面的派对筹备记忆,近期性 0.91 加重要性 0.63 加相关性 0.80,得到 2.34;购买装饰品的记忆得到 2.21;研究派对创意的记忆得到 2.20。注意这些是排序分数,不是概率,所以总分超过 1 完全正常。也不能把近期性理解成只看创建日期,这套实现还考虑上次访问的时间。相关性使用文本向量之间的相似度,重要性则由模型在写入时判断。最后检索把排名靠前且能装入上下文的记忆交给模型。于是 Isabella 回答,她期待自己在 Hoops 咖啡馆筹办的情人节派对。这个过程说明,好的记忆检索不是只匹配关键词,而是把“和问题有关、对角色重要、当前仍然活跃”三个角度组合起来。
检索分数 = α × 近期性 + β × 重要性 + γ × 相关性
示例:score = 0.91 + 0.63 + 0.80 = 2.34
我们再看这些组件有没有实际贡献。左边横向柱状图的指标是 Teki 排名评分,用来比较被评估行为的可信程度,分数越高越好。最上面的完整架构接近 30 分;去掉反思之后,分数降到 27 分左右;再去掉规划,降到 25 分左右;继续去掉观察记忆,降到 21 分左右。每根柱子末端的细线表示估计的不确定范围。这是一组逐步删除组件的消融实验:反思负责把经历提升为理解,规划让后续行为保持连续,观察记忆提供事实基础。逐个拿掉以后,行为的可信程度逐步下降。右边咖啡馆的聚会场景提醒我们,评估对象是角色在模拟生活中的表现,不是数学题准确率或通用智能分数。最下面还有众包人员的对照,分数约为 23。完整架构在这个特定评估设置中高于这个对照,但不能因此推出它全面超过人类。更值得带走的结论是:对于需要持续角色行为的场景,只保存聊天内容还不够,观察、规划、反思各自承担了不同功能。
🧹 智能体记忆架构:10:记忆维护与睡眠时计算
当记忆越来越多,另一个问题会浮现:旧事实是否已经过时,重复记录是否太多,关联是否清楚。第四种模式就是记忆维护。它关心的不只是写入和读取,而是在信息变化和系统空闲时持续整理、更新和组织记忆,让未来更容易用对。
Sleep-time Compute 可以理解成在用户尚未提问时,利用已有上下文提前做一些计算。图的左半边是蓝色睡眠阶段,右半边是橙色提问阶段。这里睡眠是一种比喻,指智能体暂时没有处理新的用户请求,并不是停止运行。先看上半部分的常规方式。原始材料说,一个杂耍者有 800 个球,其中四分之一是网球,网球中有一半是电蓝色,这些电蓝色网球中又有十分之一带标记。材料在等待期间原样保存,问题来了模型才从头算:带标记的电蓝色网球有多少?要连续计算 800 × 1/4,得到 200;再 × 1/2,得到 100;再 × 1/10,得到 10。如果接着问网球一共有多少,又要处理同一段材料。下半部分把这段计算推到提前了。左侧多个模型调用先加工原始上下文,形成右边蓝框里的 learned context,也就是整理后的上下文,里面已经包含 200 个网球、100 个电蓝色网球、10 个带标记的电蓝色网球这些中间结果。问题到达后,模型可以直接利用它们缩短在线推理过程。它没有凭空消除计算,而是把一部分工作从用户等待的路径移到提前处理阶段。相同上下文被多次提问时,这些准备结果更有复用价值;但提前算出的内容也可能用不上,所以实际系统要考虑准备成本、信息是否会变化,以及未来问题大致会问什么。
睡眠时计算 = 离线加工原始上下文 → 形成 learned context → 在线直接使用中间结果
在线成本下降 ≠ 总成本必然下降,还要看准备成本能否被多次请求分摊
同样的想法也能用于整理用户偏好。原始上下文是一串逐次出现的事实:用户叫 Chad,他喜欢蓝色,比起蓝色更喜欢红色,最喜欢灰色,而且讨厌绿色。单独保存这些句子没有错,但每次选择颜色时,模型都需要重新整理它们之间的关系。中间的睡眠时计算把这些零散描述改写成更容易使用的表示:灰色排第一,红色第二,蓝色第三,绿色放在最后并明确标注“讨厌”,用户姓名也保留下来。这里真正节省的是后续比较和整合关系的工作,而不是简单把文本缩短几个字。这还提醒我们,整理记忆必须保持原意。“讨厌绿色”和“绿色只是排名较低”并不完全一样,所以右侧保留了负面偏好标记。若之后用户明确改变偏好,排序也要重新维护。睡眠时的工作可以由单独后台智能体承担,在主智能体暂时空闲时整理记忆,之后主智能体直接读取整理好的结果。
四张图把提前准备的收益放到量化比较中。横轴都是每道问题在提问阶段平均消耗的 token 数,越靠左在线生成计算越少;纵轴是准确率,越高越好。蓝色曲线表示用了睡眠时计算,灰色曲线是通过增加并行尝试次数来扩展的 Pass@k 基线。Pass@k 关注多次尝试中至少一次成功,不能直接理解成只生成一个答案的准确率。四个子图分别对应 O3-mini、O1、Claude 3.7 Sonnet 和 DeepSeek-R1 在 T-FLAME 2024 设置上的结果。以左上角为例,蓝线到大约 4400 个在线 token 时,准确率约为 75%;灰线即使到了约 9000 个 token,也只有 60% 左右。右下角的 R1 同样显示,提前处理后可以在较少在线 token 下达到更高准确率。左下角 Claude 的蓝线并非处处单调上升,中间有一次回落,随后提升到约 47%。右上角的 O1 尤其值得仔细看:在蓝线显示的较低在线预算区域,提前处理有优势,但灰线继续增加大量预算后,最终准确率超过蓝线最后一个点。因此,不能把这张图说成所有预算范围内蓝线都永远更强。阴影主要帮助观察特定预算和准确率之间的效率差距。还有一个核算边界:横轴统计的是提问阶段的 token,并不代表包含提前准备在内的全部费用。实验说明的是计算可以被前移复用,从而改善在线响应的取舍;是否节省总成本,还要看准备成本能被多少次后续请求分摊。
接下来是 A-MEM。先比较左右两张小图。左边的传统方式中,主智能体一边与环境交互,一边按照预先设计的流程读取和写入记忆,记忆更像被动存储。右边则在记忆这一侧也放入一个模型驱动的管理过程,让它参与组织和调整记忆。因此,关键变化是把“怎么保存、怎样关联、是否更新”也当成需要判断的任务。主智能体负责外部工作,记忆管理过程负责让积累下来的信息更好地支持未来任务,这就是“为记忆管理建立一个专门的智能体”的含义。不过,图中的两个机器人是功能角色的区分,并不要求一定使用两个不同型号的模型,也不意味着所有存储规则都可以丢掉。系统仍然需要规定数据格式和可执行操作。模型带来的灵活性主要体现在根据新信息决定怎样建立笔记、连接相关经历以及修订现有记忆。
A-MEM 展开成四个部分。第一部分是笔记构建。第一次对话要求实现一个同时使用内存和磁盘的缓存系统;第二次对话补充系统已经能工作,但生产环境内存占用太高,希望加入 LRU,也就是最近最少使用淘汰策略。模型把交互变成笔记,包含时间戳、内容、上下文、关键词、标签和向量表示,这样记住的不只是孤立的一句话,还保留了它所处的任务背景。第二部分是链接生成。新笔记先到已有记忆中检索相关笔记作为候选,图中用一个新节点和几条旧笔记之间的箭头表示,模型判断它们之间是否存在有意义的联系,再把这些关联存下来。例如新增 LRU 淘汰应当关联到先前那个缓存项目,而不应该因为都提到了内存就连到毫不相关的任务。第三部分是记忆演化。新证据不仅新增一条笔记,还可能改变对旧笔记的理解。图中已有笔记和新笔记共同进入模型,经过 action 产生更新后的组织方式。例如原来只知道这个项目需要支持两级存储,后来才知道它还有生产内存压力,这会影响我们描述和检索整个项目的方式。最右边是使用阶段:用户问题先由文本模型生成查询向量,去记忆中查找相关内容,得到排名靠前的记忆,再交给主智能体回答。图中的盒子和链接强调,记忆可以逐渐形成关系网络,而不只是不断追加的列表。把建设关系和更新理解放进写入流程,目的是让后续检索拿到更连贯的任务背景。
Mem0 把记忆写入明确拆成两个阶段。左边紫色区域是抽取阶段。当前消息进来后,系统同时读取已有对话摘要和最近 M 条消息,再交给模型抽取值得保存的新记忆。顶部的摘要生成器负责维护摘要数据库,保存摘要、历史消息和记忆。这里的 M 表示保留多少条近期消息,它是控制上下文范围的参数。新抽出的内容还不能直接一股脑追加。它会进入右边绿色的更新阶段,同时从数据库取回最相似的前 S 条已有记忆,S 是候选数量。模型对照新旧内容,通过工具调用选择四种动作:ADD,新信息此前没有记录,就新增;UPDATE,新信息与已有记忆互补,就更新;DELETE,新信息与旧记忆矛盾,就移除失效的旧条目;NOOP,表示无需操作,比如重复说了一遍已经知道的事实。结果再沿上方回路写回数据库。用一个简单例子区分:第一次知道用户住在上海,可以新增;后来补充住在浦东,可以把互补细节合并到原有记忆;若用户明确说已经搬到杭州,当前居住地事实就失效了,应移除或取代旧条目,并保留新事实,不能把两座城市都当成当前住址;再次说自己住杭州,则不必生成重复记录。这里按论文中的动作定义来理解,具体实现怎样保存历史版本,是另一个需要明确的策略。这张图的重点是:写入记忆不仅是信息抽取,还包含与旧状态比较的决策。相似检索为比较提供候选,四种动作把决策变成可执行的数据库变更,从而控制重复、冲突和过时信息。
Mem0 更新动作 = ADD | UPDATE | DELETE | NOOP
ADD:新信息此前没有记录
UPDATE:新信息与旧记忆互补
DELETE:新信息与旧记忆矛盾
NOOP:重复或无需操作
如果要表达人物、地点、组织之间的联系,Mem0 还展示了图结构的记忆流程。左侧消息同时进入实体抽取器和关系生成器。实体抽取器找出节点,例如某个人和某个城市;关系生成器结合消息与节点形成三元组,也就是主体、关系、客体,例如“用户居住于上海”。这些新关系进入右侧的冲突检测器。它先搜索图中已有节点和关系,检查新事实与旧事实能否同时成立,再由更新决策模块确定如何改图,最右边的箭头最终落到下方的图记忆。论文的图扩展会把过时关系标记为无效,而不是直接物理删除,以便保留时间推理所需的历史。图里的不同颜色和连线是在示意节点及关联,不需要把颜色当成某种固定的数据类型。例如新消息说用户搬到杭州,系统需要识别这与当前居住地关系有关;但若只是说去杭州旅行,就不能贸然覆盖住址。这个例子说明,冲突判断需要理解关系和时间,而不只是匹配地名。上方展示的是图记忆扩展,底部四种动作概括基础记忆更新的思想,不能直接当成图扩展的完整算法。图里的更新还要落实到具体节点和关系,以及对应的冲突处理。后台维护的目标是让下一次检索读到与当前事实一致的状态。
到这里,我们已经看到了四种互补的模式。下面把它们放到同一张表里,从存什么、什么时候写、什么时候读以及付出什么代价这四个问题重新比较。这样面对自己的业务需求,就可以判断究竟缺少哪一种能力,而不只记住几个系统名称。以下是四种模式的比较要点。
-
从经验中学习程序:存可复用代码、工作流和操作步骤;通常在完成有价值的任务轨迹后或通过离线归纳写入;在以后遇到相关任务时读取。收益是减少重复摸索,但流程会随着接口和环境变化而过时,所以可复用不等于永远有效。
-
分层记忆:保存精简的活跃信息和外部的详细资料;信息不断到来、上下文压力增大时进行写入和搬移;活跃记忆持续参与推理,外部记忆按需检索。它能控制当前上下文大小,但代价是必须可靠决定什么留在近处、什么移到远处,以及何时取回来。
-
反思式整合:保存经验教训和较高层次推断;反馈到来或观察积累到一定程度时写入;在相关推理和规划过程中读取。抽象后的结论更容易跨情境复用,但也可能混入证据不足的推断,因此应当能够追溯原始经历。
-
记忆维护:保存修订后的事实、关联索引和提前加工好的表示;可能由新证据触发,在插入时执行,也可能在后台进行;后续读取这个不断维护的存储。组织得更好可以提升可用性,但维护本身消耗计算,压缩和改写也可能丢失信息。
这四行不必四选一。一个系统可以同时使用分层存储,在后台更新事实,再从成功或失败的经历中提炼操作经验。关键是先明确想解决的故障:是不会做、装不下、学不到教训,还是记住了过时内容,然后选择相应机制。
📏 智能体记忆架构:11:记忆评估基准
架构设计再完整,也需要证据证明它真正改善了行为。本节转向记忆评估。我们要区分三个层次:系统有没有保存关键内容,能不能在需要时找到它,以及找到以后是否真的用对。不同基准关注的环节不同,分数也必须结合任务和数据来解释。
一个有用的记忆基准至少要满足四个要求。以下是评估记忆系统时需要同时查看的四个条件。
更多推荐
所有评论(0)